fix(onramp-kit): harden money-action dispatch + repair approval window (C-03) - #282
Open
LamaSu wants to merge 1 commit into
Open
fix(onramp-kit): harden money-action dispatch + repair approval window (C-03)#282LamaSu wants to merge 1 commit into
LamaSu wants to merge 1 commit into
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
?api=/stored override.resolveApiBase()honours a manifest-suppliedapi_basein non-host mode, so before this a shared/hostile manifest could setapi_baseand harvest the viewer's key on the first fetch. Common cases are unaffected (gateway/a/:slug= same-origin;file://artifact = canonical default).denyaction. It was dispatched withviaApproval, 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.MONEY_VERB+ aMONEY_NAMESPACEgate (/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.410(endpoint removed, e.g. the old public token-approval route),404on/fund(escrow not provenance-recognised),503(fail-closed gate) now render a clear "nothing was sent" message instead of a bare status code.winShell._setFoot()didquerySelector('.pcc-win-foot')and removed the match — butrenderApproval's action bar also carries.pcc-win-foot, so_setFootdeleted the Approve/Deny controls. Theapprovalwindow has been rendering its details with no buttons. Fixed by calling_setFootbefore 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/composeaction (no money verb) still hits the gate · POST carries anIdempotency-Keyheader · triple-click Approve = exactly one POST · key withheld from an off-originapi_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_baseto 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 — trustingmanifest.api_basewith 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