From 25fc2d92e85932e7683ef1f85d06735aab47ac23 Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 24 Sep 2026 00:56:54 +0000 Subject: [PATCH 1/2] fix(artifactvault): derive staging/artifact UUIDs from idempotency key Payload-sha-only UUIDs collided under artifact_payload_staging_pkey when two task runs finalized identical deliverable bytes. Key already includes source_event_id; hash it for stable unique art-/stg- ids. --- .doc/ops/CURRENT.md | 1 + internal/artifactvault/registry.go | 11 ++++++-- internal/artifactvault/registry_uuid_test.go | 29 ++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 internal/artifactvault/registry_uuid_test.go diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 4f6c73c1..4cc1241d 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -18,6 +18,7 @@ | owner | task | branch | worktree | mutex | status | |---|---|---|---|---|---| +| Cursor | fix-fav-staging-uuid-2026-09-24 | fix/fix-fav-staging-uuid-2026-09-24 | .worktrees/fix-fav-staging-uuid-2026-09-24 | artifactvault | in_progress | | Cursor | gate-e1a-unique-project-name-2026-09-24 | fix/gate-e1a-unique-project-name-2026-09-24 | .worktrees/gate-e1a-unique-project-name-2026-09-24 | docnotify | in_progress | | Cursor | gate-unhook-docquery-2026-09-24 | fix/gate-unhook-docquery-2026-09-24 | .worktrees/gate-unhook-docquery-2026-09-24 | server+docquery | in_progress | | Cursor | integrate-jev-assist-land-2026-09-24 | feature/integrate-jev-assist-land-2026-09-24 | .worktrees/integrate-jev-assist-land-2026-09-24 | git:dev | done | diff --git a/internal/artifactvault/registry.go b/internal/artifactvault/registry.go index d21cf181..24d94f86 100644 --- a/internal/artifactvault/registry.go +++ b/internal/artifactvault/registry.go @@ -249,12 +249,14 @@ func requestTX(ctx context.Context, tx pgx.Tx, facts Facts, req PublishRequest) st := &Staging{SourceEventID: req.SourceEventID, PayloadSHA256: sha, Payload: req.Payload} return &Artifact{UUID: existing, IdempotencyKey: key, PublicationStatus: StatusRequested}, st, nil } - uuid := "art-" + sha[:16] + // Derive UUIDs from the full idempotency key (includes source_event_id + sha). + // Using payload sha alone collides when two events share identical bytes. + uuid, stgUUID := artifactIDsFromKey(key) _, err = tx.Exec(ctx, ` INSERT INTO artifact_payload_staging (staging_uuid, project_id, source_event_id, payload_sha256, encrypted_payload, classification) VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT (source_event_id, payload_sha256) DO NOTHING - `, "stg-"+uuid, req.ProjectID, req.SourceEventID, sha, req.Payload, ClassificationOK) + `, stgUUID, req.ProjectID, req.SourceEventID, sha, req.Payload, ClassificationOK) if err != nil { return nil, nil, err } @@ -277,6 +279,11 @@ func requestTX(ctx context.Context, tx pgx.Tx, facts Facts, req PublishRequest) PayloadSHA256: sha, PublicationStatus: StatusRequested, TerminalState: req.TerminalState}, &Staging{Payload: req.Payload, PayloadSHA256: sha, SourceEventID: req.SourceEventID}, nil } +func artifactIDsFromKey(key string) (artUUID, stgUUID string) { + idHash := SHA256Hex([]byte(key))[:16] + return "art-" + idHash, "stg-" + idHash +} + func nullInt(v int64) any { if v == 0 { return nil diff --git a/internal/artifactvault/registry_uuid_test.go b/internal/artifactvault/registry_uuid_test.go new file mode 100644 index 00000000..6f2b6820 --- /dev/null +++ b/internal/artifactvault/registry_uuid_test.go @@ -0,0 +1,29 @@ +package artifactvault + +import "testing" + +func TestArtifactIDsFromKeyDifferForSamePayloadDifferentEvents(t *testing.T) { + payload := []byte("# same body\n") + sha := SHA256Hex(payload) + reqA := PublishRequest{ + ArtifactType: "agent_commit", Environment: "dev", ProjectExternalKey: "p1", + LogicalResourceID: "doc1", SourceEventKind: "taskrun", SourceEventID: "taskrun:runA:completed", + } + reqB := PublishRequest{ + ArtifactType: "agent_commit", Environment: "dev", ProjectExternalKey: "p1", + LogicalResourceID: "doc1", SourceEventKind: "taskrun", SourceEventID: "taskrun:runB:completed", + } + keyA := IdempotencyKey(reqA, sha) + keyB := IdempotencyKey(reqB, sha) + if keyA == keyB { + t.Fatal("idempotency keys must differ across source events") + } + artA, stgA := artifactIDsFromKey(keyA) + artB, stgB := artifactIDsFromKey(keyB) + if artA == artB || stgA == stgB { + t.Fatalf("same-payload different events must not share UUIDs: art=%q/%q stg=%q/%q", artA, artB, stgA, stgB) + } + if artA == "art-"+sha[:16] || stgA == "stg-art-"+sha[:16] { + t.Fatal("UUIDs must not be derived from payload sha alone") + } +} From fd83e98e6c851cc11a5f6ebe403347f963bea6fa Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 24 Sep 2026 00:57:10 +0000 Subject: [PATCH 2/2] docs(ops): mark fav-staging-uuid CURRENT done --- .doc/ops/CURRENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 4cc1241d..26710eae 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -18,7 +18,7 @@ | owner | task | branch | worktree | mutex | status | |---|---|---|---|---|---| -| Cursor | fix-fav-staging-uuid-2026-09-24 | fix/fix-fav-staging-uuid-2026-09-24 | .worktrees/fix-fav-staging-uuid-2026-09-24 | artifactvault | in_progress | +| Cursor | fix-fav-staging-uuid-2026-09-24 | fix/fix-fav-staging-uuid-2026-09-24 | .worktrees/fix-fav-staging-uuid-2026-09-24 | artifactvault | done | | Cursor | gate-e1a-unique-project-name-2026-09-24 | fix/gate-e1a-unique-project-name-2026-09-24 | .worktrees/gate-e1a-unique-project-name-2026-09-24 | docnotify | in_progress | | Cursor | gate-unhook-docquery-2026-09-24 | fix/gate-unhook-docquery-2026-09-24 | .worktrees/gate-unhook-docquery-2026-09-24 | server+docquery | in_progress | | Cursor | integrate-jev-assist-land-2026-09-24 | feature/integrate-jev-assist-land-2026-09-24 | .worktrees/integrate-jev-assist-land-2026-09-24 | git:dev | done |