Skip to content

fix(runner): construct account in applyBrandInvariant when missing#644

Merged
bokelley merged 1 commit into
mainfrom
bokelley/issue-643
Apr 20, 2026
Merged

fix(runner): construct account in applyBrandInvariant when missing#644
bokelley merged 1 commit into
mainfrom
bokelley/issue-643

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

  • Closes applyBrandInvariant: reconsider silent rewrite — warn, opt-in, or remove? #643 — with a scope pivot. The original issue proposed removing or reworking applyBrandInvariant's silent-rewrite behavior; investigation during a related monkey-patch surfaced a higher-priority correctness bug in the primitive itself. Fixing that first.
  • Bug: applyBrandInvariant at `src/lib/testing/storyboard/runner.ts:1120` injects top-level `brand` and merges into any existing `account`. For tools whose schema declares `account` but not top-level `brand` — `get_media_buys`, `get_media_buy_delivery`, `list_creatives`, and several others — the request-builder produces no `account`, so the only thing the invariant sets is top-level `brand`. `SingleAgentClient.adaptRequestForServerVersion` (SingleAgentClient.ts:1129-1177) then strips the unrecognized top-level `brand` against the agent's declared tool schema, and the run-scoped brand is lost on the wire. Create→get→update→delete flows on brand-scoped sellers break because the get/update/delete calls land in the seller's `open:default` session instead of the tenant the create used.
  • Fix: when the request omits `account`, construct one via `resolveAccount(options)` so `account.brand` survives schema-aware stripping. Non-object `account` values (`null`, arrays) still pass through unchanged — the helper only synthesizes when the key is absent.

Why the existing test suite didn't catch this

The wire-level integration test (`runStoryboard: brand invariant on the wire`) uses `auth: 'none'` steps, which dispatch through `rawMcpProbe` and skip `adaptRequestForServerVersion` entirely. So the invariant appeared to work for the test harness but silently dropped brand on the wire in production (typed-client) paths.

Why the silent-rewrite question in #643 stays open (separately)

The programmatic tenant-routing use case the monkey-patch was serving is legitimate — the lint (adcontextprotocol/adcp#2527) covers storyboard authoring mistakes but not runtime SDK callers passing `options.brand`. Removing `applyBrandInvariant` would break that use case. The silent-rewrite concern (still real) can be revisited in a follow-up once this correctness fix lands.

Test plan

  • `test/lib/storyboard-brand-invariant.test.js` — flipped the assertion that pinned the bug ("does not add account when the request has no account" → "constructs account when the request omits it"); added `sandbox` passthrough coverage.
  • `npm test` — 4220 pass, 12 skipped, 0 fail.
  • Manual trace: confirmed `GetMediaBuysRequestSchema` (schemas.generated.ts:2153) declares `account` but not top-level `brand`, and `adaptRequestForServerVersion` strips by `this.cachedToolSchemas.get(taskType)` declared fields — so without the fix, top-level brand is silently dropped before the wire.

🤖 Generated with Claude Code

)

Before: the helper added only top-level `brand` and merged into any
existing `account`. When the request-builder produced no `account` (e.g.
`get_media_buys`, `get_media_buy_delivery`, `list_creatives`), the
outgoing request carried `brand` at the top level — which
`SingleAgentClient.adaptRequestForServerVersion` then stripped because
those tools' schemas declare `account`, not top-level `brand`. Net
effect: the run-scoped brand was lost on the wire for exactly the tools
most likely to read tenant-scoped state, breaking session isolation for
create→get→update→delete flows on brand-scoped sellers.

After: when the request omits `account`, construct one via
`resolveAccount(options)` so `account.brand` lands on the wire for
tools whose schema accepts `account`. Non-object `account` values
(`null`, arrays) still pass through unchanged — the helper only
synthesizes when the key is absent, not when it's explicitly malformed.

Flipped one test that pinned the buggy behavior; added coverage for
the `sandbox` passthrough. Existing wire-level integration test
(`runStoryboard: brand invariant on the wire`) continues to pass
because `list_creatives` with `auth: 'none'` goes through
`rawMcpProbe`, which skips the adapter stripping — so the old
behavior happened to work for the test harness but not production.
@bokelley bokelley force-pushed the bokelley/issue-643 branch from 95b2b14 to 04d79dc Compare April 20, 2026 12:22
@bokelley bokelley merged commit 7d50ecf into main Apr 20, 2026
8 checks passed
@bokelley bokelley deleted the bokelley/issue-643 branch April 20, 2026 13:22
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.

applyBrandInvariant: reconsider silent rewrite — warn, opt-in, or remove?

1 participant