From 09250e4e04953bbecb6ae06cfd0c23f82dca04a6 Mon Sep 17 00:00:00 2001 From: selezenart Date: Sun, 13 Sep 2026 16:02:03 +0200 Subject: [PATCH] docs: refresh README and add front-end derived banner README had drifted 23 commits behind develop. Bring it back to the shipped surface and give it the product's own nav panel as a banner. Documentation corrections: - Add packages/brand to the repository layout and describe apps/api's MCP endpoint and personal-credential role. - Replace the retired four-tab console and "Tools" section with the five cabinet sections that exist today, and document the /docs/mcp route. - Add the user-wallet, MCP, and profile-credential routes to the API table, and note that the transport and operator routes sit outside the frozen v1 contract pack. - Describe the non-custodial MCP payment flow and digest-stored personal bearers in a new Agent access section. - Record that a browser caller's workspace is derived from its verified Privy subject, and that every committed settlement captures Graph evidence through a durable outbox job with a backfill for older settlements. - Repair a duplicated sentence fragment in Project status and add the user-wallet and MCP rows to the status table. The banner is not hand-drawn. scripts/render-nav-panel.mjs reads the palette from packages/brand/src/tokens.css, the mark geometry from CommitRing.tsx, and the nav labels from apps/web/src/App.tsx, then emits one SVG per theme. GitHub strips CSS from Markdown, so the README selects between them with a element. tokens.css remains the only source of a brand colour. --- .../20260913T160000Z-readme-refresh.md | 76 +++++++ README.md | 136 +++++++++---- docs/assets/nav-panel-dark.svg | 27 +++ docs/assets/nav-panel-light.svg | 27 +++ scripts/render-nav-panel.mjs | 187 ++++++++++++++++++ 5 files changed, 412 insertions(+), 41 deletions(-) create mode 100644 .agent/context/20260913T160000Z-readme-refresh.md create mode 100644 docs/assets/nav-panel-dark.svg create mode 100644 docs/assets/nav-panel-light.svg create mode 100644 scripts/render-nav-panel.mjs 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 9c2a42d..99fafd9 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,26 +181,36 @@ 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 wallet activity is read-only: the API records bounded Graph -observations, links indexed transfers to settlements in the configured -workspace, and surfaces unmatched transfers. Graph absence or lag never changes +observations, links indexed transfers to settlements in the caller's workspace, +and surfaces unmatched transfers. 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 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: ```bash 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 @@ -237,12 +262,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; @@ -261,26 +284,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.** @@ -292,7 +343,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 @@ -300,7 +353,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');