From d69e035e8e5fe693070f05ebf377d35352101707 Mon Sep 17 00:00:00 2001 From: Vladimir Rotariu Date: Tue, 13 Oct 2026 14:00:59 -0600 Subject: [PATCH 1/2] docs: bring the documentation back in line with the code A review of every maintained page against the source found drift in 30 documentation files. The fixes that change what a reader would do: - vm-daemon-setup.md put the vsock token where the daemon never reads it and never switched the daemon to vsock; it also said make install handles rpm-ostree on its own, when it refuses without /etc overrides. - mcp.md said execution halts on failed or unknown; it halts on any status but succeeded. - SECURITY.md described the intent secret screen and the role mapping as they were before 0.25.0. - automatic-rollback.md called signed status events future work. - audit-advisories.md listed 20 accepted advisories; two remain. - introduction.md and quickstart.md said the daemon runs as root. - Contributor docs gave clippy without --all-targets, a missing `npm run tauri` script, a PR-title CI check and an e2e PR label that do not exist, and a 7-story default that is now a larger curated set. Two non-doc changes ride along. Cargo.lock moves event-listener to 5.4.2 for RUSTSEC-2026-0221, which reached the daemon and the CLI through sqlx-core; cargo audit reported it as an unsound warning, so CI stayed green. A doc comment in dispatcher.rs named credential_argv_position, renamed to credential_argv_spec. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UpyrhbDTW1gBwzGAC44GYU --- .github/workflows/e2e.yml | 4 +- CHANGELOG.md | 32 ++++++++++++ CONTRIBUTING.md | 7 ++- Cargo.lock | 14 +---- README.md | 16 +++--- ROADMAP.md | 11 ++-- SECURITY.md | 20 +++++--- crates/sysknife-daemon/src/dispatcher.rs | 2 +- docs/SUMMARY.md | 1 + docs/adr/0002-brain-provider-layer.md | 5 ++ docs/adr/0004-per-distro-prompt-dispatch.md | 7 +-- docs/architecture.md | 21 +++++--- docs/automatic-rollback.md | 23 ++++++--- docs/cli.md | 2 +- docs/configuration.md | 3 +- docs/contributing/CONTRIBUTING.md | 9 ++-- docs/contributing/testing.md | 39 ++++++++------ docs/contributing/ubuntu-story-audit.md | 9 ++++ docs/demo/demo-script.md | 8 +-- docs/developer-guide.md | 38 +++++++++----- docs/distro-support.md | 17 +++--- docs/introduction.md | 26 +++++----- docs/mcp-registry.md | 9 ++-- docs/mcp.md | 10 ++-- docs/quickstart.md | 8 +-- docs/release-readiness.md | 4 +- docs/release.md | 10 ++-- docs/security/audit-advisories.md | 44 ++++++++-------- docs/testing/user-stories.md | 57 +++++++++++++-------- docs/typed-actions.md | 10 ++-- docs/ubuntu-action-gap-analysis.md | 2 +- docs/ubuntu-action-reference.md | 17 +++--- docs/user-stories.md | 57 ++++++++++----------- docs/vm-daemon-setup.md | 46 ++++++++++++----- tests/e2e/dev-stories.sh | 2 +- 35 files changed, 364 insertions(+), 226 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a10891fe..4e0de39e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -8,7 +8,7 @@ # # 2. `container-smoke`: developer guidance only — NOT a CI gate. Runs # on workflow_dispatch only. Boots Ollama and the daemon directly in -# the runner, pulls llama3.2:3b, and runs the 14 read-only user stories. +# the runner, pulls llama3.2:3b, and runs the curated read-only user stories. # User stories test LLM planning behavior against a probabilistic system; # they are development guidance, not merge criteria. # @@ -187,7 +187,7 @@ jobs: sudo mkdir -p /var/lib/sysknife-e2e sudo touch /var/lib/sysknife-e2e/ready - - name: Run read-only stories (1-7) + - name: Run read-only stories (curated read-only set) env: SYSKNIFE_LISTEN_URI: unix:///run/sysknife/daemon.sock SYSKNIFE_LLM_PROVIDER: ollama diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a6314a..6bc2b257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,13 @@ Releases before `0.2.5` predate the public launch; their notes live in the ([#603](https://github.com/lacs-project/sysknife/pull/603)) No SysKnife build compiles quinn-proto: the crate appears in `Cargo.lock` only. `cargo audit` reads the lockfile, so I bumped it to clear the five advisories. +- **event-listener 5.4.2 (RUSTSEC-2026-0221).** + ([#PRNUM](https://github.com/lacs-project/sysknife/pull/PRNUM)) The 5.4.1 + `StackSlot` let a `!Send` tag set with `Event::with_tag` cross threads, a data + race in safe code. Unlike the other accepted `cargo audit` warnings, this one + reached the daemon and the CLI, through `sqlx-core`. `cargo audit` reports it + as an unsound warning rather than a vulnerability, so CI stayed green; nothing + in SysKnife sets a tag. The lockfile moves to 5.4.2, which carries the fix. ### Fixed @@ -178,6 +185,31 @@ Releases before `0.2.5` predate the public launch; their notes live in the excludes a package that no longer exists (closes [#411](https://github.com/lacs-project/sysknife/issues/411)). Thanks to [@yuee3](https://github.com/yuee3). +- **The docs match the code again.** + ([#PRNUM](https://github.com/lacs-project/sysknife/pull/PRNUM)) A review of + every maintained page against the source found drift in 30 files. The ones a + reader would act on: + - `docs/vm-daemon-setup.md` put the vsock token in the login user's home, + where the daemon never reads it, and never switched the daemon to vsock. + The token belongs at `/var/lib/sysknife/.config/sysknife/token`, and a + drop-in sets `SYSKNIFE_LISTEN_URI=vsock://:9734`. + - The same page said `make install` handles rpm-ostree on its own. It refuses + until you pass the `/etc` overrides, and still cannot finish (#301). + - `docs/mcp.md` said execution halts on a failed or unknown step. It halts on + anything but `succeeded`, `needs_reboot` included. + - `SECURITY.md` described the intent secret screen as matching bare words such + as "password", and the role table as a hand-written match. Both changed in + 0.25.0: the screen looks for a value, and the role comes from the action's + risk tier. + - `docs/automatic-rollback.md` called signed status transitions future work. + They shipped, and `audit verify` checks them on SQLite. + - `docs/security/audit-advisories.md` listed 20 accepted advisories. + Two remain; RustSec withdrew the gtk-rs ones and the rest left the lockfile. + - The introduction and quickstart said the daemon runs as root. It runs as the + `sysknife` system user with scoped sudo and polkit grants. + - Contributor docs gave clippy without `--all-targets`, a `npm run tauri` + script that does not exist, a CI check for PR titles that does not run, and + an `e2e` PR label that triggers nothing. ## [0.25.0] — 2026-10-01 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 35c8fa7d..d854e144 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -147,7 +147,7 @@ gate invokes these fixtures too, so install both before running it locally: git checkout -b feat/ # … implement … cargo nextest run --workspace --locked -cargo clippy --workspace --all-targets -- -D warnings +cargo clippy --workspace --all-features --all-targets --locked -- -D warnings cargo fmt --all -- --check ``` @@ -300,9 +300,8 @@ Bodies should explain *why*. The diff already says what. - One PR per logical change. Big multi-concern PRs get split. - Add or update tests for any behaviour change. - Update docs alongside the code, not in a follow-up. -- Title in Conventional Commits style. The - [`semantic-pull-request`](https://github.com/amannn/action-semantic-pull-request) - CI check enforces this. +- Title in Conventional Commits style (`fix(cli): …`, `docs: …`). No CI check + enforces it; the squash merge uses the title as the commit subject. - Sign off your commits if your employer asks for DCO; we don't require it but we accept it. diff --git a/Cargo.lock b/Cargo.lock index 9c647220..d7a63fd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -639,15 +639,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "console" version = "0.16.4" @@ -1318,11 +1309,10 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] diff --git a/README.md b/README.md index 4775b25a..a6a17566 100644 --- a/README.md +++ b/README.md @@ -206,9 +206,10 @@ npx sysknife-setup --no-binary --daemon-mode=skip Whichever way you installed, there is one command for it. ```sh -# Removes the user service, the binaries in ~/.local/bin, and the Claude Code -# MCP + agent config in the current directory. A Cursor or Codex install also -# wrote .cursor/ and ~/.codex/config.toml, and those are left in place (#526). +# Removes the user service, the binaries in ~/.local/bin, and the SysKnife +# entries in the MCP and agent config it wrote: Claude Code's in the current +# directory, .cursor/mcp.json and .cursor/rules/sysknife.mdc, the sysknife block +# in ~/.codex/config.toml, and the SysKnife block in AGENTS.md. npx sysknife-setup --uninstall # See exactly what that would touch, without touching it. @@ -233,13 +234,14 @@ privilege boundary is worse than none: sudo make uninstall ``` -All three Ubuntu LTS releases record a live-VM run of the 79-story Ubuntu -suite, and each run has a replay twin that reproduces it: 22.04, 24.04 and 26.04 +All three Ubuntu LTS releases record a live-VM run of 79 stories from the Ubuntu +suite (which has since grown to 96), and each run has a replay twin that reproduces it: 22.04, 24.04 and 26.04 all at 79/79, every twin serving every call with zero misses. The runs are in `tests/evidence/story-runs/`. The suite grew from 50 when every Debian-only action got a story, `GetHostState` first. -Fedora Atomic is the rpm-ostree target; record a current Silverblue 44 VM run -before treating a release as current-validated. Plain Fedora Workstation and +Fedora Atomic is the rpm-ostree target; `make install` does not complete on +rpm-ostree yet ([#301](https://github.com/lacs-project/sysknife/issues/301)), and +a release needs a current Silverblue 44 VM run before it counts as validated. Plain Fedora Workstation and Server remain experimental until the `dnf` action family ships. See the [`distro support matrix`](docs/distro-support.md) for evidence and scope. diff --git a/ROADMAP.md b/ROADMAP.md index 3edb8975..7552b99e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -12,24 +12,21 @@ artefacts. Tracked in the v0.4.0 milestone. Ubuntu is the primary target today (20.04 and later) because that is where the users are; Fedora Atomic 41+ is eligible and its -rpm-ostree action families work, it just needs a fresh validation run. See +rpm-ostree action families work, but `make install` cannot finish on a read-only +`/usr` yet ([#301](https://github.com/lacs-project/sysknife/issues/301)). See [docs/distro-support.md](docs/distro-support.md). -- record a current Fedora Silverblue 44 full VM run +- install on rpm-ostree hosts (#301), then record a current Fedora Silverblue + 44 full VM run - dnf action family (Fedora Workstation non-atomic) - pacman action family (Arch/Manjaro) ## Phase 9: Launch - record a demo video on real hardware with rollback visible (#32) -- extend MCP server with direct read-only tools — expose all 63 Observer-level - actions (`get_disk_usage`, `list_services`, `get_authorized_keys`, …) as - individual MCP tools so Claude Desktop can read live system state in-context; - mutating actions remain plan-only to preserve the approval gate - Telegram interface (`sysknife-bot`) — approve plans from your phone via inline buttons; the viral mechanic ## Phase 10: Ecosystem -- `sysknife audit export --json` — shareable execution history - web dashboard for teams and fleet management diff --git a/SECURITY.md b/SECURITY.md index d0544a2d..1db9ab91 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -91,10 +91,14 @@ provider: - **Secret scan**: the same pattern list used to guard the preferences file (`SENSITIVE_PATTERNS` + `SENSITIVE_PREFIXES` in `crates/sysknife-brain/src/prefs.rs`) is applied to the raw intent. - Intents containing API key prefixes (`sk-`, `ghp_`, `xoxb-`, …), - the words `password`, `token`, `api_key`, and similar are rejected - with `PlanningError::IntentContainsSensitiveData` before any network - call is made. + An intent carrying a credential value is rejected with + `PlanningError::IntentContainsSensitiveData` before any network call is + made: a labelled assignment or phrase (`password=…`, `API_KEY: …`, + `password is …`, `use token `), a `Bearer` value, a PEM private-key + header, or a known key format (`sk-`, `ghp_`, `github_pat_`, `xoxb-`, JWTs, + Vault tokens and others). Bare vocabulary passes, so "check password aging" + or "why is my disk full" still plans; the screen looks for a value, not the + word. - **Credential-taking actions**: there is no out-of-band credential entry or reference resolver, so the planner does not offer `ProAttach`, and it offers `ConfigureWifi` for open networks only. The tool schema excludes `ProAttach`, @@ -132,9 +136,11 @@ The daemon resolves the caller's Linux group membership via | `sysknife-admin` or `wheel` | Admin | All including rpm-ostree, reboot | | `sysknife-boot` | Boot | Everything (reserved for boot-time automation) | -The per-action minimum role is a compile-time exhaustive match in -`crates/sysknife-daemon/src/policy.rs`. Unknown actions return `None` and -are denied unconditionally. The caller's role is never supplied by the +The per-action minimum role comes from the action's risk tier on its +`ActionSpec` (`Low`→Observer, `Medium`→Dev, `High`→Admin), in +`min_role_for_action` in `crates/sysknife-daemon/src/policy.rs`. A short +exception list can raise an action's role above its tier and never lower it. +An action with no `ActionSpec` returns `None` and is denied unconditionally. The caller's role is never supplied by the client — it is always derived server-side from kernel credentials. The same `SO_PEERCRED` read yields the peer's **uid**, which is recorded as the diff --git a/crates/sysknife-daemon/src/dispatcher.rs b/crates/sysknife-daemon/src/dispatcher.rs index 60b99010..3e1b571e 100644 --- a/crates/sysknife-daemon/src/dispatcher.rs +++ b/crates/sysknife-daemon/src/dispatcher.rs @@ -121,7 +121,7 @@ enum CredentialArgvSpec { /// Replace the credential argv element(s) with ``. /// /// Strategy (ME2 fix): -/// 1. **Positional redaction** — if `credential_argv_position` returns a spec +/// 1. **Positional redaction** — if `credential_argv_spec` returns a spec /// for this action, redact that exact index and return immediately. This /// prevents the value-match pass from also clobbering structural argv /// elements that happen to share the same text as the credential (e.g. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index b9c3128e..beb4468f 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -28,6 +28,7 @@ - [VM + Daemon Setup](vm-daemon-setup.md) - [Audit Storage and Recovery](storage-cloud.md) - [Ubuntu Action Reference](ubuntu-action-reference.md) +- [Firewall Observations](firewall-observations.md) # Security diff --git a/docs/adr/0002-brain-provider-layer.md b/docs/adr/0002-brain-provider-layer.md index e28ccf01..e62e2065 100644 --- a/docs/adr/0002-brain-provider-layer.md +++ b/docs/adr/0002-brain-provider-layer.md @@ -76,3 +76,8 @@ pure addition — a new `ProviderConfig` variant and a `require_api_key`-backed confirming the extensibility this ADR designed for. The original Decision and Alternatives Considered sections above are left as written for historical context; this amendment records the outcome. + +The `AnthropicProvider` and `OllamaProvider` types named above no longer +exist. `LlmPlanner` now builds every provider through one of two adapters in +`crates/sysknife-brain/src/providers/`: `AsyncOpenAiAdapter` for `openai`, and +`RigCompletionAdapter` (backed by the `rig` crate) for the other seven. diff --git a/docs/adr/0004-per-distro-prompt-dispatch.md b/docs/adr/0004-per-distro-prompt-dispatch.md index 2f942324..5309cdd9 100644 --- a/docs/adr/0004-per-distro-prompt-dispatch.md +++ b/docs/adr/0004-per-distro-prompt-dispatch.md @@ -68,9 +68,10 @@ which reads `/etc/os-release` at startup. If detection fails, `distro_hint` is - Prompt size decreases by roughly 20–25% for Fedora hosts and 15–20% for Debian hosts compared to the pre-dispatch single-prompt. - The generic fallback deliberately omits all distro-specific actions. A host - with an unrecognised `/etc/os-release` gets cross-distro-only planning until - the user adds a manual `distro_hint` override in - `~/.config/sysknife/config.toml`. + with an unrecognised `/etc/os-release` gets cross-distro-only planning. + (This ADR originally promised a manual `distro_hint` override in + `~/.config/sysknife/config.toml`. That key was never implemented: the hint + always comes from `sysknife_core::distro::detect()`.) ## References diff --git a/docs/architecture.md b/docs/architecture.md index b50a9aa1..e89c55ed 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -11,8 +11,9 @@ between planning, presentation, and execution. | `crates/sysknife-types/` | `sysknife-types` | Shared domain types | | `crates/sysknife-core/` | `sysknife-core` | Config file loading, constants | | `crates/sysknife-daemon/` | `sysknife-daemon` | Privileged executor | +| `crates/sysknife-daemon-test/` | `sysknife-daemon-test` | TAP integration harness for the daemon IPC layer, run in the test VM; not published or installed | | `crates/sysknife-proto/` | `sysknife-proto` | Protobuf definitions (future use) | -| `apps/sysknife-shell/` | `sysknife-shell` | Tauri + React GUI | +| `apps/sysknife-shell/` | `sysknife-shell` | Tauri + React GUI (development paused) | | `apps/sysknife-cli/` | `sysknife` | Production CLI — also used headlessly by E2E stories (`--dry-run --json`) | ### sysknife-brain @@ -132,12 +133,13 @@ running anything. ## Request Flow -1. A user enters intent in the shell. +1. A user enters intent in the CLI, or an MCP client calls `sysknife_plan`. 2. The brain proposes a typed plan. -3. The shell sends each mutating step to the daemon for preview. +3. The client sends each mutating step to the daemon for preview. 4. The daemon persists an immutable preview and returns its transaction ID, risk level, side effects, reboot requirement, and rollback availability. -5. The shell shows the preview and captures approval. +5. The approval gate shows the preview and captures approval: `sysknife + approve` in a terminal, or the paused Tauri shell. High-risk steps require the user to type the action name explicitly. 6. The daemon issues a deterministic, domain-separated Ed25519 receipt and stores its SHA-256 commitment inside the signed transaction row. @@ -146,7 +148,7 @@ running anything. receipt, then runs the action. 9. During execution, the daemon streams live stdout output line-by-line as `JobProgress` frames. -10. The shell displays each line as it arrives. +10. The client displays each line as it arrives. 11. On failure, if `rollback_available` is true, the daemon runs the rollback action automatically and reports the result. 12. The transaction is persisted to the configured SQLite or PostgreSQL store @@ -154,8 +156,13 @@ running anything. ## IPC Protocol -The shell and daemon communicate over a Unix domain socket -(`$XDG_RUNTIME_DIR/sysknife/daemon.sock` by default, overridable via `SYSKNIFE_LISTEN_URI`). +Clients (the CLI, the MCP server, and the paused shell) reach the daemon over +a Unix domain socket or, between a host and a guest VM, over virtio-vsock. The +packaged unit sets `SYSKNIFE_LISTEN_URI=unix:///run/sysknife/daemon.sock`. +With the variable unset, the daemon falls back to +`$XDG_RUNTIME_DIR/sysknife/daemon.sock`, then `/tmp/sysknife-.sock`, for +development runs. A Unix peer is identified by its kernel-attested uid +(`SO_PEERCRED`); a vsock peer authenticates with a pre-shared token. The framing is a 4-byte little-endian `u32` length prefix followed by a UTF-8 JSON body. Each message carries a `"type"` discriminant so diff --git a/docs/automatic-rollback.md b/docs/automatic-rollback.md index a12e88bf..f1878325 100644 --- a/docs/automatic-rollback.md +++ b/docs/automatic-rollback.md @@ -23,6 +23,13 @@ before the client ever sees a terminal result. There is no separate "approve the rollback" step — a failed rollback-eligible action rolls back automatically as part of finishing that job. +One exception: when the daemon cannot confirm the failed action has stopped +(a deadline it could not enforce), it skips the rollback, because rolling back +over a running transaction would be worse than leaving it. The job stays +`Failed`, its summary ends in `(automatic rollback skipped)`, and the daemon +keeps the exclusion slot held, so no other mutating action starts until the +daemon restarts. + If the rollback command itself succeeds, the job transitions to `JobState::RolledBack` and the `JobResult` carries a `rollback_ref` describing what was reverted. If the rollback command also fails, the job stays @@ -131,14 +138,14 @@ decision — who asked for what, at what risk level). The transaction row names the requester; the accounts that approved, spent, or revoked the approval live in the approval-event chain (see [the-audit-chain.md](the-audit-chain.md)). `status` is a mutable -column and is explicitly excluded from the signed payload — see the -"Status mutations are not in the chain" note in `audit_chain.rs`. That means -a completed rollback is durably recorded and forwarded, but the chain's -tamper-evidence guarantees cover the fact that the risky action was approved, -not the specific fact that it was later rolled back. If your threat model -needs the status transition itself to be tamper-evident, that's tracked as -future work (an append-only `audit_events` table), not something SysKnife -claims today. +column and stays out of the transaction row's signed payload, but each status +change also appends a signed `status_*` event (a rollback writes +`status_rolled_back`) to the append-only `audit_events` table, in the same +SQLite transaction as the column write. On SQLite, `sysknife audit verify` +compares every live status with its newest signed status event and exits `1` +on a disagreement, so rewriting a rolled-back job's status is detectable. +PostgreSQL stores have no status cross-check yet. See +[Execution-status verification on SQLite](the-audit-chain.md#execution-status-verification-on-sqlite). ## See also diff --git a/docs/cli.md b/docs/cli.md index ebb89a46..a7b75290 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -362,7 +362,7 @@ intent and planned + executed in sequence. | Ctrl+R | Reverse incremental history search | | Ctrl+A / Ctrl+E | Jump to line start / end | | Ctrl+W | Delete word before cursor | -| Ctrl+C | Cancel current line (does not exit) | +| Ctrl+C | Exit the REPL (same as Ctrl+D; history is saved) | | Ctrl+D | Exit the REPL | | `exit` / `quit` | Exit the REPL | diff --git a/docs/configuration.md b/docs/configuration.md index 89b43100..cf7ba79d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -151,7 +151,8 @@ For maintainers — the canonical types are: `StoragePoolSection` - `crates/sysknife-brain/src/config.rs` — `BrainConfig`, `ProviderConfig` - `crates/sysknife-core/src/lib.rs` — `default_listen_uri`, - `default_database_path`, `prefs_path` + `default_database_path` +- `crates/sysknife-core/src/config.rs` — `prefs_path` Adding a new config knob: add the field to `LacsConfig`, surface the env var in `apply_defaults_to_env`, and update this document. A test in diff --git a/docs/contributing/CONTRIBUTING.md b/docs/contributing/CONTRIBUTING.md index 5f63c6e8..29e6398d 100644 --- a/docs/contributing/CONTRIBUTING.md +++ b/docs/contributing/CONTRIBUTING.md @@ -23,7 +23,7 @@ rather answer questions than lose a contributor to a misunderstanding. ## Before You Start 1. Read the [architecture overview](../architecture.md) to understand - the four-crate structure and the trust boundary. + the crate structure and the trust boundary. 2. Read the [developer guide](../developer-guide.md) for prerequisites and build instructions. 3. Read the [testing guide](testing.md) for which tests to run and when. @@ -68,7 +68,7 @@ npm test --prefix apps/sysknife-shell && npm exec --prefix apps/sysknife-shell - ``` See [docs/developer-guide.md](../developer-guide.md) for the full -list of prerequisites (Rust, a C toolchain, Node.js 22, cargo-nextest, +list of prerequisites (Rust, a C toolchain, Node.js 22.22.2+ or 24, cargo-nextest, and the hygiene tools). ## Filing Issues @@ -157,8 +157,9 @@ One paragraph on what and why. Reference the issue: Closes #N. Title format: `type(scope): short description` — for example: `feat(daemon): add apt install action`. -Add the `e2e` label if your change touches the brain, daemon IPC, -or the action catalogue — this triggers the CI smoke test. +If your change touches the brain, daemon IPC, or the action catalogue, say so +in the PR; a maintainer can dispatch the `e2e` workflow's container smoke test, +which runs on manual dispatch only. ### 5. Review diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md index 97c5b7a5..c4ae80ec 100644 --- a/docs/contributing/testing.md +++ b/docs/contributing/testing.md @@ -12,7 +12,7 @@ validation before a release. | Unit tests (TypeScript) | React components, reducers, IPC shims | <5s | Every commit, every CI run | | Integration (Rust) | Daemon IPC, safety fence, policy | <10s | Every commit, every CI run | | **Dev stories (local, no VM)** | LLM plan structure for read-only stories; runs on any Linux host | 1-3 min | After brain/prompt changes | -| CI smoke (container) | Daemon + Ollama + read-only stories in a Linux runner | 5-10 min | Opt-in (PR label `e2e` or manual trigger) | +| CI smoke (container) | Daemon + Ollama + read-only stories in a Linux runner | 5-10 min | Opt-in (manual `workflow_dispatch` only) | | E2E Atomic VM | **Real Silverblue** in QEMU/KVM, full stack, all 54 stories | 15-30 min first boot; 2-3 min subsequent | Local / pre-release | | **E2E Ubuntu VM** | **Ubuntu 24.04** cloud image, full story suite (pass rate in `tests/evidence/story-runs/`) | ~15 min first boot; ~2 min subsequent | After Ubuntu action changes | | Manual QA | Real Silverblue/Kinoite hardware, destructive actions, GUI | 30-60 min | Before releases + demo video | @@ -26,7 +26,7 @@ These run on every CI build and must pass before merge: ```sh # Rust cargo fmt --all --check -cargo clippy --workspace --all-features --locked -- -D warnings +cargo clippy --workspace --all-features --all-targets --locked -- -D warnings cargo nextest run --workspace --locked # TypeScript / React @@ -38,7 +38,8 @@ npm exec -- tsc --noEmit ## Running stories on your dev machine (no VM required) -`tests/e2e/dev-stories.sh` runs the 7 read-only user stories directly on +`tests/e2e/dev-stories.sh` runs the curated read-only story set (stories +1-7, 11-17, 21, 22, 25, 26, 29, 32, 38, 41 and 46-49) directly on your workstation. It builds the daemon and test CLI, starts the daemon on `/tmp/sysknife-daemon.sock`, runs the stories, and then stops the daemon. @@ -47,17 +48,18 @@ names, risk levels, parameters). It does **not** execute the actions — it tests plan structure only. This works on any Linux host because the story scripts check the JSON plan output, not the results of `df`, `ps`, etc. -**LLM provider:** the harness mirrors the product's `BrainConfig::from_env`, -which auto-detects exactly two providers: +**LLM provider:** the product's `BrainConfig::from_env` auto-detects exactly +two providers: | Condition | Provider | Default model | |---|---|---| | `ANTHROPIC_API_KEY` set and non-blank | `anthropic` | `claude-sonnet-4-6` | | otherwise | `ollama` | `qwen3:8b` (must be pulled; CPU-only is impractical — use `SYSKNIFE_LLM_MODEL=llama3.2:3b` on CPU) | -The other six providers are supported but never auto-detected: setting the key -alone does nothing, because `from_env` only consults `ANTHROPIC_API_KEY`. Each -needs `SYSKNIFE_LLM_PROVIDER` **and** its key: +The other six providers are supported but the product never auto-detects them: +setting the key alone does nothing for `sysknife`, because `from_env` only +consults `ANTHROPIC_API_KEY`. Each needs `SYSKNIFE_LLM_PROVIDER` **and** its +key: | `SYSKNIFE_LLM_PROVIDER` | Key | Default model | |---|---|---| @@ -74,6 +76,12 @@ uninstalled Ollama fallback and failed all fifty stories with an error naming th wrong provider. `tests/e2e/provider-parity.test.sh` now derives the provider list and the default models from `config.rs` and fails when this table drifts again. +The story harnesses go further than the product. When `SYSKNIFE_LLM_PROVIDER` +is unset, `dev-stories.sh` and `run-stories.sh` export it from the first of the +seven keys they find, so in a story run a key alone does pick its provider. +The order differs: `dev-stories.sh` tries Anthropic first, `run-stories.sh` +tries OpenAI first. + Override the model with `SYSKNIFE_LLM_MODEL` (leave it *unset* rather than empty: an empty value wins over the per-provider default). @@ -121,12 +129,11 @@ For full story 8 and 10 coverage, use the VM path. The smoke test boots Ollama and the daemon directly in a GitHub Actions runner (no VM, no real atomic desktop), pulls a small tool-capable model -(`llama3.2:3b`), and runs the 7 read-only user stories. - -**Triggers:** +(`llama3.2:3b`), and runs `tests/e2e/run-stories.sh` with no arguments, which +is the same curated read-only set. -1. Label a PR with `e2e` — the workflow runs automatically -2. Manual dispatch via Actions → **e2e** → Run workflow +**Trigger:** manual dispatch only, via Actions → **e2e** → Run workflow. No PR +label starts it. Results appear as the `container-smoke` job. Story logs are uploaded as build artifacts. @@ -135,7 +142,7 @@ build artifacts. - Daemon startup, IPC framing, policy enforcement - Brain ↔ Ollama integration, tool-use loop, safety fence -- All 7 read-only query tools and read-only action stories (1–7) +- The read-only query tools and the curated read-only stories **What it does NOT cover** (that's the VM path below): @@ -372,11 +379,11 @@ Per-story logs are written to `tests/e2e/logs/story-N.log`. ## Before opening a PR 1. `cargo nextest run --workspace && npm test --prefix apps/sysknife-shell` — required, fast -2. `cargo clippy --workspace --all-features --locked -- -D warnings` +2. `cargo clippy --workspace --all-features --all-targets --locked -- -D warnings` 3. `cargo fmt --all --check` 4. For changes to the brain, daemon, IPC, or action catalogue: - Run the VM tests locally (`atomic-vm.sh` flow) - - Add the `e2e` label to trigger the CI smoke test on your PR + - Optionally run the CI smoke test by dispatching the `e2e` workflow ## Before a release diff --git a/docs/contributing/ubuntu-story-audit.md b/docs/contributing/ubuntu-story-audit.md index a1403b4b..2e753f11 100644 --- a/docs/contributing/ubuntu-story-audit.md +++ b/docs/contributing/ubuntu-story-audit.md @@ -1,5 +1,14 @@ # Ubuntu story audit (2026-04-25) +> Historical snapshot. The remediation this audit proposed has since been +> applied: the Fedora-only stories it lists accept the Ubuntu action (story 4 +> takes `GetFirewallState` or `UfwStatus`, story 46 takes +> `AptListUpgradable`), and the distro-gated ones carry a +> `SYSKNIFE_DISTRO_FAMILY` skip guard. The counts below describe the suite on +> that date. For the current suite run `bash tests/e2e/run-stories.sh +> --metadata` (54 atomic stories, ids 1-54, and 96 Ubuntu stories, ids +> 55-150). + ## Summary - Total stories audited: 116 (104 plan-structure + 12 exec) diff --git a/docs/demo/demo-script.md b/docs/demo/demo-script.md index 6748418f..cd454a98 100644 --- a/docs/demo/demo-script.md +++ b/docs/demo/demo-script.md @@ -8,8 +8,9 @@ Silverblue hardware. - Fedora Silverblue installation (VM or bare metal) - SysKnife built and installed (`make build && sudo make install`) - `sysknife-daemon` running (`sudo systemctl start sysknife-daemon`) -- Ollama running with `llama3.2:3b` pulled (default CPU-friendly model; - `qwen3:8b` is only recommended if you have GPU passthrough — see +- Ollama running with a tool-capable model pulled. `qwen3:8b` is the default + and wants GPU passthrough; on a CPU-only VM pull `llama3.2:3b`, the CPU + fallback, and set `SYSKNIFE_LLM_MODEL=llama3.2:3b` (see [HACKING.md §8](../../HACKING.md)) - Terminal recorder: [asciinema](https://asciinema.org/) for the terminal session, or OBS/SimpleScreenRecorder for the Tauri window @@ -122,4 +123,5 @@ recorded with timestamps. 4. Place GIF at `assets/demo/demo.gif` (see `assets/demo/README.md` for the VHS tape + `gifsicle` optimization step used to regenerate it) 5. Reference it from the docs (`docs/cli.md`, `docs/introduction.md`); the - README hero is the MCP flow at `assets/demo/mcp-flow.gif` + README hero is the Ubuntu MCP flow at `assets/demo/ubuntu-flow.gif`, and + `assets/demo/mcp-flow.gif` is the secondary Fedora Atomic recording diff --git a/docs/developer-guide.md b/docs/developer-guide.md index e92d1319..32905396 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -5,7 +5,7 @@ environment and covers everything you need to contribute confidently. ## Read First -- [Architecture overview](architecture.md) — understand the four-crate +- [Architecture overview](architecture.md) — understand the crate structure and the trust boundary before writing code - [ADR 0001: System boundaries](adr/0001-system-boundaries.md) - [ADR 0002: Brain provider layer](adr/0002-brain-provider-layer.md) @@ -19,7 +19,7 @@ environment and covers everything you need to contribute confidently. |---|---|---| | Rust stable | latest stable | [rustup.rs](https://rustup.rs) | | A C compiler and linker | — | `sudo apt-get install -y build-essential` | -| Node.js | 22+ | [nodejs.org](https://nodejs.org) or your distro | +| Node.js | 22.22.2+ or 24 (CI uses 24) | [nodejs.org](https://nodejs.org) or your distro. `npx sysknife-setup` runs on any 22+; the frontend's test stack (jsdom 30) asks for 22.22.2+ | | `cargo-nextest` | latest | `cargo install cargo-nextest --locked` | **Required to reproduce the `docs-and-hygiene` job**, which is one of the five @@ -30,6 +30,8 @@ checks `main` requires: | ShellCheck | distro | `sudo apt-get install -y shellcheck` | | Python | 3.10+ | usually already present | | PyYAML | 6.x | `python3 -m pip install PyYAML` (installed explicitly in CI) | +| `yamllint` | 1.38.0 | `python3 -m pip install yamllint==1.38.0` | +| `cargo-audit` | latest | `cargo install cargo-audit --locked` (the `security-audit` job) | | `markdownlint-cli2` | 0.23.2 | `npm install --global markdownlint-cli2@0.23.2` | | `markdown-link-check` | 3.15.0 | `npm install --global markdown-link-check@3.15.0` | @@ -83,10 +85,15 @@ build stops at `error: linker cc not found`. `cmake` is not required. # Build all Rust crates (fast, no linking of the Tauri app) cargo build --workspace -# Build the Tauri app (includes the GUI) -npm run tauri --prefix apps/sysknife-shell -- build +# Build the Tauri app (includes the GUI; development is paused) +cargo install tauri-cli --locked # once: the Tauri CLI is not a project dependency +cd apps/sysknife-shell && cargo tauri build ``` +`tauri.conf.json` runs `pnpm build` (and `pnpm dev` for `cargo tauri dev`) +before compiling, so pnpm has to be on your `PATH` for these two commands even +though the rest of the repository uses npm. + ## Running Tests These run in under 15 seconds and are required before every push: @@ -158,7 +165,7 @@ published ports reset the connection, in which case use podman. ### The hygiene guards -`docs-and-hygiene` runs about two dozen host-side scripts under `tests/release/` +`docs-and-hygiene` runs about four dozen host-side scripts under `tests/release/` and `scripts/`. They are plain shell and Python, they need no build, and each one runs on its own in well under a second: @@ -214,7 +221,7 @@ cargo run -p sysknife-daemon ```sh cd apps/sysknife-shell -npm run tauri --prefix apps/sysknife-shell -- dev +cargo tauri dev ``` The shell opens as a desktop window. Type an intent and the daemon @@ -222,7 +229,7 @@ responds. The LLM is auto-detected from your environment. ## Running the E2E Stories on Your Dev Machine -`tests/e2e/dev-stories.sh` runs the 7 read-only user stories without +`tests/e2e/dev-stories.sh` runs the curated read-only story set (the `STORIES` default in the script) without a VM. It validates that the LLM proposes the correct typed plan — it does not execute the actions against your host. @@ -363,15 +370,22 @@ crates/ sysknife-daemon/ Privileged executor, 191 actions with an `ActionSpec`, IPC, rollback, SQLite sysknife-proto/ Protobuf definitions (future use) + sysknife-daemon-test/ TAP integration harness for the daemon IPC layer (VM only) apps/ - sysknife-shell/ Tauri + React GUI + sysknife-cli/ The `sysknife` binary: CLI, REPL, MCP server + sysknife-shell/ Tauri + React GUI (development paused) tests/ e2e/ dev-stories.sh Run E2E stories on any Linux host (uses sysknife --dry-run --json) - atomic-vm.sh Manage a Silverblue QEMU/KVM VM for full E2E + run-stories.sh The story runner the VMs and the container smoke test use + atomic-vm.sh Manage a Silverblue QEMU/KVM VM for full E2E + ubuntu-vm.sh Manage the Ubuntu LTS VMs for full E2E + stories/ One script per user story + release/ Host-side gate scripts run by docs-and-hygiene + evidence/ Test-count baseline, story-run evidence, macOS survey docs/ adr/ Architectural decision records - contributing/ Testing guide + contributing/ Contributor, testing and Ubuntu VM guides ``` ## CI @@ -381,7 +395,7 @@ CI runs on every pull request and push to `main`. | Check | Command | |---|---| | Rust formatting | `cargo fmt --all --check` | -| Clippy (warnings as errors) | `cargo clippy --workspace --all-features --locked -- -D warnings` | +| Clippy (warnings as errors) | `cargo clippy --workspace --all-features --all-targets --locked -- -D warnings` | | Rust tests | `cargo nextest run --workspace --locked` | | TypeScript type check | `npx tsc --noEmit` (in `apps/sysknife-shell`) | | Frontend tests | `npm test --prefix apps/sysknife-shell` | @@ -393,7 +407,7 @@ Run the Rust checks locally before pushing: ```sh cargo fmt --all --check -cargo clippy --workspace --all-features --locked -- -D warnings +cargo clippy --workspace --all-features --all-targets --locked -- -D warnings cargo nextest run --workspace --locked ``` diff --git a/docs/distro-support.md b/docs/distro-support.md index 63eb74f3..b2b17953 100644 --- a/docs/distro-support.md +++ b/docs/distro-support.md @@ -18,8 +18,10 @@ Two separate things get called "support", and conflating them is what produced the bug this section now documents: - **Eligibility** — will the daemon act on this host at all? That is - `DistroId::is_supported()` in `crates/sysknife-core/src/distro.rs`, and the - daemon refuses every *mutating* action when it is false. It is true for all + `DistroId::is_supported()` in `crates/sysknife-core/src/distro.rs`. When it + is false, or the distro cannot be detected, the daemon refuses every mutating + action and every hard-fenced (`FEDORA_ONLY` / `DEBIAN_ONLY` / `UBUNTU_ONLY`) + action, reads included. Only portable Observer reads are exempt. It is true for all Ubuntu releases from 20.04 up, Fedora Atomic 41 and later, and Debian stable releases with a known version of 12 or later. - **Validation tier** — has the full story suite been run on that release? @@ -46,6 +48,7 @@ tracked separately in [#239](https://github.com/lacs-project/sysknife/issues/239 | **Validated** | The documented full story suite passed on a real VM. | | **Smoke-tested** | Bootstrap and basic daemon/tooling checks passed; full action parity was not exercised. | | **Current validation required** | An action backend exists, but the current distro release still needs its launch-gate VM run. | +| **Blocked** | An action backend exists, but a known defect stops installation on the release; the linked issue tracks it. | | **Experimental** | Detection or partial code exists, but production support is not claimed. | | **Planned** | No complete action backend exists. | @@ -73,9 +76,9 @@ install: cannot create directory '/usr/lib/sysknife': Read-only file system ``` `HELPERS` defaults to `/usr/lib/sysknife`, and rpm-ostree mounts `/usr` -read-only. The twelve privileged helper scripts have nowhere to go, and the path -is not a free choice: twelve daemon constants and twelve `NOPASSWD` sudoers -grants name it. [#301](https://github.com/lacs-project/sysknife/issues/301) +read-only. The fifteen privileged helper scripts have nowhere to go, and the +path is not a free choice: the daemon names it, and so do the `NOPASSWD` +sudoers grants for fourteen of them (`firewall-state` runs without sudo). [#301](https://github.com/lacs-project/sysknife/issues/301) carries the options and [discussion #307](https://github.com/lacs-project/sysknife/discussions/307) is where the shape is being argued. @@ -83,8 +86,8 @@ where the shape is being argued. `make install` no longer discovers this halfway. `daemon-install-preflight` checks every directory in `INSTALL_DIRS` before the first write and refuses the whole install, naming the directories at fault. Before that, the run had already -written the daemon binary, the systemd unit, the polkit rules and all twelve -sudo grants, leaving live grants for helper scripts that did not exist. +written the daemon binary, the systemd unit, the polkit rules and the sudoers +grants, leaving live grants for helper scripts that did not exist. Everything above the install still holds: detection, the rpm-ostree action family and the atomic story family are implemented and covered by the workspace diff --git a/docs/introduction.md b/docs/introduction.md index 82a107cf..ea09c2df 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -23,14 +23,16 @@ plan, approve it, and the daemon executes it step by step with live output and automatic rollback on failure. ```sh -$ sysknife "install neovim and make sure it starts on login" - -Plan (2 steps) -────────────────────────────────────────────────────────── - 1 AddLayeredPackage neovim risk: high - 2 SetServiceEnabled neovim.service risk: medium -────────────────────────────────────────────────────────── -Approve? [y/N] +$ sysknife "install nginx and make sure it starts on boot" + + Install nginx and enable it at boot + ────────────────────────────────────────────────── + 1 AptInstall medium approval required + Install the nginx package + 2 SetServiceEnabled medium approval required + Enable nginx.service at boot + + 2 steps, medium risk — execute? [y/N] ``` The AI cannot run a shell command. It can only propose typed actions. The daemon @@ -57,8 +59,8 @@ SysKnife gives you: ``` ┌─────────────────┐ plan ┌──────────────────┐ approve ┌─────────────────┐ -│ sysknife-brain │ ────────► │ sysknife-shell │ ────────► │ sysknife-daemon │ -│ (unprivileged) │ │ (approval gate) │ │ (root, locked) │ +│ sysknife-brain │ ────────► │ sysknife approve │ ────────► │ sysknife-daemon │ +│ (unprivileged) │ │ (approval gate) │ │ (scoped grants) │ └─────────────────┘ └──────────────────┘ └─────────────────┘ LLM + tools you review here executes + audits ``` @@ -66,8 +68,8 @@ SysKnife gives you: | Component | Privilege | Job | |---|---|---| | **brain** | none | Talks to the LLM, proposes a typed plan | -| **shell** | user | Shows you the plan, collects your approval | -| **daemon** | root | Executes approved actions, writes the audit log | +| **approval gate** | user | Shows you the plan, collects your approval: `sysknife approve` in a terminal (the paused Tauri shell is a second implementation) | +| **daemon** | `sysknife` system user, with scoped sudo and polkit grants | Executes approved actions, writes the audit log | The brain proposes but **cannot touch the system**. The daemon executes but **cannot be reached without an approved plan**. diff --git a/docs/mcp-registry.md b/docs/mcp-registry.md index a9e8a57d..73306726 100644 --- a/docs/mcp-registry.md +++ b/docs/mcp-registry.md @@ -186,8 +186,9 @@ report. Running the build filled it in. `sysknife mcp-server` is the **unprivileged** half of the system. It plans, previews, renders, and forwards. It cannot change anything. Every mutation travels over a unix socket to `sysknife-daemon`, which holds the sudoers, polkit -and helper policy that `sudo make install` owns, and the socket itself is -`0750 sysknife:sysknife` at `/run/sysknife/daemon.sock`. That split is the +and helper policy that `sudo make install` owns. The socket is +`/run/sysknife/daemon.sock`, mode `0660`, inside the `0750 sysknife:sysknife` +directory `/run/sysknife`. That split is the product: it is why an agent cannot hand a shell string to root, and why every executed action lands in the signed chain. See [Architecture & Trust Boundaries](architecture.md). @@ -202,8 +203,8 @@ So the split is between **describing** and **doing**: `sysknife_execute`, `sysknife_history` and `sysknife_doctor` all reach `sysknife-daemon` over a unix socket at `/run/sysknife/daemon.sock`; the generated direct queries use the same socket and daemon authorization fence. - The socket is - (`0750 sysknife:sysknife`), which holds the sudoers, polkit and helper policy + The socket sits in the `0750 sysknife:sysknife` directory `/run/sysknife`, + and the daemon behind it holds the sudoers, polkit and helper policy `sudo make install` owns. In a container there is nothing listening, so `doctor` reports the socket absent and the rest return errors. That is the trust boundary doing its job, not a packaging defect. See diff --git a/docs/mcp.md b/docs/mcp.md index 51c8aa68..0d2abf72 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -88,9 +88,11 @@ one-time receipt printed by `sysknife approve `. | `steps` | `StepToExecute[]` | Approved steps from `sysknife_plan` | Each `StepToExecute` contains the original `transaction_id`, `action_name`, and -`params`, plus an `approval_receipt`. Execution halts on the first failed or -unknown outcome. Results already received remain in the response, followed by -the interrupted step; subsequent steps are not attempted. +`params`, plus an `approval_receipt`. Execution halts on the first step whose +status is anything other than `succeeded`: `failed`, `unknown`, `rolled_back`, +`canceled`, and `needs_reboot` all stop the plan. Results already received +remain in the response, followed by the interrupted step; subsequent steps are +not attempted. **Output** — `ExecuteOutput` @@ -104,7 +106,7 @@ Each `StepResult`: | Field | Type | Description | |------------------|------------|------------------------------------------| | `action_name` | string | Action whose execution was attempted | -| `status` | string | `"succeeded"`, `"failed"`, `"unknown"`, etc. | +| `status` | string | `"succeeded"`, `"failed"`, `"unknown"`, `"rolled_back"`, `"canceled"`, `"needs_reboot"` | | `summary` | string | Human-readable outcome | | `output` | `string[]` | Progress lines (ANSI stripped) | | `warnings` | `string[]` | Daemon warnings or outcome recovery guidance | diff --git a/docs/quickstart.md b/docs/quickstart.md index 8d9668b8..5957d706 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -71,8 +71,9 @@ build (measured 6m56s on Ubuntu 24.04, 11m43s on 22.04). > **ℹ️ Fedora / Silverblue** > -> Ubuntu 24.04 is validated by the live-VM story suite. Ubuntu 22.04 and -> 26.04 have smoke-test coverage. Fedora Atomic uses the rpm-ostree action +> Ubuntu 22.04, 24.04 and 26.04 are each validated by a committed live-VM +> story run (79/79) with a replay twin; other Ubuntu releases from 20.04 up are +> eligible without a per-release run. Fedora Atomic uses the rpm-ostree action > family and requires a current Silverblue 44 validation run for each release. > Plain Fedora remains experimental. See [distro support](distro-support.md). @@ -118,7 +119,8 @@ sysknife "what packages do I have installed as layers?" > > `--dry-run` works anywhere and is a great way to test the planner without > installing the daemon. Full execution requires `sysknife-daemon` running -> as root (enabled in Step 1). +> (enabled in Step 1). It runs as the unprivileged `sysknife` system user and +> reaches root only through the scoped sudo and polkit grants it installs. That's it. The planner proposes a typed plan, you approve, the daemon executes. diff --git a/docs/release-readiness.md b/docs/release-readiness.md index 57cba826..3d614979 100644 --- a/docs/release-readiness.md +++ b/docs/release-readiness.md @@ -10,7 +10,7 @@ security review. Run these on the exact candidate commit and retain the CI links: - [ ] `cargo fmt --all --check` -- [ ] `cargo clippy --workspace --all-features --locked -- -D warnings` +- [ ] `cargo clippy --workspace --all-features --all-targets --locked -- -D warnings` - [ ] `cargo nextest run --workspace --locked` - [ ] frontend typecheck, 72 tests, production build, and production npm audit - [ ] live PostgreSQL migration and store contract @@ -27,7 +27,7 @@ Run these on the exact candidate commit and retain the CI links: without its replay does not count: it publishes a rate nothing has reproduced, and `tests/release/cassette-replay-parity.test.sh` fails. - [ ] Fedora Silverblue 44 full atomic VM suite passes before the README badge - is promoted from **current validation required**. + is promoted from **eligible, unvalidated**. - [ ] Clean install, preview, terminal approval, execution, audit verification, upgrade, rollback/uninstall, and reinstall are exercised with no undocumented manual repair. diff --git a/docs/release.md b/docs/release.md index 15d7de47..2ad3bb60 100644 --- a/docs/release.md +++ b/docs/release.md @@ -209,8 +209,10 @@ npm pack --dry-run ``` Trusted publishing requires the npm package's publisher configuration to -match the GitHub repository and workflow exactly. Keep `id-token: write` -scoped to the npm job. +match the GitHub repository and workflow exactly. Grant `id-token: write` per +job, never at the workflow level. Three jobs hold it today: `build` (build +provenance attestations), `publish-npm` (trusted publishing) and +`publish-registry` (MCP Registry OIDC). ### crates.io @@ -241,4 +243,6 @@ sha256sum --check sha256sums-linux-x86_64.txt Also perform a clean install, `sysknife doctor`, one preview/approve/execute cycle, and uninstall on the supported OS image before announcing the release. -Keep the release private or draft until these checks pass. +The workflow creates the GitHub release as public, not as a draft, after npm +and crates.io have already published, so these checks gate the announcement, +not the publication. diff --git a/docs/security/audit-advisories.md b/docs/security/audit-advisories.md index b89d3926..ba1134bc 100644 --- a/docs/security/audit-advisories.md +++ b/docs/security/audit-advisories.md @@ -34,25 +34,24 @@ back in. | Advisory | Crate | Note | |---|---|---| -| RUSTSEC-2024-0411 | `gdkwayland-sys` | gtk-rs GTK3 bindings — unmaintained | -| RUSTSEC-2024-0412 | `gdk` | gtk-rs GTK3 bindings — unmaintained | -| RUSTSEC-2024-0413 | `atk` | gtk-rs GTK3 bindings — unmaintained | -| RUSTSEC-2024-0414 | `gdkx11-sys` | gtk-rs GTK3 bindings — unmaintained | -| RUSTSEC-2024-0415 | `gtk` | gtk-rs GTK3 bindings — unmaintained | -| RUSTSEC-2024-0416 | `atk-sys` | gtk-rs GTK3 bindings — unmaintained | -| RUSTSEC-2024-0417 | `gdkx11` | gtk-rs GTK3 bindings — unmaintained | -| RUSTSEC-2024-0418 | `gdk-sys` | gtk-rs GTK3 bindings — unmaintained | -| RUSTSEC-2024-0419 | `gtk3-macros` | gtk-rs GTK3 bindings — unmaintained | -| RUSTSEC-2024-0420 | `gtk-sys` | gtk-rs GTK3 bindings — unmaintained | -| RUSTSEC-2024-0384 | `instant` | unmaintained (transitive via GUI/webview) | -| RUSTSEC-2024-0370 | `proc-macro-error` | unmaintained (transitive) | -| RUSTSEC-2025-0012 | `backoff` | unmaintained (transitive) | -| RUSTSEC-2025-0057 | `fxhash` | unmaintained (transitive) | -| RUSTSEC-2025-0075 | `unic-char-range` | unmaintained (transitive) | -| RUSTSEC-2025-0080 | `unic-common` | unmaintained (transitive) | -| RUSTSEC-2025-0081 | `unic-char-property` | unmaintained (transitive) | -| RUSTSEC-2025-0098 | `unic-ucd-version` | unmaintained (transitive) | -| RUSTSEC-2025-0100 | `unic-ucd-ident` | unmaintained (transitive) | +| RUSTSEC-2024-0370 | `proc-macro-error` | unmaintained (transitive, Tauri build stack) | + +## Removed from this record + +- **The gtk-rs GTK3 bindings** (RUSTSEC-2024-0411 to RUSTSEC-2024-0420: + `gdkwayland-sys`, `gdk`, `atk`, `gdkx11-sys`, `gtk`, `atk-sys`, `gdkx11`, + `gdk-sys`, `gtk3-macros`, `gtk-sys`). RustSec withdrew these advisories on + 2026-08-14, so `cargo audit` no longer reports them, although the crates are + still built for the shell. +- **`instant`, `backoff`, `fxhash` and the five `unic-*` crates** + (RUSTSEC-2024-0384, RUSTSEC-2025-0012, RUSTSEC-2025-0057, RUSTSEC-2025-0075, + RUSTSEC-2025-0080, RUSTSEC-2025-0081, RUSTSEC-2025-0098, RUSTSEC-2025-0100). + None of them is in `Cargo.lock` any more. +- **`event-listener` 5.4.1** (RUSTSEC-2026-0221, unsound: a `!Send` tag set + with `Event::with_tag` could cross threads through `StackSlot`). Unlike every + other entry here it reached the daemon and the CLI, through `sqlx-core`. It + was never accepted: the lockfile moved to 5.4.2, which carries the fix, on + 2026-10-13. ## Policy @@ -61,5 +60,8 @@ back in. - **Unmaintained** advisories are reported as non-fatal warnings by default; they are left visible (not globally ignored) so the team notices if the set grows. This file is the record of the currently-accepted set and why. -- Re-evaluate the GTK3 entries when Tauri migrates its Linux backend off the - GTK3 binding stack; re-evaluate `rand` when the webview dep chain bumps it. +- An unsound or unmaintained advisory on a crate the daemon, CLI or brain links + is not accepted here; bump it or drop the dependency. Check reach with + `cargo tree -e no-dev -i ` before adding a row. +- Re-evaluate `glib` when Tauri migrates its Linux backend off the GTK3 binding + stack. diff --git a/docs/testing/user-stories.md b/docs/testing/user-stories.md index 20c075d1..3d25f914 100644 --- a/docs/testing/user-stories.md +++ b/docs/testing/user-stories.md @@ -6,7 +6,8 @@ COSMIC Atomic). Run inside a QEMU/KVM VM via `tests/e2e/atomic-vm.sh`, or on real hardware. These twenty are a readable subset, not the suite: the atomic family is 54 -stories and the Ubuntu family a further 79, all in `tests/e2e/stories/`. The +stories and the Ubuntu family a further 96 (79 of them in the recorded +live-VM runs), all in `tests/e2e/stories/`. The harness runs the family; this file explains twenty of them in prose. Each story has: @@ -18,6 +19,12 @@ Each story has: - **Pass criteria** — concrete conditions for success - **Cleanup** — how to revert any system changes +`run-stories.sh` checks plan structure only: action names, step counts, risk +levels and parameters in the `--dry-run --json` output. Pass criteria about +execution output, timing or on-host effects (a `/dev/` line in `df` output, a +key landing in `authorized_keys`) are what to look for when you run a story by +hand against a real VM; no script asserts them. + The **automated** stories (1–7, 11–17) are also covered by the container-based CI smoke test (see `.github/workflows/e2e.yml`). The **semi-automated** stories (8–10, 18–20) make real system changes and only run when @@ -237,7 +244,8 @@ CI smoke test (see `.github/workflows/e2e.yml`). The **semi-automated** stories - Goes directly to `propose_plan` — explicit key and username in the intent, no decision needed - Proposes `AddAuthorizedKey` with `{username: "lacsdev", public_key: "ssh-ed25519 ..."}` -- Plan marked `approvalRequired: true`, risk `medium` +- Plan marked `approvalRequired: true`, risk `high` (an SSH key is an + access-control change, the same class as `AddUserToGroup`) - The public_key param must NOT be truncated or modified **Automated:** only with `SYSKNIFE_ALLOW_DESTRUCTIVE=1` @@ -254,23 +262,26 @@ CI smoke test (see `.github/workflows/e2e.yml`). The **semi-automated** stories --- -## Story 11: Deployment status + kernel arguments +## Story 11: Post-update diagnostic, four actions -**Persona:** Developer curious about the booted OSTree commit and boot configuration. +**Persona:** User whose system misbehaves after an rpm-ostree update. -**Intent:** `"show me the current deployment status and what kernel arguments are set"` +**Intent:** `"My system has been acting weird since yesterday's rpm-ostree +update — show me the full deployment history, what packages I've layered on +top of the base, whether any systemd services are currently in a failed state, +and how much disk space is left"` **Expected LLM behavior:** -- Goes directly to `propose_plan` — two independent read-only requests, no - decision needed -- Proposes a 2-step plan: `ListDeployments` + `GetKernelArguments` (either order) -- Both steps Low risk, no approval required +- Resists the diagnostic framing: no `get_system_state` or `query_*` call +- Goes straight to `propose_plan` with four read-only actions **Automated:** yes (read-only) **Pass criteria:** -- Plan has exactly 2 steps, both risk `low` -- Steps contain `ListDeployments` and `GetKernelArguments` +- Plan contains a deployment action (`ListDeployments` or + `GetDeploymentHistory`), `GetLayeredPackages` (or `AptListInstalled` on an + apt host), `ListServices` and `GetDiskUsage` +- Every step is risk `low` **Cleanup:** none @@ -298,20 +309,21 @@ CI smoke test (see `.github/workflows/e2e.yml`). The **semi-automated** stories ## Story 13: Service logs for a named service -**Persona:** Admin debugging a network issue by reading firewall service logs. +**Persona:** Admin checking why SSH logins fail. -**Intent:** `"show me the logs for the firewalld service"` +**Intent:** `"show me the logs for the sshd service"` (sshd exists on both +Fedora and Ubuntu, so the story is distro-agnostic) **Expected LLM behavior:** - Goes directly to `propose_plan` — direct read request with an explicit service name -- Proposes `GetServiceLogs` with `params.unit = "firewalld"` (or `"firewalld.service"`) +- Proposes `GetServiceLogs` with `params.unit` naming the SSH unit - Does NOT call `query_services` or any other query tool first **Automated:** yes (read-only) **Pass criteria:** - Plan has 1 step, `GetServiceLogs` -- `params.unit` is `"firewalld"` or `"firewalld.service"` +- `params.unit` is `"sshd"`, `"sshd.service"`, `"ssh"` or `"ssh.service"` - Risk `low` **Cleanup:** none @@ -483,7 +495,7 @@ by the manual QA checklist (see `demo-script.md`): - **RebaseSystem** — full OS upgrade (requires real network, 20+ min) - **RebootSystem** — actual reboot (breaks VM test flow) - **Tauri GUI rendering** — the shell's React UI (requires display server; - covered by `pnpm test` at component level) + covered by `npm test` at component level) - **Reconnect banner on daemon crash** — covered by unit tests ## Running the stories @@ -512,14 +524,17 @@ installation prerequisites, Windows instructions, and troubleshooting. ### In CI -See `.github/workflows/e2e.yml`. Triggered manually via `workflow_dispatch` or -on PRs labeled `e2e`. +See `.github/workflows/e2e.yml`. Its `scripts-lint` job runs on pushes and +pull requests that touch the paths it lints. The `container-smoke` job, which +runs the stories, starts only on a manual `workflow_dispatch`; no PR label +triggers it. ### Prompt engineering observations -The system prompt in `crates/sysknife-brain/src/prompt.rs` contains three worked -examples (A, B, and C). These are **load-bearing** — removing them causes 4 of 7 -read-only stories to fail with GPT-4o. +The system prompt in `crates/sysknife-brain/src/prompt.rs` now contains six +worked examples, A through F. They are **load-bearing**: in the measurement +below, taken when the prompt had three (A, B and C), removing them caused 4 of +7 read-only stories to fail with GPT-4o. The original Example A ("check disk usage") was removed — it was a strict subset of the prose rule and the current Example A, and added no measurable diff --git a/docs/typed-actions.md b/docs/typed-actions.md index 44084018..a19cdd04 100644 --- a/docs/typed-actions.md +++ b/docs/typed-actions.md @@ -65,10 +65,12 @@ codebase. On the daemon side (`sysknife-daemon`), each action is backed by an As of this writing the catalogue defines **192 actions** across families such as Deployment, Services, Package Layering, Flatpak, Containers, Toolbox, Network, Identity, SSH Keys, Package Repositories, apt/snap/ufw/netplan/grub -(Debian-family), and rpm-ostree/AppArmor/cloud-init/Pro (Fedora-family). Each -family is also fenced by distro: `crates/sysknife-core/src/action_family.rs` -is the single source of truth for which actions are Debian-only versus -Fedora-only, and that list is shared — not duplicated — across the brain's +and AppArmor/cloud-init (Debian-family), Ubuntu Pro (Ubuntu-only), and +rpm-ostree and dnf repositories (Fedora-family). Each family is also fenced by +distro: `crates/sysknife-core/src/action_family.rs` is the single source of +truth for which actions are Fedora-only, Debian-only or Ubuntu-only (plus the +`NON_CANONICAL_ON_*` lists of portable actions a distro's planner does not +offer), and those lists are shared — not duplicated — across the brain's prompt construction, the CLI's routing guard, and the daemon's execution fence, specifically so the three can never silently drift apart. diff --git a/docs/ubuntu-action-gap-analysis.md b/docs/ubuntu-action-gap-analysis.md index 3f5f7c40..728cdef7 100644 --- a/docs/ubuntu-action-gap-analysis.md +++ b/docs/ubuntu-action-gap-analysis.md @@ -223,7 +223,7 @@ Implement in this order, smallest-first to build momentum: 3. `AptHistoryList` — 5 lines, read-only, closes audit/review workflow gap 4. `GrubGetKargs` — 5 lines, read-only, needed before `GrubSetKargs` can be safely tested 5. `SnapRevert` — 3 lines, closes the snap revision rollback gap -6. `SnapClassicInstall` — 2 lines, fixes the known D3 divergence in `ubuntu-action-reference.md` +6. `SnapClassicInstall` — 2 lines, adds classic confinement, which `SnapInstall` does not expose (since implemented) 7. `AddPpa` / `RemovePpa` — implement together; check for `software-properties-common` at runtime 8. `GrubSetKargs` — medium effort; needs a file-edit abstraction and a `update-grub` call; implement after `GrubGetKargs` is validated on a real VM 9. `AptRollback` — medium effort; runtime check for `apt-rollback` package; implement last in tier diff --git a/docs/ubuntu-action-reference.md b/docs/ubuntu-action-reference.md index 26734b92..5ff05a0a 100644 --- a/docs/ubuntu-action-reference.md +++ b/docs/ubuntu-action-reference.md @@ -164,9 +164,13 @@ Sources consulted: - **Canonical (with hold)**: `sudo sh -c "snap install --channel= && snap refresh --hold "` - **Canonical (auto-update allowed)**: `sudo snap install --channel= ` -- **SysKnife argv (auto_update=false)**: `["sudo", "sh", "-c", "snap install --channel= && snap refresh --hold "]` -- **SysKnife argv (auto_update=true)**: `["sudo", "snap", "install", "--channel=", ""]` -- **Status**: ✅ matches +- **SysKnife argv (auto_update=false, the default)**: `["sudo", "/usr/lib/sysknife/action-steps", "snap-install-hold", "", ""]`. + The helper runs `snap install --channel= ` and then + `snap refresh --hold `, and does not hold a failed install. `` + defaults to `stable`. +- **SysKnife argv (auto_update=true)**: `["sudo", "snap", "install", "--channel=", ""]`; + with no `channel` param, `--channel` is omitted. +- **Status**: ✅ matches (the hold path runs the canonical pair through the helper) - **Notes**: `--channel` format is `/` (e.g., `latest/stable`, `beta`). Default channel when none specified by SysKnife: `stable` (passed as `--channel=stable`). The hold-after-install pattern is correct: `snap refresh --hold ` pins the snap @@ -299,12 +303,13 @@ Sources consulted: ### UfwStatus — Show firewall status - **Canonical**: `sudo ufw status verbose` -- **SysKnife argv**: `["sudo", "ufw", "status", "verbose"]` +- **SysKnife argv**: `["sudo", "ufw", "status", "verbose"]`; with the optional + bool param `numbered: true`, `["sudo", "ufw", "status", "numbered"]` - **Status**: ✅ matches - **Notes**: `verbose` adds default policies (incoming/outgoing/routed) to the output. Without `verbose`, `ufw status` shows only active rules and - the Enabled/Disabled state. `status numbered` adds rule numbers, which is - how a user finds the `rule_number` argument for `UfwDeleteRule`. + the Enabled/Disabled state. `numbered: true` adds rule numbers, which is + how the planner finds the `rule_number` argument for `UfwDeleteRule`. - **Output format**: Plain text, columns: `To Action From` with directional qualifiers (`ALLOW`, `ALLOW IN`, `ALLOW OUT`). No JSON output available. - **sudo required**: Yes — ufw reads privileged iptables state. diff --git a/docs/user-stories.md b/docs/user-stories.md index bf708631..b6e2d07f 100644 --- a/docs/user-stories.md +++ b/docs/user-stories.md @@ -37,15 +37,16 @@ system. `state.planner.plan_intent(intent)`. 4. **LLM planning loop** (`crates/sysknife-brain/src/planner.rs` — `plan_intent()`) - Turn 0: LLM receives system prompt (189-action catalogue, risk rules) + + Turn 0: LLM receives system prompt (192-action catalogue, risk rules) + user message. LLM calls `get_system_state`. 5. **State injection** (`crates/sysknife-brain/src/planner.rs` — `plan_intent()`) `state_client.curated_state()` → daemon IPC → `collect_state()` (`crates/sysknife-daemon/src/state_collector.rs` — `collect_state()`) runs `hostname`, - `rpm-ostree status`, `systemctl list-units`, `flatpak list`, - `toolbox list`. Returns JSON with host, deployment, services, - flatpaks, toolboxes. + `rpm-ostree status` (once, for both the deployment and the layered + packages), `systemctl list-units`, `flatpak list`, `toolbox list`, + `podman ps` and `getent passwd`. Returns JSON with host, deployment, + services, flatpaks, toolboxes, layered packages, containers and users. 6. **LLM proposes plan** — calls `propose_plan` with: ```json @@ -88,11 +89,10 @@ system. 14. **Completion** → `sysknife:job-completed` with `succeeded` or `needs_reboot`. Transaction persisted to SQLite. -**Gap identified:** The LLM sees `flatpaks` and `services` but not -the list of already-layered packages (`GetLayeredPackages` output). -It could propose installing a package that is already layered. The -LLM would need to call `get_system_state` and receive layered -package info to avoid this. +~~**Gap identified:** The LLM sees `flatpaks` and `services` but not +the list of already-layered packages.~~ Resolved: the curated state now +carries `layered_packages`, and the planner can also call +`query_packages` before proposing `AddLayeredPackage`. --- @@ -111,7 +111,7 @@ package info to avoid this. ```json {"steps": [ {"action_name": "RebaseSystem", "risk_level": "high", - "params": {"ref": "fedora:fedora/42/x86_64/silverblue"}}, + "params": {"target_ref": "fedora:fedora/42/x86_64/silverblue"}}, {"action_name": "RebootSystem", "risk_level": "high", "params": {}} ]} @@ -155,7 +155,7 @@ execution. ```json {"steps": [ {"action_name": "ListServices", "risk_level": "low", "params": {}}, - {"action_name": "SearchFlatpakApps", "risk_level": "low", "params": {}} + {"action_name": "SearchFlatpakApps", "risk_level": "low", "params": {"term": "firefox"}} ]} ``` @@ -228,11 +228,11 @@ shell session. {"action_name": "GetFirewallState", "risk_level": "low", "params": {}}, {"action_name": "ConfigureFirewall", "risk_level": "high", - "params": {"action": "add_service", "service": "ssh"}} + "params": {"zone": "public", "service": "ssh", "enabled": true}} ]} ``` -3. Problem: the LLM can only add/remove services one at a time. +3. Problem: `ConfigureFirewall` enables or disables one service per step. "Block everything else" requires knowledge of what is currently allowed, then removing each service. The LLM cannot see the output of step 1 (`GetFirewallState`) before proposing step 2 — @@ -338,15 +338,13 @@ injection (e.g., `ci-runner; rm -rf /`). 4. Daemon runs `rpm-ostree rollback`. If this succeeds, proceeds to `RebootSystem` (`systemctl reboot`). -5. If `RollbackDeployment` fails, the automatic rollback mechanism - (`executor.rs` `rollback_spec_for`) tries to roll back the - rollback — which in this case means `rpm-ostree rollback` again, - returning to the current state. The job transitions to - `RolledBack` state. +5. If `RollbackDeployment` fails, nothing rolls it back: + `rollback_spec_for` (`executor.rs`) deliberately returns `None` for + `RollbackDeployment`, to prevent rollback recursion. The job ends + `Failed`. -**What works well:** The rollback-of-rollback semantic is correct -for deployment actions since `rpm-ostree rollback` is its own -inverse. +**What works well:** `RollbackDeployment` is itself the recovery +action, so it never chains an automatic rollback of its own. --- @@ -402,7 +400,7 @@ single-shot planning model prevents this. Anthropic. 4. **Step 2:** Wizard shows config.toml content to copy. For - Ollama: reminds user to `ollama pull llama3.2`. + Ollama: reminds the user to pull the chosen model. 5. **Step 3:** "Restart the shell to apply." @@ -411,11 +409,12 @@ single-shot planning model prevents this. (no `ANTHROPIC_API_KEY` → fallback to Ollama provider). 7. `LlmPlanner::from_config()` (`crates/sysknife-brain/src/planner.rs`) - constructs `OllamaProvider` with `http://localhost:11434` and - model `llama3.2`. + builds the Ollama provider (through `RigCompletionAdapter`) against + `http://localhost:11434`, with the configured model or, when none is + set, `qwen3:8b` (`DEFAULT_OLLAMA_MODEL`). -8. User can now type intents. Header shows - "via ollama / llama3.2". +8. User can now type intents. Header shows the provider and model, + for example "via ollama/qwen3:8b". **What works well:** Zero-API-key path with Ollama is a strong onboarding story for privacy-conscious sysadmins. @@ -431,11 +430,11 @@ setup as complete. | # | Gap | Impact | Severity | |---|-----|--------|----------| -| 1 | CuratedState lacks layered package list | LLM may propose installing already-layered packages | Medium | +| 1 | ~~CuratedState lacks layered package list~~ | Resolved: `layered_packages` is in the curated state | — | | 2 | No tool to query available ostree refs | Rebase plans can reference non-existent refs | Medium | -| 3 | No feedback loop — LLM cannot see command output | Cannot do iterative "check then act" workflows | High | +| 3 | Execution output never reaches the LLM | Read-only `query_*` tools feed results back during planning, but a plan cannot react to what its own steps print | Medium | | 4 | ~~EnterToolbox is not interactive-TTY-aware~~ | Resolved: action removed from catalogue | — | | 5 | Single-shot planning — all steps proposed at once | Cannot adapt plan based on intermediate results | High | | 6 | Raw stdout in timeline, no LLM summarization | Read-only queries produce unformatted output | Low | | 7 | SetupWizard does not verify Ollama reachability | User completes setup but gets connection errors | Low | -| 8 | System prompt says "Fedora Silverblue" only | Misleading once multi-distro ships | Low | +| 8 | ~~System prompt says "Fedora Silverblue" only~~ | Resolved: the prompt dispatches per distro (ADR 0004) | — | diff --git a/docs/vm-daemon-setup.md b/docs/vm-daemon-setup.md index 64fe419a..38e2c85a 100644 --- a/docs/vm-daemon-setup.md +++ b/docs/vm-daemon-setup.md @@ -43,8 +43,19 @@ systemd. You can verify the socket exists: ls -la /run/sysknife/daemon.sock ``` -> **Silverblue / Kinoite (rpm-ostree hosts):** `make install` uses the correct -> rpm-ostree override flags automatically. No extra steps needed. +> **Silverblue / Kinoite (rpm-ostree hosts):** `/usr` is read-only there, and +> `make install` refuses before writing anything unless you point the system +> directories at `/etc`: +> +> ```sh +> sudo make install SYSUSERS=/etc/sysusers.d TMPFILES=/etc/tmpfiles.d \ +> SYSTEMD=/etc/systemd/system POLKIT=/etc/polkit-1/rules.d +> ``` +> +> The privileged helpers still install under `/usr/lib/sysknife`, so the +> install does not complete on rpm-ostree yet; see +> [Distro Support](distro-support.md) and +> [#301](https://github.com/lacs-project/sysknife/issues/301). --- @@ -270,23 +281,32 @@ The role granted to a token-authenticated connection comes from a separate env var, `SYSKNIFE_TOKEN_ROLE` (default `Dev` if unset), read by the daemon process — not from the file contents. -The default token path is `~/.config/sysknife/token` (the daemon's XDG config -directory, next to `prefs.md`; respects `$XDG_CONFIG_HOME` if set), not -`/etc/sysknife/token`. +The daemon reads its token from `$XDG_CONFIG_HOME/sysknife/token`, falling +back to `~/.config/sysknife/token`, next to `prefs.md`, in the home of the +account the **daemon** runs as. The packaged unit runs as the `sysknife` system +user, whose home is `/var/lib/sysknife`, so the file is +`/var/lib/sysknife/.config/sysknife/token`, owned by `sysknife`, mode `0600`. +A token in your own login's home is never read. The daemon rejects a token +file that group or other can read. + +The packaged unit also listens on the Unix socket only. Switch it to vsock with +a drop-in; the Unix socket then goes away, so the SSH tunnel stops working on +that daemon. ```sh # On the host — generate a token openssl rand -hex 32 # e.g.: a3f8c2d1e4b7a0f9... -# On the guest — write the token to the daemon's token file -mkdir -p ~/.config/sysknife -echo "a3f8c2d1e4b7a0f9..." > ~/.config/sysknife/token -chmod 600 ~/.config/sysknife/token +# On the guest — write the token to the daemon account's token file +sudo install -d -o sysknife -g sysknife -m 0700 /var/lib/sysknife/.config/sysknife +echo "a3f8c2d1e4b7a0f9..." | sudo install -o sysknife -g sysknife -m 0600 \ + /dev/stdin /var/lib/sysknife/.config/sysknife/token -# Grant Admin to token-authenticated vsock connections (default is Dev) +# Listen on vsock, and optionally raise the token's role (default is Dev) sudo systemctl edit sysknife-daemon # Add under [Service]: +# Environment="SYSKNIFE_LISTEN_URI=vsock://:9734" # Environment=SYSKNIFE_TOKEN_ROLE=admin # Restart the daemon to pick up the token and role @@ -296,7 +316,7 @@ sudo systemctl restart sysknife-daemon The token file contains just the raw token, with no role prefix: ```text -# ~/.config/sysknife/token +# /var/lib/sysknife/.config/sysknife/token ``` @@ -366,7 +386,7 @@ Set `SYSKNIFE_TOKEN` in the `env` block of `.mcp.json` (see above). ### "authentication failed" on vsock The token on the host (`SYSKNIFE_TOKEN` in `.mcp.json`) does not match the -contents of `~/.config/sysknife/token` on the guest. Verify both match +contents of `/var/lib/sysknife/.config/sysknife/token` on the guest. Verify both match exactly (the file should hold just the raw token, no `role:` prefix). ### SSH tunnel drops when the terminal closes @@ -391,6 +411,6 @@ restart the VM. processes from connecting. Keep it out of version control — add `.mcp.json` to `.gitignore`. - **Socket file permissions:** the daemon socket at `/run/sysknife/daemon.sock` - is owned by `root:sysknife` with mode `0660` — only members of the + is owned by `sysknife:sysknife` with mode `0660` — only members of the `sysknife` group can connect locally. The SSH tunnel and vsock bypass this by terminating at the daemon's authenticated IPC layer. diff --git a/tests/e2e/dev-stories.sh b/tests/e2e/dev-stories.sh index 0c15b0b1..14b917d1 100755 --- a/tests/e2e/dev-stories.sh +++ b/tests/e2e/dev-stories.sh @@ -5,7 +5,7 @@ # 1. Builds sysknife-daemon and sysknife (release mode). # 2. Starts sysknife-daemon in the background on /tmp/sysknife-daemon.sock if it is # not already running there. -# 3. Runs the requested stories (default: 1-7, the read-only ones). +# 3. Runs the requested stories (default: the curated read-only set). # 4. Stops the daemon if this script started it. # # Stories 1-7 validate plan structure only — they check that the LLM proposes From 003203402cf3f57da98e09e100448188654f23b3 Mon Sep 17 00:00:00 2001 From: Vladimir Rotariu Date: Tue, 13 Oct 2026 14:01:42 -0600 Subject: [PATCH 2/2] docs(changelog): link #609 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UpyrhbDTW1gBwzGAC44GYU --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bc2b257..26543d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,7 +82,7 @@ Releases before `0.2.5` predate the public launch; their notes live in the compiles quinn-proto: the crate appears in `Cargo.lock` only. `cargo audit` reads the lockfile, so I bumped it to clear the five advisories. - **event-listener 5.4.2 (RUSTSEC-2026-0221).** - ([#PRNUM](https://github.com/lacs-project/sysknife/pull/PRNUM)) The 5.4.1 + ([#609](https://github.com/lacs-project/sysknife/pull/609)) The 5.4.1 `StackSlot` let a `!Send` tag set with `Event::with_tag` cross threads, a data race in safe code. Unlike the other accepted `cargo audit` warnings, this one reached the daemon and the CLI, through `sqlx-core`. `cargo audit` reports it @@ -186,7 +186,7 @@ Releases before `0.2.5` predate the public launch; their notes live in the [#411](https://github.com/lacs-project/sysknife/issues/411)). Thanks to [@yuee3](https://github.com/yuee3). - **The docs match the code again.** - ([#PRNUM](https://github.com/lacs-project/sysknife/pull/PRNUM)) A review of + ([#609](https://github.com/lacs-project/sysknife/pull/609)) A review of every maintained page against the source found drift in 30 files. The ones a reader would act on: - `docs/vm-daemon-setup.md` put the vsock token in the login user's home,