Skip to content

fix(compliance): route hosted 3.0 basic auth runs#5194

Merged
bokelley merged 4 commits into
mainfrom
basic-auth-prod-runs
May 30, 2026
Merged

fix(compliance): route hosted 3.0 basic auth runs#5194
bokelley merged 4 commits into
mainfrom
basic-auth-prod-runs

Conversation

@bokelley

@bokelley bokelley commented May 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • bump @adcp/sdk to 8.1.0-beta.18 and remove the temporary SDK Basic-auth patch plumbing
  • mirror hosted Bearer/Basic credentials into runtime test_kit.auth so prod/Addie 3.0 runs execute the new static-auth probes
  • select protected auth probe tasks from the discovered agent profile so non-creative agents do not get forced through list_creatives
  • keep the manual/current runner aligned with beta.18 by preserving Basic test-kit data, accepting 3.1-rc.4, and preserving buyer operation_id on framework task webhook bodies
  • keep our framework task webhook emitter wired for signed delivery while relying on beta.18's native operation-id threading

Validation

  • git diff --check
  • npm exec vitest run server/tests/unit/storyboards.test.ts
  • npm exec vitest run server/tests/integration/training-agent-webhooks.test.ts -t "delivers a signed completion webhook"
  • npm exec vitest run server/tests/integration/training-agent-webhooks.test.ts
  • npm exec vitest run server/tests/unit/addie/basic-auth-normalization.test.ts
  • npm exec vitest run server/src/training-agent/tenants/tenant-smoke.test.ts server/tests/integration/training-agent-3-0-compat-tools.test.ts server/tests/integration/training-agent-webhooks.test.ts
  • npm exec vitest run server/tests/unit/training-agent.test.ts -t "AdCP protocol compliance|get_adcp_capabilities handler|MCP Tasks protocol"
  • npm run typecheck
  • npm run test:storyboards:3.0-compat
  • npm run test:storyboards
  • precommit hook: test:unit, dynamic-import lint, callapi-state-change lint, typecheck
  • pre-push hook: version sync, current storyboard matrix, 3.0.14 storyboard matrix, Mintlify link check

Notes

This now relies on the published SDK beta.18 Basic-auth runner and framework operation-id support instead of local SDK patches.

@bokelley bokelley force-pushed the basic-auth-prod-runs branch from 898b4d7 to 3784b89 Compare May 30, 2026 15:19
@bokelley bokelley changed the title [codex] Route hosted compliance through 3.0.14 Basic auth fix(compliance): route hosted 3.0 basic auth runs May 30, 2026
@bokelley bokelley force-pushed the basic-auth-prod-runs branch from 3784b89 to 169b27c Compare May 30, 2026 15:21
@bokelley bokelley marked this pull request as ready for review May 30, 2026 15:52
aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 30, 2026

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Follow-ups noted below. Right shape for a temporary SDK compatibility shim: hash-pinned, fail-closed, idempotent, marker-versioned (V2 over V1), and the patch body itself is in-repo so reviewers can audit the resulting runtime.

Things I checked

  • withHostedAuthTestKit (server/src/services/hosted-compliance-version.ts:327-360) preserves caller-supplied test_kit.auth.{api_key|basic|probe_task} and only injects the auto probe-task marker when declaresStaticAuth is true. Bearer-vs-Basic don't fight: the patched SDK dispatches on the storyboard step's auth.type, not on credential presence, so both can coexist for steps that need different directives.
  • Idempotency across npm postinstall and the two Docker RUN calls (Dockerfile:15, 109-112): once PATCH_MARKER is present in the SDK runner source, all four patch* helpers in scripts/patch-adcp-sdk-basic-auth.cjs:344-468 short-circuit to status: undefinedresult.status === 'already-patched' → no write.
  • Fail-closed gates at scripts/patch-adcp-sdk-basic-auth.cjs:507-510 (SDK version pin) and per-region SHA-256 (EXPECTED_*_SHA256 constants). A future @adcp/sdk bump bricks npm install loudly with a guiding error — preferred over a silent partial patch.
  • Basic-auth header construction at scripts/patch-adcp-sdk-basic-auth.cjs:217-235: rejects step.auth.value (literal creds disallowed in storyboards), enforces RFC 7617 colon rule on username, and rejects CR/LF/NUL plus non-printable ASCII before base64 — error strings name the field but never echo the value.
  • tests/patch-adcp-sdk-basic-auth.test.cjs:1034-1045 re-derives all four expected hashes against the in-fixture SDK preimage, so a fixture/constant drift is caught locally before deploy.
  • CI: TypeScript Build, Server integration tests, all Storyboards matrices (current + 3.0-compat across signals/sales/governance/creative/creative-builder/brand), CodeQL, Semgrep, GitGuardian, Migration Smoke all green. Changeset Check green.
  • Test plan: 11 reproducible npm invocations listed; no live-agent end-to-end against an Authorization: Basic step, but the patched function is executed in-sandbox at tests/patch-adcp-sdk-basic-auth.test.cjs:917-925. Acceptable for a patch-shape change; CI's 3.0-compat storyboard matrix is the safety net.

