Skip to content

Grants + the see-it-work hub: the agent buys while you're away — clickable (#104) - #112

Merged
dzuluaga merged 7 commits into
mainfrom
demo/see-it-work
Jul 25, 2026
Merged

Grants + the see-it-work hub: the agent buys while you're away — clickable (#104)#112
dzuluaga merged 7 commits into
mainfrom
demo/see-it-work

Conversation

@dzuluaga

Copy link
Copy Markdown
Contributor

In plain terms

You approve once"the agent may spend up to $100 at this store, max $30 per purchase, drinks only" — and then the agent buys while you're away. Every rule is enforced by the server on every purchase: the wrong item is refused, an over-cap purchase is refused, a revoked grant is refused, and age-restricted goods always hand back to a human no matter the budget.

This is the first capability the original demo never had — and you can click through all of it on the demo hub (Section 3) and watch every ✓ and ✗ land in a live feed.

What you're approving

  • New code, additive — a new credentagent.grants resource over the already-shipped DelegatedGate engine; no existing surface changes (one optional catalog option, one optional category field).
  • Worst case if wrong: a refusal shows the wrong reason code. The refusals themselves are the engine's, already tested — plus two new checks (allowed-items, must-be-approved) each pinned by a test that fails if the check is deleted (verified red).
  • What stays safe: the grant approval is a demo stand-in for the wallet ceremony (labeled delegated-demo); no real money moves.

How to test

npm run build && npm test                 # gate 354 · storefront 86 · lint clean
node examples/demo-hub/hub.mjs            # → http://localhost:4000, Section 3

Pre-approve → Approve → try coffee (✓ $82 left), headphones (✗ not-allowed), espresso machine (✗ over the $30 cap), wine (✗ age → needs a human), six coffees (✗ budget spent), Revoke (✗ revoked).


For reviewers — the detail

Sub-issue #104 under epic #97; surface per spec 009 (grants.create/retrieve, grant.spend/revoke, typed GrantDoorCode door, FR-007 lifecycle pending → authorized/denied → revoked, idempotent replay). The allow bounds implement Ever's #95 review point in shipped code (fail-closed, not-allowed), and the _authorize/_deny seam is exactly where the wallet key-signing ceremony (#71) plugs in later. Engine refusals map: over-cap→per-spend-exceeded, over-total→budget-exceeded, out-of-scope→wrong-merchant, step-up (age non-delegable) passes through. Money: plain dollar numbers, consistent with the shipped orders surface (recorded on #104 per the spec's FR-005 reconciliation note). Also in this branch: the demo hub itself (Sections 1–2: checkout + live-verified webhook feed) and GUARANTEES.md — plain-English guarantees, each with the one command that proves it.

Refs #104 · epic #97 · spec 009

🤖 Generated with Claude Code

https://claude.ai/code/session_01Harfquzhqg5SCCURyYRbHD

dzuluaga added 2 commits July 25, 2026 12:13
…ch the signed webhook verify live

Closes the reviewer's confidence gap: a stakeholder who can't read diffs can now open one page
and validate the product by USING it. Built on the real library (orders.serve + webhooks):
- a checkout an agent drives (ungated = click-to-complete, no wallet; wine = the real 21+ gate),
- a live feed that shows each order.settled webhook arrive and get SIGNATURE-VERIFIED in real time
  (the invisible cross-service notification, made visible).

Plus GUARANTEES.md — plain-English 'what each piece guarantees + the one command that proves it',
every command a test that fails if the protection is removed. Verified the promised output matches.

  node examples/demo-hub/hub.mjs   # → http://localhost:4000

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
… while you're away (visible in the hub)

The first NEW capability the original demo never had, shipped end-to-end and CLICKABLE:

Library (grants.ts, over the real DelegatedGate engine):
- grants.create({ merchant, budget, perSpend, allow? }) → pending grant; the human approves
  once (the _authorize/_deny seam — today the demo page, the wallet ceremony later calls the
  SAME seam); grants.retrieve(id) rehydrates; grant.spend()/revoke().
