Repository navigation
fix(owner): administrative-only builder_ids are not independent owners (D-148) - #1516
Conversation
…s (D-148) verifierIndependence returned ok whenever two owners resolved to different keys in a shared namespace, WITHOUT checking assurance. Two agents whose only owner evidence is an administrative builder_id (a foreign key set as a side effect of agent creation, "NOT evidence of ownership") therefore read as independent and earned canonical RepID tagged distinct-owner. Live case found: trinity-nexus <-> trinity-apm, +60 RepID, neither with a signed binding. Fix: independence requires owner evidence ABOVE administrative on BOTH sides, in a shared namespace (MIN_INDEPENDENCE_ASSURANCE). Administrative can still prove SAMENESS (a shared FK is a real shared link) -- it just cannot prove a DIFFERENCE. Strictly fail-MORE-closed: the change only ever refuses more, never pays or awards more. No enforce flip; SAME_OWNER_SETTLEMENT_MODE unchanged. Tests: - verifier-independence-d148.test.ts (new, 14): the rule directly, over Sean's case families -- same-human/different-wallet (documented proven_wallet ceiling), overlapping claims (fail closed), unverified custody, revocation, unknown controller, cross-namespace, resolver failure, as-of-time snapshot. - same-owner-settlement.test.ts: reclassified fixtures -- genuine independence now needs a SIGNED claim; different administrative builder_ids become the D-148 regression (owner_unknown, withheld under enforce). 37 pass. tsc clean. Inert: shadow mode live, bounty route deny-by-default (no key holds bounty_verify). Merge gated on Sean (canonical-classification change). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Strix Security Review✅ No open security findings. PR summary: This PR raises the bar for treating two agents as independently owned before a bounty verification is accepted: independence now requires a proven owner binding (signed wallet or proven human) on both sides in a shared key namespace, so administrative builder_id foreign keys and unverified attested claims can no longer establish a difference. It also updates the settlement-gate fixtures and adds a regression suite for the misclassification. Review summaryReviewed the owner-independence change and its callers. The new minimum-assurance filter on Review history (3)
Review 3 on this PR · Reviewed by Strix |
…ied attested claim (D-148, Grok #1516 review) Non-authoring review (Grok) agreed the administrative fix is sound but caught a remaining hole: at a "> administrative" bar, two attested_unverified claims (custodianship links / read-time-unverified delegations) from the SAME attester under different keys still read as independent. One attester minting two keys is not two independent controllers, and the only live custodianship row today is a mock. Raise MIN_INDEPENDENCE_ASSURANCE to ASSURANCE_RANK.attested_unverified, so independence requires proven_wallet or proven_human on BOTH sides. Matches the "positive proof of authorized controller identity" bar. Weaker evidence still proves SAMENESS (sameOwner, unchanged) — it just cannot prove a difference. Shadow-replay result is unchanged: the 22 fleet flips are administrative/null builder relationships; no attested claims exist on the fleet, so the 270 RepID figure stands. One-line reversible (the constant) if attested should count. tsc clean; verifier-independence-d148 + same-owner-settlement suites green (52). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Non-authoring review (Grok, D-107) — co-signed with one hardeningGrok agreed the administrative fix is correct, and caught a remaining hole: at the original Resolved by raising the bar (
|
There was a problem hiding this comment.
The change is a pure tightening of the independence classifier in src/services/bounty-authorization.ts: the distinctKinds filter adds ASSURANCE_RANK[assurance] > MIN_INDEPENDENCE_ASSURANCE to the pre-existing namespace check, so the accepted set is a strict subset of the old one. I verified the monotonicity claim (no pair that was previously refused can now pass), the unknown/undefined/prototype-key behaviour of the rank lookup (all fail closed), and that declared rank-2 claims cannot slip through because the only producer emits ownerKeyKind: 'opaque', which the same filter excludes. Every path that awards canonical RepID or payment grounding routes through this function via partyIndependence, and each consumer keys off ok === true / counted === false rather than a default branch or the changed detail string, so no caller turns the new refusals into an award.
Two neighbouring weaknesses were investigated and rejected as not belonging to this PR: the weaker owner-key-only comparison in src/scoring/confirmed-outcome.ts is unreachable behind a settler whitelist and a default-off flag, and attested_unverified delegation claims clear the new floor but are EIP-712 signature-gated against the agent's own owning builder, so they cannot name a different principal than the administrative FK already does — the residual case is the same-human/two-wallets ceiling the PR documents and pins in its new test. The arena's ownerRelation classifier still treats administrative builder_ids as distinct, but it is pre-existing, unchanged here, and documented as RepID- and money-neutral.
One non-security note for the author: tests/bounties-verify-auth.test.ts:47 and tests/bounties-write-auth.test.ts:77 build owner claims with assurance: 'onchain', which is not a member of OwnerAssurance, so the rank lookup yields undefined and those fixtures now resolve owner_not_established — the bounty-verify happy-path assertions there likely no longer pass, which conflicts with the "all affected suites green" claim in the description. Worth fixing the fixtures to a real assurance value before merge.
Reviewed by Strix
Configure security review settings
There was a problem hiding this comment.
The change is confined to the independence classifier in verifierIndependence. The new distinctKinds predicate is the previous namespace-intersection predicate AND-ed with an assurance threshold, so the claim set that can establish a shared namespace is a strict subset of what it was before — no input can newly reach an independent verdict, and the earlier sameOwner and status gates are untouched. The threshold (ASSURANCE_RANK[x.assurance] > ASSURANCE_RANK.attested_unverified) admits exactly proven_wallet and proven_human; any unrecognised assurance value compares false and is dropped, so malformed claim shapes fail closed rather than open. Those tiers are produced only by the signature-verified binding write path, and the squad-designation overlay the new tests exercise annotates metadata only — withholding and counting continue to be driven by the settlement verdict, so the overlay cannot be used to launder a same-controller pair. Downstream consumers (bounty verify route, settlement gate, grounding, experimental eligibility, conformance readers) either refuse on any non-ok reason or key on counted === false, so the increased rate of owner_not_established only withholds more. No security issue remains in the changed code.
Reviewed by Strix
Configure security review settings
…route fixtures The `test` CI job was red (3 suites / 4 tests). Two independent causes, both from #1516: 1. LOAD-TIME CRASH. bounty-authorization.ts read `ASSURANCE_RANK.attested_unverified` at module top level. Under a circular import that evaluates this module before agent-owner-resolver finishes (tests/model-family-wiring loads the full app graph), ASSURANCE_RANK is still undefined -> `TypeError: Cannot read properties of undefined` and the whole suite fails to run. The unit test imported this module first, so it never hit it. Fix: read the rank lazily, at call time inside verifierIndependence. Value and semantics unchanged (still strictly above attested_unverified). 2. STALE FIXTURES. bounties-verify-auth and bounties-write-auth built owner claims with `assurance: 'onchain'`, which is NOT a real OwnerAssurance. Pre-D-148 nothing checked assurance strength; the new proven-binding filter does, and ASSURANCE_RANK['onchain'] is undefined (`undefined > 3` is false), so the claim is dropped and every happy path 403s (owner_not_established). Fix: use proven_wallet / proven_human, matching each fixture's stated intent ("under different owners, independence passes"). Verified locally: the 3 formerly-red suites pass (40 tests), the D-148 unit suites still pass (61 tests), tsc --noEmit clean. No change to gate logic or any live writer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Red-team + CI fix (pre-merge, inert — nothing merged; gates stay on)Adversarially traced
But CI
|
There was a problem hiding this comment.
Reviewed the owner-independence change and its callers. The new minimum-assurance filter on distinctKinds is strictly narrowing: it reads the real ASSURANCE_RANK table (proven_human 5 > proven_wallet 4 > attested_unverified 3 > declared 2 > administrative 1), an unrecognised or absent assurance value compares as undefined > 3 and is therefore dropped, and because the check is an intersection of the two narrowed kind sets it can only remove matches — there is no ordering that yields a more permissive verdict than before. Sameness is still decided over every claim, so a shared administrative builder_id or a shared attested key still resolves to same_owner rather than being filtered away, and proven_wallet/proven_human only exist because the binding write path verifies a wallet signature plus the agent's own key, so the raised bar is real evidence rather than a relabel. Both consumers fail closed on refusal: the bounty-verify route returns 403 when ok is false, and the settlement gate maps owner_not_established to owner_unknown, which withholds under enforce while the squad-designation overlay only annotates and can never un-withhold. The lazy accessor avoids the load-time circular-import dereference and its only theoretical failure mode requires the rank table to be undefined after module initialisation, which is not reachable. The test-file edits replace a stale onchain fixture assurance with the real proven values and keep the cross-namespace refusal assertion intact; no production code emits onchain. No security-relevant regression found in the changed code.
Reviewed by Strix
Configure security review settings
…nibble flip) (#1518) tests/xc1-l1-regrade-2.test.ts built its one-nibble-different recipient with `seller.address[41] === 'f' ? '0' : 'f'`. ethers returns an EIP-55 checksummed (mixed-case) address, so when the seller's last nibble is a checksum-uppercased 'F', `'F' === 'f'` is false and the flip appends 'f' — a CASE-only change that evaluateNpa (which compares addresses case-insensitively, asserted by XC1-PF-00) reads as the SAME address. The probe then sees no refusal (`[]`) and fails, ~1/32 runs (P(last nibble = f) x P(checksum uppercases it)). This intermittently reddens the `test` job on unrelated PRs (seen on #1517, not #1516). Fix: compare the last char case-insensitively so the flip is always value-distinct. Proven over all last-nibble cases: uppercase 'F' now maps to a genuinely different nibble; every other case is unchanged. Test-only; no src/ or behavior change. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… (inert) (#1519) Test-first hardening of the EXPERIMENTAL RepID namespace against the six owner-independence priorities. FINDING: every attack is already refused by code on main -- no new guard or scoring math was needed. This pins each guard and the honest cross-mode ledger semantics so a later weakening turns red. New: tests/experimental-repid/experimental-repid-adversarial.test.ts (17 tests) P1 eligibility verified+authoritative only (authoritative===true, not truthy) P2 independence the eligibilityFor path inherits D-148 (#1516): admin-only builder_ids are sameness, never independence P3 negative adverse delta persists, can't be flipped/erased by replay or clone P4 replay verbatim replay (already_consumed) vs transplant (subject_mismatch) P5 lineage content.claimed_by bound to subject agent; wrong-worker refused P6 two-number same_controller vs independent counts stay separate; label/flag fixed Each attack carries a positive control (LESSONS 5) so green is a measurement. Inert: in-memory fake store only, mode never active against a real store, no DB touched, no migration. 48 pass combined with the two existing exp suites. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
D-148 — administrative identity accepted as independently-controlled ownership
verifierIndependencereturnedokwhenever two owners resolved to different keys in a shared namespace, without checking how strong that evidence was. Two agents whose only owner evidence is an administrativebuilder_id(a foreign key set as a side effect of agent creation — "NOT evidence of ownership") therefore read as independent and earned canonical RepID tagged distinct-owner.Live case:
trinity-apm↔trinity-nexus, neither with a signed binding, 22 events / 270 RepID over 60 days labelled independent-counterparty.The fix (production:
bounty-authorization.ts)Independence now requires owner evidence above
administrativeon both sides, in a shared namespace (MIN_INDEPENDENCE_ASSURANCE). Administrative evidence can still prove sameness (a shared FK is a real shared link) — it simply cannot prove a difference. The change is strictly fail-more-closed: it only ever refuses more, never pays or awards more.Tests
verifier-independence-d148.test.ts(new, 14): the rule directly over the case families — administrative-only (the fix), shared-FK same_owner, two signed bindings, mixed assurance, overlapping claims (fail closed), unverified custody, revocation, unknown controller, cross-namespace, resolver failure, as-of-time snapshot, and the documented same-human/different-wallet proven_wallet ceiling.same-owner-settlement.test.ts(37): fixtures reclassified — genuine independence now needs a signed claim; different administrative builder_ids become the D-148 regression (owner_unknown, withheld under enforce). Designation-overlay tests updated to match.Shadow replay (read-only, as-of trade time) —
reports/2026-11-04/D148_SHADOW_REPLAY.mdindependent→owner_unknown.Safety / gating
SAME_OWNER_SETTLEMENT_MODEunchanged (shadow); the bounty-verify route is deny-by-default (no key holdsbounty_verify). No enforce flip, no schema, no on-chain.tsc --noEmitclean; all affected suites green.🤖 Generated with Claude Code