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
| Tool | Version | Install |
|---|---|---|
| Rust stable | latest stable | rustup.rs |
| Node.js | 20+ | nodejs.org or your distro |
| pnpm | latest | npm install -g pnpm |
| Tauri system deps | — | tauri.app/start/prerequisites |
| pre-commit | latest | pip install pre-commit |
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 git hooks (run once)
pip install pre-commit
pre-commit install
# Install frontend dependencies
cd apps/sysknife-shell && pnpm install && cd ../..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)
cd apps/sysknife-shell && pnpm tauri buildThese run in under 15 seconds and are required before every push:
# Rust unit + integration tests
cargo nextest run --workspace --locked
# TypeScript / React tests
cd apps/sysknife-shell && pnpm test && pnpm exec 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.
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
pnpm tauri 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.
Pre-commit runs on every git commit. Run all hooks manually before
pushing:
pre-commit run --all-filesHooks included:
| Hook | What it checks |
|---|---|
| trailing-whitespace | Removes trailing spaces |
| end-of-file-fixer | Ensures files end with a newline |
| check-yaml / check-toml / check-json | Syntax validity |
| no-commit-to-branch | Blocks direct commits to main |
| gitleaks | Detects hardcoded secrets |
| cargo fmt | Rust formatting (--check mode) |
| cargo check | Workspace compilation |
| tsc --noEmit | TypeScript type checking |
| markdownlint-cli2 | Markdown style |
| yamllint | YAML style |
Intentionally excluded from pre-commit (they run in CI instead):
cargo clippy (20–30 s), cargo nextest run (minutes), vitest (minutes).
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, 189 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 | pnpm test (in 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
optional postgres-contract job) so you catch failures before pushing,
without spending GitHub Actions minutes:
# Full run — everything CI runs, including the optional Postgres contract
# test if docker/podman is available (or SYSKNIFE_TEST_POSTGRES_URL is set)
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-postgresIt detects which tools are installed first: cargo and node are required
(missing either is a hard failure with an install link); an optional linter
that's missing (cargo-nextest, cargo-audit, markdownlint-cli2,
markdown-link-check, yamllint, shellcheck) just prints a warning with
an install hint and skips that one check. Every check still runs even after
an earlier one fails — a PASS/FAIL/WARN/SKIP summary prints at the end, and
the script exits non-zero only if something in the summary actually failed.
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/ already ships a pre-commit hook (cargo fmt --all --check +
cargo nextest run --workspace --locked) alongside the new pre-push one.
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 (see Pre-commit Hooks above) —
use one mechanism or the other, not both, per clone.
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)