P2 slice 7: backups and restore (D-30, D-31)
The server backs each book up itself: a per-book git archive, pushed fast-forward only to a git remote and to a folder, when the books open, after a quiet spell, before they close and on request; every commit, push and drill is a record in ops/backup.jsonl. Drills clone each target and verify the chain, the blobs, the vault and the head, weekly and after a failure. `wanderbooks books restore` previews, then restores beside or (with --replace) over, never moving books something holds. The archive refuses anything but a plain bare repository and pins git against the owner's configuration. books.backup and books.back_up_now for the agent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
1 parent
5bc0633cdd
commit
019d8d76a3
29 files changed
+6443
-136
No files matched your search
+75
-1
@@ -352,7 +352,8 @@ seals a new vault record, every time, so none carries it;
|
||||
`idempotent_tools_change_nothing_when_repeated` calls each candidate twice
|
||||
to check). Every tool's
|
||||
`openWorldHint` is false: the books are a closed world, and no tool
|
||||
reaches beyond them. `books.export_cpa_package`, `books.vendor_1099`
|
||||
reaches beyond them, but `books.back_up_now`, which pushes the books to
|
||||
the owner's backups (true). `books.export_cpa_package`, `books.vendor_1099`
|
||||
and `books.get_w9` keep a file for the agent (an export) and record the
|
||||
ones with TINs, so they are not read-only. No tool carries
|
||||
`requiresUserInteraction` (owner decision
|
||||
@@ -584,6 +585,79 @@ through the policy.
|
||||
every stored file. The server runs the same check, and heals the same
|
||||
way, every time it starts.
|
||||
|
||||
## Tools — backups
|
||||
|
||||
Books served from the data root back themselves up (plan 3.7, D-30,
|
||||
D-31): each keeps `archive.git`, a bare repository whose commits are its
|
||||
journal, stored files, vault, keys and access log as they were (one
|
||||
commit per backup, made under the store's lock, its message
|
||||
`seq N head H` and, below, `book <id>`), pushed fast-forward only to the targets the settings
|
||||
name: `backup.git_remote` (the owner's private repository) and
|
||||
`backup.folder` (a repository `<book>.git` in a folder on another disk).
|
||||
Not encrypted: the destinations keep it private. Backups run a little
|
||||
after the books are last written (`--backup-quiet`, 30 s), when they
|
||||
open, before they close, and on request; a failed one is retried after
|
||||
15 minutes. Each target is drilled weekly: cloned, its chain replayed,
|
||||
every stored file re-hashed, every TIN record the books name there with
|
||||
its key, its head the one its commit records, its journal a byte prefix
|
||||
of the live one (a failed drill is tried again at the server's next
|
||||
hourly look, once 15 minutes have passed). A backup's tree is checked
|
||||
before any of it is written out: plain files under the backed-up paths
|
||||
only, never a link or a submodule. git runs with no hooks (in a
|
||||
repository on this computer it pushes to as well), no ignore or
|
||||
attributes rules of anyone's (a `.gitattributes` in a book's folder
|
||||
refuses the backup) and no command-running transports; a repository on
|
||||
this computer is used only if plain (a bare repository at that very
|
||||
path, no settings of its own but where a copy came from, nothing sending
|
||||
git elsewhere, no link where it or its parts are), and a backup folder that took backups and is gone is
|
||||
reported as a disk not connected, never made again (restored books count
|
||||
the backups of the books they were). Books restored beside the books they
|
||||
were (under another id) don't push to those books' repository, which
|
||||
would stop theirs: `books.backup` says it is paused for them until
|
||||
another is named. A damaged backup record is set aside and begun again;
|
||||
it never keeps the books shut. One book's drill holds the others'
|
||||
backups until it ends (a backup at close waits at most a minute for
|
||||
it). A target
|
||||
whose push failed is left out of the backups after writes for 15
|
||||
minutes, then tried again, written to or not; the other targets don't
|
||||
wait for it. Every
|
||||
commit, push and drill is a record in the book's `ops/backup.jsonl` (a
|
||||
chain log; a backup never moves the books' head), each target's records
|
||||
keyed by the target itself. Setting `backup.*` (books.set_setting, or
|
||||
books.import_settings with a `[backup]` table) needs books:admin, as
|
||||
backing up does: a backup is the whole book, its vault and key included.
|
||||
Over stdio neither tool is offered.
|
||||
|
||||
- `books.backup {}` (books:read) → `{ head_seq, targets: [{ target:
|
||||
"repository" | "folder", at, last_ok_at, through_seq, stale,
|
||||
last_error, last_drill_at, drill_error, paused }], note }`. `paused`:
|
||||
not backed up there, these books being restored as a copy of others
|
||||
(its `last_error` says so). `stale`: the target
|
||||
lacks the head, and the first try to back up what it lacks (since its
|
||||
last good backup, or ever) is more than a day old. `last_error`: its
|
||||
last push's, or a later failed commit's. `note`
|
||||
says what to set when no target is. `books.start` adds a next step when
|
||||
none is set up, or one is stale, failing, paused or failed its drill.
|
||||
- `books.back_up_now { new_disk? }` (books:admin) → the same, after
|
||||
backing up to every target now. `new_disk`: the backup folder's disk is
|
||||
new, so a folder that took these books' backups and holds none now is
|
||||
begun again (otherwise it is a disk not connected). The one tool whose
|
||||
`openWorldHint` is true. A target paused for books restored as a copy
|
||||
of others shows `paused` (and is no failure to retry).
|
||||
|
||||
Restoring is the owner's, from a terminal: `wanderbooks books restore
|
||||
BACKUP [--id ID] [--replace] [--yes]` (the admin socket's `restore`,
|
||||
previewed then confirmed against the previewed head; without `--id`, the
|
||||
books keep the id of the books the backup holds, which the preview names,
|
||||
else the repository's name; under another id they are a copy, and don't
|
||||
push to those books' repository). The backup is
|
||||
cloned into the root, proven whole as a drill proves it (but for the
|
||||
live-journal check: there may be no live books), and built as an
|
||||
adoption builds; it arrives as new books, or replaces books (`--replace`,
|
||||
which are let go and moved whole into `legacy/<id>-<time>/`). The
|
||||
restored books keep the backup's history, so their next backups carry
|
||||
on from it.
|
||||
|
||||
## Tools — payouts
|
||||
|
||||
- `books.record_payout { date, description, deposit_account, deposit,
|
||||
|
||||
@@ -273,7 +273,8 @@ damn CPA package."
|
||||
takes a path. The MCP server
|
||||
never returns a TIN in any other tool, log, or error. Tests check that
|
||||
no other output contains a nine-digit run.
|
||||
- **Books without a vault** (another machine, a restored backup) still
|
||||
- **Books without a vault** (another machine, books adopted without
|
||||
theirs; a restored backup brings its vault and key, D-31) still
|
||||
open and verify; the 1099 worksheet and the CPA package then say which
|
||||
TINs are missing (`tin_gaps`), and the package is still built.
|
||||
- **Tools** (built; `docs/mcp-interface.md` "Tools — the TIN vault"):
|
||||
|
||||
+10
-6
@@ -27,10 +27,13 @@ easily backupable, journable":
|
||||
bytes to `journal.jsonl.torn.<time>`, truncates to the last newline, and
|
||||
journals a `journal.tail_repaired` line; `books.digest` reports it.
|
||||
Damage before the last newline is corruption and refuses to open: that
|
||||
is for a human, never repaired automatically. Better: the journal is
|
||||
git-friendly, so an optional `wanderbooks backup` that commits it to a
|
||||
private git repo gives versioned, distributed, deduplicated backup with
|
||||
full history.
|
||||
is for a human, never repaired automatically. The server does better
|
||||
(plan 3.7): it commits the journal, the blobs, the vault, its key and
|
||||
the access log to each book's own git repository (the blobs staged
|
||||
ahead, the commit under the store's lock) and pushes it to the
|
||||
owner's private repository and backup folder, versioned and
|
||||
deduplicated, with full history (docs/mcp-interface.md, "Tools —
|
||||
backups").
|
||||
- **Journable.** It literally is a journal: `{seq, ts, type, payload, prev}`.
|
||||
Greppable, diffable, replayable. The audit trail isn't a feature bolted
|
||||
on — it's the storage format.
|
||||
@@ -76,8 +79,9 @@ is correct: the journal records what happened, including abandoned work.
|
||||
`wanderbooks verify` (a `ledger-store` API, later a CLI subcommand):
|
||||
re-read the journal, recompute the hash chain, replay every line into a fresh
|
||||
in-memory `Ledger`, and compare its reports against the SQLite projection's.
|
||||
Any mismatch is a loud failure. Run it on startup (cheap at this scale) and
|
||||
before backups.
|
||||
Any mismatch is a loud failure. Run it on startup (cheap at this scale);
|
||||
a backup is then proven where it lands, by a drill (docs/mcp-interface.md,
|
||||
"Tools — backups").
|
||||
|
||||
## Blob store
|
||||
|
||||
|
||||
+2
-2
@@ -72,8 +72,8 @@ that would fail if it broke:
|
||||
| Text filed into the books is refused when it holds a TIN the vault keeps (nine digits, 3-2-4 by hyphen, space or dot, 2-7 by hyphen or space, anywhere) or a stand-alone printed SSN or EIN (123-45-6789, 12-3456789), and only then: a bank export (routing numbers, long references, a date next to an amount) and a longer number holding an SSN shape go in; a locked vault leaves the printed shapes (rules.md B-26) | `tin_files.rs` (tests) `text_with_a_vault_tin_or_a_printed_one_is_refused_and_a_bank_export_is_not`; unit tests `text_holding_a_vault_tin_is_refused_in_every_rendering`, `a_printed_ssn_or_ein_is_refused`, `a_longer_number_holding_a_tin_shape_passes`, `an_ordinary_bank_export_passes`, `binary_bytes_are_not_scanned` |
|
||||
| Filing is no TIN oracle: text with more than two TIN-shaped numbers ending in a known TIN's last four (the books' TIN and W-9 facts, then the vault's own) is refused before the vault is opened, byte for byte alike whether one is the vault's TIN or none is, the vault open or locked, by every filing tool; one or two such numbers are compared against the vault, one number written many ways counts once, and other last fours never count (rules.md B-26) | `tin_files.rs` (tests) `text_probing_for_a_tin_is_refused_alike_by_every_filing_tool`; unit tests `text_probing_for_a_tin_is_refused_alike_whether_or_not_it_holds_it` (the vault never opened for a probe), `one_or_two_candidates_are_compared_against_the_vault` |
|
||||
| The connect text fits its budgets, its lead stands alone, it states the receipt rule enforced (exact threshold in the base currency, required and exempt accounts, reversals) and the parties rule in force, and the company name can't break it (control and format characters) | `handbook.rs` `instructions_budget`, `a_typical_company_gets_the_whole_connect_text`, `the_company_name_is_one_plain_line`; `receipt_policy.rs` `the_rule_told_is_the_rule_enforced`, `a_long_or_odd_account_list_stays_short_and_plain`; a new session is told the settings in force: `a_new_session_is_told_the_settings_in_force` |
|
||||
| No text the agent is given (whole tool JSON, resources, connect text, handbook, every error template and refusal) points at files, git, GitHub, repo docs, flags or helper commands; descriptions fit 700 characters (listed exceptions, P5) | `tool_text_lint`, `tool_descriptions_fit_700_characters`; `ledger-core` `an_unknown_year_threshold_is_never_guessed` |
|
||||
| Every tool is annotated (openWorldHint false on all), and the read-only hint is true in behaviour (journal, every data-dir file's contents but `ops/`, the viewer's side channel for `books.show`, which is not the books; the database's rows, the vault and the settings unchanged) | `annotations_complete`; `spec_mcp.rs` `read_only_tools_change_nothing` (checked on every call in `Mcp::tool`) |
|
||||
| No text the agent is given (whole tool JSON, resources, connect text, handbook, every error template and refusal) points at files, git, GitHub, repo docs, flags or helper commands; descriptions fit 700 characters (listed exceptions, P5; and what git itself said of a failed backup, which `books.backup` passes on to the human as git said it, its paths hidden) | `tool_text_lint`, `tool_descriptions_fit_700_characters`; `ledger-core` `an_unknown_year_threshold_is_never_guessed` |
|
||||
| Every tool is annotated (openWorldHint false on all but `books.back_up_now`), and the read-only hint is true in behaviour (journal, every data-dir file's contents but `ops/`, the viewer's side channel for `books.show`, which is not the books; the database's rows, the vault and the settings unchanged) | `annotations_complete`; `spec_mcp.rs` `read_only_tools_change_nothing` (checked on every call in `Mcp::tool`) |
|
||||
| A bare (or empty-`since`) digest covers this month so far, on any day of it, as the server counts it (the stdio test reads the server's own month, and starts over if it turns) | `a_bare_digest_covers_this_month_so_far`; `mcp_stdio.rs` `digest_resource_bare_reads` |
|
||||
| The handbook: every section, "all" in order, the schema names every id | `handbook_sections_render_the_rule_in_force`, `the_whole_handbook_is_every_section_in_order`, `mcp_stdio.rs` `handbook_tool_and_resources_over_stdio` |
|
||||
|
||||
|
||||
Reference in new issue
Block a user