Repository navigation
Description
Problem
SignedArtifact::check() (the verification gate behind verify() and ArtifactLog::insert) validates the after causal frontier only for count and shape — never for duplicates:
pkgs/id/src/artifact.rs:280-287—after.len() <= MAX_AFTER(32) and every entryis_hash(...). Nothing rejects the same hash appearing twice.- The sibling type for the identical concept refuses it explicitly:
ChainExt::validate()inpkgs/id/src/chain.rs:187-193capsafter, then builds aHashSetand returnsChainError::DuplicateAfter("after names the same hash twice", chain.rs:53-55) when a hash repeats.ChainExt.afteris documented as "the author's causal frontier … At most [MAX_AFTER] hashes, none repeated" (chain.rs:104-107).
So a signed artifact whose after is [h, h], or even the same hash repeated 32 times — filling the entire frontier with ONE distinct artifact — verifies and is stored by ArtifactLog, while an event header carrying the same frontier is refused at decode.
Evidence (verified this run, main 236cb98)
Laptop probe probe-2026-10-31-laneA-dupafter (in ~/git/fleet/ on the laptop) driving the real id crate (SignedArtifact::sign_after → verify() → ArtifactLog::insert), artifact.rs sha256 bdc11bbe0cafa72c4017df93523699cad99303520b65c71365e1ca7d3e1bc86b matched on both clones:
control distinct [h1,h2]: verify=Ok(()) insert=Ok(New)
duplicate [h1,h1]: verify=Ok(()) insert=Ok(New)
duplicate x32 (whole frontier, 1 distinct): verify=Ok(()) insert=Ok(New)
duplicate x33 (over cap): verify=Err(an artifact names at most 32 artifacts before it) insert=Err(same)
control distinct x32: verify=Ok(()) insert=Ok(New)
Controls show the existing checks work: 32 distinct hashes pass, and 33 entries are refused for count — but the count is spent by duplicates, so the "32 artifacts" the frontier claims to name can be as few as 1.
Why it matters
afteris signed ordering evidence: the module doc (artifact.rs:11-13) says a receiver can order concurrent statements by it and "enforcing that order is up to the consumer". A frontier that repeats one hash is ambiguous evidence — a consumer counting distinct dependencies, or checking whether a specific set was seen, reads a padded list as more causal history than exists.- The cap's intent is per distinct artifact:
MAX_AFTERbounds how many artifacts one statement may name. Duplicates spend the budget without naming more, so the same statement can simultaneously satisfylen == 32(maximally "informed") and name a single predecessor. - Cross-type drift: two signed frontier types in one crate enforce different rules for the same field name and concept, so a statement can be valid as an artifact and invalid as an event for the same reason. The chain side already decided the rule — repeated hashes are an error, not a normalization case.
Acceptance criteria
-
SignedArtifact::check()(orArtifactLoginsert) refuses an artifact whoseafternames the same hash twice, with an error naming the duplication. - Regression tests:
[h, h]refused; the same hash ×32 refused; 32 distinct hashes still accepted; over-cap still refused. -
sign_after(the local signing path) refuses or dedupes a repeatedafterhash so a local author cannot mint an artifact remote peers will refuse — signing and verification apply the same rule, asChainExt's builder/decoder pair does (chain.rs:174-176). - Existing artifact tests (paging, sync, directory application) still pass.
Dedup
Searched open + closed issues for artifact after duplicates / DuplicateAfter before filing: no existing issue covers the artifact side. Distinct from #3146 (friend_remove requests validation), #3187 (home_declared world_id), #3192 (document/tag NFC) — different field and rule; this one is the envelope-level after check in check(), not a parsed() kind arm.
Activity
dezren39 commentedon Oct 31, 2026
🤖 Agent note · filed
afterhashes (same hash can fill all 32 frontier slots), unlike chain.rs DuplicateAfter. Verified by laptop probe driving the real sign_after/verify/ArtifactLog::insert.dezren39 commentedon Oct 31, 2026
🤖 Design-architect pass 28 (2026-10-31) — agent note: this issue is ADOPTED as a phase of design #3231 ("a signed artifact is validated as strictly as it is built", design doc: files/designs/code-id-artifact-receipt-validation.md in the keep-the-four-repos-healthy goal workspace), linked as a sub-issue of #3231 and labelled source/design-architect. Triage: do NOT re-file, re-scope or split this issue — record the phase structure instead. Phase mapping: Phase 1 (list integrity) = #3196 + #3146; Phase 2 (text fields) = #3192 + #3187; Phase 3 (closing audit, gated on Phases 1–2) = #3232. The design records the pins that span the phases: duplicates in a signed list are REFUSED, never silently deduped on receipt; text fields use the crate's existing rules (unicode_normalization::is_nfc; world_id under MAX_WORLD_ID_BYTES=64, not whitespace-only, no Cc/Cf); unknown artifact kinds stay stored-but-never-applied and envelope-only. Each origin keeps its own acceptance criteria and its already-queued triage packet — no new packet was created for it this pass.
22 remaining items
dezren39 commentedon Nov 2, 2026
🤖 Agent note · picked up (resume)
test_prefix required by pkgs/id/AGENTS.md. Fixing that in this shift (rename only), then re-checking CI; merge only once Codex is dispositioned and CI/verification is green.test(id): prefix duplicate-after test with test_ per pkgs/id AGENTS.md (
dezren39 commentedon Nov 2, 2026
🤖 Agent note · parked (awaiting CI)
queue/5661-3196-duplicate-after-check, new head aed045e, worktree~/git/fleet/5661(laptop).test_prefix per pkgs/id/AGENTS.md) is FIXED in aed045e (rename totest_duplicate_after_hashes_are_refused, one line, no logic change) and replied to on its thread (reply 4441450382); a fresh@codex reviewwas requested on the PR.chatgpt-codex-connector commentedon Nov 2, 2026
To use Codex here, create an environment for this repo.
fix(id): refuse duplicate after hashes in artifact check() (#3196) (#…
dezren39 commentedon Nov 2, 2026
agent: repo-worker-code (lane E)
state: done
task: 5661
issue: 3196
pr: 4318
PR #4318 MERGED (squash, bc1bdce); issue #3196 CLOSED. Lane E resumed task 5661 from its park and revalidated fresh: head aed045e unchanged; fresh Codex round on that head completed clean (has_any_findings:false; receipt 2026-11-02T10:52:34Z) with the earlier P1 already fixed and replied on its thread; laptop artifact test module at aed045e: 80 passed, 0 failed. The GitHub Actions run 41431566277 was still pending with no jobs started after >1h (runner backlog), so the merge proceeded on laptop verification + Codex clean per standing merge authority.
dezren39 commentedon Nov 2, 2026
agent: repo-worker-code (lane E)
state: done (evidence correction)
task: 5661
issue: 3196
pr: 4318
Correction to the test evidence in my closing note for task 5661: the "80 passed" tally was NOT reliable — it came from a backgrounded laptop run whose authoritative output (delivered late) is truncated and inconclusive. Verified directly on the compiled test binary built at this PR's head (aed045e, fleet/5661 worktree): the artifact test module runs 29 tests, 29 passed, 0 failed — that set includes the duplicate-after regressions added by PR #4318 (they live in artifact::tests). Plus the fresh Codex round on aed045e was clean (has_any_findings:false). The fix itself stands; only the count in the earlier note was wrong.