Skip to content

feat(addie): server-side Socket Mode for adopter conformance assistance#4007

Merged
bokelley merged 2 commits into
mainfrom
bokelley/conformance-socket-mode-server
May 4, 2026
Merged

feat(addie): server-side Socket Mode for adopter conformance assistance#4007
bokelley merged 2 commits into
mainfrom
bokelley/conformance-socket-mode-server

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 3, 2026

Summary

PR #1 of three for Addie Socket Mode — an outbound-WebSocket conformance/live-assistance channel that lets adopters' dev/staging MCP servers connect to Addie. Once connected, Addie can run storyboards against them and surface bugs in chat. No public DNS, no ngrok, no inbound exposure.

This PR lands the server-side plumbing only. Storyboard runner adaptation is PR #2, Addie chat tool integration is PR #3 (feature-flagged).

Architecture

Reverse RPC at the TCP level only. MCP is symmetric (JSON-RPC 2.0, request/response with correlation IDs), so swapping HTTP for WebSocket and flipping connection direction gives Addie a normal MCP Client against the adopter's existing MCP Server. The MCP SDK's Transport interface does the heavy lifting; we add ~250 LOC to wire up server-side WS, token issuance, and session lifecycle.

adopter dev box                    Addie (this server)
+-------------+   outbound WS      +-----------------+
| MCP server  |  <---------------  | MCP client     |
| + conform.  |  ---------------> | + WS server    |
| client      |                   | transport      |
+-------------+                   +-----------------+

Tenant scoping is enforced server-side at auth via the JWT sub claim — never in the URL. Single shared endpoint by design, Slack Socket Mode pattern. See adcontextprotocol/adcp#3991 for the full strategic context.

What's in this PR

  • server/src/conformance/ws-server-transport.ts — wraps ws.WebSocket in MCP Transport
  • server/src/conformance/session-store.ts — in-memory map keyed by WorkOS org_id
  • server/src/conformance/token.ts — HS256 JWT issuance/verification, scoped conformance, 1h TTL
  • server/src/conformance/ws-route.ts — WS upgrade handler at /conformance/connect, ping/pong heartbeat
  • server/src/conformance/token-route.tsPOST /api/conformance/token + dev _debug endpoint
  • server/src/http.ts — wires routes, attaches WS to listening server, closes sockets on shutdown
  • examples/conformance-client/ — prototype adopter library (will move to adcp-client repo before publish)
  • 5 test files: transport, session store, token, token route, end-to-end (real WebSocket round-trip with tools/list + tools/call against an in-process adopter MCP server). 29 tests, all green.

Privacy posture

Channel is dev/staging only — same constraint as comply_test_controller per #3986. Adopter explicitly opens the socket each session, can disconnect at any time, and what Addie sees stays in their Addie context. No persistent tunnel, no production exposure.

Configuration

Two new env vars:

  • CONFORMANCE_JWT_SECRET (required when feature is used) — HS256 signing key
  • CONFORMANCE_WS_PUBLIC_URL (optional) — overrides the auto-derived wss:// URL returned by the token endpoint

No env changes required for builds that don't use the feature; the routes exist but the WS handler 401s without a valid token, and the token endpoint 500s with a clear error message if the secret is unset.

Sequencing

  1. This PR — server transport + token issuance + prototype adopter (~600 LOC)
  2. PR Remove targeting_template and add pluggable product catalog system #2 — storyboard runner adapter + integration test (depends on @adcp/sdk upstream patch for AgentClient.fromMcpClient)
  3. PR Improve documentation framework and remove premature certification references #3 — Addie chat tools (issue_conformance_token, run_conformance_against_my_agent), gated on CONFORMANCE_SOCKET_ENABLED=1

Test plan

  • CI green
  • Unit: 29 conformance tests pass (transport lifecycle, session store, token issuance, token route, end-to-end with real WS)
  • Manual: start dev server, POST /api/conformance/token with WorkOS session, run examples/conformance-client/demo.ts with token, verify GET /api/conformance/_debug shows the connected session

🤖 Generated with Claude Code

