P0: pin dev tools, honest transports doc, tools-only template

mise.toml: pin cargo-mutants 27.1.0 and cargo-fuzz 0.13.2 (the latest
published, and what is installed) instead of "latest", so mutation and
fuzz runs repeat (plan/served.md I-13, P0).

docs/mcp-interface.md: the spec had two Transports sections, and the
first claimed bearer-token auth on Streamable HTTP. There is none: the
server uses rmcp's default config, whose only guard is a Host check for
localhost, 127.0.0.1 and ::1, with no token and no Origin check. One
section now says so plainly and points to plan/served.md P2 for auth.

ledger-cli AGENTS.md template: "the journal is the evidence - read it,
don't guess" led an agent to cat the journal file instead of asking the
books. The template now says to trace a figure through books.get_entry,
books.search_entries, or books.export (journal or general_ledger), and a
new bullet says the books are reached only through the wanderbooks MCP
tools: never open, list or parse the books' files, not even to look; if
a tool can't answer, say so. The other lines that sent the agent to the
data dir go too: onboarding is triggered by an empty books.chart instead
of a missing journal.jsonl, the CPA paragraph drops the ledger-gui
--data-dir books-data command, and the backup note no longer names the
journal's path. Test agents_md_reaches_the_books_only_through_tools
pins this; agents_md_carries_the_contract no longer expects journal.jsonl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-16 18:18:05 -06:00
1 parent 188c90ea99
commit 754d616a9d
4 files changed
+55 -24

No files matched your search

+15 -9
View File
@@ -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";
+26 -1
View File
@@ -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();
+12 -12
View File
@@ -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):
+2 -2
View File
@@ -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"