Follow-ups (non-blocking — file as issues)

  • Empty changeset frontmatter. .changeset/basic-auth-prod-runs.md has ---\n--- with no "adcontextprotocol": patch line. Changeset Check CI passes (it verifies presence, not contents), and this PR is server-only + forward-merge of already-released artifacts, so no version bump is the defensible outcome — but every sibling under .changeset/ declares a package + level. Either add "adcontextprotocol": patch or land a one-paragraph convention note so the empty case isn't mistaken for an oversight on the next forward-merge.
  • Fail-closed test matrix covers 1 of 4 hash-pinned regions. tests/patch-adcp-sdk-basic-auth.test.cjs:906-913 exercises authHeadersForStep hash-mismatch; resolveTaskName, the assert_contribution snippet, and the runStoryboardStep profile snippet have no equivalent assert.throws(... /hash mismatch/). Add three more cases so the SDK-bump regression is caught in a unit run, not in npm install on the deploy host.
  • Unicode passwords rejected. The header validator at scripts/patch-adcp-sdk-basic-auth.cjs:171-175, 251-253 rejects any non-printable ASCII. RFC 7617 explicitly recommends UTF-8 for Authorization: Basic. The choice is defensible (undici's header validator is strict) but worth documenting in the test-kit auth docs — adopters with accented or non-Latin passwords will hit a hard failure at probe time with no obvious fix path.
  • Defense-in-depth on patched resolveTaskName. The patched body mutates options.test_kit.auth._probe_task_unsupported = true on the caller's object (scripts/patch-adcp-sdk-basic-auth.cjs:290-292 in the patched snippet). Safe today because withHostedAuthTestKit builds a fresh shallow copy per call, but a future caller that reuses an options object across runs would carry the flag forward. Comment the invariant on withHostedAuthTestKit or deep-copy auth before threading it through.
  • _probe_task_auto trust boundary. The marker is intended as an internal signal but withHostedAuthTestKit reads currentKit.auth ?? {} and would preserve a caller-supplied _probe_task_auto: true. All current call sites build options literally and never pass test_kit.auth through from a request body — worth a one-line invariant comment so future contributors don't open that path.

Minor nits (non-blocking)

  1. Stray indent on legacy assert-contribution refresh. scripts/patch-adcp-sdk-basic-auth.cjs:399-403 inserts \n after patchedAssertContributionSnippet(), but the original legacyCurrent slice already includes the leading whitespace before the next else if. Cosmetic — bounded by the marker check on the next run — but a follow-on legacy → V2 refresh against a fresh checkout would leave a stray blank-ish line. Drop the trailing \n or trim the leading whitespace from source.slice(legacyEnd).

Notable that this lands as a patch script with V2 markers already in place — the V1 → V2 refresh paths are tested, which is the right way to ship a temporary shim that's going to get edited a few more times before @adcp/sdk catches up.

Safe to merge.

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shape looks right — credential mirror is well-scoped, three call sites route through it consistently, SDK bump trades the local patch for upstream beta.17. One question on emitFrameworkTaskWebhook gates my approval.

Things I checked

  • withHostedAuthTestKit (hosted-compliance-version.ts:326-360) — mirrors bearer/basic only, no-clobber on pre-set test_kit.auth, returns input unchanged when nothing to do. Three call sites (withHostedComplianceRunOptions, withHostedStoryboardRunOptions, withHostedTestOptions) all route through it.
  • 3.1-rc.4 allowlist additions land in all three places (task-handlers.ts:591, router.ts:50, plus six test files). No stale 3.1-beta.7 literals left.
  • SDK bump 8.1.0-beta.16 → beta.17 is one line + lockfile, integrity hash recorded, no transitive churn.
  • Credentials flow only in-memory through the SDK call. No logging of options.test_kit, no persistence path, no cross-tenant sink. The test_kit field in registry /storyboard/.../run responses is the brand-only static fixture, not the runtime auth-mirrored kit.
  • Default probe_task = 'list_creatives' is readOnlyHint: true, idempotentHint: true — safe to invoke on the user's behalf with their saved creds.
  • Empty changeset is correct per playbook: server-only change, no protocol surface.

Open question (gating)

server/src/training-agent/tenants/registry.ts:196-205 — the fallback payloadOperationId = params.operation_id conflicts with the contract documented in server/src/training-agent/webhooks.ts:119-137,194-196: WebhookEmitter.emit({operation_id}) is the seller-side idempotency scope (${principal}|${tool}.${id}) and is explicitly never placed on the wire. The legacy emitter uses extractBuyerOperationId(...) ?? webhookTaskId for the wire payload.operation_id exactly to avoid this.

What does the SDK's framework path actually pass as params.operation_id to taskWebhookEmitter.emit? If it passes the idempotency scope (same convention as legacy, which the shared WebhookEmitParams type implies), this fallback puts a principal-scoped value on the wire to a buyer-controlled receiver. For the training agent that's static:public:shared|... or b:<domain>|... — not a raw token, but tenant fingerprinting that crosses receivers.

Two ways to close it:

  1. Confirm the SDK's framework path normalizes params.operation_id to a buyer-safe value before calling the emitter hook. If so, add a one-line comment explaining why the fallback is safe.
  2. Drop the fallback to params.operation_id. Fall back to params.payload.task_id, or omit entirely.

Follow-ups (non-blocking — file as issues)

  • OAuth path silently skipped. withHostedAuthTestKit:336-340 handles bearer and basic only. resolve-user-agent-auth.ts:36-75 produces oauth and oauth_client_credentials variants too. Hosted runs against OAuth-protected agents get no test_kit auth wired and no warning. Either intentional (state it in a comment) or a coverage gap.
  • Per-tenant probe_task defaults. DEFAULT_HOSTED_AUTH_PROBE_TASK = 'list_creatives' is global. run-storyboards.ts:375-378 already has PROBE_TASK_BY_TENANT showing signals/governance need different tasks. Hosted runs against those tenants will probe a task they don't serve.
  • LoadedTestKit.basic shape drift. Widened type in run-storyboards.ts:347-351 accepts {credentials?: string} (no username/password). The SDK consumes {username, password}. A YAML test kit shipping base64 credentials will silently fail the probe. Either narrow the type to match the SDK, or pre-decode via the existing decodeBasicCredentials in server/src/db/compliance-db.ts:56-67.
  • Empty-credential guard. withHostedAuthTestKit:336-340 would mirror an empty bearer token or empty username/password into the test kit. Fails late with a confusing Basic Og== rather than fast with "no creds." Debuggability nit.
  • RuntimeTestKitAuth is an escape hatch. The intersection at hosted-compliance-version.ts:26-33 exists because the SDK's static types don't yet expose basic. Add a TODO pinning the SDK version that will obsolete it. If a future beta ships a typed basic with a different shape (e.g. {credentials: base64}), this code compiles and silently sends the wrong thing.

Minor nits (non-blocking)

  1. Test coverage in storyboards.test.ts:205-222 — bearer + basic happy paths only. Missing cases: options === undefined (early return), pre-set test_kit.auth.api_key (no-clobber), auth.type === 'oauth' (no-op), caller-supplied probe_task (no stomp), and the as any cast on auth.basic (line 219) papers over a type seam worth asserting at the structural level.
  2. PR is mergeable: CONFLICTING — rebase before merge.
  3. Test now asserts DEFAULT_HOSTED_COMPLIANCE_VERSION === '3.0.14' (storyboards.test.ts:167,178) — fine for now but will drift when the cache advances. Either pin the line (/^3\.0\.\d+$/, already done at L179) and drop the exact literal, or accept that this test goes stale on the next cache bump.

Holding on the operation_id question. LGTM once that's confirmed or the fallback is tightened.

aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 30, 2026

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Mirroring stored creds into test_kit.auth is the right shape — the SDK's beta.17 static-auth probes can now actually authenticate against the 3.0.14 receiver in hosted/Addie runs, and profile-aware probe-task selection stops forcing list_creatives on signals and governance agents.

Things I checked

  • withHostedAuthTestKit in server/src/services/hosted-compliance-version.ts — mutation-safe: currentKit, currentAuth, and auth.basic are all spread / re-objected, no shared-reference writes; the !nextAuth.probe_task guard preserves caller-supplied overrides. Ordering of withHostedAuthTestKitwithHostedComplianceOptions is fine: the latter only touches version / complianceDir / schemaRoot, never test_kit.
  • frameworkWebhookContext.enterWith(...) in server/src/training-agent/tenants/registry.ts is contained today: every MCP request is wrapped in runWithSessionContext (tenants/router.ts:299, :364), which provides the .run() boundary that scopes the enterWith effect to that request's async chain. security-reviewer walked the request entry points and confirmed no cross-tenant leak. Fragile, but safe.
  • hostedAuthProbeTaskForProfile — protocol map first, then fallback allow-list, then 'list_creatives'. A __proto__ injection in supported_protocols lands on Object.prototype, throws inside .find, and is caught by all three callers — only the eight allow-listed strings or the default are returnable on the wire.
  • 3.1-rc.4 propagates through every site referencing the prior 3.1-beta.7 pin: task-handlers.ts, tenants/router.ts:50, tenant-smoke.test.ts, training-agent.test.ts, training-agent-3-0-compat-tools.test.ts. No stale 3.1-beta.7 left in PR HEAD.
  • Webhook operation_id round-trip is covered by the new assertion in tests/integration/training-agent-webhooks.test.ts:146.
  • Changeset frontmatter ---\n---\n matches the project convention (see .changeset/3573-agent-url-canonicalization-cleanup.md) — informational-only, no workspace bump declared. Not a bug.

Follow-ups (non-blocking — file as issues)

  1. Basic password may reach production logs via SDK error capture. compliance-testing.ts:69, member-tools.ts:453, and registry-api.ts:6297 log {err, agentUrl} from testCapabilityDiscovery. The pino redact paths in logger.ts:151 list 'password' but unwildcarded paths do not match nested keys like err.cause.config.headers.authorization. If beta.17's fetch wrapper embeds the failing request into err.message/err.stack/err.cause, the base64-encoded Basic credential lands in logs verbatim. Add wildcarded redact paths ('*.headers.authorization', '*.config.auth') or wrap the catch site in a stripper before passing to the logger.
  2. test_kit.auth.basic.password may persist in request_jsonb. compliance-testing.ts:766 writes step.request.payload to DB. Confirm against beta.17 that the SDK does not echo test_kit.auth into the captured step.request.payload; if it does, strip test_kit.auth.api_key / test_kit.auth.basic inside capPayload as defense in depth.
  3. operationIdByTool keyed by tool name corrupts on double-call. tenants/registry.ts — if the same tool fires twice in one request with different push_notification_config.operation_id values, call #2 overwrites call #1's entry; the framework emitter then resolves call #1 by task_type and picks up call #2's operation_id. Narrow but real. Key by resolved task_id, or attach operation_id directly to the framework task record so the emitter reads it back via params.task_id.
  4. Discovery 401 is masked by fail-open fallback. member-tools.ts:443-453 and registry-api.ts:6291-6299 swallow discovery errors and default to list_creatives. If saved auth is wrong, the storyboard probe step also 401s — but the user sees the probe-step error, not the upstream discovery 401. Bubble OAuth-required errors, or annotate the eventual step error with the discovery-failure detail.
  5. Probe-task default fires even when the agent doesn't advertise it. hosted-compliance-version.tsHOSTED_AUTH_PROBE_TASK_FALLBACKS.find(...) ?? 'list_creatives'. An older or malformed discovery response that loses both supported_protocols and the eight fallback tool names lands on list_creatives and 404s with no signal that probe selection was the cause. Returning undefined and letting the SDK pick its own default would surface a cleaner failure.
  6. Three near-identical discover → probe-task wrappers. compliance-testing.ts:57-72, member-tools.ts:442-457, registry-api.ts:6291-6299 all do the same auth-narrowing + testCapabilityDiscovery + hostedAuthProbeTaskForProfile dance — notable for a fix PR that's already touching three call sites. Hoist a single helper alongside withHostedAuthTestKit.
  7. Extra discovery roundtrip per storyboard step. member-tools.ts:4628 — every individual step click now pays a discovery call. Memoize per (agentUrl, auth) for the Addie session lifetime or per-request via a WeakMap.

LGTM. Follow-ups noted below.

aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 30, 2026

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Follow-ups noted below. The two-step fix — mirror saved creds into test_kit.auth and infer the probe task from the discovered profile — is the right shape: hosted runs no longer force list_creatives on agents that don't expose it.

Things I checked

  • withHostedAuthTestKit (server/src/services/hosted-compliance-version.ts:291-327) — changed flag correctly avoids mutating when nothing's new; nextAuth.api_key/nextAuth.basic only set when absent; probe_task defaults only when declaresStaticAuth becomes true. No null-deref.
  • hostedAuthProbeTaskForProfileprofile?.tools ?? [] is null-safe; always returns a string from the protocol map or the fallback list, never undefined.
  • Discovery fallback path in all three call sites (member-tools.ts:447, compliance-testing.ts:67, registry-api.ts:6293) — on failure, authProbeTask stays undefined, withHostedAuthTestKit falls back to DEFAULT_HOSTED_AUTH_PROBE_TASK = 'list_creatives'. Fail-open back to prior behavior, which is correct here.
  • Tenant isolation on the new pre-discovery call at registry-api.ts:6288-6297orgId still gates resolveUserAgentAuth; the new testCapabilityDiscovery consumes the same scoped sdkAuth. validateAgentUrlParam still in front.
  • Credential exposure from duplicating tokens into test_kit.auth — no logger, error response, telemetry, or LLM-context path serializes options.test_kit. security-reviewer: clean (one defense-in-depth note, see below).
  • '3.1-rc.4' addition to SUPPORTED_RELEASE_VERSIONS is purely additive — 3.1-beta.7 preserved. Backwards-compatible. All matching test fixtures updated (training-agent.test.ts, tenant-smoke.test.ts, training-agent-3-0-compat-tools.test.ts).
  • Empty-frontmatter changeset matches the repo convention for no-package-bump infra changes (cf. .changeset/3-1-rc-release-notes-audit.md).

Follow-ups (non-blocking — file as issues)

  • compliance-testing.ts:67 calls testCapabilityDiscovery(agentUrl, options) with raw ComplyOptions — the other two probe-inference sites wrap via withHostedTestOptions(..., target). Functionally benign for discovery (we only read caps.profile), but inconsistent and a foot-gun if the SDK ever validates the probe response against schemaRoot. Either thread target through hostedAuthProbeTaskForRun or call through withHostedTestOptions.
  • emitFrameworkTaskWebhook precedence (registry.ts:200) is params.payload.operation_id ?? params.operation_id. Falls back to the framework-supplied scope value if the SDK ever fails to populate the wire field. webhooks.ts's legacy emitter falls back to task_id instead. Worth either matching that or adding a one-line comment pinning the contract.
  • Double round-trip per hosted run — pre-discovery + actual run. Fine for interactive Addie flows; the comply() path runs across many agents and could cache the profile for the duration of the run. Non-urgent.
  • Defense-in-depth on test_kit.auth (security-reviewer Low): the mirrored credentials are enumerable on the same options object the SDK consumes. If a future caller logger.debug({ options }, ...) lands anywhere on this path, both options.auth and options.test_kit.auth serialize. Add api_key/basic to the logger redactor allow-list — cheap, future-proofs the refactor.

Safe to merge.

# Conflicts:
#	server/src/addie/mcp/member-tools.ts
#	server/src/addie/services/compliance-testing.ts
#	server/src/routes/registry-api.ts
#	server/src/training-agent/tenants/registry.ts
#	server/tests/integration/training-agent-webhooks.test.ts
#	server/tests/unit/storyboards.test.ts
aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 30, 2026

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Follow-ups noted below. Right shape: removes the local SDK patch, plumbs the credential into test_kit.auth exactly where beta.18 expects it, and keeps the probe-task selection inside a closed read-only allowlist.

Things I checked

  • Credential flow stays in-tenant. resolveUserAgentAuth(orgId, agentUrl) and resolveAgentOwnerOrg(req.user.id, agentUrl) gate the lookup at server/src/routes/registry-api.ts:6286-6307; the same scoping holds at server/src/addie/mcp/member-tools.ts:4448-4470 and :4635-4658. Bearer/Basic only flow to the agent they were issued for. No cross-tenant smuggling path.
  • Probe-task allowlist is closed. HOSTED_AUTH_PROBE_TASKS_BY_PROTOCOL and HOSTED_AUTH_PROBE_TASK_FALLBACKS at server/src/services/hosted-compliance-version.ts:251-269 are constant, all read-only (list_* / get_*). An adversarial agent profile can steer to a different read but cannot escape to a mutation.
  • withHostedAuthTestKit is idempotent. Never overwrites caller-supplied api_key, basic, or probe_task; sets probe_task only when creds are present and one isn't already declared (hostedAuthProbeTaskForRun at compliance-testing.ts:65-72 short-circuits on caller-set probe_task). Fail-closed default of list_creatives.
  • Discovery error handling fails to the safe default. All three call sites catch and warn with { err, agentUrl } only — no credential reflection, no Authorization-header echo through pino's standard serializer.
  • Version enum updates are consistent. 3.1-rc.4 added to SUPPORTED_RELEASE_VERSIONS at task-handlers.ts:637 and tenants/router.ts:50 and to all the matching test fixtures — no drift across the six edited files.
  • Changeset is present (.changeset/basic-auth-prod-runs.md) and the empty-frontmatter form is the established convention here for server-side fixes that don't bump the adcontextprotocol npm package.
  • No static/schemas/source/** changes, no docs drift to audit.

Follow-ups (non-blocking — file as issues)

  • emitFrameworkTaskWebhook operation_id fallback. server/src/training-agent/tenants/registry.ts:196-208 falls back payload.operation_id ?? params.operation_id. The existing webhooks.ts:206 convention treats the emitter's operation_id argument as a seller-side idempotency scope (the comment at webhooks.ts:194 says it's never placed on the wire because it embeds the principal token). If the framework SDK's taskWebhookEmitter.emit follows the same semantic, the fallback could publish that scope as the wire operation_id. The integration test at training-agent-webhooks.test.ts:127,146 only covers the buyer-supplied path — the fallback branch is uncovered. Either confirm the SDK uses a distinct wire-safe value for params.operation_id (and drop a comment), or fall back to params.payload.task_id the way maybeEmitCompletionWebhook does at webhooks.ts:196. Both code-reviewer and security-reviewer landed on this independently.
  • Double discovery round-trip per storyboard step. Every Addie / compliance / storyboard run now fires testCapabilityDiscovery before the actual run, and the SDK does its own discovery inside runStoryboard. Net ~2× wall-clock and ~2× outbound request volume against the target agent. Cache the resolved profile per (orgId, agentUrl, complianceTarget) for the run window, or thread the discovered caps into the SDK call so it can skip the second trip.
  • Inconsistent withHostedTestOptions wrap at discovery sites. compliance-testing.ts:65 calls testCapabilityDiscovery(agentUrl, options) raw, while registry-api.ts:6293 and member-tools.ts:445 wrap with withHostedTestOptions(..., target). Pick one — silent drift the first time a target-specific schemaRoot or adcpVersion starts mattering for discovery.

Minor nits (non-blocking)

  1. RuntimeTestKitAuth index signature. server/src/services/hosted-compliance-version.ts:236-242 declares [key: string]: unknown and the test at storyboards.test.ts:224 uses (options.test_kit?.auth as any)?.basic. Both signs the local widening is hiding an SDK type that doesn't yet expose basic. Prefer keying off NonNullable<NonNullable<TestOptions['test_kit']>['auth']> so the next SDK rename (e.g. basiccredentials) trips at typecheck.
  2. declaresStaticAuth simplification. hosted-compliance-version.ts:307-310Boolean(api_key || basic || probe_task) combined with && !probe_task collapses to (api_key || basic) && !probe_task. Same behavior, easier to read.
  3. Dual key for sponsored intelligence. hosted-compliance-version.ts:265-269 carries both 'sponsored_intelligence' and 'sponsored-intelligence' — reasonable defensive aliasing between the wire snake_case and manifest kebab-case, but a one-line comment would prevent a future "dedupe" PR from quietly dropping the kebab form.
  4. Caret on prerelease SDK. package.json:122 keeps ^8.1.0-beta.16 while the lockfile pins beta.18. Caret semantics on prereleases admit any 8.1.0-beta.X. A pin (8.1.0-beta.18) would be safer for fresh installs that miss cache.

Approving on the strength of the contained credential plumbing, the closed probe-task allowlist, and consistent version-enum updates across all six fixture sites. Worth following up on the operation_id fallback before the next 3.0.14 storyboard pass.

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question on the framework webhook fallback before approving. Other findings are non-blocking.

Things I checked

  • server/src/services/hosted-compliance-version.tswithHostedAuthTestKit mirror is per-request, scoped to a freshly-resolved ComplyOptions object; no shared cache or persistence path picks it up. Tenant isolation preserved by the upstream resolveUserAgentAuth(orgId, agentUrl, …). Surface widens within one in-memory options object — acceptable.
  • hostedAuthProbeTaskForProfile correctly disambiguates by supported_protocols first, then falls back across the union, then to list_creatives. Profile-aware selection looks right for non-creative agents.
  • .changeset/basic-auth-prod-runs.md empty frontmatter is consistent with other no-version-bump entries in this repo (3-1-active-development-banner.md, etc.). Fine.
  • Test additions in server/tests/unit/storyboards.test.ts:217-252 exercise both bearer/basic mirror and profile-based probe selection. Integration test in server/tests/integration/training-agent-webhooks.test.ts:130,146 pins the buyer-supplied operation_id round-trip.

Question that blocks approval

server/src/training-agent/webhooks.ts:389-396 — the new fallback:

operation_id: params.payload.operation_id ?? params.operation_id,

The legacy path in the same file at line 207 passes operation_id: idempotencyScope to emitter.emit, where idempotencyScope is principal-scoped (${principal}|...) and the comment at lines 134-138 / 191-196 is explicit: this value is never placed on the wire because it embeds the seller-side principal token. resolveIdempotencyPrincipal at tenants/registry.ts:215-232 returns ctx.authInfo?.clientId, and clientId is bridged at tenants/router.ts:198 as static:demo:<bearer-token> — i.e., it embeds the demo bearer token as a substring.

If params.operation_id reaching emitFrameworkTaskWebhook carries that same principal-scoped value in beta.18, the fallback emits the seller principal — and a substring of the demo bearer — in a signed, retried, idempotency-stored wire payload to a buyer-controlled URL. That happens precisely when the buyer omits push_notification_config.operation_id. The new test only covers the buyer-supplied case.

The PR description says "relying on beta.18's native operation-id threading," which implies the SDK now threads the buyer-supplied value to params.operation_id at the framework boundary. That would make the fallback safe. But the same WebhookEmitParams.operation_id field is used as the idempotency scope in the legacy call (line 207), so the semantics inside this codebase are now contradictory.

To flip to approve, please confirm one of:

  1. In @adcp/sdk 8.1.0-beta.18's framework task webhook path, params.operation_id is the buyer-supplied push_notification_config.operation_id (not the principal-scoped idempotency key) — link to SDK code or release note.
  2. Or change the fallback to match the safe pattern at line 197: params.payload.operation_id ?? (params.payload.task_id as string \| undefined), and add a regression test asserting that when buyer omits operation_id and the caller authenticates with a billable bearer token, body.operation_id does not equal params.operation_id and does not contain the bearer-token substring.

Follow-ups (non-blocking — file as issues)

  • server/src/addie/services/compliance-testing.ts:60-78hostedAuthProbeTaskForRun calls testCapabilityDiscovery(agentUrl, options) with the raw options, while member-tools.ts:447 and registry-api.ts:6291 wrap with withHostedTestOptions. Discovery here will probe with the SDK default schema root rather than the hosted target. Likely benign for capability discovery, but worth aligning for consistency.
  • Pre-discovery adds one extra round trip per storyboard run, storyboard step, and comply call — no caching. Tail latency on long storyboards roughly doubles for the first probe. Consider memoizing per (agentUrl, auth-fingerprint) for the request lifetime, or accept and note the cost.
  • server/src/services/hosted-compliance-version.ts:35-42 — both sponsored_intelligence and sponsored-intelligence keys exist in HOSTED_AUTH_PROBE_TASKS_BY_PROTOCOL. If this aliases an SDK protocol-name drift, a comment would help; if not, pick one.

Minor nits (non-blocking)

  1. Test casts. server/tests/unit/storyboards.test.ts:235,238 use as any to reach test_kit.auth.basic. Export RuntimeTestKitAuth or widen the SDK type so the cast goes away.

Will re-review once the operation_id semantics question is answered.

aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 30, 2026

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Follow-ups noted below. The split between idempotency-scope (in-memory) and wire operation_id (payload) is preserved correctly, and the probe-task allowlist is closed against hostile agent profiles.

Things I checked

  • withHostedAuthTestKit (server/src/services/hosted-compliance-version.ts) only fills empty slots — existing test_kit.auth.api_key/basic is preserved. Mirrored creds do not reach DB writes: compliance-testing.ts:687-705 header allowlist excludes Authorization, and outbound-log-db logs no credential fields.
  • Adversarial-agent profile cannot inject arbitrary task names. hostedAuthProbeTaskForProfile returns only values from HOSTED_AUTH_PROBE_TASKS_BY_PROTOCOL / HOSTED_AUTH_PROBE_TASK_FALLBACKS / DEFAULT_HOSTED_AUTH_PROBE_TASK. All allowlisted tasks are read-only AdCP reads.
  • Multi-tenant isolation preserved. resolveUserAgentAuth(agentContextDb, orgId, agentUrl, logger) still org-scopes every cred fetch; the new pre-discovery call reuses the same sdkAuth, no cross-tenant leakage.
  • emitFrameworkTaskWebhook (server/src/training-agent/webhooks.ts:389-397) overlays payload.operation_id with params.payload.operation_id ?? taskId. Top-level params.operation_id is the SDK's in-memory idempotency-store key and is never serialized — the new test at training-agent-webhooks.test.ts "does not copy the framework idempotency scope into webhook operation_id" pins this invariant.
  • Probe-task selection threads through the three callers (member-tools.ts:4467/4655, compliance-testing.ts:comply, registry-api.ts:6306). Failure mode is a logger.warn and a fall-back to the default probe — fail-closed against the auth path, fail-open against discovery brittleness, which is the right shape.
  • Changeset present (.changeset/basic-auth-prod-runs.md). Empty-type frontmatter matches the existing repo pattern, so non-blocking.

Follow-ups (non-blocking — file as issues)

  • De-duplicate the pre-discovery pattern. Three call sites duplicate the same testCapabilityDiscovery → hostedAuthProbeTaskForProfile block: server/src/addie/mcp/member-tools.ts:441-454, server/src/addie/services/compliance-testing.ts:56-71, server/src/routes/registry-api.ts:6289-6296. They already drift: compliance-testing.ts short-circuits on a pre-set probe_task, the other two don't; the registry-api inline copy runs before the context/dry_run body validation at :6299-6304, so malformed requests now incur an outbound probe. Extract one helper from hosted-compliance-version.ts (e.g. inferHostedAuthProbeTask(agentUrl, auth, target)) and call it from all three.
  • Pre-discovery options drift in compliance-testing.ts:65. The pre-discovery call uses raw options and not withHostedTestOptions(options, target) — so it probes without the hosted schemaRoot/adcpVersion the real run uses. The other two paths wrap. Worst case is the inferred profile differs from the production probe path and you fall back to list_creatives, which is harmless, but the asymmetry is worth straightening.
  • Doubles authenticated probe traffic. Every storyboard run now hits the agent twice (once for discovery, once for the run). Defense-in-depth, not a security issue. Cache discovery per (orgId, agentUrl, version) for the request scope.
  • Logger error redaction is not depth-wildcarded. logger.ts:151-171 redacts authorization/token/api_key at top-level only. The new logger.warn({ error, agentUrl }, ...) paths rely on the SDK's thrown errors not embedding the Authorization header in .message/.stack. Currently true. Add '*.headers.authorization' to the redact list as belt-and-braces.

Minor nits (non-blocking)

  1. RuntimeTestKitAuth widens with [key: string]: unknown. server/src/services/hosted-compliance-version.ts:20-25. The intermediate widening means future SDK auth-shape tightening won't surface at the type level. Narrow to the SDK's actual auth union if @adcp/sdk/testing exports one.
  2. Empty-string operation_id slips through. server/src/training-agent/webhooks.ts:395 uses params.payload.operation_id ?? taskId — an empty-string operation_id (no framework path supplies one today) would pass through as the wire value. || instead of ?? if any framework caller might pass ''.
  3. (options.test_kit?.auth as any)?.basic at server/tests/unit/storyboards.test.ts:228. Cast to the new RuntimeTestKitAuth instead of any.

Approving on the strength of the credential-scope separation in emitFrameworkTaskWebhook plus the closed-allowlist hostedAuthProbeTaskForProfile.

# Conflicts:
#	server/tests/integration/training-agent-webhooks.test.ts

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Focused fix that pushes Basic-auth handling out of local SDK patches and onto the published beta.18 path — narrower blast radius, fewer drift points.

Things I checked

  • 9 files, +325/-13. Largest deltas: server/src/services/hosted-compliance-version.ts (+93/-3) and the new integration test (+103/-1). No file crosses the 200-net-line rule.
  • Changeset present (.changeset/basic-auth-prod-runs.md). Empty frontmatter matches the repo convention for non-spec-versioning entries (e.g. .changeset/3-1-rc-release-notes-audit.md). No static/schemas/source/** changes → spec-coherence audit N/A.
  • withHostedAuthTestKit precedence (!nextAuth.api_key) is the right shape: buyer-supplied test_kit credentials win over an OAuth-resolved bearer. Unit coverage at server/tests/unit/storyboards.test.ts:217-254 exercises both bearer and basic threading.
  • Webhook fallback at server/src/training-agent/webhooks.ts:389-397: payload.operation_id ?? task_id. Test at server/tests/integration/training-agent-webhooks.test.ts:267-296 proves the framework's signed scope static:demo:demo-billing-agent-billable-v1|create_media_buy.mb_secret does NOT leak into the wire body. Right invariant, right assertion.
  • OAuth user tokens are not mirrored into test_kit.auth.api_keyadaptAuthForSdk returns user-OAuth as type: 'oauth', so the bearer-mirror branch only fires for already-exchanged client-credentials tokens. No new exposure surface.
  • Pre-discovery probe goes only to the storyboard's agentUrl, which the user already authorized — no new attacker surface.

Follow-ups (non-blocking — file as issues)

  • Protocol keys are kebab-case but the schema enum is snake-case. server/src/services/hosted-compliance-version.ts:37-44 lists 'media-buy' and 'sponsored-intelligence', but static/schemas/source/protocol/get-adcp-capabilities-response.json:138-146 restricts supported_protocols to media_buy / sponsored_intelligence. The hyphen keys never match — bug is latent because the flat fallback list at hosted-compliance-version.ts:46-55 happens to cover the same probe tasks. Rename to snake_case and drop the kebab dupes. Add a test asserting hostedAuthProbeTaskForProfile({ tools: ['get_media_buy_delivery'], supported_protocols: ['media_buy'] }) returns 'get_media_buy_delivery'.
  • Discovery wrap drift across the three pre-discovery sites. server/src/addie/services/compliance-testing.ts:64 calls testCapabilityDiscovery(agentUrl, options) with raw ComplyOptions. The other two callers (server/src/addie/mcp/member-tools.ts:451, server/src/routes/registry-api.ts:6293) wrap via withHostedTestOptions(..., target). Addie's compliance path therefore probes against SDK defaults, not the buyer-selected compliance target — likely returns a different profile for agents pinned to a non-default version. Three near-identical implementations, and one of them has already drifted; extract a single inferHostedAuthProbeTask(agentUrl, sdkAuth, target, { existingProbeTask? }) in hosted-compliance-version.ts and call it from all three.
  • Missing precedence test for withHostedAuthTestKit. server/tests/unit/storyboards.test.ts:217-254 covers the happy path but not the precedence claim that the function will not overwrite a pre-set test_kit.auth.api_key or test_kit.auth.basic. One expect(withHostedAuthTestKit({ auth: { type: 'bearer', token: 'fresh' }, test_kit: { auth: { api_key: 'preset' } } }).test_kit.auth.api_key).toBe('preset') plus the analogous basic case is enough — without it, a future refactor that flips the !nextAuth.api_key guard ships silently.
  • Discovery-failure log path can round-trip credentials. server/src/addie/mcp/member-tools.ts:453, server/src/addie/services/compliance-testing.ts:67, server/src/routes/registry-api.ts:6296 log { err, agentUrl }. Pino redact at server/src/logger.ts:151-171 is path-based and will not scrub Authorization: Bearer ... or Basic ... substrings embedded in err.message / err.stack (the common axios/undici behavior on auth-rejected requests). No known leak today, but the new pre-discovery call is now a credential-bearing path. Either log { name: err.name, code: err.code, agentUrl } only, or scrub bearer/basic patterns before logging.

Minor nits (non-blocking)

  1. Buyer operation_id echoed unbounded. server/src/training-agent/webhooks.ts:389-397 now round-trips a buyer-supplied payload.operation_id through the JWS-signed body with only a non-empty check upstream. A 1 MB string or one with control characters rides through. The fallback to task_id is server-derived and safe — only the explicit buyer-echo path needs a length and charset bound.

LGTM. Follow-ups noted below.

@bokelley bokelley merged commit 225bbce into main May 30, 2026
28 checks passed
@bokelley bokelley deleted the basic-auth-prod-runs branch May 30, 2026 20:36
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