Validate portable workspace identifiers

This commit is contained in:
2026-08-10 17:35:52 +00:00
parent 0b40cf8026
commit 1dccf5f140
31 changed files with 490 additions and 48 deletions

View File

@@ -10,6 +10,7 @@ import (
"time"
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
)
// Store persists manifests to and from durable storage.
@@ -79,6 +80,9 @@ func (s *LocalStore) Save(ctx context.Context, path string, m *Manifest) error {
if m.CreatedAt.IsZero() {
return fmt.Errorf("save manifest: created_at is required")
}
if err := validateManifestIdentities(m.SessionID, m.Campaign, m.RunID); err != nil {
return fmt.Errorf("save manifest: %w", err)
}
m.UpdatedAt = time.Now().UTC()
if m.Stages == nil {
@@ -202,6 +206,9 @@ func (s *LocalStore) SaveRun(ctx context.Context, path string, m *RunManifest) e
if m.CreatedAt.IsZero() {
return fmt.Errorf("save run manifest: created_at is required")
}
if err := validateManifestIdentities(m.SessionID, m.Campaign, m.RunID); err != nil {
return fmt.Errorf("save run manifest: %w", err)
}
m.UpdatedAt = time.Now().UTC()
if m.Stages == nil {
@@ -230,6 +237,9 @@ func validateLoadedManifest(m *Manifest) error {
if m.UpdatedAt.IsZero() {
return fmt.Errorf("updated_at is required")
}
if err := validateManifestIdentities(m.SessionID, m.Campaign, m.RunID); err != nil {
return err
}
return nil
}
@@ -265,10 +275,40 @@ func validateLoadedRunManifest(m *RunManifest) error {
if m.UpdatedAt.IsZero() {
return fmt.Errorf("updated_at is required")
}
if err := validateManifestIdentities(m.SessionID, m.Campaign, m.RunID); err != nil {
return err
}
return nil
}
func validateManifestIdentities(sessionID, campaign, runID string) error {
identities := []struct {
field string
value string
required bool
}{
{field: "session_id", value: sessionID, required: true},
{field: "campaign", value: campaign},
{field: "run_id", value: runID},
}
for _, identity := range identities {
value := strings.TrimSpace(identity.value)
if value == "" && !identity.required {
continue
}
if err := pathsafe.ValidateOpaqueSegment(identity.value); err != nil {
return fmt.Errorf(
"manifest %s %q is not a portable opaque identifier; migrate the legacy manifest before use: %w",
identity.field,
identity.value,
err,
)
}
}
return nil
}
func normalizeRunManifest(m *RunManifest) {
if m.Stages == nil {
m.Stages = map[string]*RunStageRecord{}