feat(host): Keychain and Credential Manager token stores, a macOS/Windows CI job (P11a step 0)

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
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-29 12:41:01 -06:00
1 parent f98471516e
commit ee4f8cd58a
10 files changed
+306 -34

No files matched your search

+42
View File
@@ -34,3 +34,45 @@ jobs:
- name: What the build used - name: What the build used
if: always() if: always()
run: df -h / | tail -1 run: df -h / | tail -1
# The destination is one installation for macOS, Windows and Linux (D-56,
# P11a step 0): the workspace must build and pass its tests on each, not
# only here. `just check` (fmt, clippy, rustdoc, proofs' friends) stays
# the Linux job's; these run the build and the tests. The job tells
# which OS a failure is on; neither is allowed to hide the other.
portable:
name: portable (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
experimental: false
- os: windows-latest
experimental: true
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
# Same disk discipline as above: no incremental caches, line tables only.
env:
CARGO_INCREMENTAL: "0"
CARGO_PROFILE_DEV_DEBUG: line-tables-only
CARGO_PROFILE_TEST_DEBUG: line-tables-only
steps:
- uses: actions/checkout@v4
- name: Rust (the version mise.toml pins)
shell: bash
run: rustup toolchain install 1.99 --profile minimal && rustup default 1.99
- name: Room for the build
shell: bash
run: df -h . | tail -1
- name: The server-only build (no viewer)
run: cargo build -p ledger-cli --no-default-features
- name: Everything compiles
run: cargo build --workspace --all-targets
- name: Tests
run: cargo test --workspace
- name: The real OS keyring keeps and forgets a token
run: cargo test -p ledger-host --lib -- --ignored os_keyring
- name: What the build used
if: always()
shell: bash
run: df -h . | tail -1
Generated
+32
View File
@@ -249,6 +249,17 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "apple-native-keyring-store"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b350bfd03649e07aa05c0a81b3e15934374e585c98204a57e20b9d49f49bb9a"
dependencies = [
"keyring-core",
"log",
"security-framework",
]
[[package]] [[package]]
name = "approx" name = "approx"
version = "0.5.1" version = "0.5.1"
@@ -877,6 +888,12 @@ dependencies = [
"syn 3.0.7", "syn 3.0.7",
] ]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "byteorder-lite" name = "byteorder-lite"
version = "0.1.0" version = "0.1.0"
@@ -3964,6 +3981,7 @@ dependencies = [
name = "ledger-host" name = "ledger-host"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"apple-native-keyring-store",
"base64 0.22.1", "base64 0.22.1",
"keyring-core", "keyring-core",
"ledger-core", "ledger-core",
@@ -3978,6 +3996,7 @@ dependencies = [
"tempfile", "tempfile",
"tokio", "tokio",
"tokio-util", "tokio-util",
"windows-native-keyring-store",
"zbus-secret-service-keyring-store", "zbus-secret-service-keyring-store",
"zeroize", "zeroize",
] ]
@@ -8723,6 +8742,19 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-native-keyring-store"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8"
dependencies = [
"byteorder",
"keyring-core",
"regex",
"windows-sys 0.61.2",
"zeroize",
]
[[package]] [[package]]
name = "windows-numerics" name = "windows-numerics"
version = "0.3.1" version = "0.3.1"
+8 -6
View File
@@ -67,10 +67,11 @@ Sheet is frozen as of 2026-10-11. The remaining work, its order and its
hand-off are `plan/served.md` section 8.0; the destination is one hand-off are `plan/served.md` section 8.0; the destination is one
installed app for Mac, Windows and Linux (P11), then the hosted service installed app for Mac, Windows and Linux (P11), then the hosted service
the plan describes and does not build. Today the agent path runs on the plan describes and does not build. Today the agent path runs on
Linux: tokens live in the OS keyring and only the Linux Secret Service Linux: tokens live in the OS keyring (the Secret Service on Linux, the
store exists, so on macOS the viewer and server run but `wanderbooks login Keychain on macOS, the Credential Manager on Windows; the last two
connect` and the agent bridge wait for P11's keyring stores; Windows is are written and compiled by the CI matrix, P11a step 0, and not yet
WSL2 only. proven on a machine of ours); the rest of the host is Unix-only, so
Windows is WSL2 only until its sockets and data root are ported.
See [`plan/STATUS.md`](plan/STATUS.md) for where things stand and See [`plan/STATUS.md`](plan/STATUS.md) for where things stand and
[`plan/roadmap.md`](plan/roadmap.md) for where it's going. [`plan/roadmap.md`](plan/roadmap.md) for where it's going.
@@ -90,8 +91,9 @@ curl -fsSL https://wanderbooks.ai/install.sh | sh -s -- acme
``` ```
(Linux today. On macOS the same line installs and sets up, and the (Linux today. On macOS the same line installs and sets up, and the
viewer runs, but connecting an agent waits for P11's keyring store; on viewer runs; connecting an agent uses the login Keychain, new with
Windows run it inside WSL2.) P11a step 0 and proven only by the CI matrix so far. On Windows run it
inside WSL2.)
Already installed, three commands: `wanderbooks setup`, `wanderbooks books Already installed, three commands: `wanderbooks setup`, `wanderbooks books
create acme`, `wanderbooks connect claude --book acme` (or `connect codex`). create acme`, `wanderbooks connect claude --book acme` (or `connect codex`).
+8 -1
View File
@@ -19,7 +19,8 @@ base64 = "0.22"
sha2 = "0.10" sha2 = "0.10"
tokio-util = { version = "0.7", features = ["rt"] } tokio-util = { version = "0.7", features = ["rt"] }
# Agents' tokens are kept in the OS keyring (plan 3.3, 3.5): the Secret # Agents' tokens are kept in the OS keyring (plan 3.3, 3.5): the Secret
# Service on Linux. macOS and Windows stores come with P11. # Service on Linux, the login Keychain on macOS, the Credential Manager on
# Windows (P11a step 0).
keyring-core = "1" keyring-core = "1"
# The owner passphrase is typed at the terminal, unechoed. # The owner passphrase is typed at the terminal, unechoed.
rpassword = "7" rpassword = "7"
@@ -32,6 +33,12 @@ libc = "0.2"
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
zbus-secret-service-keyring-store = { version = "1", features = ["rt-async-io-crypto-rust"] } zbus-secret-service-keyring-store = { version = "1", features = ["rt-async-io-crypto-rust"] }
[target.'cfg(target_os = "macos")'.dependencies]
apple-native-keyring-store = { version = "1.0.2", features = ["keychain"] }
[target.'cfg(windows)'.dependencies]
windows-native-keyring-store = "1.1.0"
[dev-dependencies] [dev-dependencies]
tempfile = "3" tempfile = "3"
+148 -25
View File
@@ -188,6 +188,49 @@ fn delete_all<I: Items>(items: &I, account: &Account) -> Result<(), KeyringError
/// The OS keyring. /// The OS keyring.
struct Os; struct Os;
/// What a keyring error says (INVARIANT 4: locked is said as such).
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
fn refused(e: keyring_core::Error) -> String {
match e {
keyring_core::Error::NoStorageAccess(_) => {
format!("this computer's keyring is locked, or wouldn't let it be read ({e})")
}
e => format!("the keyring refused: {e}"),
}
}
/// Make an item for `account` in a store's default collection.
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
fn make_in(
store: &dyn keyring_core::api::CredentialStoreApi,
account: &Account,
text: &str,
) -> Result<(), String> {
let label = account.label();
let modifiers = std::collections::HashMap::from([("label", label.as_str())]);
// Only the Secret Service takes a label; the other stores refuse
// modifiers they don't know, so they are built without.
let modifiers = cfg!(target_os = "linux").then_some(&modifiers);
store
.build(SERVICE, &account.key(), modifiers)
.and_then(|entry| entry.set_password(text))
.map_err(refused)
}
/// An item's text, and its removal, the same on every store.
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
fn read_item(item: &keyring_core::Entry) -> Result<String, String> {
item.get_password().map_err(refused)
}
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
fn remove_item(item: keyring_core::Entry) -> Result<(), String> {
match item.delete_credential() {
Ok(()) | Err(keyring_core::Error::NoEntry) => Ok(()),
Err(e) => Err(refused(e)),
}
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
mod os { mod os {
use super::{Account, Items, SERVICE}; use super::{Account, Items, SERVICE};
@@ -207,13 +250,49 @@ mod os {
} }
} }
/// What a keyring error says (INVARIANT 4: locked is said as such). impl Items for Service {
fn refused(e: keyring_core::Error) -> String { type Item = keyring_core::Entry;
match e {
keyring_core::Error::NoStorageAccess(_) => { fn find(&self, account: &Account) -> Result<Vec<Self::Item>, String> {
format!("this computer's keyring is locked, or wouldn't let it be read ({e})") let key = account.key();
let spec = HashMap::from([("service", SERVICE), ("username", key.as_str())]);
self.0.search(&spec).map_err(super::refused)
} }
e => format!("the keyring refused: {e}"),
fn read(&self, item: &Self::Item) -> Result<String, String> {
super::read_item(item)
}
fn remove(&self, item: Self::Item) -> Result<(), String> {
super::remove_item(item)
}
fn make(&self, account: &Account, text: &str) -> Result<(), String> {
super::make_in(&*self.0, account, text)
}
}
}
/// The macOS login Keychain (plan 3.3, D-56): one generic password per
/// account, under the service "wanderbooks".
#[cfg(target_os = "macos")]
mod os {
use super::{Account, Items, SERVICE};
use apple_native_keyring_store::keychain::Store;
use keyring_core::api::CredentialStoreApi;
use std::collections::HashMap;
use std::sync::Arc;
/// The login keychain, reached for one keeper call.
pub(super) struct Service(Arc<Store>);
impl Service {
pub(super) fn reach() -> Result<Self, String> {
Store::new().map(Self).map_err(|e| {
format!(
"this Mac's keychain can't be reached ({e}): is the login keychain unlocked?"
)
})
} }
} }
@@ -222,33 +301,77 @@ mod os {
fn find(&self, account: &Account) -> Result<Vec<Self::Item>, String> { fn find(&self, account: &Account) -> Result<Vec<Self::Item>, String> {
let key = account.key(); let key = account.key();
let spec = HashMap::from([("service", SERVICE), ("username", key.as_str())]); let spec = HashMap::from([("service", SERVICE), ("user", key.as_str())]);
self.0.search(&spec).map_err(refused) self.0.search(&spec).map_err(super::refused)
} }
fn read(&self, item: &Self::Item) -> Result<String, String> { fn read(&self, item: &Self::Item) -> Result<String, String> {
item.get_password().map_err(refused) super::read_item(item)
} }
fn remove(&self, item: Self::Item) -> Result<(), String> { fn remove(&self, item: Self::Item) -> Result<(), String> {
match item.delete_credential() { super::remove_item(item)
Ok(()) | Err(keyring_core::Error::NoEntry) => Ok(()),
Err(e) => Err(refused(e)),
}
} }
fn make(&self, account: &Account, text: &str) -> Result<(), String> { fn make(&self, account: &Account, text: &str) -> Result<(), String> {
let label = account.label(); super::make_in(&*self.0, account, text)
let modifiers = HashMap::from([("label", label.as_str())]);
self.0
.build(SERVICE, &account.key(), Some(&modifiers))
.and_then(|entry| entry.set_password(text))
.map_err(refused)
} }
} }
} }
#[cfg(target_os = "linux")] /// The Windows Credential Manager: one generic credential per account
/// (its target name is unique, so a second item for an account cannot
/// exist; INVARIANT 4 holds by construction and is still checked).
#[cfg(windows)]
mod os {
use super::{Account, Items, SERVICE};
use keyring_core::api::CredentialStoreApi;
use std::sync::Arc;
use windows_native_keyring_store::Store;
/// The Credential Manager, reached for one keeper call.
pub(super) struct Service(Arc<Store>);
impl Service {
pub(super) fn reach() -> Result<Self, String> {
Store::new()
.map(Self)
.map_err(|e| format!("this computer's Credential Manager can't be reached ({e})"))
}
}
impl Items for Service {
type Item = keyring_core::Entry;
/// The credential for the account, if one is there: its target
/// name is the account, so a probe finds it or finds none.
fn find(&self, account: &Account) -> Result<Vec<Self::Item>, String> {
let entry = self
.0
.build(SERVICE, &account.key(), None)
.map_err(super::refused)?;
match entry.get_password() {
Ok(_) => Ok(vec![entry]),
Err(keyring_core::Error::NoEntry) => Ok(Vec::new()),
Err(e) => Err(super::refused(e)),
}
}
fn read(&self, item: &Self::Item) -> Result<String, String> {
super::read_item(item)
}
fn remove(&self, item: Self::Item) -> Result<(), String> {
super::remove_item(item)
}
fn make(&self, account: &Account, text: &str) -> Result<(), String> {
super::make_in(&*self.0, account, text)
}
}
}
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
impl Keeper for Os { impl Keeper for Os {
fn get(&self, account: &Account) -> Result<Option<String>, KeyringError> { fn get(&self, account: &Account) -> Result<Option<String>, KeyringError> {
let service = os::Service::reach().map_err(KeyringError::Unavailable)?; let service = os::Service::reach().map_err(KeyringError::Unavailable)?;
@@ -266,7 +389,8 @@ impl Keeper for Os {
} }
} }
#[cfg(not(target_os = "linux"))] /// Any other OS has no keyring store yet.
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
impl Keeper for Os { impl Keeper for Os {
fn get(&self, _: &Account) -> Result<Option<String>, KeyringError> { fn get(&self, _: &Account) -> Result<Option<String>, KeyringError> {
Err(KeyringError::Unavailable(NOT_YET.into())) Err(KeyringError::Unavailable(NOT_YET.into()))
@@ -279,9 +403,8 @@ impl Keeper for Os {
} }
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
const NOT_YET: &str = const NOT_YET: &str = "agents' tokens are kept in the keyring on Linux, macOS and Windows only";
"agents' tokens are kept in the keyring on Linux only, until the macOS and Windows apps (P11)";
/// Files, for tests (INVARIANT 3): a folder per collection (`default/` /// Files, for tests (INVARIANT 3): a folder per collection (`default/`
/// where tokens are kept), a file a token. A collection holding a file /// where tokens are kept), a file a token. A collection holding a file
@@ -532,7 +655,7 @@ mod tests {
/// The real keyring, on a desktop with one (`cargo test -p /// The real keyring, on a desktop with one (`cargo test -p
/// ledger-host -- --ignored os_keyring`): kept, read, replaced and /// ledger-host -- --ignored os_keyring`): kept, read, replaced and
/// forgotten, under a service no one else uses. /// forgotten, under a service no one else uses.
#[cfg(target_os = "linux")] #[cfg(any(target_os = "linux", target_os = "macos", windows))]
#[test] #[test]
#[ignore] #[ignore]
fn os_keyring_keeps_and_forgets() { fn os_keyring_keeps_and_forgets() {
+1 -1
View File
@@ -35,7 +35,7 @@ the specification in `bookspec/` (which never depends on the engine).
| `ledger-import` | formats: QuickBooks CSV in; IIF, XLSX, Typst PDFs (statements, the invoice, labels) out | `ledger-core` | | `ledger-import` | formats: QuickBooks CSV in; IIF, XLSX, Typst PDFs (statements, the invoice, labels) out | `ledger-core` |
| `ledger-package` | the shared read-only view over `Store` or `Snapshot` (`PackageBooks`): the CPA package, the 1099 worksheet, the invoice document, Needs attention | `ledger-core`, `ledger-store`, `ledger-config`, `ledger-import`, `ledger-vault` | | `ledger-package` | the shared read-only view over `Store` or `Snapshot` (`PackageBooks`): the CPA package, the 1099 worksheet, the invoice document, Needs attention | `ledger-core`, `ledger-store`, `ledger-config`, `ledger-import`, `ledger-vault` |
| `ledger-mcp` | the books server: the MCP tools (122), resources, the handbook, scopes, the door and the access log, backups' scheduling | all of the above | | `ledger-mcp` | the books server: the MCP tools (122), resources, the handbook, scopes, the door and the access log, backups' scheduling | all of the above |
| `ledger-host` | the host and its leases, the agent bridge (`wanderbooks mcp`), tokens in the OS keyring, the owner's commands, uploads and downloads | `ledger-store`, `ledger-mcp`, `ledger-config` | | `ledger-host` | the host and its leases, the agent bridge (`wanderbooks mcp`), tokens in the OS keyring (Secret Service, macOS Keychain, Windows Credential Manager), the owner's commands, uploads and downloads | `ledger-store`, `ledger-mcp`, `ledger-config` |
| `ledger-gui` | the viewer: read-only, live, egui; the Books menu hands work to the server | `ledger-core`, `ledger-store`, `ledger-config`, `ledger-import`, `ledger-package`, `ledger-vault`, `ledger-host` | | `ledger-gui` | the viewer: read-only, live, egui; the Books menu hands work to the server | `ledger-core`, `ledger-store`, `ledger-config`, `ledger-import`, `ledger-package`, `ledger-vault`, `ledger-host` |
| `ledger-cli` | `wanderbooks`, the one executable: every mode (`serve`, `server`, `host`, `mcp`, `gui`, `setup`, `books`, `token`, `connect`, `doctor`, …) and the over-the-wire test suites | everything | | `ledger-cli` | `wanderbooks`, the one executable: every mode (`serve`, `server`, `host`, `mcp`, `gui`, `setup`, `books`, `token`, `connect`, `doctor`, …) and the over-the-wire test suites | everything |
| `bookspec/checker`, `bookspec/runner` | the example schema and parser, the rule catalogue, the `Engine` trait; the per-rule runner | nothing of ours (the adapter in `ledger-core`'s tests depends on them) | | `bookspec/checker`, `bookspec/runner` | the example schema and parser, the rule catalogue, the `Engine` trait; the per-rule runner | nothing of ours (the adapter in `ledger-core`'s tests depends on them) |
+42
View File
@@ -0,0 +1,42 @@
# Packaging and updates: the trial (P11a step 1)
Status 2026-10-29: **the trial has not been run.** It needs clean Linux,
macOS and Windows machines for the six formats (`.deb`, `.rpm`, AUR,
AppImage, `.dmg`, NSIS `.exe`), and the one machine that built step 0 is a
Linux desktop with neither a macOS nor a Windows target, no container
runtime and none of the packager tools installed. The tools are pinned in
`mise.toml` now (`cargo-packager` 0.11.8, `cargo-packager-updater` 0.2.3,
`cargo-generate-rpm` 0.21.1), so the trial is `mise install` and the VMs.
Nothing below is a result; it is what is known before the trial, and the
table is filled by the person who runs it.
| format | built by | installs as | updated by | signed by |
|---|---|---|---|---|
| `.deb` | `cargo packager` | (trial) | apt repository (step 3) | (trial) |
| `.rpm` | `cargo-generate-rpm` | (trial) | dnf repository (step 3) | (trial) |
| AUR `PKGBUILD` | hand-written | (trial) | the package manager | n/a |
| AppImage | `cargo packager` | (trial) | `cargo-packager-updater` | (trial) |
| `.dmg` | `cargo packager` | (trial) | `cargo-packager-updater` | Developer ID (step 5) |
| NSIS `.exe` | `cargo packager` | (trial) | `cargo-packager-updater` | Windows identity (step 5) |
## What step 0 found about the platforms (read before the macOS and Windows rows)
The agent path was Linux-only because of the keyring. Step 0 adds the
macOS login Keychain and the Windows Credential Manager behind
`ledger-host/src/tokens.rs::Keeper`; CI (`portable` job) is the evidence.
Beyond the keyring, the Windows host is a port, not a `cfg` gap. Today
Unix-only, found by grep (2026-10-29):
- `ledger-host`: `bridge`, `host`, `start`, `opener` and others are
`#[cfg(unix)]` modules; `host.rs`/`start.rs` use `setsid`, `umask`,
`close_range`, `geteuid`, Unix sockets and `libc` unguarded;
`commands.rs` calls `isatty`/`tcgetpgrp`.
- `ledger-mcp`: the owner-only socket check reads peer credentials
(`SO_PEERCRED`, `getpeereid`); `admin`/`local` listen on Unix sockets.
Windows needs named pipes with a peer SID check (plan 3.9).
- `ledger-store`: `ops.rs` takes `flock` locks; `root.rs` has no owner-only
DACL (`set_mode` is a no-op off Unix) and the data root falls back to
`%USERPROFILE%`, not the known-folder `%LOCALAPPDATA%`; `refuse.rs`
(seccomp) and `renameat2` are Linux-only (macOS needs its own
equivalents or `cfg` fences).
- The `portable` job runs Windows as allowed-to-fail until that port lands.
+19
View File
@@ -3401,3 +3401,22 @@ arms, the 8 KiB cut, the 64 KiB hand-over threshold and `page - 1`.
`ledger-package::attention`: the extraction `seq` comparison and the `ledger-package::attention`: the extraction `seq` comparison and the
`Failed` filter, the owed-account name test, the credit side and the `Failed` filter, the owed-account name test, the credit side and the
`settles_entry` skip. `settles_entry` skip.
## P11a step 0: keyring stores for macOS and Windows (2026-10-29)
No new test body: the keeper's laws are tested once, over the `Items`
trait, through the `file_keyring` knob (`the_test_keyring_keeps_and_forgets`,
`two_items_for_one_token_are_said_never_one_of_them_given`,
`a_locked_or_unreachable_keyring_is_an_error_never_no_token`,
`a_token_kept_is_read_back_or_refused`, all in `ledger-host/src/tokens.rs`),
and the macOS Keychain and Windows Credential Manager stores implement
that same trait. The ignored `os_keyring_keeps_and_forgets` (same file) is
now compiled on all three OSes; the `portable` job of
`.github/workflows/check.yml` runs it on `macos-latest` and
`windows-latest` beside `cargo build --workspace --all-targets` and
`cargo test --workspace`. Not run on the author's machine (Linux only; the
Secret Service run needs a desktop): the CI run is the evidence.
**For P12 (mutation targets, none run, D-33):** `tokens.rs`'s `os` modules
are not reachable by mutation testing (they need the real OS store); the
`Items` logic they share is, through the file store.
+5
View File
@@ -23,3 +23,8 @@ node = "26"
# A compile cache shared by every jj workspace's target dir: point # A compile cache shared by every jj workspace's target dir: point
# cargo's build.rustc-wrapper at it (~/.cargo/config.toml). # cargo's build.rustc-wrapper at it (~/.cargo/config.toml).
sccache = "0.18.0" sccache = "0.18.0"
# P11a: the packaging trial and the installers (docs/packaging.md).
"cargo:cargo-packager" = "0.11.8"
"cargo:cargo-packager-updater" = "0.2.3"
"cargo:cargo-generate-rpm" = "0.21.1"
+1 -1
View File
@@ -1596,7 +1596,7 @@ Each unit follows the 8.0.4 template. "A-n" is an owner answer numbered in 8.3.1
5. **Files:** owns `crates/ledger-host/src/update.rs`, `deploy/packaging/*`, `.github/workflows/release.yml`, `docs/packaging.md`; touches `commands.rs`, `crates/ledger-gui/src/owner.rs`, `mise.toml`. 5. **Files:** owns `crates/ledger-host/src/update.rs`, `deploy/packaging/*`, `.github/workflows/release.yml`, `docs/packaging.md`; touches `commands.rs`, `crates/ledger-gui/src/owner.rs`, `mise.toml`.
6. **Tests and proofs:** named above. 6. **Tests and proofs:** named above.
7. **Done when:** (1) on a clean Linux VM the AppImage installs, opens, onboards and posts an entry with no terminal and no admin password, and updates itself from a test feed; (2) on Arch, Debian and Fedora the packages install and upgrade with the head unchanged; (3) an update leaves every book's head and goldens unchanged and a bridge session carries on across it; (4) a backup made on one system restores on another; (5) macOS and Windows: the same as (1), once the signing prerequisites exist (record which are done in the As-built block); (6) `just check` green. 7. **Done when:** (1) on a clean Linux VM the AppImage installs, opens, onboards and posts an entry with no terminal and no admin password, and updates itself from a test feed; (2) on Arch, Debian and Fedora the packages install and upgrade with the head unchanged; (3) an update leaves every book's head and goldens unchanged and a bridge session carries on across it; (4) a backup made on one system restores on another; (5) macOS and Windows: the same as (1), once the signing prerequisites exist (record which are done in the As-built block); (6) `just check` green.
8. **As built:** none yet. 8. **As built:** (steps 0 and 1 only, 2026-10-29; steps 2 to 7 are later waves.) **Step 0, partly.** Built: the macOS login Keychain store (`apple-native-keyring-store` 1.0.2, feature `keychain`) and the Windows Credential Manager store (`windows-native-keyring-store` 1.1.0) behind `tokens.rs::Keeper`, on the same `Items` trait and the same laws as the Secret Service (`os` modules per OS; shared `refused`, `make_in`, `read_item`, `remove_item`); `NOT_YET` now only for other OSes; the ignored `os_keyring_keeps_and_forgets` compiles on all three; the `portable` job in `.github/workflows/check.yml` (`macos-latest` gating, `windows-latest` allowed to fail) builds `ledger-cli --no-default-features`, `cargo build --workspace --all-targets`, `cargo test --workspace` and the ignored keyring test. Checked here: Linux only (fmt, clippy, the tokens tests); no macOS/Windows target is installed, so both stores and the job are proven only by the first CI run. **Not built, and not a `cfg` gap: the Windows host.** Windows data root from the known-folder API with an owner-only DACL, named pipes with a SID check, `flock` and the Unix-only `host`, `start`, `bridge`, `opener` and `local`/`admin` sockets are a port of several crates (inventory in `docs/packaging.md`); macOS needs the CI run to show which Linux-only items (`seccomp` in `refuse.rs`, `renameat2`, `close_range`) need fences. So the step 0 Done-when ("setup ... gui work on each OS") is open for both. **Step 1 (the trial): not run**: it needs clean macOS, Windows and Linux machines; `mise.toml` now pins `cargo-packager` 0.11.8, `cargo-packager-updater` 0.2.3, `cargo-generate-rpm` 0.21.1 and `docs/packaging.md` holds the empty table and what is known.
#### P11b: the agent dock and books homes (D-27) #### P11b: the agent dock and books homes (D-27)