Outbound-WebSocket channel that lets adopter dev/staging MCP servers
connect to Addie. Adopter installs a small library, runs it locally,
points it at Addie. Addie sees the connected session and (in PR #2)
runs storyboards against it conversationally — surfacing bugs in chat,
no public DNS, no inbound exposure.

Reverse RPC at the TCP level only — the MCP protocol is symmetric, so
a server-side Transport over an inbound WebSocket plus an MCP Client
that consumes it gives Addie a normal MCP client interface against the
adopter's existing MCP Server.

What lands here:

- server/src/conformance/ws-server-transport.ts — wraps a ws.WebSocket
  in the MCP Transport interface. Strict JSON-RPC parsing, idempotent
  close, error-without-disconnect on malformed frames.
- server/src/conformance/session-store.ts — in-memory map keyed by
  WorkOS org_id. Last-writer-wins on duplicate connects, auto-evicts
  on transport close.
- server/src/conformance/token.ts — issues/verifies HS256 JWTs scoped
  to "conformance", 1h TTL, signed with CONFORMANCE_JWT_SECRET. Distinct
  from WorkOS-signed tokens; these are first-party Addie tokens.
- server/src/conformance/ws-route.ts — attaches the WS upgrade handler
  to the existing http.Server. Single shared endpoint at
  /conformance/connect; tenant scoping enforced server-side via JWT
  sub claim, never in URL. Heartbeat ping/pong.
- server/src/conformance/token-route.ts — POST /api/conformance/token
  authenticated via existing requireAuth + resolveCallerOrgId.
  GET /api/conformance/_debug for dev introspection.
- server/src/http.ts — wires both routes and shutdown closes sockets
  cleanly.
- examples/conformance-client/ — prototype adopter library. Will move
  to adcp-client repo as @adcp/conformance-client before publish.
- 5 unit + integration tests covering transport, session store, token,
  token route, and end-to-end (real WebSocket round-trip with
  tools/list + tools/call against an in-process adopter).

PR #2 adds the storyboard runner adapter (depends on upstream
@adcp/sdk patch for AgentClient.fromMcpClient). PR #3 adds the Addie
chat tools, feature-flagged on CONFORMANCE_SOCKET_ENABLED.

Closes nothing; tracks under #3991.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit to adcontextprotocol/adcp-client that referenced this pull request May 3, 2026
* feat(server): ConformanceClient Socket Mode primitive

Outbound-WebSocket bridge so adopter dev/staging MCP servers can be
reached by a remote AdCP runner without public DNS, ngrok, or inbound
firewall exposure. Slack Socket Mode pattern.

Three-line integration:

    import { ConformanceClient } from '@adcp/sdk/server';
    import { mcpServer } from './my-mcp-server';

    const client = new ConformanceClient({
      url: 'wss://addie.agenticadvertising.org/conformance/connect',
      token: process.env.ADCP_CONFORMANCE_TOKEN!,
      server: mcpServer,
    });
    await client.start();

Reverse RPC at the TCP level only — MCP is symmetric (JSON-RPC 2.0,
request/response with correlation IDs), so swapping HTTP for WebSocket
and flipping connection direction gives the runner a normal MCP Client
against the adopter's existing MCP Server. The SDK already exposes
`AgentClient.fromMCPClient()` (line 248 of core/AgentClient.ts) so
runners can wrap that MCP Client for the storyboard pipeline.

