diff --git a/.agent/context/20260913T160000Z-readme-refresh.md b/.agent/context/20260913T160000Z-readme-refresh.md new file mode 100644 index 0000000..d64d995 --- /dev/null +++ b/.agent/context/20260913T160000Z-readme-refresh.md @@ -0,0 +1,76 @@ +# README refresh and front-end derived banner — active context + +## Date/time + +- UTC: 2026-09-13T16:00:00Z + +## User goal + +Audit the repository, bring `README.md` back in line with the current code, and +put the product's own `.top-nav` panel ("OneShot / SETTLEMENT ENGINE") into the +README, derived from the front-end source rather than from a screenshot. + +## Acceptance criteria + +- README reflects the routes, workspace sections, API surface, repository + layout, and delivery status that exist on `develop` today. +- The README banner is generated from `packages/brand/src/tokens.css`, + `packages/brand/src/CommitRing.tsx`, and `apps/web/src/App.tsx`; no colour, + mark geometry, or nav label is retyped by hand. +- `markdownlint`, `prettier --check`, `eslint`, and `tsc -b` stay green. +- No behaviour, contract, or configuration change. + +## Assumptions + +- GitHub strips CSS from Markdown, so the panel ships as two static SVGs (one + per theme) chosen by a `` element rather than as live markup. +- Advance widths in the renderer are estimates; Rubik cannot be measured + without a font engine, and sub-pixel slack inside a pill is not visible. + +## Non-goals + +- No change to `apps/web`, the API, or any adapter. +- No sponsor-qualification claim beyond what `docs/settlement/LIVE_EVIDENCE.md` + and the C06 report already support. + +## Branch state + +- Branch: `feature/readme-refresh` +- Base: `origin/develop` at `65200cc2dfcf22912e532a157232e439d623044f` +- Untracked `packages/brand/test/slice-styles.test.ts` is unrelated user work + and stays out of this change. + +## Drift corrected in README + +- `packages/brand` and the MCP/agent role of `apps/api` were missing from the + repository layout. +- The `/docs/mcp` route, the five cabinet sections, and the Profile MCP bearer + flow were undocumented; the old copy still described the legacy four-tab + console and a "Tools" section that no longer exists. +- The API table was missing `/v1/jobs/user-wallet/prepare`, + `/v1/jobs/{jobId}/user-wallet/submit`, `/mcp`, and the + `/v1/profile/mcp-token` routes. +- Workspace identity is now derived from the verified Privy subject, not from + the configured workspace id. +- Graph evidence is captured for every committed settlement, with a backfill. +- A duplicated sentence fragment in Project status was repaired. + +## Commands and results + +- `node scripts/render-nav-panel.mjs` — wrote both SVGs. +- `npx markdownlint-cli2 README.md` — 0 errors. +- `pnpm format:check` — all matched files use Prettier style. +- `pnpm lint` — clean. +- `pnpm typecheck` — clean. + +## Gate state + +The user explicitly waived FreePi Gate A and Gate B for this documentation-only +change and asked for a draft pull request instead. No gate verdict exists, so +the PR stays in draft until a human decides how to proceed. + +## Remaining risk + +- Local `pnpm test` / `pnpm test:browser` were not re-run; the change touches + no source consumed by either suite. +- No independent review evidence backs this tree. diff --git a/README.md b/README.md index 21a2bb0..cd0eed2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ + + + + OneShot settlement engine — Arc Testnet, USDC, open workspace + + # OneShot **One job. Many retries. One settlement.** @@ -8,10 +22,10 @@ parallel workers, and multiple agent instances. Product direction: **resumable paid tools for business agents** — resume the job, not the payment. The settlement engine includes one team-operated testnet -report supplier, task-bound order/result delivery, and separate public (`/`) -and authenticated cabinet (`/app`) routes. Resumable external work requires -supplier support; this is not a guarantee of exactly-once execution for -arbitrary tools. +report supplier, task-bound order/result delivery, a one-tool MCP endpoint for +agents, and separate public (`/`), agent-docs (`/docs/mcp`), and authenticated +cabinet (`/app`) routes. Resumable external work requires supplier support; this +is not a guarantee of exactly-once execution for arbitrary tools. The cardinality it protects is: @@ -137,9 +151,10 @@ lock: OneShot's durable state is. ## Repository layout ```text -apps/api HTTP seam -apps/web composed operator UI (intent, settlement, recovery) +apps/api HTTP seam, MCP endpoint, personal MCP credentials +apps/web landing, agent docs, and operator workspace apps/worker settlement and reconciliation workers +packages/brand brand tokens, commit-ring mark, hero geometry packages/supplier-adapter idempotent team-operated testnet report connector packages/contracts frozen v1 contract pack, OpenAPI, fixtures packages/domain intent, attempt, and settlement state @@ -166,20 +181,29 @@ pnpm build:frontend pnpm --filter @oneshot/web dev ``` -Open `http://localhost:3000/`. The app shell composes create/replay, -authoritative status, settlement evidence, and recovery evidence tabs. The -settlement tab reads the configured OneShot API; the recovery tab projects the -frozen `recovery-view` API into the C05 timeline model, with labelled -fail-closed fallbacks for legacy or unavailable evidence. The P5 browser -acceptance suite runs with Playwright/Chromium in CI. +Open `http://localhost:3000/` for the public landing page, `/docs/mcp` for the +agent connection guide, and `/app` for the authenticated workspace. The +workspace composes five sections — Overview, Payment services, Requests, +Payment proof, and Profile. Payment proof reads the configured OneShot API and +projects the frozen `recovery-view` API into the C05 timeline model, with +labelled fail-closed fallbacks for legacy or unavailable evidence, and +distinguishes a Graph observation that is still pending from proof that no +payment happened. Profile issues and rotates the personal MCP bearer. The P5 +browser acceptance suite runs with Playwright/Chromium in CI. Authenticated site activity is read-only: the API automatically queries the configured Arc subgraph for every payer wallet recorded in the workspace, records bounded Graph observations, and displays one audit row for every site payment request, including rejected, failed-safe, uncertain, and committed outcomes. Indexed transfers are linked to settlements and unmatched transfers -remain visible. Graph absence or lag never changes payment authority; a missing -or reverted transfer event is not proof that no payment happened. +remain visible. Every committed settlement captures Graph evidence through a +durable, idempotent outbox job, including a backfill for settlements that +predate that capture. Graph absence or lag never changes payment authority; a +missing or reverted transfer event is not proof that no payment happened. + +A browser caller's workspace is derived from its verified Privy subject, so +jobs, results, and activity are scoped to the signed-in operator rather than to +a caller-supplied identifier. Integration tests need a database: @@ -187,8 +211,9 @@ Integration tests need a database: pnpm test:integration ``` -Copy `.env.example` to `.env` and fill in placeholders. Never commit a real -secret; see `docs/settlement/SETTLEMENT_CONFIG_V1.md` for how each variable is +Copy `.env.example` to `.env` and `apps/web/.env.example` to +`apps/web/.env.local`, then fill in placeholders. Never commit a real secret; +see `docs/settlement/SETTLEMENT_CONFIG_V1.md` for how each variable is classified. ### Operator sign-in @@ -240,12 +265,10 @@ Cloudflare Workers Build checkout. ### Arc Testnet transfer demo The resumable job flow uses a deliberately labelled team-operated supplier -until an external supplier is selected. In Tools, enter the exact Arc Testnet -recipient and USDC amount for the purchase. The amount must be within the -settlement cap. The existing worker authorizes and submits the exact quote -through Privy on Arc Testnet. A committed job's settlement and ArcScan -evidence remain authoritative; delivery resume never submits a replacement -payment. +until an external supplier is selected. In Payment services, enter the exact Arc +Testnet recipient and USDC amount for the purchase. The amount must be within +the settlement cap. A committed job's settlement and ArcScan evidence remain +authoritative; delivery resume never submits a replacement payment. `pnpm demo:r4` runs the response-loss drill offline by default. The live mode requires an explicit Arc Testnet confirmation and the reviewed worker hook; @@ -264,26 +287,54 @@ shown for retries; users do not need to invent one. After settlement, the job list links directly to ArcScan and keeps the supplier result separate from payment evidence. -| Method | Path | Purpose | -| ------ | -------------------------------- | ------------------------------------------------------------- | -| `POST` | `/v1/intents` | Create an intent; an identical replay returns the same result | -| `GET` | `/v1/intents/{id}` | Authoritative intent, attempts, settlement, evidence | -| `POST` | `/v1/intents/{id}/reconcile` | Trigger read-only reconciliation; never submits | -| `GET` | `/v1/intents/{id}/recovery-view` | Local authority plus labelled provider observations | -| `POST` | `/v1/jobs` | Start/replay one workspace-scoped team report task | -| `POST` | `/v1/jobs/quote` | Return a non-chargeable quote before explicit approval | -| `GET` | `/v1/jobs` | List workspace jobs and delivery state | -| `GET` | `/v1/jobs/{jobId}` | Read a workspace-owned job | -| `POST` | `/v1/jobs/{jobId}/resume` | Resume original supplier delivery; never submits payment | -| `GET` | `/v1/jobs/{jobId}/result` | Retrieve an existing supplier result; never submits payment | -| `GET` | `/v1/activity` | Last bounded Graph activity observation and local comparison | -| `POST` | `/v1/activity/refresh` | Manually refresh Graph activity; no settlement action | -| `GET` | `/v1/metrics` | Operational metrics | -| `GET` | `/health/live` | Process liveness | -| `GET` | `/health/ready` | Configuration and Arc identity readiness | +| Method | Path | Purpose | +| ------ | ------------------------------------- | -------------------------------------------------------------- | +| `POST` | `/v1/intents` | Create an intent; an identical replay returns the same result | +| `GET` | `/v1/intents/{id}` | Authoritative intent, attempts, settlement, evidence | +| `POST` | `/v1/intents/{id}/reconcile` | Trigger read-only reconciliation; never submits | +| `GET` | `/v1/intents/{id}/recovery-view` | Local authority plus labelled provider observations | +| `POST` | `/v1/jobs` | Start/replay one workspace-scoped team report task | +| `POST` | `/v1/jobs/quote` | Return a non-chargeable quote before explicit approval | +| `POST` | `/v1/jobs/user-wallet/prepare` | Bind a payer wallet and return the exact transfer to sign | +| `GET` | `/v1/jobs` | List workspace jobs and delivery state | +| `GET` | `/v1/jobs/{jobId}` | Read a workspace-owned job | +| `POST` | `/v1/jobs/{jobId}/user-wallet/submit` | Bind a signed transaction hash and verify its receipt | +| `POST` | `/v1/jobs/{jobId}/resume` | Resume original supplier delivery; never submits payment | +| `GET` | `/v1/jobs/{jobId}/result` | Retrieve an existing supplier result; never submits payment | +| `GET` | `/v1/activity` | Last bounded Graph activity observation and local comparison | +| `POST` | `/v1/activity/refresh` | Manually refresh Graph activity; no settlement action | +| `GET` | `/v1/metrics` | Operational metrics | +| `GET` | `/health/live` | Process liveness | +| `GET` | `/health/ready` | Configuration and Arc identity readiness | The contract is defined in `packages/contracts/openapi/openapi.v1.json`. +### Agent access (MCP) + +Agents reach the same durable job path through one Streamable HTTP MCP +endpoint. The flow is non-custodial: `arc_payment` creates or replays a +payer-bound job and returns the exact Arc Testnet USDC transaction request, the +user's own wallet signs and broadcasts it, and `arc_payment_submit` hands back +the transaction hash so OneShot can bind it and verify the receipt and its +single matching `Transfer` log. + +The MCP bearer authenticates a workspace. It does not authorize a server payer +and cannot sign or broadcast anything. Personal bearers are issued from the +authenticated Profile and stored only as SHA-256 digests; the legacy +operator-controlled `ONESHOT_MCP_BEARER_TOKEN` remains optional. + +| Method | Path | Purpose | +| ------ | ------------------------------ | -------------------------------------------------------------- | +| `ALL` | `/mcp` | MCP endpoint exposing `arc_payment` and `arc_payment_submit` | +| `GET` | `/v1/profile/mcp-token` | Report whether this workspace holds a personal MCP bearer | +| `POST` | `/v1/profile/mcp-token` | Issue a personal MCP bearer; only its digest is stored | +| `POST` | `/v1/profile/mcp-token/rotate` | Replace the personal MCP bearer | + +These operator and agent-transport routes sit outside the frozen v1 contract +pack. See [`docs/MCP_ARC_PAYMENT.md`](docs/MCP_ARC_PAYMENT.md) for deployment, +client configuration, and the live walkthrough, or open `/docs/mcp` in the +running web app. + ## Project status Under active development. **Testnet only.** @@ -295,7 +346,9 @@ Under active development. **Testnet only.** | Recovery evidence and safety core | Live Graph/Vertex path implemented; deterministic core remains authoritative | | Graph discovery and LLM recovery agent | Studio GraphQL path implemented; fresh sponsor trace pending; deterministic core remains final | | Resumable team report job | Local code: task/order/intent binding, separate delivery and result retrieval | -| Public landing and cabinet | Local code at `/` and `/app`; live R4 demonstration evidence remains pending | +| User-wallet payments (browser and MCP) | Local code: payer binding, wallet-side signing, receipt and `Transfer` verification | +| Agent MCP endpoint | Deployed; bearer authentication and tool discovery verified, no live MCP payment trace yet | +| Public landing, agent docs, cabinet | Local code at `/`, `/docs/mcp`, and `/app`; live R4 demonstration evidence remains pending | **One live testnet settlement has been executed.** A Privy-controlled execution wallet and scoped policy authorized one 1.00 USDC Arc Testnet transfer; live @@ -303,7 +356,8 @@ wrong-recipient and above-cap denials produced zero broadcasts. A lost-response drill entered `UNKNOWN` and reconciled to that original settlement without a replacement payment. Privy and Arc are `QUALIFIED` for the documented testnet claim; see `docs/settlement/LIVE_EVIDENCE.md` and -`packages/reconciliation/docs/c06/QUALIFICATION_REPORT.md`. The Graph live +`packages/reconciliation/docs/c06/QUALIFICATION_REPORT.md`. + The Graph recovery path is currently `NOT VERIFIED` for sponsor qualification: Studio GraphQL is implemented, but a fresh live trace showing its material effect on the model and deterministic core is still required. diff --git a/docs/assets/nav-panel-dark.svg b/docs/assets/nav-panel-dark.svg new file mode 100644 index 0000000..b02f412 --- /dev/null +++ b/docs/assets/nav-panel-dark.svg @@ -0,0 +1,27 @@ + + OneShot — settlement engine + + + + + + + + + + + + OneShot + SETTLEMENT ENGINE + + + + Arc Testnet + + USDC + + LIGHT THEME + + + Open workspace + diff --git a/docs/assets/nav-panel-light.svg b/docs/assets/nav-panel-light.svg new file mode 100644 index 0000000..51fcba5 --- /dev/null +++ b/docs/assets/nav-panel-light.svg @@ -0,0 +1,27 @@ + + OneShot — settlement engine + + + + + + + + + + + + OneShot + SETTLEMENT ENGINE + + + + Arc Testnet + + USDC + + LIGHT THEME + + + Open workspace + diff --git a/scripts/render-nav-panel.mjs b/scripts/render-nav-panel.mjs new file mode 100644 index 0000000..8a95905 --- /dev/null +++ b/scripts/render-nav-panel.mjs @@ -0,0 +1,187 @@ +/** + * Renders the README banner from the product's own front-end source. + * + * The banner is the `.top-nav` panel of `apps/web` — the commit-ring mark, the + * wordmark, the network and token badges, the theme control, and the workspace + * link. Nothing here is redrawn by hand: the palette is read from + * `packages/brand/src/tokens.css`, the ring geometry from + * `packages/brand/src/CommitRing.tsx`, and the nav labels from + * `apps/web/src/App.tsx`, so the README cannot drift from the shipped UI + * without this script failing or producing a visibly different panel. + * + * GitHub strips CSS from Markdown, so the panel ships as two static SVGs (one + * per theme) selected by a `` element in the README. + * + * node scripts/render-nav-panel.mjs + */ + +import { readFile, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +const root = new URL('../', import.meta.url); +const read = (path) => readFile(new URL(path, root), 'utf8'); +const out = (path) => fileURLToPath(new URL(path, root)); + +/** Geometry mirrored from `apps/web/src/styles.css`, in px at a 16px root. */ +const NAV = { + width: 1000, + paddingX: 20, // .top-nav padding-inline 1.25rem + paddingY: 13.6, // .top-nav padding-block 0.85rem + radius: 22, // --os-radius-lg + ring: 36, // + brandGap: 12, // .brand-group gap 0.75rem + brandTextGap: 1.6, // .brand-text gap 0.1rem + nameSize: 16, // .brand-name 1rem + nameTracking: 1.6, // .brand-name letter-spacing 0.1em + tagSize: 10.4, // .brand-tag 0.65rem + tagTracking: 1.248, // .brand-tag letter-spacing 0.12em + statusGap: 9.6, // .nav-status-group gap 0.6rem + badgeSize: 12, // .status-badge 0.75rem + badgePadX: 12, // .status-badge padding-inline 0.75rem + badgeHeight: 24, // 0.3rem block padding around a 12px line + badgeContentGap: 7.2, // .status-badge gap 0.45rem + dot: 7, // .status-dot + toggleSize: 12.48, // .theme-toggle 0.78rem + toggleTracking: 1.248, // .theme-toggle letter-spacing 0.1em + togglePadX: 14.4, // .theme-toggle padding-inline 0.9rem + toggleHeight: 34, // .theme-toggle min-height + linkSize: 12.8, // .nav-console-link 0.8rem + linkPadX: 16, // .nav-console-link padding-inline 1rem + linkHeight: 30, // 0.45rem block padding around a 12.8px line +}; + +/** + * Pulls one selector's `--os-*` declarations out of the token sheet. Values + * are kept verbatim, so `rgba()` lines survive alongside the hex ones. + */ +function readTheme(css, selector) { + const block = new RegExp(`${selector}\\s*\\{([^}]*)\\}`, 'u').exec(css)?.[1]; + if (block === undefined) throw new Error(`missing token block for ${selector}`); + const tokens = {}; + for (const match of block.matchAll(/--os-([a-z-]+):\s*([^;]+);/gu)) { + const [, name, value] = match; + if (name !== undefined && value !== undefined) tokens[name] = value.trim(); + } + return tokens; +} + +/** Extracts a quoted string list or single literal from a source file. */ +function readArcs(source) { + const attempts = [ + ...(/const ATTEMPT_ARCS[\s\S]*?=\s*\[([\s\S]*?)\];/u.exec(source)?.[1] ?? '').matchAll( + /'([^']+)'/gu, + ), + ].map((match) => match[1]); + const committed = /const COMMITTED_ARC\s*=\s*'([^']+)'/u.exec(source)?.[1]; + if (attempts.length === 0 || committed === undefined) { + throw new Error('could not read commit-ring geometry from CommitRing.tsx'); + } + return { attempts, committed }; +} + +/** Reads the nav labels from the landing page so the banner cannot drift. */ +function readLabels(app) { + const pick = (className) => + new RegExp(`className="${className}">([^<]+)<`, 'u').exec(app)?.[1]?.trim(); + const name = pick('brand-name'); + const tag = pick('brand-tag'); + if (name === undefined || tag === undefined) { + throw new Error('could not read brand labels from App.tsx'); + } + const network = /className="status-badge network-badge">[\s\S]*?\/>\s*([A-Za-z ]+)\s*([^<]+)\s*([^<]+?)\s* text.replace(/&/gu, '&').replace(//gu, '>'); + +function panel(theme, arcs, labels) { + const height = Math.round(NAV.paddingY * 2 + NAV.ring); + const middle = height / 2; + const sans = theme['font-primary']; + const mono = theme['font-mono']; + + // Brand group, left-aligned inside the panel padding. + const ringY = (height - NAV.ring) / 2; + const textX = NAV.paddingX + NAV.ring + NAV.brandGap; + const stack = NAV.nameSize * 1.2 + NAV.brandTextGap + NAV.tagSize * 1.2; + const stackTop = (height - stack) / 2; + const nameBaseline = stackTop + NAV.nameSize * 0.95; + const tagBaseline = stackTop + NAV.nameSize * 1.2 + NAV.brandTextGap + NAV.tagSize * 0.95; + + // Status group, centred like the flex row that holds it. + const networkWidth = + NAV.badgePadX * 2 + NAV.dot + NAV.badgeContentGap + advance(labels.network, NAV.badgeSize); + const tokenWidth = NAV.badgePadX * 2 + advance(labels.token, NAV.badgeSize, 0, true); + const toggleLabel = 'Light theme'.toUpperCase(); + const toggleWidth = NAV.togglePadX * 2 + advance(toggleLabel, NAV.toggleSize, NAV.toggleTracking); + const statusWidth = networkWidth + tokenWidth + toggleWidth + NAV.statusGap * 2; + const statusX = (NAV.width - statusWidth) / 2; + const tokenX = statusX + networkWidth + NAV.statusGap; + const toggleX = tokenX + tokenWidth + NAV.statusGap; + + // Workspace link, right-aligned against the panel padding. + const linkWidth = NAV.linkPadX * 2 + advance(labels.link, NAV.linkSize); + const linkX = NAV.width - NAV.paddingX - linkWidth; + + const ringScale = NAV.ring / 64; + const attempts = arcs.attempts + .map( + (d) => + ` `, + ) + .join('\n'); + + return ` + OneShot — settlement engine + + + + +${attempts} + + + + + ${escape(labels.name)} + ${escape(labels.tag)} + + + + ${escape(labels.network)} + + ${escape(labels.token)} + + ${escape(toggleLabel)} + + + ${escape(labels.link)} + +`; +} + +const tokensCss = await read('packages/brand/src/tokens.css'); +const light = readTheme(tokensCss, ':root'); +const dark = { ...light, ...readTheme(tokensCss, ":root\\[data-theme='dark'\\]") }; +const arcs = readArcs(await read('packages/brand/src/CommitRing.tsx')); +const labels = readLabels(await read('apps/web/src/App.tsx')); + +await writeFile(out('docs/assets/nav-panel-light.svg'), panel(light, arcs, labels), 'utf8'); +await writeFile(out('docs/assets/nav-panel-dark.svg'), panel(dark, arcs, labels), 'utf8'); +console.log('wrote docs/assets/nav-panel-{light,dark}.svg');