rustledgerpi is an illustrative Pi coding agent for
plain-text bookkeeping. It lets a CPA or bookkeeper ask an LLM to help maintain a
Beancount ledger, while the code narrows what
the agent can change and gives it a deterministic ledger engine:
rustledger compiled to WebAssembly
(@rustledger/wasm).
The repository is intentionally small. Its purpose is educational: show how a financial professional can reason about an AI-assisted workflow in control terms, then trace those controls directly to code.
See the accompanying YouTube video here
- Segregation of capability: the agent gets only
read,write,edit, andrustledger; shell execution is removed from the normal tool list. - Restricted change surface:
writeandeditare blocked unless the target is a top-level*.beancountfile in the current working directory. - Independent validation: the custom
rustledgertool parses, validates, formats, queries, and reportsBALANCESusing a deterministic WASM accounting engine instead of relying on the model's arithmetic. - Readable evidence: tool outputs include validation errors, query columns, row counts, and truncation flags so a reviewer can see what was checked.
Think of the LLM as a junior preparer. It can propose and make bookkeeping entries, but the extension places controls around what it can touch, what tools it can use, and how ledger correctness is checked.
- Prevent unrelated file changes. The agent should not be able to edit code, notes, configuration, or files outside the demo ledger folder.
- Prevent shell-based escape routes. The agent should not run arbitrary
commands such as
rm,curl, package installers, or scripts. - Keep the ledger scope understandable. Multi-file operations should only
use top-level
.beancountfiles that are visible in the working directory. - Validate with accounting software, not vibes. Balancing, parsing, and BQL queries should run through rustledger.
- Make failures explicit. Refused paths, missing entry points, parse errors, and validation errors are returned as structured messages.
package.jsonstarts Pi with an explicit tool list:pi --tools read,write,edit,rustledger -e ./extensions/bookkeeping-agent.ts.extensions/bookkeeping-agent.tscallssetActiveToolsonsession_startso the same allowlist is reinforced after the extension is loaded.extensions/bookkeeping-agent.tshandlestool_callevents and blocks:bashtool calls;writecalls whose path is not a top-level.beancountfile;editcalls whose path is not a top-level.beancountfile.
- The helper
assertTopLevelBeancount()performs the path control. It checks the filename extension, rejects subdirectories, rejects paths outside the current working directory, and usesrealpathto catch existing files or symlinks that resolve somewhere else. - The helper
loadTopLevelBeancountFiles()reads only top-level.beancountfiles for ledger-wide rustledger actions.
These controls are implemented in code. They are not merely prompt instructions. The prompt still matters for workflow guidance, such as reminding the model to validate after edits, but the write/edit boundary is enforced by the extension.
This is a demo control pattern, not a full sandbox.
readis intentionally left available for developer convenience and is not restricted by this extension to.beancountfiles.- Pi extensions run with your operating-system user privileges.
- The controls apply to Pi tool calls handled by this extension. They do not protect you from other processes, manual edits, editor extensions, or commands you run outside the agent.
- For production or client data, add stronger isolation such as a separate OS user, a container, a temporary workspace, network controls, and review/approval gates.
extensions/bookkeeping-agent.tscontains the Pi extension, tool controls, and customrustledgertool.main.beancountis the sample ledger.scripts/smoke-rustledger.mjsverifies that the WASM package can load and validate a minimal Beancount directive without involving an LLM.AGENTS.mddescribes the bookkeeping-agent operating rules used by coding agents in this workspace.package.jsondefines the agent, TypeScript check, and smoke-test scripts.
- Node.js 20+ recommended because this project uses ESM and WASM.
- A Pi-supported LLM provider, either through an API key or
pilogin. See Pi providers.
cd /path/to/rustledgerpi
npm installFrom the repository root:
npm run agentThis runs:
pi --tools read,write,edit,rustledger -e ./extensions/bookkeeping-agent.ts
Pi resolves the extension as TypeScript. The extension loads WASM from the
installed @rustledger/wasm package by reading rustledger_wasm_bg.wasm from
disk and passing the bytes to initWasm({ module_or_path: bytes }).
To load the same extension whenever you run pi in this repo, add
.pi/settings.json with paths relative to .pi/:
{
"extensions": ["../extensions/bookkeeping-agent.ts"]
}You should still start Pi with --tools read,write,edit,rustledger, or an
equivalent allowlist, if you want to exclude bash and other built-in tools at
startup.
flowchart LR
user[CPA or developer]
pi[Pi agent]
tools[read write edit rustledger]
ext[bookkeeping-agent.ts]
wasm["@rustledger/wasm"]
ledger[Top-level .beancount files]
user --> pi
pi --> tools
tools --> ext
ext --> wasm
ext --> ledger
- The user starts Pi through
npm run agent. - Pi exposes only the requested tool names to the model.
- The extension reinforces the active tool allowlist after session startup.
- Every attempted
bash,write, oredittool call passes through the extension'stool_callhandler. writeandeditpaths must passassertTopLevelBeancount()before Pi can perform the operation.- Ledger validation and reporting go through the
rustledgertool.
The model can call rustledger with these parameters:
| Field | Meaning |
|---|---|
action |
One of validate, parse, format, query, or balances. |
path |
Optional top-level .beancount file, such as main.beancount. Required for format. For validate and parse, omit it to use the whole top-level ledger set. |
entry_point |
Optional root file for multi-file mode. Defaults to main.beancount if present, otherwise the first sorted .beancount file. |
bql |
Required when action is query. |
Large BQL result sets are truncated in the text output with a rowsTruncated
flag so the agent does not flood its context window.
Create main.beancount in the same directory where you run npm run agent:
option "title" "Demo ledger"
option "operating_currency" "USD"
2024-01-01 open Assets:Bank USD
2024-01-01 open Expenses:Food USD
2024-01-15 * "Coffee"
Expenses:Food 5.00 USD
Assets:Bank -5.00 USD
Start the agent and ask:
- "Validate my ledger with rustledger and report errors."
- "Show balances."
- "Add a transaction for groceries and re-validate."
- "Try to edit README.md." The extension should refuse because README.md is not a
top-level
.beancountfile.
Run these checks without involving an LLM:
npm run check
npm run smoke:wasmnpm run check type-checks the TypeScript extension. npm run smoke:wasm loads
the rustledger WASM package and validates a minimal Beancount directive.
This project is licensed as GPL-3.0-only to match @rustledger/wasm.
License terms are part of software governance. CPAs are used to asking, "What are the obligations, restrictions, and downstream risks?" Open-source licenses answer those questions for code.
Common licenses differ in how much freedom they give downstream users and what they require if someone redistributes modified software:
| License | Plain-English Summary | Typical Obligations | Business Implication |
|---|---|---|---|
| MIT | Very permissive. You can use, modify, distribute, and include the code in proprietary products. | Keep the copyright notice and license text. | Low friction for commercial use. Often preferred for libraries and examples. |
| Apache-2.0 | Permissive like MIT, with more explicit patent protections. | Keep notices, include the license, preserve NOTICE files if present, and follow patent terms. | Common in enterprise settings because patent language is clearer. |
| BSD-2-Clause / BSD-3-Clause | Permissive licenses similar to MIT. BSD-3 adds a non-endorsement clause. | Keep copyright and license notices; do not imply endorsement under BSD-3. | Low friction, widely accepted. |
| MPL-2.0 | File-level copyleft. Modified MPL-covered files must remain open, but larger combined works can use other licenses. | Publish source for modified MPL files when distributing them. | Middle ground between permissive and GPL-style copyleft. |
| LGPL-3.0 | Weaker copyleft, commonly used for libraries. Applications can link to the library under certain conditions. | Allow users to replace or relink the LGPL component; publish modifications to the LGPL component. | More commercial flexibility than GPL, but still requires compliance controls. |
| GPL-3.0 | Strong copyleft. If you distribute a derivative work, the whole covered work generally must be available under GPL-3.0. | Provide source code, preserve license notices, state changes, and grant recipients the same freedoms. | Good for ensuring improvements remain open; requires care before embedding in proprietary distributed software. |
| AGPL-3.0 | GPL-3.0 plus network-use obligations. Running modified software as a service can trigger source-sharing duties. | GPL-3.0 obligations plus source availability to network users. | Important for hosted SaaS use cases; often reviewed closely by legal/compliance teams. |
For this repository, the key point is simple: because the demo depends on
@rustledger/wasm, and that package is GPL-licensed, this project uses
GPL-3.0-only as well.
Practical notes:
- Using this repository internally for learning or experimentation is usually simpler than redistributing it.
- If you modify and distribute this project, expect to provide the corresponding source code under GPL-3.0-only.
- If you plan to embed this code in a commercial product, especially one you distribute to clients, review the dependency licenses with counsel.
- Ledger data, client records, and your own
.beancountfiles are not automatically licensed as GPL just because this tool can read or edit them. The license applies to the software, not to your accounting data.