Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,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).**
([#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
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

Expand Down Expand Up @@ -186,6 +193,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.**
([#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,
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

Expand Down
7 changes: 3 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ gate invokes these fixtures too, so install both before running it locally:
git checkout -b feat/<short-name>
# … 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
```

Expand Down Expand Up @@ -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.

Expand Down
14 changes: 2 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
</details>
Expand Down
11 changes: 4 additions & 7 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 13 additions & 7 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <value>`), 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`,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/sysknife-daemon/src/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ enum CredentialArgvSpec {
/// Replace the credential argv element(s) with `<REDACTED>`.
///
/// 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.
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions docs/adr/0002-brain-provider-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 4 additions & 3 deletions docs/adr/0004-per-distro-prompt-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 14 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -146,16 +148,21 @@ 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
with the final job state.

## 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-<uid>.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
Expand Down
23 changes: 15 additions & 8 deletions docs/automatic-rollback.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
3 changes: 2 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading