Security model
The rule: the agent only ever sees 1s and 0s about a file.
The human approves every plan in a console the agent cannot drive, in an account the agent cannot read. Skills, rules and MCP tool descriptions only guide the agent; the console’s checks are the guarantee. This page summarises the full security model.
Boundaries
| Boundary | Enforced by |
|---|---|
| The agent cannot read the file, the credentials, the plugins or the audit | a separate OS account lockfile (home 0700); lockfile doctor checks from the agent’s side that opening it fails |
| The agent cannot drive the console | lockkit’s isolation checks: the console runs in its own login session (or a desktop terminal of its own account, Wayland only), refuses X11 on production, refuses relays |
| Only the clients group reaches the console | socket directory /run/lockfile (2750, setgid group lockfile-clients), socket 0660, kernel peer credentials checked on every connection |
| The agent cannot replace the binary the console runs | /usr/local/bin/lockfile owned by root; install refuses a user-writable source unless its sha256 is pinned |
| A plugin cannot read anything but its input, nor talk to the network | Landlock (execute and read its own binary only; TCP denied with ABI ≥ 4), seccomp (no sockets, ptrace, mount, bpf, keyctl, second execve), rlimits, empty environment, input as a read-only fd 3, stdout capped at 3 bytes, stderr discarded; see the hardening coming in v0.2.1 |
| A plugin answers only its signed question | the manifest’s question and meanings of 1 and 0 are signed with the binary; marketplace review; a changed question is a loosening the human types to accept |
| Plugins and releases are what their publisher built | cosign keyless signatures checked through lockkit (index, manifest, binary, lockfile releases); no unsigned mode; a missing cosign refuses |
| The agent cannot widen the policy | no socket method installs, trusts or loosens anything; a change request only queues text for the human |
What the agent can see: the exhaustive list
Everything the console sends to the client (socket, CLI, MCP) is one of these, from closed lists. The end-to-end tests walk every response of every method against this list.
| Output | Why it is not file information |
|---|---|
plugin answer 1 / 0 | the one allowed thing |
result: null + plugin_error | absence of an answer; residual: it depends on the file (a plugin failed on it); at most one symbol per plugin, capped by max_plugins_per_handle |
fetch_failed | the plan could not start; residual: says “this key could not be fetched” (missing, too big, denied and network are not distinguished) |
handle f_…, plan id pl_… | random, from crypto/rand |
plugin name, version, official, warning: unofficial_marketplace, question, meaning of 1/0 | plugin metadata from the signed manifest |
profile names and production flag, busy | lockfile configuration; busy means the console is waiting for the human on another request |
approval_denied, approval_timeout | a human decision; the reason the human types is never forwarded |
refusal codes (E_*, with a static hint) | computed from the request and the local install, before or without reading the file; plugin_yanked is in the list but not produced yet by v0.2.0 |
status: console up, production, auto mode, security level, timers, plugin protocol, sandbox ok | console state |
Never sent: size, media type, sha256, key listings, file or path names, page or byte counts, library or OS error text, per-plugin timing, progress during a run, stderr. The client also refuses to forward any console text it does not recognise: an unknown error becomes console error.
Side channels
- Timing. A run’s response leaves the console only at the next multiple of
answer_quantum(default 5 s) after the run started; a fetch failure after approval too. Residual: a plan that takes longer than one quantum shows it took longer. - The console window. At desktop security level 1 the agent’s account can read the console’s window. It shows the agent’s own request, the plugin metadata and the outcome codes; causes, sizes and hashes go to the audit only. Residual: the start-up lines show the profile’s location, and a reason you type with a denial (
n <reason>) is echoed in the window: do not type file details there. - Exit codes depend on the outcome only, never on an answer.
Data at rest
- Fetched files live in a random
0700workspace in the console account, deleted onrelease, on handle expiry, on console exit, and swept at start. Deletion is an unlink, not a secure erase. - Credentials: the S3 SDK keeps the secret access key in the console’s memory for the session; core dumps are disabled.
- The audit log (
~lockfile/.local/state/lockfile/audit.log,0600) is the one place file metadata exists: key, size, sha256, fetch cause, each plugin’s precise outcome, stderr byte count. Never file bytes.lockfile auditreads it, in the console account only.
Sandbox hardening coming in v0.2.1
“The agent only ever sees 1s and 0s” is the design goal and what the wire carries; the v0.2.0 plugin sandbox is not yet complete around it. A review found side channels a plugin could use to pass more than its bit to another process on the machine:
- SysV IPC and POSIX message queues are not denied to plugins;
/procis visible to plugins unless it is mounted withhidepid;- on older Landlock ABIs, signals through
fcntlare not blocked.
v0.2.1 denies SysV IPC and message queues and requires /proc mounted with hidepid=invisible. Until then, install only plugins you trust from the official marketplace.
Residual risks
- A plugin whose code does not match its question (it lies, or answers a content question under a structural name) is mitigated by review, build from reviewed source, signing and the unofficial-marketplace warnings; it is not prevented.
- Many plugins over one file combine bits: bounded by
max_plugins_per_handleand by the per-plan approval that states how many answers the plan reveals. - A question can itself be about content (“is the amount above 10 000?”): the official marketplace’s review refuses such questions; an unofficial marketplace may not.
fetch_failedandplugin_erroreach carry one symbol that depends on the file.- Fetched files are unlinked, not securely erased (a residual on SSDs and copy-on-write filesystems).
- Malware running as the console account, or as root, is out of scope.
Read the source documents:
- docs/security-model.md: the security model in full.
- docs/plugin-contract.md: the plugin runtime and answer rules.