Skip to content

feat(server): construction-time warn when testController lacks resolveAccount (#1784)#1788

Merged
bokelley merged 3 commits into
mainfrom
bokelley/bridge-warn-no-resolve-account
May 16, 2026
Merged

feat(server): construction-time warn when testController lacks resolveAccount (#1784)#1788
bokelley merged 3 commits into
mainfrom
bokelley/bridge-warn-no-resolve-account

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

Closes #1784. When createAdcpServer is called with testController set but neither resolveAccount nor resolveAccountFromAuth configured, emit a one-shot logger.warn at construction.

The dispatch-time sandbox gate (src/lib/server/create-adcp-server.ts:3973-3981) admits requests where ctx.account === undefined. Without an account resolver, ctx.account is always undefined, so the only remaining check is the buyer-supplied account.sandbox / context.sandbox marker — caller-controlled, not a trust boundary. This is the gap the security review on #1786 flagged as M1; the JSDoc updates in #1786 and #1787 document it, but JSDoc doesn't fire when adopters misconfigure. This PR adds the runtime equivalent.

Why a warn, not a throw

Storyboard-runner deployments without account scoping are a legitimate and intentional configuration — the runner drives state directly, no buyer authentication path needed. Failing construction would break that case. The warn is the right severity: visible in any logging setup, ignorable for runners, actionable for adopters.

OAuth-passthrough handling

The check gates on resolveAccountFromAuth as well as resolveAccount. OAuth-passthrough setups (createOAuthPassthroughResolver, the canonical Shape-B adopter path from the v6.7 → 6.9 migration) only configure the latter — either resolver populates ctx.account at dispatch and gives the gate its account-side teeth, so warning on resolveAccountFromAuth-only setups would be a spurious noise floor.

JSDoc cross-link

TestControllerBridge (in test-controller-bridge.ts) now mentions the construction-time warn so adopters know what triggers it and how to silence it.

Test plan

  • Five regression tests in test/lib/seed-per-tool-wiring.test.js:
    • warn fires once when testController + neither resolver
    • no warn when testController is omitted
    • no warn when resolveAccount is configured
    • no warn when resolveAccountFromAuth is configured (OAuth passthrough)
    • warn fires exactly once across construction + multiple dispatches (not per-request)
  • All 143 existing tests in the file pass unchanged
  • tsc --noEmit clean
  • prettier --check clean

🤖 Generated with Claude Code

bokelley and others added 2 commits May 16, 2026 09:32
…eAccount (#1784)

When `createAdcpServer` is called with `testController` set but neither
`resolveAccount` nor `resolveAccountFromAuth` configured, emit a one-shot
logger.warn at construction. The dispatch-time sandbox gate admits requests
where ctx.account is undefined, so without a resolver the only remaining
check is buyer-supplied account.sandbox / context.sandbox — caller-controlled,
not a trust boundary. The warn makes that silent failure mode loud once.

Deliberately a warn, not a throw: storyboard-runner deployments without
account scoping are legitimate; the warn message tells them to ignore it.
Production bindings get the actionable signal.

The check gates on resolveAccountFromAuth as well as resolveAccount, so
OAuth-passthrough setups (createOAuthPassthroughResolver, the canonical
Shape-B path) don't get a spurious warn.

Five regression tests cover: presence/absence of testController, presence
of either resolver, and that the warn fires once across construction + N
requests (not per-dispatch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…trimmed message

DX review caught that the default `logger` is `noopLogger`, which swallows
.warn — defeating the purpose of the warn for the day-one misconfig case.

Switch to dual-emit:
- `process.emitWarning(message, { type: 'AdcpServerConfigWarning',
  code: 'ADCP_BRIDGE_NO_RESOLVER' })` writes to stderr by default so the
  signal is visible without configured logging
- `logger.warn(message)` still fires so adopters with logging pipelines
  see it through their normal channel

Storyboard runners that knowingly run without account scoping can silence
via `node --no-warnings=ADCP_BRIDGE_NO_RESOLVER`.

Message changes:
- Lead with the action ("configure resolveAccount...")
- Trim trust-model paragraph; JSDoc carries the long form
- Prefix with [adcp/createAdcpServer] for parity with sibling warns
- Add public docs URL so coding agents reading the warn from a log can
  fetch the framing without an editor

New test: synchronous spy on process.emitWarning (avoids event-loop
flush timing dependencies). Verifies stderr channel fires with default
logger.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley
Copy link
Copy Markdown
Contributor Author

Expert review pass — three reviewers

Ran three parallel expert reviews (code-reviewer, security, dx). Synthesis:

Convergent feedback addressed in commit e83bacd:

  1. DX (substantive) — default logger is noopLogger, which swallows .warn. The day-one misconfig case (no logger wired yet) is exactly the population this warn is targeting, and they'd see nothing. Fix: dual-emit via process.emitWarning(message, { type: 'AdcpServerConfigWarning', code: 'ADCP_BRIDGE_NO_RESOLVER' }) (stderr by default, idiomatic Node, silenceable via --no-warnings=ADCP_BRIDGE_NO_RESOLVER) AND logger.warn(message) for adopters with configured logging.
  2. DX (message) — lead with the action, trim the trust-model paragraph (JSDoc carries the long form), add public docs URL for coding agents reading the warn from a log.
  3. Code review (cosmetic) — prefix message with [adcp/createAdcpServer] for parity with sibling warns at lines 3271/3304.

Security — agent stalled but partial output confirmed the M1 finding from #1786 is closed by this PR: the warn correctly targets the legacy createAdcpServer surface (where the misconfig path exists); createAdcpServerFromPlatform already enforces the sandbox-authority gate via Phase 2 of #1435.

New test for the stderr channel uses a synchronous spy on process.emitWarning itself (the original async process.on('warning', ...) approach had event-loop-flush timing dependencies that flaked under the node test runner).

Verdict from each reviewer: ship after the DX fix lands. Done in e83bacd.

CI re-running.

@bokelley
Copy link
Copy Markdown
Contributor Author

Second review pass — both clear

Code-reviewer and security-reviewer re-fired on commit `e83bacd` (the DX fold-in).

Security: M1 fully closed by the dual-emit. Stderr content is safe (no tenant data, no auth, no payload). `--no-warnings=ADCP_BRIDGE_NO_RESOLVER` silencer is operator-scoped, not buyer-controllable. Per-construction emit cardinality is acceptable; process-level dedup would actually hide per-tenant signal in multi-tenant deployments. Ship.

Code: No blockers. Two nits folded in commit 6cfff71:

Other code-review notes (all non-blocking):

  • Spy-and-restore pattern in the new test is safe under node:test's default sequential `it` execution within a file; sibling `agent-class-deprecation.test.js` patches `process.emitWarning` too but runs in a separate worker.
  • Dual-emit doesn't double-count (the channels don't cross-flow).
  • Sibling-warn prefix `[adcp/createAdcpServer]` matches existing usages at lines 3280/3313.

CI green → admin merging.

@bokelley bokelley merged commit 7c88567 into main May 16, 2026
10 checks passed
@bokelley bokelley deleted the bokelley/bridge-warn-no-resolve-account branch May 16, 2026 14:01
bokelley added a commit that referenced this pull request May 16, 2026
…n model (#1795)

* docs: align bridge framing with revised single-dimension certification model

Maintainer walked back the two-badge proxy-vs-state-local split from #1782
and proposed a single-dimension framing (Wire Conformance / Live Integration
Verified) where every seller faces the same verifiability gap, and the bridge
is one of two mechanisms for closing the seed→read loop, not a special path
for one seller class.

JSDoc on AdcpServerConfig.testController:
- Drops "only upstream-proxy sellers" as primary framing
- "Pick by where your read handlers fetch from, not by seller class"
- "Either path earns wire-conformance credit; it is *not* a separate
  certification category"

skills/build-seller-agent/SKILL.md:
- New "Test surfaces" section frames the verifiability gap as universal
- Names the two implementations (state-local store vs TestControllerBridge)
- Hedges on certification names while #1782 settles

No SDK behavior change. Marker contract, trust-boundary docs, and dual-emit
warn from #1786/#1787/#1788 all stay as-is — they describe mechanism without
committing to certification taxonomy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(skill): fold docs-expert feedback — tighten Test surfaces section + in-repo JSDoc link

---------

Co-authored-by: Claude Opus 4.7 (1M context) <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.

Bridge: construction-time debug when testController is registered without resolveAccount

1 participant