Skip to content

fix(onramp-kit): harden money-action dispatch + repair approval window (C-03) - #282

Open
LamaSu wants to merge 1 commit into
masterfrom
fix/onramp-kit-c03-money-actions
Open

fix(onramp-kit): harden money-action dispatch + repair approval window (C-03)#282
LamaSu wants to merge 1 commit into
masterfrom
fix/onramp-kit-c03-money-actions

Conversation

@LamaSu

@LamaSu LamaSu commented Aug 3, 2026

Copy link
Copy Markdown
Owner

What

Client-side C-03 hardening for the on-ramp kit (apps/dashboard/public/ui-kit/v1/pcc-ui.js) — the renderer that turns an untrusted LLM-authored manifest into a dashboard in a context where the viewer's PCC key is in scope. Closes the money-action gaps that survived the server-side C-03 containment, and repairs a latent bug that silently disabled the approval surface.

Changes

  1. Origin hard-bind for the viewer key (the most serious) — the Bearer key now travels ONLY to a base the viewer vouches for: same-origin, the canonical PCC origin, or a ?api=/stored override. resolveApiBase() honours a manifest-supplied api_base in non-host mode, so before this a shared/hostile manifest could set api_base and harvest the viewer's key on the first fetch. Common cases are unaffected (gateway /a/:slug = same-origin; file:// artifact = canonical default).
  2. Deny is UI-only — the approval window's Deny button no longer dispatches the manifest's deny action. It was dispatched with viaApproval, which SKIPS the money gate — so a manifest could make "Deny" a one-click unapproved payment. Deny now just closes the surface; nothing is sent.
  3. Fail-closed money detection — broadened MONEY_VERB + a MONEY_NAMESPACE gate (/api/{escrow,fiat-ramp,compose}) so a write into a money route can't slip past the Approval gate as "not money". Action-button styling uses the same predicate as the gate.
  4. Real Idempotency-Key HEADER — the server's idempotency middleware reads the header, not a body field (the 4th arg was silently ignored). The key is now stable per action instance, so a double-click/retry dedupes instead of double-charging; plus a re-entrancy guard (one in-flight POST per action) and a gate-open guard (no stacked approval modals).
  5. Honest messages for the C-03 endpoint changes410 (endpoint removed, e.g. the old public token-approval route), 404 on /fund (escrow not provenance-recognised), 503 (fail-closed gate) now render a clear "nothing was sent" message instead of a bare status code.
  6. Repair (latent bug found while testing): winShell._setFoot() did querySelector('.pcc-win-foot') and removed the match — but renderApproval's action bar also carries .pcc-win-foot, so _setFoot deleted the Approve/Deny controls. The approval window has been rendering its details with no buttons. Fixed by calling _setFoot before appending the action bar.

Test

7 new live-mode jsdom cases in packages/spec/src/__tests__/ui-kit-render.smoke.test.ts (fetch mocked), each asserting one item through the real render/dispatch path: Deny sends nothing · a /api/compose action (no money verb) still hits the gate · POST carries an Idempotency-Key header · triple-click Approve = exactly one POST · key withheld from an off-origin api_base · key sent to the canonical origin · 410 → "no longer available".

15/15 pass (8 pre-existing snapshot tests + 7 new), verified on DGX Spark.

Reviewer note — one behavioral tradeoff

The origin hard-bind means a self-hosted PCC manifest that sets api_base to a non-canonical origin will get public reads only until the viewer opts in (?api= or the Connect bar). That's intentional for the shared-artifact threat model — trusting manifest.api_base with the viewer's key is exactly the exfil hole being closed — but flagging it in case a first-party non-canonical deployment relies on auto-send.

Scope

Client kit + its smoke test only. No server, contract, or agent-package change. Complements the merged pack fix (#280) and server containment (#274).

🤖 Generated with Claude Code

…w (C-03)

The on-ramp kit renders an UNTRUSTED manifest in a context where the viewer's
PCC key is in scope. This closes the money-action gaps that survived the C-03
containment, plus a latent bug that silently disabled the approval surface.

- Origin hard-bind: the viewer's Bearer key now travels ONLY to a base the
  VIEWER vouches for (same-origin, the canonical PCC origin, or a ?api=/stored
  override) — never a manifest-chosen api_base. A shared/hostile manifest could
  previously set api_base and harvest the viewer's key on the first fetch.
- Deny is UI-only: the approval window's Deny button no longer dispatches the
  manifest's deny action (which could be a money POST, and was fired with
  viaApproval — SKIPPING the money gate). Deny just closes the surface.
- Fail-closed money detection: broadened MONEY_VERB + a MONEY_NAMESPACE gate
  (/api/escrow|fiat-ramp|compose) so a write into a money route can't slip past
  the Approval gate as "not money". Button styling now uses the same predicate.
- Real Idempotency-Key HEADER (the server reads the header, not a body field),
  with a STABLE per-action key so a double-click/retry dedupes instead of
  double-charging; plus a re-entrancy guard (one in-flight POST per action) and
  a gate-open guard (no stacked approval modals).
- Clear messages for the C-03 endpoint changes: 410 (endpoint removed), 404 on
  /fund (escrow not provenance-recognised), 503 (fail-closed gate) — honest,
  "nothing was sent", instead of a bare status code.
- Repair: winShell._setFoot() removed the approval action bar because it shared
  the .pcc-win-foot class; call _setFoot before appending the bar so the
  Approve/Deny controls survive. The approval window rendered no buttons before.

Tests: 7 new live-mode jsdom cases in ui-kit-render.smoke.test.ts assert each
of the above through the real render/dispatch path. 15/15 pass (no regression).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4MciSE6gExuVxay7nvGfs
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