diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 58293e8..014a993 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,21 +1,24 @@ # rome-cli — architecture & reference -`rome` gives a builder — human or AI agent — **grounded Rome facts** and the **right build pattern**, through two aligned surfaces over one capability core. This document explains how it's put together, how to run it, every capability, and the security model. +`rome` carries a builder — human or AI agent — across the whole build-on-Rome lifecycle through **two aligned surfaces over one capability core**: grounded facts, the right pattern, contract calls, the Rome-unique on-ramp (`fund`/`bridge`), deploy/send, cross-VM diagnosis, and the both-lane works-gate (`verify`). This document explains how it's put together, how to run it, every capability, and the security model. -## The shape: one core, two surfaces +## The shape: one core, two surfaces, two kinds ``` - ┌──────────── capability core (read-only) ─────────────┐ - │ facts: chain · tokens · contracts · gas · balance │ - │ · programs (registry + RPC) │ - │ cookbook: cpi-recipe · patterns (SDK + curated) │ - └───────────────┬────────────────────┬─────────────────┘ - rome rome mcp - (CLI — humans + agent (stdio MCP server — - shell-outs; one-shot) MCP-native agents) + ┌───────────────── capability core (src/core/capabilities.ts) ─────────────────┐ + │ READS (no keys): facts · cookbook · call · doctor · tx · preset │ + │ ACTIONS (key-gated): deploy · send · fund · bridge · verify │ + └───────────────────────────┬───────────────────────────┬─────────────────────┘ + rome (CLI — all commands) rome mcp (stdio MCP server) + humans + agent shell-outs MCP-native agents — READS ONLY ``` -Both surfaces are generated from a **single `CAPABILITIES` registry** (`src/core/capabilities.ts`). A capability's CLI command (`facts chain`) and its MCP tool (`facts_chain`) are the *same* handler with the *same* name-stem — so the two surfaces cannot drift. That invariant is asserted by a test (`test/alignment.test.ts`) and a behavioral test that drives the real CLI dispatch (`test/cli.test.ts`). +Both surfaces are generated from a **single `CAPABILITIES` registry**. Each capability declares a **kind**: + +- **`read`** — a pure lookup or diagnosis; **holds no key**; exposed on **both** the CLI and the MCP server. +- **`action`** — writes/signs on-chain; **CLI-only and key-gated** (the signing key comes from the environment); **never** registered as an MCP tool. + +A capability's CLI command (`facts chain`) and its MCP tool (`facts_chain`) are the *same* handler with the *same* name-stem, so the surfaces cannot drift. The invariant — **MCP tools === the read capabilities, actions absent** — is asserted by `test/alignment.test.ts`, plus a behavioral test that drives the real CLI dispatch (`test/cli.test.ts`) and an end-to-end MCP client smoke (`scripts/mcp-smoke.mjs`). ## Running it — CLI vs MCP server @@ -23,67 +26,69 @@ Both surfaces are generated from a **single `CAPABILITIES` registry** (`src/core ### CLI — standalone, one-shot ```bash -rome facts chain hadrian # prints JSON, process exits -rome cookbook patterns lending +rome facts chain hadrian # prints JSON, process exits +rome fund hadrian --from base-sepolia --amount 1 # an action (needs a key; see Security) ``` -Each invocation is its own short-lived process. Nothing stays running. Pipe the JSON to `jq`, or let an agent shell out to `rome …` and read stdout. +Each invocation is its own short-lived process. Pipe the JSON to `jq`, or let an agent shell out to `rome …` and read stdout. Commands take positionals or `--flags` interchangeably. ### MCP — a stdio server your client launches for you -`rome mcp` starts a [Model Context Protocol](https://modelcontextprotocol.io) server over **stdio** (stdin/stdout) — **not** a network daemon. You do **not** run it yourself or host it anywhere. You register it once in an MCP client: +`rome mcp` starts a [Model Context Protocol](https://modelcontextprotocol.io) server over **stdio** — **not** a network daemon. You do not run or host it. Register it once: ```json -{ - "mcpServers": { - "rome": { "command": "rome", "args": ["mcp"] } - } -} +{ "mcpServers": { "rome": { "command": "rome", "args": ["mcp"] } } } ``` -The client (Claude Code, Claude Desktop, Cursor, …) spawns `rome mcp` as a **child process on demand**, speaks JSON-RPC to it over stdin/stdout, and terminates it when the session ends. The process stays alive only while the client holds its stdin open. No port, no hosting, no process manager. The MCP server exposes each capability as a tool (`facts_chain`, `facts_gas`, `cookbook_cpi_recipe`, …). +The client (Claude Code, Claude Desktop, Cursor, …) spawns `rome mcp` as a **child process on demand**, speaks JSON-RPC over stdin/stdout, and terminates it when the session ends. No port, no hosting. It exposes **only the read capabilities** as tools — an agent gets grounded facts, patterns, diagnosis, and toolchain config, but can never sign or move funds through MCP. -**When to use which:** a person or a shell script → the CLI. An MCP-native agent → configure `rome mcp` and call the tools. Same capabilities, same data, either way. +**When to use which:** a person or shell script → the CLI. An MCP-native agent → configure `rome mcp` for reads, and shell out to `rome ` (with a key in its environment) when it needs to fund/deploy/verify. ## Capabilities -All read-only. Facts resolve against the public `@rome-protocol/registry` (Hadrian + Martius) and the chain's RPC; the cookbook is grounded on the SDK's real values + a curated index. - -### `facts` — grounded chain facts (kills hallucination) +### Reads — CLI + MCP, no keys | CLI | MCP tool | What it returns | Source | |---|---|---|---| -| `rome facts chain ` | `facts_chain` | chain id, RPC, explorer, rome-evm program id, gas token | registry `getChain` | -| `rome facts tokens ` | `facts_tokens` | token list (address, mint, symbol, decimals, kind) + match-by-mint note | registry `getTokens` | -| `rome facts contracts ` | `facts_contracts` | deployed contract addresses | registry `getContracts` | -| `rome facts gas ` | `facts_gas` | live gas price + the estimate-over-predicts / exact-charge / ~1.48M-native-transfer caveats | RPC `eth_gasPrice` | -| `rome facts balance ` | `facts_balance` | native (gas-token) balance for an address | RPC `eth_getBalance` | -| `rome facts programs ` | `facts_programs` | Solana program ids for `devnet`/`testnet`/`mainnet` | registry `getPrograms` | - -Chains resolve by id, name, or slug (`200010`, `hadrian`, `200010-hadrian`, `Rome Hadrian`) — by **exact** match, so an ambiguous prefix or a bad id fails loudly rather than returning the wrong chain. - -### `cookbook` — the right pattern (kills "don't know how") - -| CLI | MCP tool | What it returns | Source | -|---|---|---|---| -| `rome cookbook cpi-recipe [program]` | `cookbook_cpi_recipe` | the CPI account-rules agents get wrong (accounts non-empty; operator + program_id excluded; sign as `HELPER.pda(address(this))`, not `tx.origin`) + the SDK encoders + real precompile addresses | `@rome-protocol/sdk` `PRECOMPILE_ADDRESSES` | -| `rome cookbook patterns [goal]` | `cookbook_patterns` | which example repo + guide fits a goal (lending → aerarium; AMM → rome-dex; CPI → cardo; from-home → appia; oracle → oracle-gateway; scaffold → create-rome-app) | curated index | +| `facts chain ` | `facts_chain` | chain id, RPC, explorer, rome-evm program id, gas token | registry `getChain` | +| `facts tokens ` | `facts_tokens` | token list (address, mint, symbol, decimals, kind) | registry `getTokens` | +| `facts contracts ` | `facts_contracts` | deployed contract addresses | registry `getContracts` | +| `facts gas ` | `facts_gas` | live gas price + the estimate-vs-charge / ~1.48M-native caveats | RPC | +| `facts balance ` | `facts_balance` | native (gas-token) balance | RPC | +| `facts programs ` | `facts_programs` | Solana program ids for a network | registry `getPrograms` | +| `cookbook cpi-recipe [prog]` | `cookbook_cpi_recipe` | the CPI account-rules + SDK encoders + real precompile addresses | `@rome-protocol/sdk` | +| `cookbook patterns [goal]` | `cookbook_patterns` | which example repo + guide fits a goal | curated index | +| `cookbook errors [query]` | `cookbook_errors` | decode a Rome failure → cause + fix (the error taxonomy) | curated | +| `call [args]` | `call` | read a contract via `eth_call` | RPC | +| `doctor [--address]` | `doctor` | preflight: chain live? RPC reachable? program set? wallet funded? | registry + RPC | +| `tx ` | `tx` | EVM receipt + the Solana settlement tx(s) (`rome_solanaTxForEvmTx`) + Via link | RPC | +| `preset ` | `preset` | ready Rome toolchain config (RPC + chainId) + quirks | registry | + +Chains resolve by id, name, or slug (`200010`, `hadrian`, `Rome Hadrian`) — by **exact** match, so an ambiguous prefix or bad id fails loudly rather than returning the wrong chain. + +### Actions — CLI-only, key-gated, never MCP + +| CLI | Signs with | What it does | +|---|---|---| +| `deploy [args]` | `ROME_EVM_KEY` | deploy a compiled artifact, handling Rome's gas quirks | +| `send [args]` | `ROME_EVM_KEY` | write to a contract via `submitRomeTx` (the correct Rome write path) | +| `fund --from --amount ` | `ROME_EVM_KEY` | bridge USDC → Rome **gas** (CCTP); the "from home" on-ramp | +| `bridge --from --amount [--intent gas\|wrapper]` | `ROME_EVM_KEY` | bridge USDC in as gas or wUSDC | +| `verify [--path solidity]` | `ROME_EVM_KEY` + `ROME_SOLANA_KEY` | the **both-lane works-gate**: deploy a probe, drive it from the EVM lane *and* the Solana lane, assert parity | + +`fund`/`bridge` orchestrate `@rome-protocol/sdk/bridge` (quote → sign the source burn → settle → register → poll); `verify` orchestrates `submitRomeTx` (EVM lane) + `submitRomeTxSolanaLane` (Solana lane). Every action prints what it did; `fund`/`bridge` preview with `--dry-run`. ## Grounding — why the facts are trustworthy -Every value comes from a real source, never a model's memory: -- **Chain facts** — `@rome-protocol/registry` (the generated, public projection) + the chain's own RPC. -- **Precompile addresses** in the CPI recipe — imported from `@rome-protocol/sdk`'s `PRECOMPILE_ADDRESSES` constant, not hardcoded here. -- **The pattern index** — a small curated map that mirrors the [ecosystem map](https://docs.rome.builders/getting-started/ecosystem). +Every value comes from a real source, never a model's memory: chain facts from `@rome-protocol/registry` + the chain's RPC; precompile addresses from `@rome-protocol/sdk`; the pattern index mirrors the [ecosystem map](https://docs.rome.builders/getting-started/ecosystem). If the registry doesn't publish a chain, `rome` says so (with the known set) instead of guessing. -If the registry doesn't publish a chain, `rome facts` says so (with the known set) instead of guessing. +## Security model — the read/action boundary -## Security model +The read/action split runs along the **surface boundary**, and that is the whole security design: -**Read-only by charter — the tool holds no keys and cannot sign.** -- No capability writes, signs, or sends a transaction. Every one is a read. -- The code imports `viem` only as `createPublicClient` (reads: `getGasPrice`/`getBalance`) and `@rome-protocol/sdk` only for the `PRECOMPILE_ADDRESSES` **constant** — no signer, no wallet client, no encoder is ever invoked to submit. -- **Zero `process.env` reads** — the tool cannot pick up a private key. +- **Reads hold no key** and are safe to wire into any agent over MCP — they can never sign, fund, or leak a secret. +- **Actions are CLI-only and key-gated.** The signing key is read from the **environment only** (`ROME_EVM_KEY`, and `ROME_SOLANA_KEY` for `verify`'s Solana lane) — **never** a flag, never logged, never sent through the MCP server. A missing key fails fast with a clear error before any network call. +- **The MCP server registers only `read` capabilities** (asserted by test) — so a key can never reach the MCP surface, and an agent given `rome mcp` cannot move funds no matter what it's asked. -This is *why* the read/act split runs along the surface boundary: the **MCP server is safe to wire into any agent** — it can never leak a secret or move funds. When write actions land (see below), they are **CLI-only**, with the key supplied via the local environment, never through MCP. +Funded testing uses agent-generated wallets funded by the operator; keys live in a git-excluded location, never in the repo. ## Project layout @@ -91,19 +96,23 @@ This is *why* the read/act split runs along the surface boundary: the **MCP serv src/ core/ capabilities.ts the single CAPABILITIES registry (both surfaces read this) - facts.ts chain/token/contract/gas/balance/programs handlers + resolveChainId - cookbook.ts cpi-recipe + patterns handlers + the curated index - deps.ts injectable RPC client (viem public client; stubbed in tests) - cli.ts `rome ` dispatch (exported main; no side effects on import) - mcp.ts `rome mcp` — registers each capability as an MCP tool over stdio - bin.ts the `rome` binary entry (only file that runs on invocation) -test/ facts + cookbook + alignment + behavioral CLI tests (+ scripts/mcp-smoke.mjs e2e) + facts.ts chain/token/contract/gas/balance/programs + resolveChainId + cookbook.ts cpi-recipe + patterns + errors (the taxonomy) + curated index + actions.ts call (read) · deploy · send (viem + submitRomeTx) + bridge.ts fund · bridge — the inbound CCTP flow engine (orchestrates the SDK) + doctor.ts preflight checklist + tx.ts cross-VM diagnosis (rome_solanaTxForEvmTx; no debug_trace) + verify.ts the both-lane works-gate (+ probe.ts, the bundled Store probe) + presets.ts foundry/hardhat config + keys.ts requireEvmKey / requireSolanaKey (env-only) + eip1193.ts Node EIP-1193 shim for submitRomeTx + deps.ts injectable RPC client (stubbed in tests) + cli.ts `rome ` dispatch + the --flag parser (exported main) + mcp.ts `rome mcp` — registers each READ capability as an MCP tool + bin.ts the `rome` binary entry +test/ per-module unit tests + alignment + behavioral CLI (+ mcp-smoke.mjs e2e) ``` -## Extending — v1.1 (planned) - -v1 is **grounding-first** (read-only). The next tier adds Rome-*unique* actions to the **CLI only**: -- `rome fund` — bridge USDC into a Rome chain (the funding on-ramp; no faucet). -- `rome verify` — the funded both-lane works-gate (fund → deploy → act → assert). +## Roadmap -Deploy stays with Foundry / Hardhat / `create-rome-app` — `rome` does not re-wrap them. Actions take the signing key from the local environment, never a flag and never through the MCP server. +Shipped: the full four-paths surface above (reads + actions), all funded-verified on a live Rome chain. Next: `verify --path solana-program` / `--path from-home` slices · bridge ETH (Wormhole) + outbound (`from-rome`) · `new` (wraps `create-rome-app`). Deploy/build stays orchestrated (Foundry/Hardhat/create-rome-app) — `rome` is the connective tissue + the Rome-unique gaps, not a re-implementation of the EVM toolchain. diff --git a/docs/GUIDES.md b/docs/GUIDES.md index 919d113..28b8be7 100644 --- a/docs/GUIDES.md +++ b/docs/GUIDES.md @@ -6,6 +6,7 @@ Concrete, copy-paste recipes: real commands with real output, and how to fold `r - [Integrate into an AI agent (MCP)](#integrate-into-an-ai-agent-mcp) - [The agent grounding loop](#the-agent-grounding-loop) - [Fund a wallet from another chain (`fund` / `bridge`)](#fund-a-wallet-from-another-chain-fund--bridge) +- [Prove it works — `rome verify`](#prove-it-works--rome-verify) - [Shell & scripting recipes](#shell--scripting-recipes) - [Use it in CI](#use-it-in-ci) - [End-to-end: build a price-reading contract, grounded by rome](#end-to-end-build-a-price-reading-contract-grounded-by-rome) @@ -96,6 +97,53 @@ $ rome cookbook patterns lending ``` No goal → the full index (AMM → rome-dex, CPI → cardo, from-home → appia, oracle → rome-oracle-gateway, scaffold → create-rome-app). +### `rome cookbook errors [query]` +```console +$ rome cookbook errors gas +[ { "symptom": "eth_estimateGas returns a huge value (often 10-50× the real charge)…", + "cause": "Rome charges the EXACT gas used; the estimate is a loose upper bound.", + "fix": "Don't hard-fail or size budgets off the estimate. A native transfer is ~1.48M gas, not 21k." } ] +``` +No query → the full taxonomy (`Custom(0)`/AccountLocked, `CpiProhibitedInIterativeTx`, `UnknownInstruction(N)`, `forge --skip-simulation`, Solana-lane `Custom(1)`, …). + +### `rome doctor [--address 0x…]` +```console +$ rome doctor hadrian --address 0x1Fc3… +{ + "chain": { "id": 200010, "name": "Rome Hadrian", "network": "devnet", "status": "live" }, + "checks": [ + { "name": "chain-live", "ok": true, "detail": "status=live" }, + { "name": "program-configured", "ok": true, "detail": "RPTWwELX…" }, + { "name": "rpc-reachable", "ok": true, "detail": "https://hadrian…/ (gasPrice …)" }, + { "name": "wallet-funded", "ok": true, "detail": "… wei (USDC)" } + ], + "ok": true +} +``` + +### `rome tx ` +```console +$ rome tx hadrian 0x8d99… +{ + "status": "success", + "receipt": { "gasUsed": "0x1607b0", "from": "0x…", "to": "0x…", "blockNumber": "0x…" }, + "solanaSettlement": ["5Lwke7W1…"], + "explorer": "https://via-hadrian.testnet.romeprotocol.xyz/tx/0x8d99…", + "note": "Confirmed. solanaSettlement lists the Solana tx(s) that settled this EVM tx." +} +``` +Rome has no `debug_trace*`; `tx` maps the EVM hash to its Solana settlement via `rome_solanaTxForEvmTx`. + +### `rome preset ` +```console +$ rome preset foundry hadrian +{ + "tool": "foundry", "chainId": 200010, "filename": "foundry.toml", + "config": "[rpc_endpoints]\nrome-hadrian = \"https://hadrian.testnet.romeprotocol.xyz/\"", + "notes": ["forge script --skip-simulation …", "Rome charges exact gas; estimateGas over-predicts …", "…"] +} +``` + --- ## Integrate into an AI agent (MCP) @@ -117,9 +165,10 @@ npm install -g github:rome-protocol/rome-cli#v0.4.0 **3. Verify** the tools are live: ```bash -rome mcp # starts the stdio server; your client lists: facts_chain, facts_tokens, - # facts_contracts, facts_gas, facts_balance, facts_programs, - # cookbook_cpi_recipe, cookbook_patterns (Ctrl-C to stop the manual check) +rome mcp # starts the stdio server; your client lists the READ tools: facts_chain, + # facts_tokens, facts_contracts, facts_gas, facts_balance, facts_programs, + # cookbook_cpi_recipe, cookbook_patterns, cookbook_errors, call, doctor, tx, preset + # (actions — deploy/send/fund/bridge/verify — are CLI-only, never on MCP) ``` **4. Use it in a prompt.** Now the agent can call the tools instead of hallucinating: @@ -189,6 +238,29 @@ Supported source chains come from the registry's bridge config for the target Ro --- +## Prove it works — `rome verify` + +The keystone: prove a contract actually works on Rome, driven from **both lanes**. `verify --path solidity` deploys a probe, sets a value from the EVM lane (`submitRomeTx`) *and* the Solana lane (`submitRomeTxSolanaLane`), and asserts the same contract answered on each — the litmus test, runnable. + +A funded **action**: needs `ROME_EVM_KEY` + `ROME_SOLANA_KEY` (env-only), CLI-only, never on MCP. + +```console +$ rome verify hadrian --path solidity +{ + "path": "solidity", + "probe": "0x368744e9…", + "checks": [ + { "lane": "evm", "wrote": "42", "read": "42", "ok": true }, + { "lane": "solana", "wrote": "43", "read": "43", "ok": true } + ], + "ok": true +} +``` + +`ok: true` means one contract on Rome answered correctly whether an EVM wallet *or* a Solana wallet drove it — the dual-lane promise, verified on-chain. (`--path solana-program` and `--path from-home` follow.) + +--- + ## Shell & scripting recipes `rome` prints JSON — pipe it to `jq`.