Commit 52b15fa
feat(decisioning): credential-leak strip on every echo path + ctx_metadata guidance (#481)
* feat(decisioning): wire credential strip into dispatch + webhook + registry, add ctx_metadata guidance
The typed projections (`to_wire_account` / `to_wire_sync_accounts_row`
/ `to_wire_sync_governance_row` from PR #469) shipped as public-API
helpers but no framework code called them — adopters returning loose
dicts or Pydantic ``extra='allow'`` models bypassed the strip
entirely. This wires defense-in-depth across every wire-emit
boundary so the strip is load-bearing regardless of return shape.
H1 — `_invoke_platform_method` runs `strip_credentials_from_wire_result`
on every sync return, recursively scrubbing
`governance_agents[i].authentication` and `billing_entity.bank` from
dicts/lists. Method-gated to `CREDENTIAL_BEARING_METHODS` so non-account
tools (`get_products`, `get_signals`) skip the walk on the hot path.
Typed Pydantic response models pass through (the response-side schema
forbids `authentication` structurally).
H2 — `maybe_emit_sync_completion` re-applies the strip before passing
`result` to the buyer-controlled webhook URL. Defense-in-depth so the
strip fires regardless of upstream sanitization (custom shims,
direct adopter calls).
H3 — `_project_handoff` strips before `await registry.complete(...)`.
Durable registries (Postgres, Redis) write the artifact to disk; even
in-memory, `tasks/get` returns it verbatim.
M1 — INTERNAL_ERROR `caused_by` now carries only the exception class
name, not its `str()`. Any truncation length useful for diagnostics
(200 chars) also fits a full OAuth client secret. Full traceback
stays in server logs via `logger.exception`.
M2 — `adcp.server.responses._serialize` runs `_strip_write_only_fields`
on dict items before emit. Adopters hand-building responses with
``{**db_record, ...}`` no longer smuggle credentials through.
M3 — `_build_request_context` fail-closes when `ctx_metadata` carries
keys ending in `credential`, `credentials`, `token`, `secret`,
`api_key`, `apikey`, `password`, `bearer` (case-insensitive, walks
nested dicts). The AdCP context-echo contract round-trips metadata
into responses; credential-shaped keys belong in
`AuthInfo.credential` / typed credential classes. Documented in the
new "ctx_metadata: write-only credentials prohibited" section of
CLAUDE.md.
Paths now covered:
- Synchronous return path through `_invoke_platform_method`
- TaskHandoff completion path through `registry.complete`
- Sync-completion webhook path through `maybe_emit_sync_completion`
- INTERNAL_ERROR wire envelope (`caused_by.message` removed)
- Hand-built response builder path through
`adcp.server.responses._serialize`
- ctx_metadata fail-closed gate at `_build_request_context`
Refs #463, #452. Completes the wiring PR #469's projections were missing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(decisioning): close credential-leak gaps in builder, registry, and ctx_metadata gate
Three security-review should-fixes against the credential-leak strip:
* sync_governance_response / sync_accounts_response builders now route
items through `_serialize`, so loose-dict adopters spreading
`governance_agents[i].authentication` or `billing_entity.bank` onto a
hand-built response get the same scrub as `_invoke_platform_method`.
Replaces a placeholder test that documented the gap with `if leaked: pass`.
* `InMemoryTaskRegistry.complete()` now strips before persisting. The
WorkflowHandoff path has the adopter calling `registry.complete()`
directly from an external workflow — the framework is not on the call
stack at that point, so the dispatcher-side strip cannot fire. Method
gate uses the persisted `record.task_type`; idempotent on already-stripped
payloads, so the dispatcher's pre-strip remains a no-op double-pass.
* `_validate_ctx_metadata_credentials` now recurses into list values via
`_walk_ctx_metadata_list`. A buyer-supplied
`{"upstream_configs": [{"api_token": "..."}]}` previously slipped past
silently; it now trips the gate the same as a flat key, with the diagnostic
walking to the offending list index.
Tests assert real behavior on every gap (no `if leaked: pass`); broad
regression suite stays green.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 9985086 commit 52b15fa
8 files changed
Lines changed: 1191 additions & 46 deletions
File tree
- src/adcp
- decisioning
- server
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
46 | 87 | | |
47 | 88 | | |
48 | 89 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
329 | 329 | | |
330 | 330 | | |
331 | 331 | | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
332 | 439 | | |
| 440 | + | |
333 | 441 | | |
334 | 442 | | |
| 443 | + | |
335 | 444 | | |
336 | 445 | | |
337 | 446 | | |
| |||
0 commit comments