- allow bounds: WHAT may be bought (SKUs/categories), enforced server-side fail-closed →
  'not-allowed' (#95 review, Ever's point — now in shipped code, not just the spec).
- Typed GrantDoorCode door mapping the engine refusals (per-spend-exceeded, budget-exceeded,
  wrong-merchant, step-up, revoked…). Age is NON-delegable: wine refuses step-up/needs-human.
- Lifecycle per spec FR-007: pending → authorized/denied(terminal) → revoked; idempotent replay.
- CatalogEntry gains optional category (additive); client gains { catalog } option.

Hub (Section 3): pre-approve '$100 · $30/purchase · Beverages' → YOU approve/deny → play the
agent: coffee ✓ ($82 left), headphones ✗ not-allowed, espresso ✗ per-spend, wine ✗ step-up,
6th coffee ✗ budget-exceeded, revoke → ✗ revoked — every outcome lands in the live feed.

Money decision (spec FR-005 reconciliation): grants use plain dollar numbers, consistent with
the shipped orders surface — recorded for #104.

Tests: 9 (grants.test.ts); the two NEW controls (allow bounds, status gate) proven RED when
removed. Full suite green: gate 354, storefront 86; lint clean. Verified end-to-end headless
AND in the browser (feed screenshot).

Refs #104 · epic #97 · spec 009

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
credentagent-demo Ready Ready Preview, Comment Jul 25, 2026 6:24pm

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 51771e2af1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/credentagent-gate/src/grants.ts
Comment thread packages/credentagent-gate/src/grants.ts
Comment thread packages/credentagent-gate/src/grants.ts
Comment thread packages/credentagent-gate/src/grants.ts Outdated
Comment thread packages/credentagent-gate/src/grants.ts Outdated
…roveUrl real, honest doors

All five Codex findings on #112:
- P1 sealed bounds: create() now deep-freezes a structuredClone snapshot — neither mutating
  grant.allow nor the caller's original options object can widen what the human approved.
  Proven RED when the freeze is removed.
- P1 approveUrl: grants.serve(app) (new grants-serve.ts) serves the approve/deny page at
  /credentagent/grants/:id + the POST actions — the documented create-and-send-the-link flow
  works end-to-end (verified over HTTP: page 200 → public approve → authorized → spend ok).
  The page is the demo stand-in for the wallet ceremony; #71 replaces it via the same seams.
- P2 refusal replay: an idempotency key now replays the ORIGINAL outcome, refusal included —
  a refused key can't be repurposed with a cheaper item. Proven RED when reverted.
- P2 multi-item: items must be exactly one → typed 'invalid-request' (key NOT consumed);
  nothing is silently dropped.
- P2 honest mapping: every engine RefusalCode mapped deliberately (invalid-amount surfaces
  as itself; unpermitted-presentment → step-up; not-yet-valid → expired); the unreachable
  integrity class coarsens to the catch-all 'refused' — never misreported as 'revoked'.

Lifecycle-status refusals stay deliberately uncached (pending → authorized transitions must
let a retry proceed) — documented inline.

Tests: 14 (was 9); gate 359 + storefront 86 green; lint clean.

Refs #104 · addresses #112 review

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
dzuluaga added 3 commits July 25, 2026 13:18
Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
…API; facade deleted

Answering the duplication audit on #112:
- examples/grants-proto/grants.mjs (the #95 hand-rolled GrantsProto facade + usd shim +
  REASON_MAP) DELETED — it duplicated what grants.ts now ships. Per the spec's graduation
  promise, server.mjs is REWIRED onto the real credentagent.grants API (same two-pane UI;
  verified over HTTP: create+authorize → wine ✓ $20 → case ✗ per-spend-exceeded →
  replay ✓ → revoke → ✗ revoked).
- examples/hnp-draws: marked as the lower-level DelegatedGate seams reference, superseded
  for everyday use by grants + the demo hub.
- examples/README: HNP section points at the graduated demo.

Audited, NOT duplicated: orders-proto already consumes the real API (no facade); Ever's #103
is the external verify/settle rail — zero grants surface (only client.ts/index.ts merge-order
overlap). #44 remains recommended for closure (superseded by #41 + this).

Merged main in (brings #95 protos + #111). Gate 359 + storefront 86 green; lint clean.

Refs #104 · addresses the #112 duplication audit

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
…redentagent into demo/see-it-work

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
@dzuluaga
dzuluaga merged commit 1b37c19 into main Jul 25, 2026
7 checks passed
dzuluaga added a commit that referenced this pull request Jul 25, 2026
Version bump for both packages (gate dep → ^0.3.0 in the storefront). Since 0.2.0:
- credentagent.orders.* + orders.serve(app) — the checkout in one call (#98)
- credentagent.webhooks — signed HTTP order.settled + constructEvent (#102)
- credentagent.grants.* + grants.serve(app) — approve once, spend while away;
  allow bounds, typed GrantDoorCode, immutable sealed bounds, refusal replay (#112)
- OrderDoorCode typed union (#111); demo hub + guarantees (#112)

README: adds the missing Grants section (published docs must match the surface);
honesty labels unchanged (presence-only-demo / delegated-demo / server-issued-demo).

The quickstart's dep bump to ^0.3.0 follows AFTER publish (its CI smoke installs
from the registry, so bumping first would 404).

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
dzuluaga added a commit that referenced this pull request Jul 25, 2026
…115)

Version bump for both packages (gate dep → ^0.3.0 in the storefront). Since 0.2.0:
- credentagent.orders.* + orders.serve(app) — the checkout in one call (#98)
- credentagent.webhooks — signed HTTP order.settled + constructEvent (#102)
- credentagent.grants.* + grants.serve(app) — approve once, spend while away;
  allow bounds, typed GrantDoorCode, immutable sealed bounds, refusal replay (#112)
- OrderDoorCode typed union (#111); demo hub + guarantees (#112)

README: adds the missing Grants section (published docs must match the surface);
honesty labels unchanged (presence-only-demo / delegated-demo / server-issued-demo).

The quickstart's dep bump to ^0.3.0 follows AFTER publish (its CI smoke installs
from the registry, so bumping first would 404).

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
dzuluaga added a commit that referenced this pull request Jul 26, 2026
… main)

main advanced 11 commits past this branch's fork point (the orders / webhooks /
grants merges — #98, #102, #112, #118, #121, #123 — and the #120 browse-products
fix). GitHub flagged PR #103 CONFLICTING. Reconcile MERGE (not rebase — the repo's
precedent is reconcile merges, and rebasing would rewrite pushed, DCO-signed history).

Two files conflicted (both-add — the two features touched adjacent lines, neither
restructured the other's seam); resolved by keeping BOTH sides:

- packages/credentagent-storefront/src/server.ts — the StorefrontOptions interface.
  BOTH won: kept the branch's `verifier?: DelegatedVerifier` (008 delegated ceremony)
  AND main's `grants?: Grants` + `merchant?: string` (spec 009 human-not-present).
  They are independent optional fields; a delegated storefront and a grants storefront
  compose. (Restored a `/**` the raw resolution dropped — caught by tsc, re-verified.)

- packages/credentagent-storefront/src/server.test.ts — two separate describe blocks
  appended at end of file, sharing the trailing closers. BOTH won: kept the branch's
  "008 — delegated ceremony completes through the storefront (S5)" suite (the e2e
  delegated payment + the wrong-amount refusal) AND main's "browse-products is
  agent-legible (#120)" suite; closed each block explicitly.

Auto-merged with both sides preserved (no hand resolution needed): client.ts (main's
grants/webhooks wiring AND the branch's `this.delegated` verifier wiring), index.ts
(main's grants exports AND the branch's DelegatedVerifier exports), the two READMEs.
The #103 double-settle fix (completion.ts, ceremony/types.ts, keyed-mutex.ts) was
branch-only — main never touched completion.ts, so it merged untouched.

Verified after resolving: build (both packages typecheck), gate suite 380 + storefront
99 green, lint clean, and the security-critical files re-run explicitly — the delegated
verify refusals + amount binding (delegated-payment 37, server delegated e2e), the new
double-settle test (completion 33), and main's grants tests (grants 14 + grants-tools 10)
all pass. Neither feature regressed the other.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5Y1aX9ffAF26hP4fgQFi7
Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
dzuluaga added a commit that referenced this pull request Jul 28, 2026
…cents (#104) (#135)

* fix(grants,#104): serialize concurrent same-key spends + price in integer cents

Two correctness bugs shipped in the just-merged grants feature (PR #112), both found by
comparing against the closed alternative implementation (PR #106) and reproduced against
the merged code. This ports #106's fixes forward with NO public API change — additive,
plain-dollar surface unchanged.

1. Concurrent same-key spends were misreported as "revoked". Two spend() calls with the
   SAME idempotencyKey that overlap in time both missed the in-memory idempotency cache
   and reached the engine; the loser hit the atomic single-use ledger's "consumed" branch,
   which the spend door maps to "revoked"/terminal — telling an unattended agent the grant
   is dead when nothing was revoked and the purchase actually succeeded once. A per-grant
   async mutex now serializes each grant's spend + lifecycle transitions, so a losing retry
   replays the original outcome cleanly (ok, replayed:true). Money was never double-charged
   (the ledger is atomic); the bug was the misreport + broken retry-safety under concurrency.

2. Exact-budget spends on non-round prices were falsely refused. Pricing ran in
   floating-point dollars, so 4.9 + 4.9 + 4.9 === 14.700000000000001, which exceeds a $14.70
   budget and refused the third spend with budget-exceeded. The engine now receives the
   catalog + per-grant caps in integer cents and its amounts are converted back to dollars at
   the door, so every cap comparison is exact. The public API stays plain dollars (no Money
   type — the spec-009 FR-005 decision stands).

Adds the concurrency + money-boundary tests the merged suite lacked (which is exactly why
these shipped): three load-bearing bypass tests (each verified RED with its control reverted,
then restored) plus two regressions. delegated.ts — the unit-agnostic engine — is untouched.

Known follow-up, NOT in this PR: grants are in-memory only, so "authorize now / spend later
from a worker" (spec-009 FR-007) holds only within one process; a shared store + CAS is the
durable multi-instance step (see the issue #104 comparison comment).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>

* fix(grants,#104): address Codex review — revoke-wins mid-spend, live cents catalog, sub-cent reject

Three follow-ups on PR #135 from the automated Codex review.

1. P1 — revoke-wins mid-spend. #135 wrapped revoke() in the per-grant mutex, so a revoke
   issued while a spend was in flight QUEUED behind that spend and could not reach the
   engine's revocation ledger until after the draw committed — the spend settled after
   revocation, which spec 009 FR §136-140 requires be refused. The ledger revoke now runs
   IMMEDIATELY, outside the per-grant queue, so an in-flight spend's atomic settle-time
   re-check sees it and refuses; the status flip + the authorize-race re-revoke stay
   serialized. The same-key serialization from #135 is unchanged.

2. P1 — live catalog re-pricing. #135's centsCatalog() snapshotted prices at first engine
   construction, so a host that re-priced an item in memory kept spending at the stale
   price (and a sealed cap was enforced against it). Replaced with a live cents VIEW (a
   Proxy) the engine reads per spend, restoring the pre-#135 per-read behavior — a
   re-price, or a newly-added item, is honored at the next spend.

3. P2 — sub-cent inputs. toCents() silently rounded ($0.006 -> $0.01, 1.005 -> $1.00).
   Per the DX rubric it now REJECTS sub-cent precision with a clear error at the earliest
   point — grant caps at create(), catalog prices when they are priced — rather than
   silently changing the value.

Each fix has a load-bearing bypass test (verified RED with its control reverted, then
restored): a spend refused by a revoke landing mid-flight; a re-price honored at the next
spend with the cap enforced against the live price; sub-cent rejected at config and at
pricing. delegated.ts stays untouched; the public API stays plain dollars.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>

---------

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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