From codex-security candidate verification. Items 1 and 2 were reproduced against the built dist and are straightforward code fixes. Item 3 is a deliberate documented default that needs a maintainer decision, not a patch.
The common thread: three places where a warning stands in for a gate. In all three the repo already agrees with itself on the right shape — createAdcpServer refuses InMemoryStateStore outside {NODE_ENV=test, NODE_ENV=development} unless ADCP_DECISIONING_ALLOW_INMEMORY_STATE=1, and assertSandboxAccount fails closed on undefined. A state-mutating test surface deserves at least what an in-memory store gets.
1. Test-controller bridge: the documented trust boundary does not hold (candidate-cd887c5188044f75) — High
The scan's framing ("when no account resolver is configured") understates this. Reproduced against a server that does wire resolveAccount:
construction warns matching trust-boundary: 0
creatives: [ 'real-1', 'FIXTURE-LEAK' ]
response sandbox flag: true
Request was list_creatives with no account field and context: { sandbox: true }.
Cause: create-adcp-server.ts:5007 runs the resolver only when params.account != null, and the else if at :5047 requires resolveAccountFromAuth. Wire only resolveAccount — the primary documented path — and ctx.account stays undefined for any request omitting the account ref, so the merge gate's ctx.account === undefined || arm admits on a buyer-supplied marker alone. The #1784 warning at :3745 fires only when both resolvers are absent, so the misconfiguration that actually matters is silent. Same hole when resolveAccountFromAuth returns null (:5062, unknown principal).
Impact: seeded-fixture disclosure into production read responses, plus responses falsely stamped sandbox: true — a wire-honesty violation. Not by itself cross-tenant real-data disclosure, but makeAutoSeedBridge (from-platform.ts:6535-6537) falls back to the buyer-claimed account_id when ctx.account is absent, so the fixture namespace is caller-selectable.
Second, independent defect found while confirming: the gate compares ctx.account.sandbox === true literally and never calls getAccountMode. An account resolved as { account_id: 'sb', mode: 'sandbox' } is refused (reproduced: mode:sandbox account -> creatives: []). So the bridge gate and the comply-controller gate implement the same trust boundary two different ways, and adopters who migrate to mode per src/lib/server/account-mode.ts silently lose their fixtures.
Fix: require ctx.account !== undefined for the merge; use isSandboxOrMockAccount(ctx.account) rather than raw .sandbox === true; fire the construction warning unless both resolvers are wired.
2. Comply controller: env flags suppress the warning rather than gating anything (candidate-ecd76e4fd9107db2) — Medium
src/lib/testing/comply-controller.ts:338-355, :685-778. With no sandboxGate, reproduced:
force response: {"status":"completed","ok":true}
adapter invoked with: [{ media_buy_id: 'mb_victim', status: 'completed', ... }]
Note what ADCP_SANDBOX / ADCP_COMPLY_CONTROLLER_UNGATED at :757-763 actually do: they silence the warning. They gate nothing. That is the inverse of the pattern used correctly elsewhere in this repo.
Partial refutation worth recording: createAdcpServerFromPlatform bypasses controller.register and registers the tool itself behind a real resolved-account gate (from-platform.ts:2394-2620) — Phase 2 of #1435 is present, contradicting the stale "lands in a follow-up" comment at src/lib/server/account-mode.ts:11-13 (worth deleting). So this is scoped to adopters calling createComplyController(...).register(server) directly via @adcp/sdk/testing.
Two residual questions: the mcp == null branch at :2503-2508 deliberately falls back to the ungated controller.register(server), and it's unclear whether A2A dispatch routes through the MCP-registered handler (and therefore the gate) or has its own path. Both worth a targeted check.
Fix: make sandboxGate required, or fail closed in register() unless NODE_ENV ∈ {test, development} and an explicit ack env is set.
3. createTenantStore ref-routed default — needs your call, not a patch (candidate-bd22564f2d8ecfb6)
REFUTED as an undiscovered bug. src/lib/server/decisioning/tenant-store.ts:217-235 behaves exactly as the scan described, but test/lib/create-tenant-store.test.js:109-117 is a deliberate characterization test — its name is "default ('ref-routed') resolves a cross-tenant ref without checking the caller" — paired with four 'auth-scoped' tests proving the fail-closed alternative works. CHANGELOG.md:259 records that refAccess was added specifically to close this, and that the docs were corrected because they "previously implied resolve was always an isolation gate; it is not." The module header, the refAccess JSDoc, and the createTenantStore JSDoc all say so.
What is real is a docs gap with real teeth. skills/build-holdco-agent/SKILL.md — the file an agent-builder actually reads, whose front-matter promises "per-tenant data isolation" and whose § "Account resolution + tenant-isolation gate" presents createTenantStore as the answer — never mentions refAccess. Its "What the helper guarantees" list covers upsertRow / syncGovernanceRow / non-writability and omits the resolve carve-out entirely.
That matters more than "reads" implies: accounts.resolve is the account path for create_media_buy and update_media_buy too, so under the default a Meridian credential naming Pinnacle's operator resolves Pinnacle's Account (including its ctx_metadata) and every downstream handler runs scoped to Pinnacle — cross-tenant spend, not just cross-tenant reads. The upsert/syncGovernance gate doesn't cover this; those only guard sync_accounts / sync_governance.
Recommendation:
- Now: add the
resolve carve-out to skills/build-holdco-agent/SKILL.md, in the "What the helper guarantees" list.
- Next major: flip the default to
'auth-scoped', keeping 'ref-routed' available behind an explicit opt-in. This is the one item here that breaks real agency-hub adopters, which is why it's your decision rather than a patch.
From
codex-securitycandidate verification. Items 1 and 2 were reproduced against the builtdistand are straightforward code fixes. Item 3 is a deliberate documented default that needs a maintainer decision, not a patch.The common thread: three places where a warning stands in for a gate. In all three the repo already agrees with itself on the right shape —
createAdcpServerrefusesInMemoryStateStoreoutside{NODE_ENV=test, NODE_ENV=development}unlessADCP_DECISIONING_ALLOW_INMEMORY_STATE=1, andassertSandboxAccountfails closed onundefined. A state-mutating test surface deserves at least what an in-memory store gets.1. Test-controller bridge: the documented trust boundary does not hold (
candidate-cd887c5188044f75) — HighThe scan's framing ("when no account resolver is configured") understates this. Reproduced against a server that does wire
resolveAccount:Request was
list_creativeswith noaccountfield andcontext: { sandbox: true }.Cause:
create-adcp-server.ts:5007runs the resolver only whenparams.account != null, and theelse ifat:5047requiresresolveAccountFromAuth. Wire onlyresolveAccount— the primary documented path — andctx.accountstaysundefinedfor any request omitting the account ref, so the merge gate'sctx.account === undefined ||arm admits on a buyer-supplied marker alone. The#1784warning at:3745fires only when both resolvers are absent, so the misconfiguration that actually matters is silent. Same hole whenresolveAccountFromAuthreturnsnull(:5062, unknown principal).Impact: seeded-fixture disclosure into production read responses, plus responses falsely stamped
sandbox: true— a wire-honesty violation. Not by itself cross-tenant real-data disclosure, butmakeAutoSeedBridge(from-platform.ts:6535-6537) falls back to the buyer-claimedaccount_idwhenctx.accountis absent, so the fixture namespace is caller-selectable.Second, independent defect found while confirming: the gate compares
ctx.account.sandbox === trueliterally and never callsgetAccountMode. An account resolved as{ account_id: 'sb', mode: 'sandbox' }is refused (reproduced:mode:sandbox account -> creatives: []). So the bridge gate and the comply-controller gate implement the same trust boundary two different ways, and adopters who migrate tomodepersrc/lib/server/account-mode.tssilently lose their fixtures.Fix: require
ctx.account !== undefinedfor the merge; useisSandboxOrMockAccount(ctx.account)rather than raw.sandbox === true; fire the construction warning unless both resolvers are wired.2. Comply controller: env flags suppress the warning rather than gating anything (
candidate-ecd76e4fd9107db2) — Mediumsrc/lib/testing/comply-controller.ts:338-355,:685-778. With nosandboxGate, reproduced:Note what
ADCP_SANDBOX/ADCP_COMPLY_CONTROLLER_UNGATEDat:757-763actually do: they silence the warning. They gate nothing. That is the inverse of the pattern used correctly elsewhere in this repo.Partial refutation worth recording:
createAdcpServerFromPlatformbypassescontroller.registerand registers the tool itself behind a real resolved-account gate (from-platform.ts:2394-2620) — Phase 2 of #1435 is present, contradicting the stale "lands in a follow-up" comment atsrc/lib/server/account-mode.ts:11-13(worth deleting). So this is scoped to adopters callingcreateComplyController(...).register(server)directly via@adcp/sdk/testing.Two residual questions: the
mcp == nullbranch at:2503-2508deliberately falls back to the ungatedcontroller.register(server), and it's unclear whether A2A dispatch routes through the MCP-registered handler (and therefore the gate) or has its own path. Both worth a targeted check.Fix: make
sandboxGaterequired, or fail closed inregister()unlessNODE_ENV ∈ {test, development}and an explicit ack env is set.3.
createTenantStoreref-routeddefault — needs your call, not a patch (candidate-bd22564f2d8ecfb6)REFUTED as an undiscovered bug.
src/lib/server/decisioning/tenant-store.ts:217-235behaves exactly as the scan described, buttest/lib/create-tenant-store.test.js:109-117is a deliberate characterization test — its name is"default ('ref-routed') resolves a cross-tenant ref without checking the caller"— paired with four'auth-scoped'tests proving the fail-closed alternative works.CHANGELOG.md:259records thatrefAccesswas added specifically to close this, and that the docs were corrected because they "previously impliedresolvewas always an isolation gate; it is not." The module header, therefAccessJSDoc, and thecreateTenantStoreJSDoc all say so.What is real is a docs gap with real teeth.
skills/build-holdco-agent/SKILL.md— the file an agent-builder actually reads, whose front-matter promises "per-tenant data isolation" and whose § "Account resolution + tenant-isolation gate" presentscreateTenantStoreas the answer — never mentionsrefAccess. Its "What the helper guarantees" list coversupsertRow/syncGovernanceRow/ non-writability and omits theresolvecarve-out entirely.That matters more than "reads" implies:
accounts.resolveis the account path forcreate_media_buyandupdate_media_buytoo, so under the default a Meridian credential naming Pinnacle's operator resolves Pinnacle'sAccount(including itsctx_metadata) and every downstream handler runs scoped to Pinnacle — cross-tenant spend, not just cross-tenant reads. Theupsert/syncGovernancegate doesn't cover this; those only guardsync_accounts/sync_governance.Recommendation:
resolvecarve-out toskills/build-holdco-agent/SKILL.md, in the "What the helper guarantees" list.'auth-scoped', keeping'ref-routed'available behind an explicit opt-in. This is the one item here that breaks real agency-hub adopters, which is why it's your decision rather than a patch.