feat(testing): per-specialism storyboard routing — runStoryboard({ agents }) (#1066)#1355
Conversation
…ents }) (#1066) Storyboards spanning multiple specialisms can now route each step to the tenant that owns its tool, instead of fanning every call at one URL and relying on cross-tenant comply seeds. Maps directly to the prod test-agent topology (/sales, /signals, /governance, /creative, /creative-builder, /brand) and to local Hello-cluster setups. Routing: tool → TASK_FEATURE_MAP → first protocol → unique agent that claims it via get_adcp_capabilities. Multi-claim conflicts fail-fast at storyboard-build time (not silently first-wins) with named conflicting agents and step IDs. Tools with no specialism mapping fall back to default_agent or fail-fast as unroutable_task. Discovery: parallel per-agent get_adcp_capabilities at storyboard start. Any tenant's failure surfaces as a hard storyboard failure, not a per-step skip — broken tenant in a multi-tenant flow is a topology bug. CLI: adcp storyboard run --agents-map ./agents.yaml <storyboard_id>, repeatable --agent key=url, --default-agent key. Result: new StoryboardResult.agent_map echoes resolved key→url for JUnit/CI consumers. Per-step agent_url/agent_index populated in routed mode (previously only in multi-instance replica mode). Mutually exclusive with multi_instance_strategy (replica round-robin — different concept), _client (single client cannot serve multiple agents), and a positional URL alongside the map. Eight distinct entry-guard error paths, each with explicit messages. 13 new tests covering protocol-index build, conflict detection, per-step override, default_agent fallback, and all entry guards. Closes #1066. Hello-cluster adapters tracked separately as #1332-#1335. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s views Code review caught: `getOrDiscoverProfile` short-circuits when `options._profile` is set, returning the cached profile without probing the agent. comply() routinely primes `_profile` and `agentTools` for its single-tenant fast path, so without an explicit clear in `buildAgentOptions`, every per-agent discovery in routed mode would hand back comply()'s primary-tenant profile — silently breaking the protocol-claim index that routing depends on. Defense-in-depth fix in `buildAgentOptions`: spread-clear `_profile` and `agentTools` alongside `_client` and `agents`, with a comment pinning the rationale so a future refactor doesn't reintroduce the leak. Regression test pins the contract: a routing context built with `options._profile` set to the sales tenant's profile still routes `get_signals` to signals and `create_media_buy` to sales. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expert reviewFour reviewers in parallel — protocol, code, security, product. Three said ship; code-reviewer caught one real bug, now fixed. Code review — fixed in 6631e0fConcern: Fix: spread-clear Other code-review items (primary-profile heuristic, unioned-tools gate, discovery error path, per-step failure rendering, closeConnections semantics, CLI parsing) all returned OK. Protocol review — ship
Security review — ship
Product review — ship
DispositionReady to merge. Five follow-ups identified across reviewers — none blocking. Will track separately:
|
|
Acknowledged. Review synthesis received — four reviewers converge on ship, the Triaged by Claude Code. Session: https://claude.ai/code/session_01GsEvmh7bbaTBFZSTZBSEhE Generated by Claude Code |
…ub, env var, KNOWN_PROTOCOLS comment Addresses four of the six follow-ups identified in expert review of PR #1355. The two remaining items are upstream `adcontextprotocol/adcp` schema changes filed separately. 1. Inline comment at `KNOWN_PROTOCOLS` documenting that `compliance_testing` arrives via `parseCapabilitiesResponse`'s normalization, not as a wire-level entry in `supported_protocols`. Defends against a future refactor that strips that normalization without dropping `compliance_testing` from the routing set. 2. Entry guard rejects `agents` + `prerequisites.controller_seeding: true` combination unless `skip_controller_seeding: true` is also set. Today's `runControllerSeeding` dispatches against the FIRST per-agent client, which is wrong for cross-specialism storyboards whose `fixtures:` block declares seeds owned by different tenants (e.g., `seed_product` for sales, `seed_signal_provider` for signals). Per-tenant seed dispatch is a follow-up. 3. `scrubAuthSecrets()` redacts `Authorization: Bearer …`, standalone `Bearer …`, and `?token=…`/`&token=…` patterns from upstream discovery error messages before they propagate into `StoryboardResult.error` / CI logs / JUnit. The leakable token is the caller's own bearer for that tenant — already in the caller's CI environment, so this is parity with single-agent mode rather than a fix for a new leak. Defense-in-depth. 4. CLI `parseAgentsMapArgs` honors `ADCP_AGENTS_MAP` env var as a fallback for the `--agents-map` flag. Suits CI matrices (Buildkite, GHA) that prefer env injection over file mounts for ephemeral creds. CLI flags override the env var when both are set. Tests: +4 (controller-seeding guard, three bearer-scrub regex shapes). 18/18 in `test/lib/agent-routing.test.js`. Full lib suite still 5845/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 32421079 | Triggered | Generic High Entropy Secret | 206d4b0 | test/lib/agent-routing.test.js | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
Follow-ups landedAll six items from the expert review batched into one follow-up commit (e93e61d) on this branch, plus two upstream issues filed against In this PR (e93e61d)
Tests: +4 (controller-seeding guard, three bearer-scrub regex shapes). 18/18 in Upstream issues filed against
|
|
Acknowledged — follow-ups noted. The four in-PR items (KNOWN_PROTOCOLS comment, controller-seeding guard, Generated by Claude Code |
bokelley
left a comment
There was a problem hiding this comment.
A few comments from the perspective of locking #1335 (hello-cluster orchestrator) against this parser. Listed by priority — recommend ship after the bug fix and the two doc tightenings.
Real bug — string shorthand is unreachable
In parseAgentsMapArgs:
if (!entry || typeof entry !== 'object') {
console.error(`ERROR: ${filePath} agents.${key} must be an object with at least \`url\``);
process.exit(2);
}
const url = typeof entry === 'string' ? entry : entry.url;The string-handler on the second line is dead code — strings fail the typeof !== 'object' check above with a misleading "must be an object" error. Either drop the shorthand handler (and document objects-only) or move validation after it. The PR description doesn't promise shorthand, so dropping the dead line is probably right.
Doc contradicts code — AgentEntry.auth optionality
JSDoc says:
"Per-agent auth is required from day one — production multi-tenant deployments almost always use per-tenant credentials."
…but auth?: is optional and the next sentence says it falls back to StoryboardRunOptions.auth. Pick one. I'd keep it optional (uniform bearer across the 6-tenant test-agent is the canonical example) and rewrite the JSDoc to "per-tenant override; falls back to run-level auth." Current wording will push adopters into setting auth redundantly on every entry.
Doc overstates fail-fast — multi-claim conflicts
PR description says multi-claim conflicts "fail-fast at storyboard-build time, before any non-discovery network calls." Per the documented routing order, step.agent overrides resolve conflicts per-step — so multi-claim only fails-fast for steps without an explicit override. Suggest tightening to "for affected steps lacking step.agent override." Important framing: it tells adopters step.agent is the disambiguator, not just an esoteric escape hatch.
Framing — step.agent is broader than "conflict resolver"
StoryboardStep.agent JSDoc says:
"Use only when a tool's specialism is claimed by multiple agents..."
This is the explicit-routing primitive for cross-domain workflows too — e.g. signal_marketplace/governance_denied needs agent: governance on sync_governance and agent: signals on activate_signal because those tools are owned by different specialisms, not because of a conflict. Current framing makes it sound like an unusual escape hatch when it's the canonical multi-tenant pattern.
Open questions worth answering before merge (won't block, but adopters will ask)
- Webhook receiver in routed mode. If a step uses
expect_webhook*, which tenant gets the receiver URL in its notification config? One shared, or per-tenant?validateAutoTunnelArgsis called inhandleAgentsRoutedStoryboardRunso it's wired — just undocumented. - Discovery hard-fail brittleness. "Any tenant's discovery failure is a HARD storyboard failure" — defensible, but in CI with hello-cluster running 6 tenants, one flaky boot fails the whole run even for storyboards that don't touch that tenant. Worth an opt-in
discovery_resilient: truemode or a tracking issue. - Capability-driven full assessment in routed mode. Explicitly not supported. Real adopter ask the moment someone has a multi-tenant prod stack and wants "run everything." Worth filing now so the gap is visible.
agent_mapcompleteness. Does it include declared-but-unrouted-to agents? JUnit consumers will want the full topology, not just the touched subset.
Minor
- The PR description's example uses
default_agent: 'sales'for asignal_marketplace/governance_deniedstoryboard that touchessignalsandgovernancebut notsales. Decorative — pick adefault_agentthat's actually exercised, otherwise readers infer the wrong semantics. - The PR mixes "per-tenant" (prose) with "per-specialism" (actual routing semantics — agents claim specialisms, not tenants). Same idea, but adopters who deploy one tenant per business unit (vs one per specialism) will get confused. Pick one term.
Concrete adopter perspective from #1335
I locked the hello-cluster's manifest shape against this parser end-to-end. The shape works: default_agent at top level, agents.<key>.{url, auth} with bearer, plus a top-level pending: [...] sibling that the parser ignores cleanly (it only reads parsed.agents). The orchestrator emits a manifest that round-trips through yaml.parse to the exact shape runStoryboard({ agents }) expects. Once this lands, npm run hello-cluster --silent > agents.yaml && adcp storyboard run --agents-map agents.yaml signal_marketplace/governance_denied will be the one-line cross-specialism smoke for adopters.
|
GitGuardian false-positives addressed in 57958b3:
No behavior change. 18/18 tests still pass; format clean. The rescan should clear all three findings. |
|
Thanks for the fix-up — Generated by Claude Code |
|
Agreed — Generated by Claude Code |
|
Acknowledged — confirmed, no action needed on the placeholder strings. The Generated by Claude Code |
|
Thanks for confirming — noted, no further action on those items. Generated by Claude Code |
Addresses bokelley's review on PR #1355. **Real bug — dead code in CLI parser (`bin/adcp.js parseAgentsMapArgs`)**: The string-shorthand handler `typeof entry === 'string' ? entry : entry.url` was unreachable because the line above rejects strings with a misleading "must be an object" error. PR description doesn't promise shorthand; drop the dead expression. **Doc tightenings**: 1. `AgentEntry.auth` JSDoc said "required from day one" but the field is optional and falls back to run-level auth. Rewrite to "per-tenant override" with the explicit guidance that the prod test-agent's shared-bearer pattern is the canonical case (set run-level once, leave entries empty). 2. `StoryboardRunOptions.agents` JSDoc said multi-claim conflicts "fail-fast at discovery time and require either removing one from the map or resolving each affected step with an explicit `agent:` override." That was wrong — `step.agent` short-circuits conflict detection per-step (the runner only flags steps WITHOUT overrides). Rewrite to clarify the override is the per-step disambiguator. 3. `StoryboardStep.agent` JSDoc framed the field as "use only when ... the default conflict-resolution can't pick one" — an esoteric escape hatch. It's actually the canonical primitive for cross-specialism storyboards: a `signal_marketplace/governance_denied` storyboard NEEDS explicit `agent:` on each step because the tools are owned by different specialisms, not because of a conflict. Reframe accordingly. 4. Changeset summary updated with the corrected fail-fast wording. No behavior change. 18/18 tests still pass; format clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review actions landed (2d0c0c0)Real bug — fixed
Doc tightenings — fixed
Open questions — answered or filedQ4 — The other three filed as separate issues:
Hello-cluster confirmationConfirmed the parser's manifest shape locks against the hello-cluster (#1335) end-to-end: 18/18 tests pass; format clean. |
|
Thanks for the thorough summary. The CLI dead-code fix, JSDoc clarifications, and filed follow-ups (#1366, #1367, #1368) all look like the right calls — especially repositioning Triaged by Claude Code. Session: https://claude.ai/code/${CLAUDE_CODE_REMOTE_SESSION_ID} Generated by Claude Code |
|
Triage note from #1366: the webhook receiver topology question you flagged in your own review was filed as a tracking issue. The fix scope — JSDoc on Generated by Claude Code |
|
Triage note — fold candidate: Issue #1367 ("opt-in Triaged by Claude Code. Session: https://claude.ai/code/session_01TtF7pmpGEw6W5KvbbFgVLz Generated by Claude Code |
|
Folded in ceaf1a4. Two docs-only changes:
Format clean, no behavior change. Closes #1366. Generated by Claude Code |
|
Acknowledged — docs-only, no behavior change. Noted that ceaf1a4 closes #1366. Generated by Claude Code |
…ub, env var, KNOWN_PROTOCOLS comment Addresses four of the six follow-ups identified in expert review of PR #1355. The two remaining items are upstream `adcontextprotocol/adcp` schema changes filed separately. 1. Inline comment at `KNOWN_PROTOCOLS` documenting that `compliance_testing` arrives via `parseCapabilitiesResponse`'s normalization, not as a wire-level entry in `supported_protocols`. Defends against a future refactor that strips that normalization without dropping `compliance_testing` from the routing set. 2. Entry guard rejects `agents` + `prerequisites.controller_seeding: true` combination unless `skip_controller_seeding: true` is also set. Today's `runControllerSeeding` dispatches against the FIRST per-agent client, which is wrong for cross-specialism storyboards whose `fixtures:` block declares seeds owned by different tenants (e.g., `seed_product` for sales, `seed_signal_provider` for signals). Per-tenant seed dispatch is a follow-up. 3. `scrubAuthSecrets()` redacts `Authorization: Bearer …`, standalone `Bearer …`, and `?token=…`/`&token=…` patterns from upstream discovery error messages before they propagate into `StoryboardResult.error` / CI logs / JUnit. The leakable token is the caller's own bearer for that tenant — already in the caller's CI environment, so this is parity with single-agent mode rather than a fix for a new leak. Defense-in-depth. 4. CLI `parseAgentsMapArgs` honors `ADCP_AGENTS_MAP` env var as a fallback for the `--agents-map` flag. Suits CI matrices (Buildkite, GHA) that prefer env injection over file mounts for ephemeral creds. CLI flags override the env var when both are set. Tests: +4 (controller-seeding guard, three bearer-scrub regex shapes). 18/18 in `test/lib/agent-routing.test.js`. Full lib suite still 5845/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses bokelley's review on PR #1355. **Real bug — dead code in CLI parser (`bin/adcp.js parseAgentsMapArgs`)**: The string-shorthand handler `typeof entry === 'string' ? entry : entry.url` was unreachable because the line above rejects strings with a misleading "must be an object" error. PR description doesn't promise shorthand; drop the dead expression. **Doc tightenings**: 1. `AgentEntry.auth` JSDoc said "required from day one" but the field is optional and falls back to run-level auth. Rewrite to "per-tenant override" with the explicit guidance that the prod test-agent's shared-bearer pattern is the canonical case (set run-level once, leave entries empty). 2. `StoryboardRunOptions.agents` JSDoc said multi-claim conflicts "fail-fast at discovery time and require either removing one from the map or resolving each affected step with an explicit `agent:` override." That was wrong — `step.agent` short-circuits conflict detection per-step (the runner only flags steps WITHOUT overrides). Rewrite to clarify the override is the per-step disambiguator. 3. `StoryboardStep.agent` JSDoc framed the field as "use only when ... the default conflict-resolution can't pick one" — an esoteric escape hatch. It's actually the canonical primitive for cross-specialism storyboards: a `signal_marketplace/governance_denied` storyboard NEEDS explicit `agent:` on each step because the tools are owned by different specialisms, not because of a conflict. Reframe accordingly. 4. Changeset summary updated with the corrected fail-fast wording. No behavior change. 18/18 tests still pass; format clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ceaf1a4 to
b590d5e
Compare
JSDoc on `StoryboardRunOptions.webhook_receiver` now documents multi-agent
behavior: one shared receiver per run, delivery correlation by step-keyed
URL path, no per-tenant receiver needed or supported.
Routing module header (`agent-routing.ts`) gains a "Webhook receiver topology
in routed mode" section explaining the same contract at the implementation
level — why a shared receiver is sufficient, what `{{runner.webhook_base}}`
resolves to across agents, and the escape hatch (distinct step IDs already
produce distinct URL paths so delivery-source disambiguation is free).
Docs-only — no behavior change. Refs #1366.
https://claude.ai/code/session_01JbF134ueSaBz7kvPwBXBbo
) (#1370) `examples/hello-cluster.ts` boots every per-specialism hello adapter on its declared port (signals 3001 → retail-media 3006), preflights each adapter's upstream backend, health-checks via MCP tools/list, and emits a YAML routing manifest the storyboard runner (#1066, landing as #1355) consumes via `--agents-map`. Manifest shape locked end-to-end against #1355's parser: `agents.<key>.{ url, auth }` matching `TestOptions['auth']`, top-level `default_agent`, plus a top-level structured `pending: [...]` sibling that the runner ignores but tooling can surface. Today only the signals hello adapter exists; the rest (#1332/#1333/#1334 plus sales / retail-media) appear in the pending list and are picked up automatically when their entrypoint files land. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitGuardian flagged two patterns as false-positives in the bearer-scrub helper added by #1066: 1. The regex character class `[A-Za-z0-9._~+/=-]+` in `scrubAuthSecrets` — mixed-charset run looks high-entropy to GG's detector even though it's the regex source, not an encoded secret. 2. Placeholder tokens (`FAKE_*_PLACEHOLDER`) in the test fixtures. Rebasing the offending commit out of git history removed the literal strings but did not clear GitGuardian's persistent dashboard incidents — GG re-scans on push but treats prior incidents as still-open until resolved in the dashboard. This config tells `ggshield` (the GG CLI used by the GitHub Action) to treat these specific match shapes as known false-positives. The patterns are narrow enough that a real leaked credential of the same shape would still trigger; we're not blanket-ignoring entropy-tier detectors or paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…atterns" The allowlist file is read by ggshield (the CLI) but not by the GitHub App that posts `GitGuardian Security Checks` status to PRs — that App uses server-side workspace config. Removing to keep the PR diff minimal; dashboard-side incident triage is the actual remediation path.
Summary
Storyboards that span specialisms (e.g.
signal_marketplace/governance_denied:sync_governancethenactivate_signal) can now route each step to the tenant that actually owns the tool, instead of fanning every call at one URL and relying on each tenant accepting "foreign" comply seeds. Closes #1066.CLI:
The 6-tenant prod test-agent (
adcontextprotocol/adcp:server/src/training-agent/index.ts) is the canonical target:signals,sales,governance,creative,creative-builder,brand. Shared bearer auth across all six (per-tenantauthoverride is supported but not required).How routing works
step.agent(escape hatch for cross-domain tools)TASK_FEATURE_MAP[task]→ first protocol → unique agent that claims it viaget_adcp_capabilitiesoptions.default_agentRoutingError(fails the step with a precise diagnostic)Multi-claim conflicts (two agents claim the same protocol AND a step lacks
agent:override) fail-fast at storyboard-build time, before any non-discovery network calls. Error names the conflicting agents and the affected step IDs.Discovery runs in parallel across all agents at storyboard start. Any tenant's discovery failure is a HARD storyboard failure (not a per-step skip) — a broken tenant in a multi-tenant flow is a topology bug.
API additions
StoryboardRunOptions.agents?: Record<string, AgentEntry>StoryboardRunOptions.default_agent?: stringStoryboardStep.agent?: string(per-step override; YAML escape hatch)AgentEntry { url; auth?; transport? }— per-tenant overrides shadow run-level defaultsStoryboardResult.agent_map?: Record<string, string>— echoes resolvedkey → urlfor JUnit/CI consumersagent_url/agent_indexpopulated in routed mode (previously only in multi-instance replica mode)CLI
--agents-map <path>— YAML or JSON file withagents:(and optional top-leveldefault_agent:)--agent <key>=<url>— repeatable, overrides file entries--default-agent <key>— fallback for unmapped tools--url: HTTPS in prod, HTTP only with--allow-http, no embedded credentialsMutual exclusion
Three combinations throw at
runStoryboard()entry, each with a specific error message:agents+multi_instance_strategy(replica round-robin is a different concept — replicas test horizontal scaling of one agent; routing dispatches per-tool across different agents)agents+_client(single client cannot serve multiple agents)agents+ non-empty positional URL (ambiguous routing intent)Empty
agentsmap, missingurl,default_agentnot in map, andstep.agentnot in map are also caught at entry.Tests
13 new tests in
test/lib/agent-routing.test.js:buildRoutingContextFromProfilestest seam — no live agents)runStoryboard()Full lib suite: 5839 pass, 0 fail (no regressions).
Out of scope (filed as separate issues)
governancespecialismcreativespecialismbrand-rightsspecialismnpm run hello-cluster) + manifestThe 2-agent integration smoke against the existing signals + sales adapters can land in this branch as a follow-up if reviewers prefer; today the routing logic is unit-tested with mocked profiles and the entry guards are exercised end-to-end through
runStoryboard().Test plan
npm run buildcleannpm run format:checkcleannode --test test/lib/agent-routing.test.js— 13/13 passnpm run test:lib— 5839/5839 pass, no regressionsadcp storyboard run --agents-map ./examples/prod-test-agent.agents.yaml signal_marketplace/governance_denied --auth $TRAINING_AGENT_TOKEN— pending committed manifest file (will land alongside Hello cluster orchestrator: boot N specialism adapters with one command #1335)🤖 Generated with Claude Code