Windows step 1 and 2 of D-62. Where Unix keeps the books' private folder
at mode 0700 and refuses one others can open, Windows now makes the same
promise with a DACL: `winsec::set_owner_only` gives the folder (and,
inherited, everything made in it) one protected allow entry for this
account's SID; `winsec::is_private` reads the effective DACL and owner
back and answers false for anything else (a missing DACL, an entry for
Everyone), so `Root::open` refuses a loosened root and `Root::setup`
makes it private again, exactly as the 0700 checks do. Every question the
system cannot answer is "not private". The root itself comes from the
shell's known folders (`SHGetKnownFolderPath`: the profile, and
LocalAppData wherever it was redirected), never from environment
variables the process that started us chose.
`rename_new` (the move that must never go over what is there, renameat2
RENAME_NOREPLACE on Linux) is `MoveFileExW` without the replace flag on
Windows, written through.
File locking sat behind `libc::flock` in ops.rs alone (the rest already
used std's File::try_lock, which is LockFileEx on Windows). It now goes
through `lock.rs`, which uses only std, so there is no second unsafe copy
of either call.
New dependency: windows-sys 0.61 behind cfg(windows), the raw bindings
only (no runtime), already in the lockfile through other crates; the
smallest Microsoft crate that covers SIDs, DACLs, the known-folder API,
pipe process ids and MoveFileExW. mise.toml adds the Windows target to
the pinned toolchain so `cargo check --target x86_64-pc-windows-msvc`
type-checks this arm on Linux.
CHOICE (P11c): nothing above the root is checked on Windows (the Unix
rule about folders above it being swappable has no Windows authority; the
profile's folders belong to the account and the Administrators).
Tests: tests/windows_root.rs (Windows job only), the cross-platform
`a_viewer_s_presence_is_a_shared_lock_others_can_see` in ops.rs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
P11a step 0 pinned cargo-packager, cargo-packager-updater and
cargo-generate-rpm for the trial it did not run. mise-action compiles
every pinned tool on every CI run of every job, so three cargo tools
nobody calls yet would cost minutes per run. They return to mise.toml
with the recipe that calls them (AGENTS.md: if a recipe needs a tool,
it goes in mise.toml), when the trial step starts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
The agent path was Linux-only because tokens had a keyring store only
there (every other OS hit NOT_YET, so `connect` and the bridge failed).
D-56's destination is one installation for macOS, Windows and Linux.
Both new stores sit behind the same Items trait as the Secret Service,
so INVARIANT 4 (one account, one item, locked is an error) is kept once
and tested once through the file keyring. Windows credentials have a
unique target name, so a probe stands in for a search.
New dependencies (ledger-host only, target-gated): apple-native-keyring-store
(feature keychain) and windows-native-keyring-store, the stores the
keyring-core 1 API we already use is published with. No ledger-core change.
The `portable` job builds and tests the workspace on macos-latest and
windows-latest. Windows is allowed to fail: the rest of the host is
Unix-only (inventory in docs/packaging.md). Not verified locally: no
macOS or Windows target is installed; CI is the evidence.
Also pins cargo-packager, cargo-packager-updater and cargo-generate-rpm
in mise.toml for the trial, and records that the trial is not yet run.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
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
Max-testing pass, ranks 4-6 and step 9 for ledger-core.
Fuzzing (rank 4): fuzz_op_sequence now drives balanced pairs, foreign
lines, periods, closes, and amounts from raw decimal bits, and asserts
the books stay valid (TB balances, posted entries validate, no draft in
a closed period); new fuzz_core_serde (every persisted core type
round-trips) and fuzz_store_open (a corrupt journal is an error, never a
panic); hangs count (-timeout=10). 54M executions; one crash:
- fuzz_journal_line: a number too big for an integer parsed as a float,
and its canonical form (which the hash chain covers) re-canonicalized
differently. The journal never holds floats (R-2), so the reader now
refuses them (JournalLine::parse, used by every journal reader);
regression a_journal_line_with_a_float_is_refused. Checked first: the
real books' journal has none. Rerun: 13.2M executions, clean.
Seeds in fuzz/seeds/ (synthetic, minimal); the grown corpora stay out of
git. `just fuzz <target> <seconds>`.
Miri (rank 4): the core's unit, bridge, and invariant tests under Miri
(our code has no unsafe; rust_decimal and chrono do): no undefined
behavior. `just miri`. A pinned nightly (nightly-2026-10-01, with miri)
is now in mise.toml.
Benchmarks (rank 6): criterion (new dev-dependency: the playbook's Rust
benchmark tool; none existed) in benches/books.rs; baselines in
docs/testing.md. The store's per-command rehearsal clone costs ~1 ms at
10k entries. No budget yet, so nothing gates on them. `just bench`.
CI: .github/workflows/check.yml runs `just check` on every push;
scheduled.yml runs proofs, the GnuCash oracle, fuzzing, and mutation
weekly. Tools come from mise.toml.
docs/testing.md: every layer, its oracle rank, command, runtime, and
gate; benchmark baselines; the ASSUMED invariants for the owner to reject;
the gap list with triggers. docs/oracle.md's fuzzing paragraph had
claimed persistent corpora that didn't exist; corrected.
Also: ledger-import now inherits the workspace lints (nightly cargo
warned).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
Rust (with clippy and rustfmt), just, Node (for deploys), Kani, Lean,
cargo-mutants, and cargo-fuzz, so `mise install` is the whole bootstrap
and nothing gets installed by hand. Lean is 4.34.1, the newest release
past mise's minimum release age; verify/lean/lean-toolchain matches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX