LACS
July 22, 2026 · View on GitHub
Version: 0.1 (initial draft)
Status: Draft
License: CC0 1.0 (public domain) — see LICENSE
Reference implementation: SysKnife (v0.2.5)
LACS is a protocol for letting an AI agent administer a Linux host without ever handing the agent a shell. It defines how a language model may request system changes, how those requests are classified by risk, authorized, approved, executed, and recorded in a tamper-evident audit log.
The key words MUST, MUST NOT, SHOULD, and MAY are to be interpreted as described in RFC 2119.
Throughout, blocks marked Reference binding (SysKnife) describe the concrete choices the reference implementation makes to satisfy a normative requirement. They are informative, not normative: a different implementation MAY make other choices as long as the surrounding requirement is met. Every reference binding in this document corresponds to code in the SysKnife repository.
1. Rationale
The common approach to "AI that runs commands" is to let a model emit a shell string and filter it with an allowlist or regex. That is a losing arms race: shell is a language designed to make quoting, substitution, and argument smuggling easy, so a filter must anticipate every evasion.
LACS takes the opposite stance. The agent is never given a channel that can carry a shell string. Its entire vocabulary for changing the system is a fixed, enumerated catalogue of typed actions. If an operation is not in the catalogue, the agent cannot request it.
2. Typed actions
2.1 The catalogue
An LACS implementation MUST expose a fixed, enumerated set of action names. An agent MAY request only an action name from this set plus a structured parameter object. An implementation MUST NOT provide any field, tool, or channel through which the agent can supply a free-form shell command, script, or arbitrary command line.
An action name that is not in the catalogue MUST be rejected before any effect occurs.
Reference binding (SysKnife). The catalogue is the constant
KNOWN_ACTION_NAMES(crates/sysknife-types/src/lib.rs); membership is checked byActionName::parse. The planning model is driven through a singlepropose_plantool whose schema admits only a catalogue name plus a parameter object. At the time of writing the catalogue enumerates 189 actions across families such as packages, services, users, SSH keys, firewall, storage (LVM/mounts/swap), kernel parameters, logging, PAM policy, and audit tooling.
2.2 Action specification
Each catalogue entry MUST be backed by a specification that fixes, independently of anything the agent asserts:
- the mechanism — the single privileged operation the action performs;
- a risk level (§3);
- whether the action requires a reboot;
- whether an automatic rollback is available (§8).
The mechanism MUST be one of a closed set of shapes. An implementation MUST re-derive the mechanism from the action name and validated parameters itself; it MUST NOT accept a mechanism, command line, or file path proposed directly by the agent.
Reference binding (SysKnife). The specification is the
ActionSpecstruct (crates/sysknife-daemon/src/actions/mod.rs):{ action_name, mechanism, risk_level, reboot_required, rollback_available }.ActionMechanismis a closed enum with exactly five shapes:
Command { program, args }— a fixed program and a typed argument vector;FileScan { path };FileWrite { path, content };FilePatch { path, search, replace };FileDelete { path }.Parameters are validated (allowlisted charsets, ranges, absolute paths, no
..) at the executor before the mechanism is built.
3. Risk classification
Every action MUST carry exactly one risk level:
| Level | Meaning |
|---|---|
| Low | Read-only inspection; no state change. |
| Medium | A reversible change to user-space configuration. |
| High | An access-control change, a destructive change, or one whose reversal is not guaranteed. |
The risk level of an action MUST be a fixed property of its specification, not a value asserted by the agent per request.
Reference binding (SysKnife).
RiskLevelisenum { Low, Medium, High }(crates/sysknife-types/src/lib.rs).
4. Roles and authorization
4.1 Roles
A caller acts in a role. LACS defines these roles in increasing authority:
| Role | Authorized for |
|---|---|
| Observer | Low-risk (read-only) actions. |
| Dev | Observer + Medium-risk actions. |
| Admin | Dev + High-risk actions. |
| Boot | System/boot-time actions (not derived from the risk→role mapping below). |
4.2 Minimum role
The minimum role required to run an action MUST be derived from its risk level:
Low -> Observer
Medium -> Dev
High -> Admin
4.3 Enforcement
The caller's role MUST be established by the implementation from a trusted source — never from a value supplied by the agent or the calling client in band. The executor MUST verify that the caller's role meets the action's minimum role before producing a preview and again before executing.
Reference binding (SysKnife).
CallerRoleisenum { Observer, Dev, Admin, Boot }; the risk→role mapping isrole_for_risk_leveland the per-action minimum ismin_role_for_action(crates/sysknife-daemon/src/policy.rs). The role is resolved by the daemon, not the client:
- Unix socket:
SO_PEERCRED→ the peer process's Linux group membership.- vsock: a pre-shared token (distributed out of band) validated against a file, mapping to a configured role.
The role check runs before preview and before execute (
crates/sysknife-daemon/src/dispatcher.rs).
5. Request lifecycle
An LACS request MUST proceed through preview → approval → execute. A change MUST NOT be executed before it has been previewed and the exact previewed change has been approved.
- Request. The caller submits an action name, a parameter object, and its role. The implementation computes a request hash — a stable digest of the action name and canonicalized parameters — that identifies the exact requested change.
- Preview. The implementation returns a preview describing the change: a summary, the risk level, the current and proposed state, expected side effects, whether a reboot is required, whether rollback is available, and any warnings. The preview MUST carry the same request hash.
- Approval. The caller approves the previewed change (§6).
- Execute. The implementation runs the action's mechanism and returns a result carrying the terminal job state, a transaction id, and — when applicable — a reboot-required flag and a rollback reference.
The request hash binds every stage together: an approval issued for one request hash MUST NOT authorize execution of a different one.
Reference binding (SysKnife). The wire envelopes are
RequestEnvelope,PreviewEnvelope, andResultEnvelope(crates/sysknife-types/src/lib.rs). The request hash isSHA-256(action_name || 0x00 || canonical_json(params)), hex-encoded (compute_request_hash,crates/sysknife-daemon/src/dispatcher.rs);canonical_jsonsorts object keys so the digest is stable across key ordering. Terminal states areJobState ∈ { Queued, Running, Succeeded, Failed, Canceled, RolledBack, NeedsReboot }.
6. Approval receipts
Execution MUST be authorized by an approval that is:
- Bound to the exact preview — tied to the request hash and the transaction, so it cannot authorize any other change;
- One-time — verification and consumption MUST be atomic, so a single approval cannot execute an action twice;
- Expiring — an approval that is not consumed within a bounded time MUST become invalid, and the pending transaction MUST be canceled.
An implementation MUST reject an execute request whose approval is missing, does not match the persisted preview, has already been consumed, or has expired.
Reference binding (SysKnife). Every
executecarries a one-time receipt issued for the exact persisted preview; verification and consumption are atomic (crates/sysknife-daemon/src/dispatcher.rs). The receipt is a deterministic Ed25519 signature over a domain-separated, injectively framed encoding of(transaction_id, request_hash)(AuditKey::approval_receipt); a SHA-256 commitment to the receipt is stored in the audit chain (approval_commitment), so the log records that an approval occurred without storing a reusable bearer credential. The receipt TTL is 15 minutes (APPROVAL_RECEIPT_TTL_MINUTES); a background sweep cancelsQueuedtransactions that outlive it. A mismatched, consumed, or expired approval yields astale_approvalfailure.
7. Audit log
Every mutating action MUST be recorded in an append-only, tamper-evident audit log that is verifiable by a party that does not hold the signing key.
The log MUST:
- record, for each entry, the immutable facts of the authorization decision (at least: the action name, its risk level, the request hash, and the approval that authorized it);
- chain entries so that altering or reordering a past entry is detectable;
- be verifiable with a public key only, so an auditor or log aggregator can detect tampering but cannot forge entries.
A symmetric MAC does not satisfy the public-verifiability requirement, because the verifier would also be able to forge. The signature scheme therefore MUST be asymmetric.
Reference binding (SysKnife). The log is a forward hash chain: each
transactionsrow storeschain_hashandprev_chain_hash, wherechain_hash = Ed25519_sign( ROW_DOMAIN || canonical(immutable_fields) || prev_chain_hash )hex-encoded, with
prev_chain_hash = ""for the first row. Ed25519 is deterministic (RFC 8032). The signed immutable fields areChainContent:{ seq, key_id, transaction_id, request_id, request_hash, action_name, risk_level, summary, approval_id, warnings_json, created_at }(crates/sysknife-daemon/src/audit_chain.rs).
- Public-key verification.
sysknife audit verifywalks rows inseqorder, checks each signature with the exported public key (<key>.pub, hex), and reports the first broken link.- Domain separation. Row, checkpoint, and approval-receipt signatures use distinct, prefix-free domain tags so a signature made in one context can never verify in another.
- Key management. The Ed25519 private key is a 32-byte seed in a file created mode
0600; the daemon refuses to start if it is group- or world-readable. Location:$SYSKNIFE_AUDIT_KEY_PATH, else<db_dir>/audit-key.- Scope. Only the immutable authorization fields are chained; the mutable
statusfield is intentionally excluded — the chain protects the authorization decision, not live execution state.- Truncation / rewrite. A chain walk alone cannot detect deletion of the tail; the implementation anchors signed checkpoints to an independent append-only sink so truncation and rewrite become detectable and the tamper window is bounded to "after host compromise".
Storage backends: SQLite or PostgreSQL.
8. Rollback
An action specification MUST declare whether an automatic rollback is available for it. When a rollback-capable action fails during execution, the implementation SHOULD perform the declared rollback automatically, within the same job, before returning a terminal result to the caller.
The availability of a rollback MUST be a fixed property of the action specification (a lookup), not a judgement the agent makes at request time.
Reference binding (SysKnife).
ActionSpec.rollback_availablemarks rollback-capable actions; on failure the daemon runs the mapped rollback in the same job. Seedocs/automatic-rollback.mdin the reference implementation.
9. Conformance
An implementation conforms to LACS 0.1 if it satisfies every MUST in §§2–7, namely:
- It exposes only a fixed, enumerated action catalogue and gives the agent no channel for free-form shell/command strings (§2.1).
- Each action has a fixed mechanism drawn from a closed set of shapes, derived by the implementation rather than the agent (§2.2).
- Each action has a fixed risk level ∈ {Low, Medium, High} (§3).
- It resolves the caller's role from a trusted source and enforces the risk-derived minimum role before both preview and execute (§4).
- It requires preview before execute and binds the two by a request hash (§5).
- It authorizes execution with a one-time, preview-bound, expiring approval (§6).
- It records every mutating action in an append-only, asymmetrically signed, public-key-verifiable audit log (§7).
10. Reference implementation
SysKnife is the reference implementation of LACS, written in Rust and licensed MIT. It runs a privileged root daemon, a planning brain driving an LLM tool-use loop, a CLI, and an MCP server that lets IDE agents (Claude Code, Cursor, Codex) drive the same approval-gated interlock. It is validated on Ubuntu 22.04, 24.04, and 26.04 and on Fedora atomic hosts.
Other implementations, for other distributions and languages, are explicitly encouraged. This specification is CC0: you may implement, adapt, or extend it without permission or attribution.