Dev/staging only by design — production deployments must not expose
`comply_test_controller` on any surface, including this channel
(adcontextprotocol/adcp#3986). The ConformanceClient JSDoc names the
constraint and points at the spec rule.

What lands:

- src/lib/server/socket-mode/ws-transport.ts — WebSocket-backed MCP
  Transport. Strict JSON-RPC parsing via JSONRPCMessageSchema, rejects
  binary frames, idempotent close, error-without-disconnect on
  malformed frames.
- src/lib/server/socket-mode/conformance-client.ts — high-level
  adopter-facing class. Status callback, exponential-backoff reconnect
  capped at 30s, opt-out via close().
- src/lib/server/socket-mode/index.ts — module exports.
- src/lib/server/index.ts — re-exports ConformanceClient + types from
  the @adcp/sdk/server entry, alongside the existing server primitives.
- src/lib/server/socket-mode/conformance-client.test.ts — 3 tests
  covering full WebSocket round-trip with tools/list + tools/call,
  status transitions, and error path on unreachable URL.
- ws ^8 added as a regular dep; @types/ws as devDep.

Server-side terminator (the runner accepting these connections) is the
adopter's choice — Addie ships one in adcontextprotocol/adcp#4007 and
will host the canonical endpoint at addie.agenticadvertising.org. The
transport is neutral; any orchestrator that speaks the same handshake
could host the other end.

cc @bokelley

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

* fix(deps): move @types/ws to dependencies so adopter d.ts type-checks

The new socket-mode/ws-transport.ts and conformance-client.ts both
declare private fields typed against `WebSocket` from `ws`. That type
leaks into the published .d.ts; without `@types/ws` in dependencies,
adopters get TS7016 ("Could not find a declaration file for module
'ws'") on a clean install.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The in-tree prototype at examples/conformance-client/src/ shipped
upstream as ConformanceClient in @adcp/sdk 6.9 (adcp-client#1506).
Replace the local copy with an import from @adcp/sdk/server so
adopters who already install the SDK get the primitive for free
and we avoid the version-skew risk of two parallel implementations.

- Bump @adcp/sdk dep from ^6.7.0 to ^6.9.0.
- Delete examples/conformance-client/src/ (170 LOC of duplicate
  ConformanceClient + WebSocketTransport).
- Rewrite demo.ts to import from @adcp/sdk/server.
- Update README to point at the published package and drop the
  "lives here while we prototype" framing.

The server-side ConformanceWSServerTransport stays in
server/src/conformance/ — that's the runner half of the channel
and lives here because the runner is Addie. Only the adopter-side
primitive moved upstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 4, 2026
PR #2 of 3 for Addie Socket Mode. PR #1 added the server-side WS
plumbing; this PR adds the runner that lets Addie execute a
storyboard against a connected adopter session.

The adapter is small (~80 LOC) because the SDK already gives us
everything we need:

- `AgentClient.fromMCPClient(mcpClient)` is the in-process injection
  factory the SDK has shipped since 6.7. It wraps any pre-connected
  MCP `Client` into an AgentClient that the storyboard runner
  consumes via `_client` (the same path `comply()` uses internally).
- `runStoryboard(agentUrl, storyboard, options)` is the existing
  runner. We pass a placeholder `adcp-conformance-socket://<orgId>`
  URL plus `_client: agentClient` and the runner happily ignores
  the URL and dispatches via the injected client.

Net result: zero changes to `server/src/services/storyboards.ts`,
`server/src/addie/services/compliance-testing.ts`, or
`server/src/addie/jobs/compliance-heartbeat.ts`. The conformance
runner is a separate function picking storyboards from the same
registry.

Files:

- server/src/conformance/run-storyboard-via-ws.ts — the adapter.
  Throws `ConformanceNotConnectedError` when no live session,
  `StoryboardNotFoundError` when storyboard id is unknown.
- server/src/conformance/index.ts — re-export.
- server/tests/unit/conformance-run-storyboard.test.ts — 3 tests
  covering both error paths and the success path. The success
  test stands up a real WebSocket connection (proving the wiring
  is end-to-end) but mocks `runStoryboard` itself so we can
  inspect the AgentClient and options the adapter passes
  through, without needing a real sales agent + test kits to
  actually run a storyboard.

PR #3 adds the Addie chat tools (`issue_conformance_token`,
`run_conformance_against_my_agent`) that consume this adapter,
gated behind `CONFORMANCE_SOCKET_ENABLED=1`.

Stacked on `bokelley/conformance-socket-mode-server` (PR #4007).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 4, 2026
…script

Adds docs/building/addie-socket-mode.mdx — an adopter-facing walkthrough
of the conformance Socket Mode channel from PRs #4007/#4051/#4054. Covers
when to use it (vs the public-endpoint AAO heartbeat), prerequisites, the
five-minute setup, what Addie can do once connected, privacy/safety
posture, and troubleshooting for the common failure modes I hit while
smoke-testing.

Mounted in the "Build" sidebar between validate-your-agent and grading
so it lives next to the other agent-development tools rather than
buried in implementation reference. Cross-linked from the existing
get-test-ready and aao-verified pages where appropriate via inline
references in the body.

Also adds scripts/smoke-conformance.ts — the end-to-end smoke I ran
against the stack before writing the doc. Spins up the server-side
conformance routes, connects a real adopter via @adcp/sdk 6.9
ConformanceClient, exercises both Addie chat tools (issue_token + run
storyboard). Stays as a runnable artifact for future regression checks
and as a worked example for anyone who wants to see the full flow in
~150 LOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 3caa57c into main May 4, 2026
6 checks passed
@bokelley bokelley deleted the bokelley/conformance-socket-mode-server branch May 4, 2026 14:50
bokelley added a commit that referenced this pull request May 4, 2026
PR #2 of 3 for Addie Socket Mode. PR #1 added the server-side WS
plumbing; this PR adds the runner that lets Addie execute a
storyboard against a connected adopter session.

The adapter is small (~80 LOC) because the SDK already gives us
everything we need:

- `AgentClient.fromMCPClient(mcpClient)` is the in-process injection
  factory the SDK has shipped since 6.7. It wraps any pre-connected
  MCP `Client` into an AgentClient that the storyboard runner
  consumes via `_client` (the same path `comply()` uses internally).
- `runStoryboard(agentUrl, storyboard, options)` is the existing
  runner. We pass a placeholder `adcp-conformance-socket://<orgId>`
  URL plus `_client: agentClient` and the runner happily ignores
  the URL and dispatches via the injected client.

Net result: zero changes to `server/src/services/storyboards.ts`,
`server/src/addie/services/compliance-testing.ts`, or
`server/src/addie/jobs/compliance-heartbeat.ts`. The conformance
runner is a separate function picking storyboards from the same
registry.

Files:

- server/src/conformance/run-storyboard-via-ws.ts — the adapter.
  Throws `ConformanceNotConnectedError` when no live session,
  `StoryboardNotFoundError` when storyboard id is unknown.
- server/src/conformance/index.ts — re-export.
- server/tests/unit/conformance-run-storyboard.test.ts — 3 tests
  covering both error paths and the success path. The success
  test stands up a real WebSocket connection (proving the wiring
  is end-to-end) but mocks `runStoryboard` itself so we can
  inspect the AgentClient and options the adapter passes
  through, without needing a real sales agent + test kits to
  actually run a storyboard.

PR #3 adds the Addie chat tools (`issue_conformance_token`,
`run_conformance_against_my_agent`) that consume this adapter,
gated behind `CONFORMANCE_SOCKET_ENABLED=1`.

Stacked on `bokelley/conformance-socket-mode-server` (PR #4007).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 4, 2026
PR #3 of 3 for Addie Socket Mode. PRs #1+#2 added the server WS
plumbing and the storyboard runner adapter; this PR exposes them
to the user via two Addie chat tools:

- `issue_conformance_token` — mints a fresh JWT bound to the
  caller's WorkOS organization, returns shell exports + a copy-
  paste @adcp/sdk/server ConformanceClient snippet so the adopter
  can wire it into their dev environment in under a minute.

- `run_conformance_against_my_agent` — runs a storyboard against
  the adopter MCP server connected to the live conformance
  session for the caller's org. Renders phase/step pass/fail/
  skipped status as markdown with trimmed error text on failures.

Both tools are bound to a WorkOS organization. Anonymous chats
get a not-mapped hint; orgs with no live conformance session get
a connect-the-client hint with the exact snippet they need.

Wiring:

- server/src/addie/mcp/conformance-tools.ts — tool definitions +
  handler factory `createConformanceToolHandlers(memberContext)`.
- server/src/addie/bolt-app.ts — registration block gated on
  `CONFORMANCE_SOCKET_ENABLED=1`. Server-side WS plumbing remains
  always-wired (the chat surface is what the flag toggles).
- server/src/addie/tool-sets.ts — new `agent_conformance` toolset
  for the router.
- server/tests/unit/conformance-addie-tools.test.ts — 8 unit
  tests covering org-binding enforcement, missing-secret error,
  not-connected hint, missing-id message, passing+failing
  storyboard markdown rendering.

Stacked on `bokelley/conformance-storyboard-runner` (PR #4051),
which is itself stacked on `bokelley/conformance-socket-mode-server`
(PR #4007).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 4, 2026
…script

Adds docs/building/addie-socket-mode.mdx — an adopter-facing walkthrough
of the conformance Socket Mode channel from PRs #4007/#4051/#4054. Covers
when to use it (vs the public-endpoint AAO heartbeat), prerequisites, the
five-minute setup, what Addie can do once connected, privacy/safety
posture, and troubleshooting for the common failure modes I hit while
smoke-testing.

Mounted in the "Build" sidebar between validate-your-agent and grading
so it lives next to the other agent-development tools rather than
buried in implementation reference. Cross-linked from the existing
get-test-ready and aao-verified pages where appropriate via inline
references in the body.

Also adds scripts/smoke-conformance.ts — the end-to-end smoke I ran
against the stack before writing the doc. Spins up the server-side
conformance routes, connects a real adopter via @adcp/sdk 6.9
ConformanceClient, exercises both Addie chat tools (issue_token + run
storyboard). Stays as a runnable artifact for future regression checks
and as a worked example for anyone who wants to see the full flow in
~150 LOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 4, 2026
… tools (#4082)

* feat(addie): conformance Socket Mode storyboard runner adapter

PR #2 of 3 for Addie Socket Mode. PR #1 added the server-side WS
plumbing; this PR adds the runner that lets Addie execute a
storyboard against a connected adopter session.

The adapter is small (~80 LOC) because the SDK already gives us
everything we need:

- `AgentClient.fromMCPClient(mcpClient)` is the in-process injection
  factory the SDK has shipped since 6.7. It wraps any pre-connected
  MCP `Client` into an AgentClient that the storyboard runner
  consumes via `_client` (the same path `comply()` uses internally).
- `runStoryboard(agentUrl, storyboard, options)` is the existing
  runner. We pass a placeholder `adcp-conformance-socket://<orgId>`
  URL plus `_client: agentClient` and the runner happily ignores
  the URL and dispatches via the injected client.

Net result: zero changes to `server/src/services/storyboards.ts`,
`server/src/addie/services/compliance-testing.ts`, or
`server/src/addie/jobs/compliance-heartbeat.ts`. The conformance
runner is a separate function picking storyboards from the same
registry.

Files:

- server/src/conformance/run-storyboard-via-ws.ts — the adapter.
  Throws `ConformanceNotConnectedError` when no live session,
  `StoryboardNotFoundError` when storyboard id is unknown.
- server/src/conformance/index.ts — re-export.
- server/tests/unit/conformance-run-storyboard.test.ts — 3 tests
  covering both error paths and the success path. The success
  test stands up a real WebSocket connection (proving the wiring
  is end-to-end) but mocks `runStoryboard` itself so we can
  inspect the AgentClient and options the adapter passes
  through, without needing a real sales agent + test kits to
  actually run a storyboard.

PR #3 adds the Addie chat tools (`issue_conformance_token`,
`run_conformance_against_my_agent`) that consume this adapter,
gated behind `CONFORMANCE_SOCKET_ENABLED=1`.

Stacked on `bokelley/conformance-socket-mode-server` (PR #4007).

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

* feat(addie): conformance Socket Mode chat tools (feature-flagged)

PR #3 of 3 for Addie Socket Mode. PRs #1+#2 added the server WS
plumbing and the storyboard runner adapter; this PR exposes them
to the user via two Addie chat tools:

- `issue_conformance_token` — mints a fresh JWT bound to the
  caller's WorkOS organization, returns shell exports + a copy-
  paste @adcp/sdk/server ConformanceClient snippet so the adopter
  can wire it into their dev environment in under a minute.

- `run_conformance_against_my_agent` — runs a storyboard against
  the adopter MCP server connected to the live conformance
  session for the caller's org. Renders phase/step pass/fail/
  skipped status as markdown with trimmed error text on failures.

Both tools are bound to a WorkOS organization. Anonymous chats
get a not-mapped hint; orgs with no live conformance session get
a connect-the-client hint with the exact snippet they need.

Wiring:

- server/src/addie/mcp/conformance-tools.ts — tool definitions +
  handler factory `createConformanceToolHandlers(memberContext)`.
- server/src/addie/bolt-app.ts — registration block gated on
  `CONFORMANCE_SOCKET_ENABLED=1`. Server-side WS plumbing remains
  always-wired (the chat surface is what the flag toggles).
- server/src/addie/tool-sets.ts — new `agent_conformance` toolset
  for the router.
- server/tests/unit/conformance-addie-tools.test.ts — 8 unit
  tests covering org-binding enforcement, missing-secret error,
  not-connected hint, missing-id message, passing+failing
  storyboard markdown rendering.

Stacked on `bokelley/conformance-storyboard-runner` (PR #4051),
which is itself stacked on `bokelley/conformance-socket-mode-server`
(PR #4007).

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

* docs(addie): user-facing Pair-with-Addie (Socket Mode) guide + smoke script

Adds docs/building/addie-socket-mode.mdx — an adopter-facing walkthrough
of the conformance Socket Mode channel from PRs #4007/#4051/#4054. Covers
when to use it (vs the public-endpoint AAO heartbeat), prerequisites, the
five-minute setup, what Addie can do once connected, privacy/safety
posture, and troubleshooting for the common failure modes I hit while
smoke-testing.

Mounted in the "Build" sidebar between validate-your-agent and grading
so it lives next to the other agent-development tools rather than
buried in implementation reference. Cross-linked from the existing
get-test-ready and aao-verified pages where appropriate via inline
references in the body.

Also adds scripts/smoke-conformance.ts — the end-to-end smoke I ran
against the stack before writing the doc. Spins up the server-side
conformance routes, connects a real adopter via @adcp/sdk 6.9
ConformanceClient, exercises both Addie chat tools (issue_token + run
storyboard). Stays as a runnable artifact for future regression checks
and as a worked example for anyone who wants to see the full flow in
~150 LOC.

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

* feat(conformance): dev-only run-storyboard trigger + training-agent smoke

Adds a dev-only POST /api/conformance/_debug/run-storyboard endpoint
that triggers runStoryboardViaConformanceSocket(orgId, storyboardId)
against a live conformance session. Gated on NODE_ENV !== 'production'
alongside the existing /_debug session-list endpoint, requires auth,
and exists so local smoke harnesses can exercise the full PR #2 path
without the Addie chat surface.

Also adds scripts/smoke-conformance-training-agent.ts — a "proxy
adopter" that connects to the local conformance endpoint via @adcp/sdk
6.9 ConformanceClient and forwards every inbound MCP request to the
locally running training agent's /api/training-agent/sales/mcp HTTP
endpoint. Demonstrates the full Socket Mode → training agent path
end-to-end without modifying the training agent's HTTP-bound setup.

Run output (storyboard: media_buy_state_machine):
  ✓ Capability discovery
  ✓ Create a media buy (2/2)
  ✗ Valid state transitions (Pause + Resume passed, Cancel returned a
    training-agent 500 — separate bug to file)
  ⊘ Terminal state enforcement (skipped on prerequisite failure)

The failures here are real training-agent issues surfaced by the
storyboard runner reaching it through Socket Mode + the proxy. The
transport itself is invisible above MCP: same shape as running the
storyboard over direct HTTP, plus a hop through the WebSocket.

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

* fix(conformance): close expert-review findings on Socket Mode stack

Five fixes from the security + code-review pass:

1. **Race-eviction on same-org reconnect (must-fix).** The close-listener
   on each WS connection removed the session by orgId only. When a
   second adopter from the same org connected, register()'s last-writer-
   wins displacement closed the prior socket, which fired its close
   listener and deleted the just-registered new session — leaving the
   org permanently unreachable until the next disconnect. Fix: identity-
   keyed eviction, only remove if `sessions.get(orgId)?.transport`
   still points at THIS transport. New regression test covers it.

2. **Pre-register disconnect leak.** If the adopter closed the socket
   between `client.connect(transport)` and `register(...)`, we'd register
   a session whose underlying socket is already gone. Fix: check
   `ws.readyState === OPEN` before registering; bail otherwise.

3. **Liveness check before runner dispatch.** `runStoryboardViaConformanceSocket`
   now bails with `ConformanceNotConnectedError` if the resolved
   session's transport is closed (and evicts it from the store as a
   side effect). Avoids handing a dead AgentClient to the runner.
   Adds `ConformanceWSServerTransport.isClosed()` for the check.

4. **Subprotocol-sentinel tightening.** Earlier code accepted
   `Sec-WebSocket-Protocol: mcp, <token>` as a fallback. Drop it;
   require the explicit `adcp.conformance` sentinel. Reordered token
   extraction to prefer the header path (out of access logs) over
   the `?token=` query (which lands in pino/proxy logs). Documented
   the staging-log-as-token-equivalent caveat for the query fallback.
   New regression test rejects the wrong-sentinel form.

5. **Debug endpoint tenant scoping.** `_debug/run-storyboard` previously
   accepted arbitrary `org_id` from the request body, letting any
   authenticated user (on a misconfigured staging) fan storyboards
   into another tenant's session. Fix: normal callers must run against
   their own resolved org; static-admin-API-key callers retain the
   ability to specify `org_id` for local smoke tools (consistent with
   the rest of the admin-key surface). Production builds still skip
   the entire `_debug/*` block.

42/42 conformance tests pass (added 2 regression tests for #1 and #4).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 4, 2026
…4098)

* fix(training-agent): tenant router lock + storyboard-smoke --brand

Two follow-ups surfaced by the Socket Mode stack work (#4007/#4082)
against the local training agent.

1. **Tenant router race (closes #4084).** The tenant router shares one
   MCP `Server` instance per tenant from the framework's
   `DecisioningAdcpServer` registry. Each request created a fresh
   `StreamableHTTPServerTransport`, called `server.connect(transport)`,
   handled the request, and `server.close()`'d. Two concurrent requests
   against the same tenant overlapped and the second `.connect()` threw
   "Already connected to a transport" — surfaced as intermittent 500s
   under back-to-back HTTP load.

   Fix: per-tenant async lock (`withTenantLock`) serializes the
   `connect/handle/close` window per tenant so the shared server only
   ever has one transport bound at a time. Throughput is gated by the
   in-flight request's wallclock; the storyboard runner's sequential
   dispatch makes this a non-issue, and the compliance heartbeat runs
   one tenant at a time. A future improvement could pool servers per
   tenant for true parallelism — this lock is the minimum-mass
   correctness change.

   Verification: 10 concurrent `tools/list` POSTs against
   `/api/training-agent/sales/mcp` return all `"result"` (was a mix of
   "result" and 500s before); server log shows zero "Already connected"
   errors (was 7+ per run before).

2. **storyboard-smoke `--brand` flag (closes #4083).** That issue
   reported `update_media_buy` on a cancelled buy returning
   `MEDIA_BUY_NOT_FOUND` instead of `INVALID_STATE`. After diagnosis
   the training agent is correct — the bug is in the upstream SDK
   runner's `update_media_buy` enricher, which fabricates an account
   from `resolveBrand(options)` (defaulting to `test.example`) when
   options.brand is unset. Positive-path steps get rewritten to
   `test.example`; `expect_error` steps skip the enricher and keep the
   YAML's literal `acmeoutdoor.example`. Result: split-brain session
   keying and stale-state reads on the negative-path probes.

   Fix: storyboard-smoke now accepts `--brand <domain>`. With
   `--brand acmeoutdoor.example`, the SDK runner's `applyBrandInvariant`
   normalizes both step kinds to the kit's domain and the storyboard
   passes 9/9. Long-form rationale in the JSDoc.

Also un-blocks the pre-commit typecheck by adding a `// @ts-expect-error`
on the v6-sales-platform `handoffToTask` call. PR #4080 bumped @adcp/sdk
to 6.11 expecting the two-arg signature from adcp-client#1554, but the
published 6.11 .d.ts still declares the single-arg shape only. Runtime
accepts the second arg correctly — pure typings gap. Drop the directive
when 6.12+ ships the matching typing.

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

* fix(training-agent): drop @ts-expect-error — local was stale on @adcp/sdk

CI surfaced "Unused '@ts-expect-error' directive" because CI's npm install
resolved @adcp/sdk 6.11.0 (which has the two-arg `handoffToTask` typing
from adcp-client#1554), while my local node_modules was still on 6.9.0
(no typing for the second arg) — the version mismatch made the directive
necessary locally but unused on CI.

Refreshed node_modules to 6.11.0 and dropped the directive. Both surfaces
typecheck clean now.

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

* docs(training-agent): expand tenant-lock comments per code review

Two doc-only fixes from the PR #4098 code review pass:

1. `withTenantLock` — explain why the lock includes flushDirtySessions
   and server.close() (not just the transport window): in-memory session
   state mutations from request N must persist before N+1 runs against
   the same shared `DecisioningAdcpServer`. Narrowing the lock would race
   on the v5 handlers' session-context state.

2. `withTenantLock` — explain the `.catch(() => {})` chain-keepalive
   pattern so a future reader doesn't "fix" it by removing the catch
   (which would poison every subsequent same-tenant request).

3. `storyboard-smoke.ts` — add `--brand` to the usage block so the flag
   is discoverable from the file header.

No behavior change.

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

---------

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.

1 participant