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:
1 parent
f98471516e
commit
ee4f8cd58a
10 files changed
+307
-35
No files matched your search
@@ -34,3 +34,45 @@ jobs:
|
||||
- name: What the build used
|
||||
if: always()
|
||||
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
@@ -249,6 +249,17 @@ version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "approx"
|
||||
version = "0.5.1"
|
||||
@@ -877,6 +888,12 @@ dependencies = [
|
||||
"syn 3.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
@@ -3964,6 +3981,7 @@ dependencies = [
|
||||
name = "ledger-host"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"apple-native-keyring-store",
|
||||
"base64 0.22.1",
|
||||
"keyring-core",
|
||||
"ledger-core",
|
||||
@@ -3978,6 +3996,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"windows-native-keyring-store",
|
||||
"zbus-secret-service-keyring-store",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -8723,6 +8742,19 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
|
||||
@@ -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
|
||||
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
|
||||
Linux: tokens live in the OS keyring and only the Linux Secret Service
|
||||
store exists, so on macOS the viewer and server run but `wanderbooks
|
||||
connect` and the agent bridge wait for P11's keyring stores; Windows is
|
||||
WSL2 only.
|
||||
Linux: tokens live in the OS keyring (the Secret Service on Linux, the
|
||||
login Keychain on macOS, the Credential Manager on Windows; the last two
|
||||
are written and compiled by the CI matrix, P11a step 0, and not yet
|
||||
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
|
||||
[`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
|
||||
viewer runs, but connecting an agent waits for P11's keyring store; on
|
||||
Windows run it inside WSL2.)
|
||||
viewer runs; connecting an agent uses the login Keychain, new with
|
||||
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
|
||||
create acme`, `wanderbooks connect claude --book acme` (or `connect codex`).
|
||||
|
||||
@@ -19,7 +19,8 @@ base64 = "0.22"
|
||||
sha2 = "0.10"
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
# 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"
|
||||
# The owner passphrase is typed at the terminal, unechoed.
|
||||
rpassword = "7"
|
||||
@@ -32,6 +33,12 @@ libc = "0.2"
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
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]
|
||||
tempfile = "3"
|
||||
|
||||
|
||||
@@ -188,6 +188,49 @@ fn delete_all<I: Items>(items: &I, account: &Account) -> Result<(), KeyringError
|
||||
/// The OS keyring.
|
||||
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")]
|
||||
mod os {
|
||||
use super::{Account, Items, SERVICE};
|
||||
@@ -207,13 +250,49 @@ mod os {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a keyring error says (INVARIANT 4: locked is said as such).
|
||||
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}"),
|
||||
impl Items for Service {
|
||||
type Item = keyring_core::Entry;
|
||||
|
||||
fn find(&self, account: &Account) -> Result<Vec<Self::Item>, String> {
|
||||
let key = account.key();
|
||||
let spec = HashMap::from([("service", SERVICE), ("username", key.as_str())]);
|
||||
self.0.search(&spec).map_err(super::refused)
|
||||
}
|
||||
|
||||
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> {
|
||||
let key = account.key();
|
||||
let spec = HashMap::from([("service", SERVICE), ("username", key.as_str())]);
|
||||
self.0.search(&spec).map_err(refused)
|
||||
let spec = HashMap::from([("service", SERVICE), ("user", key.as_str())]);
|
||||
self.0.search(&spec).map_err(super::refused)
|
||||
}
|
||||
|
||||
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> {
|
||||
match item.delete_credential() {
|
||||
Ok(()) | Err(keyring_core::Error::NoEntry) => Ok(()),
|
||||
Err(e) => Err(refused(e)),
|
||||
}
|
||||
super::remove_item(item)
|
||||
}
|
||||
|
||||
fn make(&self, account: &Account, text: &str) -> Result<(), String> {
|
||||
let label = account.label();
|
||||
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)
|
||||
super::make_in(&*self.0, account, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
fn get(&self, account: &Account) -> Result<Option<String>, KeyringError> {
|
||||
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 {
|
||||
fn get(&self, _: &Account) -> Result<Option<String>, KeyringError> {
|
||||
Err(KeyringError::Unavailable(NOT_YET.into()))
|
||||
@@ -279,9 +403,8 @@ impl Keeper for Os {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
const NOT_YET: &str =
|
||||
"agents' tokens are kept in the keyring on Linux only, until the macOS and Windows apps (P11)";
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
|
||||
const NOT_YET: &str = "agents' tokens are kept in the keyring on Linux, macOS and Windows only";
|
||||
|
||||
/// Files, for tests (INVARIANT 3): a folder per collection (`default/`
|
||||
/// 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
|
||||
/// ledger-host -- --ignored os_keyring`): kept, read, replaced and
|
||||
/// forgotten, under a service no one else uses.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn os_keyring_keeps_and_forgets() {
|
||||
|
||||
@@ -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-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-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-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) |
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
`Failed` filter, the owed-account name test, the credit side and the
|
||||
`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.
|
||||
@@ -23,3 +23,8 @@ node = "26"
|
||||
# A compile cache shared by every jj workspace's target dir: point
|
||||
# cargo's build.rustc-wrapper at it (~/.cargo/config.toml).
|
||||
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
@@ -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`.
|
||||
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.
|
||||
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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user