Real composition behind POST /api/compose: bundle the SDK, key stays server-side - #30
Conversation
The compose screen now posts to the real route and maps the response through fromServer() instead of the client-side stub-composer, which is deleted. The result header is honest about provenance: ENCLAVE copy only for a real signature, fallback copy surfaces the server's reason, plus a proof line and validator violations when present. Also fixes a latent bug this surfaced: the SDK's opcodes.ts read its config JSON via node:fs at import time. That module is now reachable from the browser bundle (from-server.ts imports TEMPLATES from grammar.ts, which needs the opcode table), so it is loaded through a static JSON import instead, matching the pattern already used for config/addresses.8453.json in the app.
Sealed inference and the deterministic gate are wired via /api/compose; market/pair context beyond the user's own book (F3 job 2), the RecommendationRegistry commit path, and the ship Multicall remain unwired.
parseAmount rightly refuses to round a too-long fraction (rounding a ceiling up hands the composer more budget than the user typed), but that meant a model-emitted amount with more fraction digits than the token's decimals (e.g. "1000.3333333" USDC) silently vanished — empty facts, Position with legs: []. Truncate the fraction to the token's decimals before parsing; that direction is safe, since it can only understate the ceiling. Also, while touching the same mapping: - templateShort now reads the human part of the SDK's template label (after the first "·"), instead of regressing to the raw template slug. - The Ship button is disabled when the validator rejected the set, with a line explaining why, matching the Compose button's disabled styling. - Recommendation cards key on templateId+index, so a repeated template from the model no longer collides on key.
prompt.ts and templates.ts were unreachable — nothing imports either one (templates.ts was only ever imported by prompt.ts). Delete both. grammar.ts described the old six-slot grammar the SDK's own grammar.ts says explicitly was superseded and could not be built against the deployed router at all. Its only remaining consumer was slot-table.tsx's footnote, which is reworded here to state the settled reality (slots filled by selection within the deployed router's instruction set; the tables render 4 rows, not six) instead of citing an open F1 question that closed a while ago. With that gone, grammar.ts has no importers left, so it goes too. Updated the README's Layout table to match: dropped the rows for the deleted files, and fixed the claim that src/lib/compose owns "prompt assembly" — that now lives server-side, in the SDK's buildComposeMessages behind /api/compose.
A budget array with the same token address twice (case-insensitively) was accepted and produced a 200 whose fallback trips the validator's I10 (canonical, no-duplicates token order) — a client-fixable input error surfacing as a downstream validation failure instead of a clear 400 at the trust boundary.
compose() rebuilt buildComposeMessages(req, ctx) fresh for its return value, which drops the rejection-feedback suffix that a real retry carried. serve.ts then rebuilt it again for the response. Either way, the messages field could disagree with what was actually sent. compose() now threads through the messages it used for whichever attempt was last (first attempt, or the retry's, including on the internal TEMPLATE_FALLBACK path where retries were exhausted but something was genuinely sent), and serve.ts passes that through instead of reconstructing it. Also drop the module-scope dotenv/config import from subgraph.ts: it sits in the server request path (serve.ts -> context.ts -> subgraph.ts) where env is already loaded by the host, and every CLI that needs it already loads its own copy.
compose() (via #20) now runs the deterministic gate in its re-infer loop, so the facade stops re-validating enclave results with divergent chain state. An ENCLAVE result is gate-approved by construction; the internal fallback's rec is validated on its own merits, and the last model attempt's violations surface in the reason string.
2c7d7ef to
364508f
Compare
|
Review (merge conflicts with What holds up well
Findings1. Two different clocks validate the same recommendation (medium — will surface as live I7 failures). 2. Silent stub degradation in the API response (medium — honesty). 3. 4. Unauthenticated spend (low for a hackathon — worth a README line). Nits (no action needed): benign cold-start race in Review produced with Claude Code. |
…ion (#32) Fixes the four findings from the PR #30 review: - The validator now runs on the server's wall clock, computed once per request and passed into compose()'s re-infer loop — validating on the snapshot's own clock could never catch a stale or degenerate snapshot (a lagging indexer, a null _meta timestamp). liveContext() also stops anchoring observedAt at 0 when the subgraph carries no timestamp. - ServerComposeResult carries contextSource, so a stub book (subgraph down, or the no-key path) is labelled end-to-end per F3 — the compose screen now says which book the recommendation was composed against. - PROMPT_VERSION moves to the SDK prompt builder (sluice.compose/2; v1 was the app-side contract deleted in #30) and rides every response, per F2 §9. The orphaned app-side constant is removed. - README notes that /api/compose is unauthenticated spend — put a same-origin check or rate limit in front before sharing a deployment. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
PR #30 (real composition behind /api/compose) independently deleted the app's prompt/stub/grammar module family and made opcodes.ts browser-safe — everything conflicting resolves to main's side. What survives from this branch, rebuilt on main's conventions: - demo-book.ts: the 8 fixtures on the settled four templates, with labels and slot rows mirroring from-server.ts so a demo card and a shipped card read identically (main still carried the six-slot relic with instructions that have no opcode on the deployed router) - prompt-box.tsx: the oracle-limit example (inexpressible on this venue) replaced with a banded intent - slot-table.tsx: escape the apostrophe #30 shipped — `npm run lint` fails on current main without it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two fixes found by the first live run against Galileo. dotenv: PR #30 moved `import "dotenv/config"` out of config.ts into each CLI entrypoint. The spike is an entrypoint and never imported it, so loadConfig could not see ZG_PRIVATE_KEY. The dry run could not catch this — it never calls loadConfig. Failure diagnostics: the report said 75% fell back and gave nobody a lead. It now prints the failure kind per run and a violation-code histogram per arm, plus one sample rejection text. That is what turned an opaque "100% malformed" into a diagnosis: sub-second empty responses, which is a drained compute ledger rejecting requests, not a model failing to comply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What
Replaces the compose screen's client-side stub with the real F2 path — the arbitration-sdk's 0G enclave composition — running behind a single server route, so
ZG_PRIVATE_KEYnever reaches the browser and the app can deploy to Vercel.packages/arbitration-sdk/src/serve.ts, the app's only import): env config, broker singleton reused across warm invocations, live book context from the subgraph (degrades to a labelled stub if it's down),compose()with retry, the deterministic validator. It never funds the ledger — that staysnpm run fund, out-of-band.POST /api/compose(Node runtime,maxDuration: 60): body carries only{ user, prompt, budget }; limits are server policy. 400 for malformed bodies (including duplicate budget tokens), 500 only for bugs.source: TEMPLATE_FALLBACKand a human-readablereason. Provenance is never blurred; the UI renders the reason verbatim.from-server.tsmaps the response into the existing UI shapes (template labels come from the SDK's ownTEMPLATES— no second copy); the stub and its dead prompt/templates/grammar modules are deleted.ENCLAVEprovenance is reachable for the first time; a validator-rejected set renders its violations and cannot ship.ZG_*documented in the app's.env.example(server-only, neverNEXT_PUBLIC_); README covers the two modes, funding, and Vercel notes.Verification
POST /api/composereturns 200TEMPLATE_FALLBACKwith the reason; bad body → 400.Not in this PR
The funded-key live gate (needs a funded Galileo key in the deploy env — run it before demo day; note the composer prompt doesn't currently tell the model the wall-clock time, so I7 deadline violations are possible on live runs and render honestly). Prompt display consuming
messagesis deferred;RecommendationRegistry/commit path and the real shipMulticallremain unwired.