Welcome to SysKnife. This guide gets you from zero to a running dev environment and covers everything you need to contribute confidently.
- Architecture overview — understand the four-crate structure and the trust boundary before writing code
- ADR 0001: System boundaries
- ADR 0002: Brain provider layer
- ADR 0003: IPC wire protocol
Required. Without these nothing builds:
| Tool | Version | Install |
|---|---|---|
| Rust stable | latest stable | rustup.rs |
| A C compiler and linker | — | sudo apt-get install -y build-essential |
| Node.js | 22+ | nodejs.org or your distro |
cargo-nextest |
latest | cargo install cargo-nextest --locked |
Required to reproduce the docs-and-hygiene job, which is one of the five
checks main requires:
| Tool | Version | Install |
|---|---|---|
| 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) |
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 |
Required only for the jobs that name them:
| Tool | Needed by | Install |
|---|---|---|
| Podman or Docker | the live postgres-contract job |
sudo apt-get install -y podman |
| Tauri system deps | building the paused GUI app | tauri.app/start/prerequisites |
Install
cargo-nextestbefore you trust a green run. If it is missing,scripts/ci-local.shnow reports the Rust test step as skipped, finishes INCOMPLETE, and exits non-zero.--allow-missing-toolsis an explicit opt-out for cases where you intentionally accept that local CI gap.
This project uses npm. apps/sysknife-shell carries a package-lock.json
and CI runs npm ci against it. There is no pnpm or yarn lockfile in the tree.
No API key is required to get started. SysKnife auto-detects a local
Ollama instance (http://localhost:11434) when no cloud API key is set.
If you do not have Ollama installed, you can still run all unit and
integration tests without it.
git clone https://github.com/lacs-project/sysknife
cd sysknife
# Install the git hooks (run once). This sets core.hooksPath to .githooks,
# which is what wires up the real pre-commit and pre-push gates.
scripts/ci-local.sh --install-hooks
# Install frontend dependencies
npm ci --prefix apps/sysknife-shellDo not use
pip install pre-commit && pre-commit install. This repository drives its hooks throughcore.hooksPath, so anything written into.git/hooksis ignored by Git and you would end up with no gate at all..pre-commit-config.yamlis left over from an earlier setup and is not used.
The workspace builds native code (TLS via aws-lc-sys/ring, SQLite via
libsqlite3-sys), so a C compiler and linker are required on top of rustup:
sudo apt-get install -y build-essential on Debian/Ubuntu. Without one the
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 -- buildThese run in under 15 seconds and are required before every push:
# Rust unit + integration tests
cargo nextest run --workspace --locked
# TypeScript / React tests
npm test --prefix apps/sysknife-shell
npm exec --prefix apps/sysknife-shell -- tsc --noEmitSee docs/contributing/testing.md for the full test pyramid, including how to run the LLM-driven E2E stories on your workstation and in a Fedora Atomic VM.
main requires five checks. scripts/ci-local.sh runs most of them in one go
and is the command to reach for first:
scripts/ci-local.sh # everything it can run here
scripts/ci-local.sh --fast # what the pre-push hook runsThe exit code and summary now agree. If a CI-required helper such as
cargo-nextest is missing, the skipped check is named, the run ends
ci-local: INCOMPLETE, and the command exits non-zero. Use
--allow-missing-tools only when you intentionally want those missing-tool
skips to remain advisory.
CI and every script here run cargo nextest run --workspace --locked. Plain
cargo test runs the whole binary's tests as threads in one process, and a few
tests here set process-global environment variables, so it fails intermittently
on main through no fault of your branch. nextest gives each test its own
process and does not have that problem. If you have no choice, cargo test -- --test-threads=1 is stable.
postgres-contract is the only required check that needs a server, and it is
the one people assume they cannot run. You can. CI starts postgres:17-alpine
as a service container; do the same locally:
podman run -d --name sysknife-pg -p 55987:5432 \
-e POSTGRES_USER=sysknife -e POSTGRES_PASSWORD=sysknife \
-e POSTGRES_DB=sysknife_test docker.io/library/postgres:17-alpine
export SYSKNIFE_TEST_POSTGRES_URL=postgres://sysknife:sysknife@127.0.0.1:55987/sysknife_test
export SYSKNIFE_REQUIRE_POSTGRES=1
cargo test -p sysknife-daemon --test postgres_store --locked -- --include-ignored
cargo test -p sysknife-cli --test cli_smoke --locked \
audit_verify_exits_with_code_1_when_anchor_is_truncated -- --ignored --exact
podman rm -f sysknife-pgThose tests are #[ignore] by default, so a normal run reports them as ignored
rather than passing over a database that was never there. SYSKNIFE_REQUIRE_POSTGRES=1
without a URL panics on purpose: a misnamed variable used to report success.
Podman works rootless and needs no daemon. Docker works too; on some hosts its published ports reset the connection, in which case use podman.
docs-and-hygiene runs about two 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:
bash tests/release/public-claims.test.sh
bash scripts/check_release_versions.shIf one goes red with no output, that is #347, not you.
Reviewing someone else's PR means running their code. Do it in a container with no network and nothing from your home directory mounted:
podman run --rm --network=none -v "$PWD:/repo:z" -w /repo \
docker.io/library/python:3-slim bash -c 'bash tests/release/public-claims.test.sh'For Rust, give the container a throwaway copy of your cargo home with an overlay
mount so a build script cannot write to yours, and point CARGO_HOME at it. The
official rust image sets CARGO_HOME=/usr/local/cargo, so a mount alone is
ignored:
podman run --rm --network=none -v "$PWD:/repo:z" -v "$HOME/.cargo:/cargo:O" \
-w /repo -e CARGO_HOME=/cargo -e CARGO_TARGET_DIR=/repo/.container-target \
-e CARGO_NET_OFFLINE=true docker.io/library/rust:1-slim \
cargo test -p sysknife-cli --bins --offlinesysknife-cli is a binary crate, so its unit tests need --bins; --lib finds
no target.
You need two terminals.
Terminal 1 — daemon
# Binds $SYSKNIFE_LISTEN_URI, else $XDG_RUNTIME_DIR/sysknife/daemon.sock
# (last resort /tmp/sysknife-$UID.sock). The CLI resolves the same default,
# so `sysknife doctor` works without setting a socket env var.
# Privileged system actions (rpm-ostree, useradd, etc.) require root.
# For development you can run without root — read-only queries still work.
cargo run -p sysknife-daemonTerminal 2 — shell (GUI)
cd apps/sysknife-shell
npm run tauri --prefix apps/sysknife-shell -- devThe shell opens as a desktop window. Type an intent and the daemon responds. The LLM is auto-detected from your environment.
tests/e2e/dev-stories.sh runs the 7 read-only user stories without
a VM. It validates that the LLM proposes the correct typed plan — it
does not execute the actions against your host.
# With an Anthropic key
ANTHROPIC_API_KEY=sk-ant-... tests/e2e/dev-stories.sh
# With an OpenAI key
OPENAI_API_KEY=sk-proj-... tests/e2e/dev-stories.sh
# With local Ollama (must have a tool-capable model pulled)
tests/e2e/dev-stories.sh
# Specific stories only
OPENAI_API_KEY=sk-... tests/e2e/dev-stories.sh 3 6 7Run this tier after any change to crates/sysknife-brain/src/prompt.rs or
the planning tools. See the testing guide for full details.
The daemon speaks length-prefixed JSON over a Unix socket. You can poke it manually without the GUI:
cargo run -p sysknife-daemon &
socat - UNIX-CONNECT:"$XDG_RUNTIME_DIR/sysknife/daemon.sock"Type or paste a JSON message (with a 4-byte LE length prefix). This is useful for debugging the dispatcher or previewing action output.
After scripts/ci-local.sh --install-hooks, Git runs .githooks/pre-commit
on every git commit. From the repository root, the hook runs these steps
in order and stops at the first failure:
scripts/check_no_secrets.sh --staged
cargo fmt --all --check
cargo clippy --workspace --all-features --all-targets --locked -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace --locked
scripts/test_baseline.shThese check staged secrets, Rust formatting, Clippy warnings, rustdoc warnings,
and the workspace test baseline. scripts/test_baseline.sh runs
cargo nextest run --workspace --locked and checks the measured test count.
Run bash .githooks/pre-commit to invoke the same gate manually.
The secret screen recognizes the credential formats of every key-bearing
provider in the setup wizard, including OpenAI service-account and None
keys, DeepSeek's hexadecimal keys, and xAI keys. Mistral's keys have no
distinctive prefix, so the screen requires a MISTRAL_API_KEY assignment
(shell or JSON syntax) instead of rejecting arbitrary identifiers. It is a
format screen: encoded credentials or prefixless values without that named
assignment can still escape detection. tests/release/no-secrets.test.sh
checks each setup provider and rejects inventory additions without coverage;
Ollama is explicitly exempt while it requires no API key.
Vitest is not part of this pre-commit hook; it runs in CI.
.pre-commit-config.yaml is an unused remnant of the earlier framework setup,
not the active Git hook configuration.
Config file: ~/.config/sysknife/config.toml (created manually, optional):
[daemon]
socket = "/run/sysknife/daemon.sock" # raw path, not URI
database = "/var/lib/sysknife/daemon.sqlite"
[llm]
provider = "ollama" # ollama | anthropic | openai | gemini | groq | deepseek | mistral | xai
model = "llama3.2:3b"
ollama_url = "http://localhost:11434"
max_turns = 10Config file values act as defaults. Environment variables always win.
| Variable | Default | Description |
|---|---|---|
SYSKNIFE_LISTEN_URI |
$XDG_RUNTIME_DIR/sysknife/daemon.sock (prod: /run/sysknife/daemon.sock) |
Daemon socket URI (where the daemon binds) |
SYSKNIFE_SOCKET |
falls back to the same default as SYSKNIFE_LISTEN_URI |
CLI / MCP-server socket override — where the client dials (unix://, vsock://, or a bare path) |
SYSKNIFE_DATABASE_PATH |
$XDG_STATE_HOME/sysknife/daemon.sqlite (fallback ~/.local/state/sysknife/daemon.sqlite) |
SQLite database path |
SYSKNIFE_LLM_PROVIDER |
auto-detect | anthropic, openai, gemini, ollama, groq, deepseek, mistral, or xai |
ANTHROPIC_API_KEY |
— | Required for the Anthropic provider |
OPENAI_API_KEY |
— | Required for the OpenAI provider |
GEMINI_API_KEY |
— | Required for the Gemini provider |
GROQ_API_KEY |
— | Required for the Groq provider |
DEEPSEEK_API_KEY |
— | Required for the DeepSeek provider |
MISTRAL_API_KEY |
— | Required for the Mistral provider |
XAI_API_KEY |
— | Required for the xAI provider |
SYSKNIFE_OLLAMA_URL |
http://localhost:11434 |
Ollama base URL |
SYSKNIFE_LLM_MODEL |
provider default | Override the model name |
SYSKNIFE_BRAIN_MAX_TURNS |
10 |
Planning loop turn limit |
SysKnife remembers user preferences in ~/.config/sysknife/prefs.md. The
planner injects them at the start of each plan_intent() call.
Preferences are user-stated intentions that inform planning decisions. Do not store system facts as preferences — those are queried live.
Manage preferences through natural language:
- "Remember that I always prefer vim-enhanced over vim"
- "Forget my vim preference"
Or edit ~/.config/sysknife/prefs.md directly. Maximum 10 KB; SysKnife
rejects passwords, API keys, and tokens automatically.
The ListJobHistory action and query_job_history planning tool
expose the daemon's SQLite transaction log. Ask "what has SysKnife done
recently?" or "did my update succeed?" and the planner queries the
log directly.
ListJobHistory is Observer-level (read-only, no approval required).
crates/
sysknife-brain/ LLM planner, provider adapters, safety fence
sysknife-types/ Shared domain types (CallerRole, RiskLevel, JobState, …)
sysknife-core/ Config loading, shared constants
sysknife-daemon/ Privileged executor, 191 actions with an `ActionSpec`,
IPC, rollback, SQLite
sysknife-proto/ Protobuf definitions (future use)
apps/
sysknife-shell/ Tauri + React GUI
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
docs/
adr/ Architectural decision records
contributing/ Testing guide
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 |
| 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 |
| Markdown lint | markdownlint-cli2 on contributor-facing docs |
| Link check | markdown-link-check on contributor-facing docs |
| YAML lint | yamllint on issue templates and workflows |
Run the Rust checks locally before pushing:
cargo fmt --all --check
cargo clippy --workspace --all-features --locked -- -D warnings
cargo nextest run --workspace --lockedscripts/ci-local.sh mirrors the runnable jobs from
.github/workflows/ci.yml (rust, frontend, hygiene, security, and the
required postgres-contract job) so you catch failures before pushing,
without spending GitHub Actions minutes:
# Full run — including the required Postgres contract when a URL or runtime
# is available (Podman is preferred over Docker)
scripts/ci-local.sh
# Fast subset — rust fmt/clippy/nextest + frontend tsc/vitest only
scripts/ci-local.sh --fast
# Skip the postgres-contract job even if a container runtime is available
scripts/ci-local.sh --no-postgres
# Explicitly accept missing helper tools as advisory warnings
scripts/ci-local.sh --allow-missing-toolsmake check runs the same fast subset, including the workspace test baseline
and Clippy with all features and all targets. It does not run the full hygiene,
security or PostgreSQL groups; use the full command above before submitting.
The hygiene group discovers every tests/release/*.test.sh and
tests/e2e/*.test.sh; there are no deliberate exclusions. It also reads the
workflow invocations and identifies tests that CI runs under sudo. If the
local run is not root, those privileged assertions are named as required skips
instead of being allowed to hide behind an exit-zero test result.
A missing Postgres runtime/URL, --no-postgres, a missing CI-required helper,
or an unexercised root-only test produces INCOMPLETE and a non-zero exit.
Set SYSKNIFE_TEST_POSTGRES_URL or install Podman for the Postgres contract.
--allow-missing-tools is an explicit opt-out only for missing helper tools;
it does not waive Postgres or root-required checks.
Tool detection still happens up front: cargo and node are hard prerequisites.
Missing cargo-nextest, cargo-audit, markdownlint-cli2,
markdown-link-check, yamllint, or shellcheck is reported with an install
hint and counts as a required skip unless explicitly opted out. Checks continue
after an earlier failure so the final PASS/FAIL/WARN/SKIP summary shows the
whole board. The command exits non-zero for either hard failures or required
checks that did not run.
scripts/ci-local.sh --install-hooks runs git config core.hooksPath .githooks, which wires up .githooks/pre-push to run scripts/ci-local.sh --fast automatically before every git push and block the push on
failure. Bypass a single push with git push --no-verify; undo the hook
entirely with git config --unset core.hooksPath.
.githooks/ also ships the pre-commit hook with the ordered checks listed
under Pre-commit Hooks.
Both are opt-in via the same core.hooksPath setting. Note that
core.hooksPath is a single switch: pointing it at .githooks means Git
stops looking in .git/hooks, so it supersedes hooks installed by the
pre-commit framework. Use the repository's .githooks setup, not the framework.
ci-local.sh runs the same commands as CI, but not inside the same
container/runner image, so it cannot catch environment-specific breakage.
For an exact, Docker-based replay of the GitHub Actions workflow (all jobs,
the real ubuntu-latest image), use
act instead.
- keep changes small and reviewable
- keep behavior typed and explicit
- keep the daemon as the only privileged executor
- update docs when user-facing behavior changes
- add or update tests for every behavior change
- write the failing test first; verify it fails before writing code
Before merging, a change should be:
- understandable without reading every dependency
- covered by deterministic tests
- documented if it changes user-visible behavior
- safe by default (fail closed, not open)
- consistent with the trust boundary (daemon is the only executor)