Skip to content

fix(owner): administrative-only builder_ids are not independent owners (D-148) - #1516

Merged
DealAppSeo merged 3 commits into
mainfrom
claude/2026-11-04-d148-min-assurance
Nov 6, 2026
Merged

DealAppSeo merged 3 commits into
mainfrom
claude/2026-11-04-d148-min-assurance

Conversation

@DealAppSeo

Copy link
Copy Markdown
Owner

D-148 — administrative identity accepted as independently-controlled ownership

verifierIndependence returned ok whenever 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 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: 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 administrative on 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.md

  • 22 events / 270 RepID flip independent → owner_unknown.
  • 0 remain independent under the new rule (none had a real binding), so nothing provably independent is withheld.
  • same_builder (176) and null_builder (48) events are unchanged.

Safety / gating

  • Inert. SAME_OWNER_SETTLEMENT_MODE unchanged (shadow); the bounty-verify route is deny-by-default (no key holds bounty_verify). No enforce flip, no schema, no on-chain.
  • tsc --noEmit clean; all affected suites green.
  • Merge is gated on Sean — this changes the canonical independence classification (D-107 review-together), not an auto-merge.

🤖 Generated with Claude Code

…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

strix-security Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

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 summary

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.

Review history (3)
  • Review 3 · 65b5716 · Nov 5 05:15 UTC · Nothing new
  • Review 2 · 5c18952 · Nov 5 01:51 UTC · Nothing new
  • Review 1 · ea593b8 · Nov 5 01:48 UTC · Nothing new

Review 3 on this PR · 65b5716 · requested Nov 5 05:13 UTC on a new commit


Reviewed by Strix
Re-run review · Configure security review settings

…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>
@DealAppSeo

Copy link
Copy Markdown
Owner Author

Non-authoring review (Grok, D-107) — co-signed with one hardening

Grok agreed the administrative fix is correct, and caught a remaining hole: at the original > administrative bar, two attested_unverified claims (custodianship links / read-time-unverified delegations) from the same attester under different keys would still read as independent — and that tier is currently all-mock.

Resolved by raising the bar (5c189526): independence now requires a proven binding (proven_wallet / proven_human) on both sides. This matches the "positive proof of authorized controller identity" standard. It's one-line reversible (the MIN_INDEPENDENCE_ASSURANCE constant) if attested evidence should later count.

  • Shadow-replay figures unchanged (the 22 flips / 270 RepID are administrative/null relationships; no attested claims exist on the fleet).
  • tsc clean; verifier-independence-d148 + same-owner-settlement green (52).
  • Still inert; merge gated on Sean.

@strix-security strix-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@strix-security strix-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@DealAppSeo

Copy link
Copy Markdown
Owner Author

Red-team + CI fix (pre-merge, inert — nothing merged; gates stay on)

Adversarially traced verifierIndependence and the settlement wrapper for a false-pass (a same-owner pair reading as independent → self-dealing payout). The core D-148 logic holds and fails closed:

  • Any shared (ownerKeyKind, ownerKey) across all claims (any assurance) trips same_owner. The positive check only passes on a proven shared-namespace pair with different keys, so a pass genuinely means distinct owners under the key model.
  • Cross-namespace same-owner (a builder_id and an SBT naming one human) is refused by the shared-namespace requirement, not passed. Weak-only / unknown / throwing resolvers all refuse. The documented proven_wallet same-human/different-wallet ceiling is explicitly tested as accepted.

But CI test was red — two real bugs this PR introduced. Fixed in 65b5716.

  1. Load-time crash (model-family-wiring suite failed to run). 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 defining the rank (the full app graph), that read is undefined → TypeError: Cannot read properties of undefined (reading 'attested_unverified'). The unit test imports this module first, so it never hit it. Fix: read the rank lazily at call time inside verifierIndependence; value/semantics unchanged.
  2. Stale fixtures (4 assertions, 403 where 200/409 expected). bounties-verify-auth / bounties-write-auth built owner claims with assurance: 'onchain' — not a real OwnerAssurance. Pre-D-148 nothing checked assurance; now ASSURANCE_RANK['onchain'] is undefined so the proven-binding filter drops the claim and every happy path 403s. Fix: proven_wallet / proven_human, matching each fixture's stated intent.

Local: the 3 formerly-red suites pass (40 tests), D-148 unit suites still green (61), tsc --noEmit clean. No change to gate logic or any live writer.

One NON-BLOCKING note for a later, separate change (not this PR's file) [R, not verified in prod]

probeBinding (agent-owner-resolver.ts) can emit a proven claim whose ownerKeyKind is 'builder_id' but whose ownerKey is actually a wallet address — the ?? row.human_wallet fallback when builder_id is null. If that same wallet appears as ownerKeyKind: 'wallet' on the counterparty (a live delegation claim), the cross-kind sameOwner check misses the shared link while the positive check passes on the independent builder claims → a shared-wallet pair could read as independent. This contradicts the module's own "a shared attested key is a real shared link". Reachability is data-dependent (needs a proven wallet binding with null builder_id and a same-wallet 'wallet'-kind claim on the other side; custody is a mock today, delegations are live), and the mislabel leans toward over-blocking elsewhere, so this is defense-in-depth, not a merge blocker. The fix (label the wallet fallback 'wallet', or compare wallet-bearing keys across kinds) is a MEDIUM owner-logic change on a money path → Sean/Grok's call, in its own PR.

@strix-security strix-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

DealAppSeo added a commit that referenced this pull request Nov 6, 2026
…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>
@DealAppSeo
DealAppSeo merged commit 0c994e9 into main Nov 6, 2026
18 checks passed
@DealAppSeo
DealAppSeo deleted the claude/2026-11-04-d148-min-assurance branch November 6, 2026 18:22
DealAppSeo added a commit that referenced this pull request Nov 8, 2026
… (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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant