Permit safe cross-run output declarations
This commit is contained in:
@@ -155,7 +155,7 @@ func Resolve(
|
||||
result.SkippedMissing = append(result.SkippedMissing, requirement.Name)
|
||||
continue
|
||||
}
|
||||
selectedRel, err := publishedOutputRelativePath(previousSessionPrefix, current.RunID, selectedArtifact.DestinationKey)
|
||||
selectedRel, err := publishedOutputRelativePath(previousSessionPrefix, selectedArtifact.DestinationKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve declared previous-session artifact %q: %w", requirement.Name, err)
|
||||
}
|
||||
@@ -209,11 +209,14 @@ func committedPublishedArtifactBySource(commit *artifacts.RemoteCommitManifest,
|
||||
return artifacts.RemoteArtifact{}, false
|
||||
}
|
||||
|
||||
func publishedOutputRelativePath(sessionPrefix, runID, destinationKey string) (string, error) {
|
||||
prefix := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, runID), "published") + "/"
|
||||
rel, ok := strings.CutPrefix(strings.TrimSpace(destinationKey), prefix)
|
||||
func publishedOutputRelativePath(sessionPrefix, destinationKey string) (string, error) {
|
||||
identity, err := artifacts.ParseImmutableRunObjectKey(sessionPrefix, destinationKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rel, ok := strings.CutPrefix(identity.RelativePath, "published/")
|
||||
if !ok || strings.TrimSpace(rel) == "" {
|
||||
return "", fmt.Errorf("destination %q is outside selected run published-output prefix %q", destinationKey, prefix)
|
||||
return "", fmt.Errorf("destination %q is outside its immutable run published-output prefix", destinationKey)
|
||||
}
|
||||
normalized, err := pathsafe.NormalizeRelativeDestination(rel)
|
||||
if err != nil {
|
||||
|
||||
@@ -42,6 +42,23 @@ func TestResolveUsesExactCommittedDeclaration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveUsesCarriedDeclarationFromEarlierRun(t *testing.T) {
|
||||
cfg, paths := previousCacheTestConfig(t)
|
||||
store := &storage.FakeBackend{}
|
||||
declaration := committedPublishedArtifactForTest("narratio.artifact.session_recap", "artifacts/session_recap.md", []byte("# recap\n"), "recap-generation")
|
||||
declaration.DestinationKey = strings.Replace(declaration.DestinationKey, "/runs/previous-run/", "/runs/origin-run/", 1)
|
||||
exactKey := seedCommittedPreviousState(t, store, cfg, previousManifestWithOutput(t, cfg, "artifacts/session_recap.md", nil), []artifacts.RemoteArtifact{declaration})
|
||||
|
||||
plan, err := Resolve(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{{Name: "session_recap", Required: true}}, store)
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
record := findRecord(plan.Records, InputKindArtifact)
|
||||
if record == nil || record.RemoteKey != exactKey {
|
||||
t.Fatalf("artifact record = %#v, want exact carried key %q", record, exactKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDerivesLocalPathFromCommittedDestination(t *testing.T) {
|
||||
cfg, paths := previousCacheTestConfig(t)
|
||||
cfg.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{Enabled: true, OutputPath: "configured/ignored.md"}
|
||||
@@ -63,12 +80,12 @@ func TestResolveDerivesLocalPathFromCommittedDestination(t *testing.T) {
|
||||
|
||||
func TestPublishedOutputRelativePathRejectsUnsafeDestination(t *testing.T) {
|
||||
sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-04-26")
|
||||
_, err := publishedOutputRelativePath(sessionPrefix, "previous-run", artifacts.S3RunPrefix(sessionPrefix, "previous-run")+"artifacts/session_recap.md")
|
||||
if err == nil || !strings.Contains(err.Error(), "outside selected run published-output prefix") {
|
||||
_, err := publishedOutputRelativePath(sessionPrefix, artifacts.S3RunPrefix(sessionPrefix, "previous-run")+"artifacts/session_recap.md")
|
||||
if err == nil || !strings.Contains(err.Error(), "outside its immutable run published-output prefix") {
|
||||
t.Fatalf("publishedOutputRelativePath() error = %v, want prefix rejection", err)
|
||||
}
|
||||
_, err = publishedOutputRelativePath(sessionPrefix, "previous-run", artifacts.S3RunPrefix(sessionPrefix, "previous-run")+"published/../session_recap.md")
|
||||
if err == nil || !strings.Contains(err.Error(), "published output destination") {
|
||||
_, err = publishedOutputRelativePath(sessionPrefix, artifacts.S3RunPrefix(sessionPrefix, "previous-run")+"published/../session_recap.md")
|
||||
if err == nil || !strings.Contains(err.Error(), "clean relative object key") {
|
||||
t.Fatalf("publishedOutputRelativePath() error = %v, want unsafe destination rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user