diff --git a/crates/ledger-cli/src/main.rs b/crates/ledger-cli/src/main.rs index 62e4903..bc3702c 100644 --- a/crates/ledger-cli/src/main.rs +++ b/crates/ledger-cli/src/main.rs @@ -126,12 +126,17 @@ a second. Everything else they see, you show them. requires a receipt — the server enforces this, don't argue with it. One file per receipt: PDF preferred, an image is fine. - **Show the numbers.** After every mutation, show the affected balances. - When the human questions a figure, trace it to journal entries. + When the human questions a figure, trace it to journal entries with the + tools (`books.get_entry`, `books.search_entries`). +- **Only through the tools.** The books are reached only through the + wanderbooks MCP tools. Never open, list or parse the books' files on + disk (journal, blobs, database, vault), not even to look. If a tool + can't answer, say so. ## First run: onboarding -If `books-data/journal.jsonl` is missing or empty, onboard before recording -anything. Keep it conversational; ask a few things at a time. +If the books are empty (`books.chart` lists no accounts), onboard before +recording anything. Keep it conversational; ask a few things at a time. 1. Ask: company legal name, entity type (sole proprietorship or single-member LLC, partnership or multi-member LLC, S corp, C corp), @@ -200,8 +205,8 @@ Bank feeds aren't automatic here: import the bank's CSV export `books.verify` is clean. - A CPA who wants to look around can get a read-only copy of this directory (it's a git repository: push it to a private remote and give - them read access) and open it with `wanderbooks gui`, or `ledger-gui - --data-dir books-data`. Nothing they do there can change the books. + them read access) and open it with `wanderbooks gui`. Nothing they do + there can change the books. ## Session start @@ -222,14 +227,15 @@ human tells you their preference, write it there. search). - `books.digest` is the review surface — "what did you do with my books lately." Offer it; don't make the human ask. -- Reports are pure functions of the journal. If a number looks wrong, the - journal is the evidence — read it, don't guess. +- Reports are pure functions of the journal. If a number looks wrong, + trace it through the tools: `books.get_entry`, `books.search_entries`, + or `books.export` with report `journal` or `general_ledger`. Don't guess. - Integrity: `books.verify` checks the journal's hash chain, the database against the journal (rebuilding it if it drifted), and every stored file. Run it when a number looks wrong or the human asks. - Back up: `just backup` after every session that posted entries. The git - history of this directory is the archive; `books-data/journal.jsonl` is - the system of record and the SQLite database is disposable. + history of this directory is the archive; the journal is the system of + record and the database is disposable. "#; const CLAUDE_MD: &str = "@AGENTS.md\n"; diff --git a/crates/ledger-cli/tests/init.rs b/crates/ledger-cli/tests/init.rs index ace4229..2327832 100644 --- a/crates/ledger-cli/tests/init.rs +++ b/crates/ledger-cli/tests/init.rs @@ -144,7 +144,6 @@ fn agents_md_carries_the_contract() { "Posted is forever", "reversing entries", "receipt", - "journal.jsonl", "onboard", "chart_template", "to the cent", @@ -156,6 +155,32 @@ fn agents_md_carries_the_contract() { } } +/// The agent reaches the books only through the MCP tools. The scaffolded +/// AGENTS.md once said "the journal is the evidence — read it", and an +/// agent `cat`ed the journal file instead of asking the books. +#[test] +fn agents_md_reaches_the_books_only_through_tools() { + let tmp = tempfile::tempdir().unwrap(); + let books = tmp.path().join("b"); + assert!(new(tmp.path(), &books, &[]).0); + let md = fs::read_to_string(books.join("AGENTS.md")).unwrap(); + for phrase in [ + "only through the", + "wanderbooks MCP tools", + "Never open, list or parse", + "books.get_entry", + "books.search_entries", + "general_ledger", + "can't answer, say so.", + ] { + assert!(md.contains(phrase), "AGENTS.md says {phrase:?}"); + } + // Nothing points the agent at the books' files. + for path in ["books-data", "journal.jsonl", "wanderbooks.db", "read it"] { + assert!(!md.contains(path), "AGENTS.md never mentions {path:?}"); + } +} + #[test] fn rerun_refused_without_force_and_never_clobbers_the_profile() { let tmp = tempfile::tempdir().unwrap(); diff --git a/docs/mcp-interface.md b/docs/mcp-interface.md index f2d762e..8985a51 100644 --- a/docs/mcp-interface.md +++ b/docs/mcp-interface.md @@ -77,11 +77,18 @@ they are control roles in the journal (rules.md B-23). ## Transports -1. **stdio** (build first). For Claude Code, Claude Desktop, local agents. - No auth — the OS user boundary is the auth. -2. **Streamable HTTP** (later in the phase). For remote access (phone, travel - laptop). Bearer token, binds 127.0.0.1 or the tailnet interface only. - Binding 0.0.0.0 is an explicit human decision, never a default. +- **stdio** (default): `ledger-mcp --data-dir PATH`. For Claude Code, + Claude Desktop and other local agents. No auth: the OS user boundary is + the only boundary. +- **Streamable HTTP**: `ledger-mcp --data-dir PATH --http 127.0.0.1:8080`. + Serves MCP at `http://127.0.0.1:8080/mcp` via rmcp's tower integration. + **There is no authentication on this transport today**: no token and + no Origin check. The only guard is rmcp's default `Host` check, which + accepts `localhost`, `127.0.0.1` and `::1`. Any local process that can + reach the port can read and change the books. Bind it to loopback only; + binding a LAN, tailnet or public interface is an explicit human + decision, never a default. Auth, scopes and Origin validation arrive + with the served books (`plan/served.md`, P2). ## Server instructions @@ -725,13 +732,6 @@ posting. the agent downloads from Google Drive via the CLI to a temp path, then calls this. Keeps Drive credentials out of the server. -## Transports - -- **stdio** (default): `ledger-mcp --data-dir PATH`. For local agents. -- **Streamable HTTP**: `ledger-mcp --data-dir PATH --http 127.0.0.1:8080`. - Serves MCP at `http://127.0.0.1:8080/mcp` via rmcp's tower integration. - For network clients; bind to loopback unless you know what you're doing. - ## Resources (read-only) Hand-rolled on `ServerHandler` (rmcp 3.x has no resource macros): diff --git a/mise.toml b/mise.toml index 31144ca..d2d001e 100644 --- a/mise.toml +++ b/mise.toml @@ -18,5 +18,5 @@ node = "26" # verify/lean/lean-toolchain. "github:leanprover/lean4" = "4.34.1" # Mutation testing and fuzzing (docs/oracle.md). -"cargo:cargo-mutants" = "latest" -"cargo:cargo-fuzz" = "latest" +"cargo:cargo-mutants" = "27.1.0" +"cargo:cargo-fuzz" = "0.13.2"