Repository navigation
fix(adcp): resolve account_id-only refs, reject invalid refs, lookup-only reference resolvers (#547 bugs 1+3) - #554
Conversation
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean fail-closed fix with full test coverage.
seller.go:149 adds && acctRef.AccountID == "" so an account_id-only AccountReference now reaches the resolver instead of silently falling through to the public-pricing no-account path. This replaces fail-open behavior with fail-closed (unknown IDs yield ACCOUNT_NOT_FOUND), and the doc comment was updated to match. Seven table/unit tests cover value, pointer, unknown→NOT_FOUND, empty/nil/sandbox-only, brand-only, resolver-error, and nil-resolver paths.
No critical/high/medium findings. No schema/generated-type, TMP signing, TEE, or protocol-skill files touched; no exported-symbol/wire-shape changes. gated_paths is false and high_risk is false. No no-auto-approve team match. Rows 1–8 do not fire; falls through to row 9.
torbenbrodt
left a comment
There was a problem hiding this comment.
Thanks @sujanchalla0510, the guard change in resolveAccount is right and I'd like to see it merged. core/account-ref.json (3.2.2) is a oneOf whose first branch is account_id alone, so {"account_id": "acc_acme_001"}, the schema's own first example, names an account and must not fall through to the no-account path. On a local merge with today's main, four of the seven new tests fail without the change and all seven pass with it, and go vet / go test ./... in adcp/v3 are green.
The triage on #547 asked for the resolver contract to be reviewed alongside this guard change. Doing that turns up two things I'd fix in the same PR:
-
The documented resolver now takes the server down. The
Registerdoc comment (adcp/v3/seller.go:37) showsreturn db.FindAccount(ref.Brand.Domain, ref.Operator). With this PR,get_productswith"account": {"account_id": "acc_acme_001"}calls it withref.Brand == nil. go-sdk v1.5.0 has norecover(), and it runs the tool handler on its own goroutine, so the nil dereference ends the process. I reproduced that withcallRegisteredToolover the in-memory transport. An operator-only reference could already reach the resolver with a nil brand, but that payload is schema-invalid. Suggest changing the example to branch onref.AccountID, and adding toConfig.ResolveAccount: "Exactly one form is set:AccountID, orBrandandOperator;Brandis nil for an account_id reference." A release-note line would help anyone who copied the example. -
The ACCOUNT_NOT_FOUND an unknown account_id now gets points the buyer at the wrong fix. Today it is:
{"adcp_error":{"code":"ACCOUNT_NOT_FOUND","field":"account","message":"Account not found. Call sync_accounts first to register this brand and operator.","recovery":"correctable","suggestion":"Call sync_accounts with the brand and operator before making this request."}}enums/error-code.jsongives ACCOUNT_NOT_FOUND"recovery": "terminal"and the suggestion "for a natural key, provision through sync_accounts or omit account for public discovery; for an account_id, verify via list_accounts or contact seller". The message and suggestion could branch onacctRef.AccountID != "". Thecorrectableis #547's second bug, which #543 fixed inadcp/errors.gobut which is still inadcp/v3/errors.go'sdefaultRecovery.
Smaller, not blocking:
-
The repo's own resolvers don't read
AccountID.skills/build-seller-agent/SKILL.md,reference/seller-agentandreference/hello-sellerkey accounts asacct-<brand.domain>-<operator>, which becomesacct--for an account_id reference:reference/seller-agentreturns an active account for any account_id;hello-sellerstores oneacct--that every account_id caller shares;- the skill returns ACCOUNT_NOT_FOUND even for the
account_idits ownsync_accountsreturned.
In the skill and
reference/seller-agent, the map is keyed by that same id, so a lookup onref.AccountIDfirst would fix them. The reference-seller part overlaps #547's third bug. -
adcp/seller.go:407has the identical guard and is unchanged. #547's triage said any fix must touch both packages. If v1 is left out on purpose, a line in the description would settle it. -
Three test payloads are schema-invalid.
{}and{"sandbox": true}match neither branch, and abrandwithoutoperatorfails the second branch'srequired. The natural-key case would read better with the schema's own{"brand": {"domain": "acme-corp.com"}, "operator": "acme-corp.com"}. Also,{"account_id": ""}is schema-valid and still takes the no-account path.
Out of scope here: AccountReference still drops currency, operator_unit, timezone and brand.countries from the schema's fourth example on a round trip, before and after this PR. #564 tracks that.
Our interest: we're building a multi-tenant seller on adcp/v3 and refer to accounts by account_id, so we'd like this one in.
…dcontextprotocol#554 - Register doc examples (v1+v3) branch on ref.AccountID before dereferencing ref.Brand (account_id refs have Brand == nil; the documented resolver nil-derefed) - Config.ResolveAccount docs (v1+v3): exactly one form is set - AccountID, or Brand and Operator together - ACCOUNT_NOT_FOUND message/suggestion branch on the reference form (v1+v3): account_id -> verify via list_accounts; recovery in v3 defaultRecovery corrected correctable -> terminal per enums/error-code.json, matching v1 - v1 resolveAccount guard gains the AccountID check (triage on adcontextprotocol#547: both packages) - Reference resolvers (hello-seller, seller-agent, build-seller-agent skill) look up ref.AccountID first instead of deriving acct-- for account_id refs - Tests: natural-key case uses the schema example (brand+operator); empty-account_id case pinned; new NOT_FOUND guidance tests in v3 + v1 account_ref tests
|
Thanks for the thorough review, @torbenbrodt — addressed in 42fd0b6:
Left alone as you noted: the Local verification: |
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean pass, no blocking findings.
Delta commit 42fd0b6 ports the previously-approved v3 account_id-only resolveAccount fix into the v1 adcp package, adds ACCOUNT_NOT_FOUND guidance that branches on reference form (account_id→list_accounts, natural key→sync_accounts), and corrects the v3 recovery classification (correctable→terminal) to match the pinned enums/error-code.json and existing v1 behavior (adcp/errors.go:131). Reference resolvers now look up ref.AccountID before deriving the natural key.
Checked: no schema/types_gen/TMP/identity-agent/protocol-skill surface touched; no breaking wire change; changes consistent across packages and well-tested. No Critical/High/Medium findings.
Gate review: gated_paths is false and high_risk is false, so no hard path gate applies. No no-auto-approve team match. review_decision is REVIEW_REQUIRED, but with gated_paths false that does not force escalate. Decision table rows 1–8 do not fire → row 9 approve.
|
Thanks @sujanchalla0510, 42fd0b6 does what you describe. On a fresh merge with
From our side this is good to merge. Three small things, none blocking:
|
|
Thanks @torbenbrodt — glad this is good to merge from your side. Two follow-ups from your notes:
|
There was a problem hiding this comment.
Approving the code. 42fd0b6 resolves my review, and merged onto today's main (188cdcf) CI's core checks pass locally: generator tests, regeneration with no diff, lint.py --strict and the any-coverage gate in both modules, and go vet with go test -race for root, adcp, adcp/v3 and the reference agents. #547 tracks the remaining defaultRecovery mismatch and bug 3.
Not tracked yet:
- The new doc text says "Exactly one form is set", but the SDK doesn't enforce it. A reference with only
operatorstill reaches the resolver withBrandnil, and the documented example then dereferencesref.Brand.Domain. That was already true on main. - The example's
account_idbranch looks the account up without checking that the caller may act for it. It also ignoressandbox, which anaccount_idreference now carries through to the resolver.
|
@bokelley, two decisions for you before this merges:
Separately, adopters who copied the old example will crash on |
|
Thanks @torbenbrodt — approving review actioned. The 'not tracked yet' items are now recorded on #547 so they survive this PR:
Your two questions for @bokelley — landing bug 1 before bug 3, and whether |
|
@torbenbrodt @sujanchalla0510 — maintainer decisions, posted by Brian's coding agent at his direction:
This is an explicit security-related hold; a lazy-consensus timeout does not clear it. These decisions supersede the earlier “none of these block #554” tracking note on #547. |
|
Thanks @bokelley — decisions noted and accepted; recording them here so the hold state is unambiguous:
I'll implement on this branch and re-request review here when the combined behavior is in and tested; the hold stays until you or @torbenbrodt release it. |
…visioning on discovery (adcontextprotocol#547 bugs 1+3)
07b4005
|
Implemented per your decisions, @bokelley — new head
Local: @bokelley @torbenbrodt — re-requesting review; the hold stays until you release it. |
There was a problem hiding this comment.
Ladon verdict: Approve
Approve.
Verified against the decision table top-down:
- No critical/high/medium findings (reviewer reports a clean pass).
- gated_paths: false — row 2 does not apply.
- high_risk: false — rows 3 and 5 do not apply.
- No no-auto-approve team match — row 7 does not apply.
- Fewer than three medium findings (zero) — row 8 does not apply.
- Prior decision was
approve(not escalate) — row 6 does not apply.
Falls through to row 9 → approve.
What was checked: the PR implements the #554 security-hold items — a fail-closed validAccountReference guard in adcp/seller.go and adcp/v3/seller.go (returns INVALID_REQUEST before the resolver for references matching neither pinned account-ref form), reference sellers (hello-seller, seller-agent) converted to lookup-only with provisioning routed through sync_accounts, plus new unit/MCP tests across both SDK packages and both reference modules. Guard placement (after the empty-reference early return) preserves the no-account path for omitted/sandbox-only refs; oneOf branching and required BrandReference.Domain match the documented contract; the bug-3 fabrication-on-miss path is removed and pinned by tests. No schema/types_gen coherence, TMP signing, TEE boundary, protocol-skill, or wire-break concerns triggered.
Note (non-blocking, not a finding): the reviewer could not diff accountDomainPattern against the live schema because schemas are gitignored and network fetch was blocked. Worth a human eye if brand-ref's domain is unconstrained upstream, but this is speculative and not actionable here.
|
Follow-up on
Local verification with the exact CI script ( |
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean delta pass, no blocking findings.
This is a subsequent review; the prior decision was approve and the developer pushed additional commits. The reviewer's delta pass covered the single meaningful change: ~24 net added lines in reference/seller-agent/cmd/seller-agent/main.go that pre-provision two sandbox accounts at startup (the acme-outdoor storyboard buyer and the test.example fixture) so the media_buy_seller storyboard runs without a sync_accounts step.
Checked: both seeded keys match the new resolveAccount natural-key format (acct-{brand.domain}-{operator}) and are reachable via the account_id path; the #547 bug-3 fix is preserved (resolution fabricates nothing — only sync_accounts and this explicit startup seed create accounts). No schema/types_gen coherence concerns, no TMP signing changes, no identity-agent TEE changes, no protocol-managed skill edits, no wire-shape breaks.
No Critical/High/Medium findings. Decision table: rows 1–8 do not fire — no critical/high findings, gated_paths is false, high_risk is false, no medium findings, no no-auto-approve team match, fewer than three mediums. Note review_decision is REVIEW_REQUIRED, but with gated_paths false that imposes no approval gate here. Falls through to row 9 → approve.
torbenbrodt
left a comment
There was a problem hiding this comment.
Thanks @sujanchalla0510, this is a good implementation of @bokelley's 2026-10-19 decisions. On a fresh merge with today's main I checked:
- Regeneration (no diff),
lint.py --strict, the generator tests,go vetandgo test -raceinadcpandadcp/v3, andreference/seller-agentandreference/hello-selleras CI runs them, all pass. - Decision 2: I ran about 30 reference shapes through
resolveAccountand throughRegisterover an in-memory MCP client in both packages, and compared each tocore/account-ref.json(Draft 7).- Operator-only, brand-only and mixed
account_idplus natural key getINVALID_REQUEST(field:"account",correctable, matchingenums/error-code.json) and never reach a resolver. The only nil-Brand call is the documentedaccount_idform. - Nothing is stricter than the schema, and
accountDomainPatternis identical to the pinned pattern.
- Operator-only, brand-only and mixed
- Decision 1: both reference resolvers are lookup-only. I ran hello-seller end to end against the updated SDK (patched only for its unrelated
Budgetcompile error). Unknown id and key giveACCOUNT_NOT_FOUNDwith nothing created, andsync_accountsthen makes both forms resolve. - The storyboard: I reproduced 6
ACCOUNT_NOT_FOUNDfailures at 07b4005 and 83 steps passing at 9a05946. Dropping only thetest.exampleseed brings the failures back. Your explanation is right: the resolver stopped fabricating, and the SDK did not regress.
Findings, with a fix each:
- Authorization in the examples is a comment only. The code is
db.FindAccountByID(ref.AccountID). The v1 example namesadcp.PrincipalFromContext, which does not exist in packageadcp, and v1'sConfig.ResolveAccountdoc has no "MUST check the principal" paragraph. Please pass the principal in the example code, use the right symbol per package, and add SWAP notes to the reference resolvers about principal andref.Sandbox. - The #540 note is a comment on the release PR, so it will not reach the changelog, and it predates the implementation. It does not mention the new
INVALID_REQUEST, thataccount_idreferences now resolve, the v3 recovery change onACCOUNT_NOT_FOUND, or the reference sellers no longer auto-creating accounts. Please add aBREAKING CHANGE:footer or release-please override to the merge commit and refresh the note. - Title and description are stale. The top still says bug 3 stays open, and 9a05946 is not mentioned. The squash message inherits this.
- A few schema-invalid references still pass.
{account_id, sandbox:true}reaches the resolver (schema:additionalProperties:false; rejecting it is not a stricter rule). A supplied{}or{"sandbox":true}on the pointer-typed requests takes the public path, because nil and a pointer to a zero value are collapsed.{"account_id":""}is schema-valid but gets public discovery instead ofACCOUNT_NOT_FOUND.brand_idis not validated against^[a-z0-9_]+$. Please reject the first, treat a non-nil empty pointer deliberately, and say in the contract what is intentionally left out. create_media_buywith no account oraccount:{}still runs the handler with a nil account. hello-seller created a buy for both. That sidesteps the unknown-account refusal. This looks like a question for @bokelley; at minimum the contract should say so.- hello-seller's new
SyncAccountscreatesacct-unknown-for a schema-valid entry that has onlyaccount. seller-agent's handler does the same. Please skip or reject entries without brand and operator. - The hello-seller README sample
create_media_buynow returnsACCOUNT_NOT_FOUND. It needs async_accountsstep first. - Tests: the new lifecycle fixtures lack
billingandbuying_mode. Please useassertSchemaValidfrom #587, and add a test that ties the regexp and the case table to the bundle.
Smaller points: the reference modules do not compile against the in-repo v3 (ProductAllowedAction, Budget), so the real reference code is never exercised against the tree. That is worth its own issue. The seed ties seller-agent to the runner's default account, so it may be worth asking in adcp whether sellers must resolve unprovisioned accounts.
Our interest is unchanged: we call adcp/v3 by account_id and want this one in.
From the original reviewer's side, the substance of the hold looks met: bug 3 and bug 1 land together, and schema-invalid forms are rejected before the resolver. What I would still want is 1 and 2 (the authorization example and a real release note), and a decision on 4 and 5. I am leaving the release of the hold to @bokelley.
…n Register examples, brand_id validation, sync_accounts skips keyless entries - Register doc examples (both packages) now perform the authorization check in code: lookup, nil for unknown, PERMISSION_DENIED when the principal may not act for the account. v3 uses PrincipalFromContext; v1's Config.ResolveAccount doc gains the MUST paragraph and names that v1 adds no principal to ctx (the old v1 example cited the v3-only symbol). - Config.ResolveAccount contract (both packages): supplied-but-empty references and account_id:"" are deliberately indistinguishable from omission (documented, not rejected); with no account supplied, handlers run with a nil account (public discovery). - validAccountReference: a present brand_id must match core/brand-id.json (^[a-z0-9_]+$). Sandbox passthrough on the account_id arm stays — it is pinned by TestResolveAccountSandboxFormsStillResolve; flagged for a maintainer decision on adcontextprotocol#554 instead of flipped unilaterally. - AccountInput (both packages) gains the settings-update `account` key shape from sync-accounts-request.json. - Reference sellers (hello-seller, seller-agent, build-seller-agent skill): sync_accounts skips entries without a natural key instead of minting acct-unknown- accounts; resolver SWAP notes cover the principal check and ref.Sandbox; hello-seller README provisions via sync_accounts before its create_media_buy sample. - Tests: schema-edge forms (brand_id valid/invalid, sandbox forms) in both SDK packages; lifecycle fixtures carry billing (provisioning mode) and buying_mode (required by get-products-request); new hello-seller unit test and seller-agent MCP test pin the skip.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — refines prior-approved account-ref work with no blocking issues.
What I checked:
validAccountReferencenow correctly rejectsaccount_id+sandbox(schema account_id arm isadditionalProperties:false) and enforcescore/brand-id.json(^[a-z0-9_]+$).AccountInputgains theaccountsettings-update key shape matching theSyncAccountsRequesttwo-key-shape schema.AccountInputis a hand-writtenKNOWN_TYPESescape hatch, so notypes_gen.goregen is required — schema↔generated-type coherence preserved.- Reference
sync_accountshandlers now skip entries without a natural key instead of fabricatingacct-unknown-(fixes adcp-go#547 bug 3). Well-tested. - No TMP signing/verification, identity-agent TEE, or protocol-managed-skill changes. No wire-breaking exported-symbol changes requiring a conventional-commit marker.
Gates: No no-auto-approve team match; gated_paths: false; high_risk: false. Although review_decision is REVIEW_REQUIRED, row 2 only fires when gated_paths is true, which it is not here — so no escalation from that path.
Only one Low finding (dead/panic-prone defensive id check at hello-seller/main_test.go:256; count assertions carry the real coverage), which is omitted per severity rules. Falls through to row 9.
…t.account gap Brings the branch current with main (through ccc7a3c) so CI tests the real merge. The merge exposed one stale KNOWN_SCHEMA_GAPS entry: AccountInput.account was listed as missing_in_go (adcontextprotocol#564) and this PR adds the field, so the entry is deleted per lint.py's own rule. Verified in the merge tree: lint.py --strict (both modules), generator unittests, and go test -race -count=1 ./... in adcp and adcp/v3 all pass; reference module tests pass.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean subsequent pass, no findings.
This is a re-review after a rebase onto newer main. The prior decision on head df7c5b3 was approve with no findings; none recur. The 160-file delta is overwhelmingly rebase-imported code, and the PR-surface touches (SyncAccountsAuthorizer confirmed already present in base via identity/grants.go, and the lint.py KNOWN_SCHEMA_GAPS 'account' removal now that AccountInput carries the Account field) were verified correct. The PR's own #547 fix (resolveAccount/validAccountReference in adcp/ and adcp/v3/, ACCOUNT_NOT_FOUND→terminal in errors.go, the Account wire field, lookup-only reference resolvers, and tests) is unchanged from the approved SHA and consistent with the pinned schema and new base tooling.
Gate checks: no no-auto-approve team match; gated_paths false; high_risk false; no deleted sensitive files. No critical/high/medium findings. Rows 1–8 do not fire; falls through to row 9 → approve.
|
Thanks for the full verification pass, @torbenbrodt — all eight findings addressed in
The merge also verified clean locally before pushing: |
|
Thanks @sujanchalla0510. On 2b5a4bf I re-ran regeneration, One thread is open: |
… strings.HasPrefix
|
@torbenbrodt — the open thread is resolved in
Re-approval is yours when you're ready; the hold release and the account-less-calls question stay with @bokelley, as you said. |
torbenbrodt
left a comment
There was a problem hiding this comment.
Re-approving at adf7e97: the inverted guard is now strings.HasPrefix(id, "acct-unknown"), the description carries the BREAKING CHANGE: footer and no longer leaves the sandbox case open, and CI is green. From my side the hold's conditions are met; releasing it, and the account-less-calls question, stay with @bokelley.
Fixes #547 bugs 1 + 3 together (per @bokelley's 2026-10-19 security-hold decisions on this PR), plus the v3 ACCOUNT_NOT_FOUND recovery correction from bug 2.
Bug 1 — account_id-only references resolve. In
resolveAccount, anAccountReferencecarrying onlyaccount_id—{ "account_id": "acc_123" }, the schema's own first example incore/account-ref.json— hit theBrand == nil && Operator == ""early return and was treated as no account, so the handler ran with public pricing silently. Now any reference with a non-emptyaccount_idis handed toResolveAccount, and an unknown ID surfaces the standardACCOUNT_NOT_FOUNDpath.Hold decision 2 — schema-invalid references rejected before the resolver (both packages).
resolveAccountinadcpandadcp/v3validates a supplied reference against the pinnedcore/account-ref.jsonforms (validAccountReference):{account_id}alone, or the natural key{brand, operator}with both domains matching the schemas' domain pattern (brand-ref requiresdomain); a presentbrand_idmust matchcore/brand-id.json. Anything else — in particular an operator-only reference — returnsINVALID_REQUEST(recovery: correctable) and never reaches a resolver with a nil Brand. Deliberate decoded-value handling, documented in theConfig.ResolveAccountcontract: an omitted account (including a decoded reference carrying no identifier, a supplied-but-empty{}, andaccount_id: ""— indistinguishable from omission in the flattened Go struct) keeps the no-account path and does not invoke the resolver; with no account supplied, handlers run with a nil account (public discovery). Sandbox may accompany only the natural-key form, where it passes through as a modifier for the resolver to honor;{account_id, sandbox: true}is schema-invalid (the account_id arm isadditionalProperties: false) and is rejected withINVALID_REQUEST— pinned inTestResolveAccountSchemaEdgeFormsin both packages.Hold decision 1 — bug 3: reference resolvers are lookup-only.
reference/seller-agent's resolver (extracted tobackend.resolveAccount, matching thebuild-seller-agentskill example) andreference/hello-seller's resolver return(nil, nil)for unknown references — by account_id or natural key — and store nothing; the SDK answersACCOUNT_NOT_FOUND. Provisioning happens only through the provisioning path: seller-agent's existingSyncAccounts, and a newSyncAccountshandler for hello-seller (created/updated semantics mirroring seller-agent). seller-agent'smain()pre-provisions exactly the two fixture accounts the storyboard names (its Acme test-kit account and the runner default), since fabrication no longer resolves them.sync_accountshandlers in both reference sellers (and the skill example) skip entries without a natural key instead of mintingacct-unknown-accounts, andAccountInput(both packages) now decodes the settings-updateaccountkey shape.Register examples + authorization. Both
Registerdoc examples branch onref.AccountIDbefore dereferencingref.Brand(the previous example nil-derefed — a crash for anyone copying it) and now perform the principal check in code (PERMISSION_DENIEDwhen the principal may not act for the account); v1'sConfig.ResolveAccountdoc gains the MUST-check-the-principal paragraph and names that v1 adds no principal to ctx (PrincipalFromContextis v3-only — the old v1 example cited it anyway). Reference resolvers carry SWAP notes for the principal check andref.Sandbox.Wire change:
ACCOUNT_NOT_FOUNDmessage/suggestion branch on the reference form (unknown account_id → "Verify the account_id via list_accounts, or contact the seller."), and v3defaultRecoveryclassifiesACCOUNT_NOT_FOUNDasterminal(wascorrectable), matchingenums/error-code.jsonenumMetadata and the v1 module.Release note: the action-required note on #540 was refreshed 2026-10-25 with the full behavior list; the suggested
BREAKING CHANGE:footer for the squash commit is reproduced at the end of this description so the merge commit carries it.How it was tested:
TestRegisteredAccountLifecyclein both SDK packages drives the registered tools with a reference-style lookup-only resolver (unknown account_id →ACCOUNT_NOT_FOUND, unknown natural key →ACCOUNT_NOT_FOUNDwith nothing provisioned, operator-only →INVALID_REQUEST, thensync_accountsprovisions andget_productssucceeds by natural key and by returned account_id; fixtures carrybillingandbuying_modeper the pinned schemas). Schema-edge tests pin brand_id valid/invalid and the sandbox forms in both packages. New hello-seller unit test + seller-agent MCP test pin the sync_accounts skip. Reference sellers keep their pinnedadcp/v3 v3.0.0(they don't compile against in-repo v3 for unrelated 3.3-dev reasons — tracked in #597), so their committed tests pin the resolver/skip contracts directly. Local:go test ./...+go vetgreen inadcp,adcp/v3,reference/hello-seller,reference/seller-agent.Not in this PR: the rest of #547 — bug 2's remaining per-code
defaultRecoverydivergence from enumMetadata (owned by @torbenbrodt per his 2026-10-20 comment on #547) and theAccountReferenceround-trip field drops tracked by #564.Suggested squash-commit footer (from the #540 release note):