Skip to content

Security model

GurtOS assumes the agent in a task can be wrong or can be turned against you, by a bad instruction, a buggy plan or a web page with hidden instructions. So it does not rely on the agent's judgement or on its own permission prompts. Each task runs with high autonomy inside limits the operating system enforces. This page lists those limits, layer by layer, and what each one stops.

For what GurtOS does and does not defend against, read the threat model. For a picture of the zones, read trust boundaries.

Every task runs as a new system user, box-<id>, with a uid between 30000 and 39999. The user has no password, no login shell (/usr/sbin/nologin), no sudo and no group shared with anything else. Its home is a folder inside the task's own directory. When the task ends, the box kills its processes and deletes the user.

Stops: a task reading or changing another task's files, the box's state, or your files, through ordinary file permissions. A task cannot become root through sudo.

See Agents as their own users.

The worker starts as a transient systemd unit, box-task-<id>, with these properties:

Property Effect
NoNewPrivileges=yes No setuid program or file capability can raise its privileges
CapabilityBoundingSet= No Linux capabilities at all
RestrictSUIDSGID=yes It cannot create setuid or setgid files
ProtectSystem=strict The whole file system is read-only to it
ReadWritePaths= its copy and its home The only two places it can write
ProtectHome=yes /home, /root and /run/user are invisible
PrivateTmp=yes Its own /tmp, not shared with other tasks
PrivateDevices=yes No access to physical devices
ProtectProc=invisible It sees only its own processes in /proc, not the box's or other tasks'
InaccessiblePaths=-/run/dbus No D-Bus
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK No raw packet sockets or other socket families
MemoryMax=2G At most 2 GB of memory
TasksMax=512 At most 512 processes and threads

Stops: writing outside its copy, seeing or signalling other processes, using devices, gaining privileges, and one task exhausting the machine's memory or process table.

A task never works on the real files. It gets an overlayfs copy: its own changes on top of the changes of tasks that finished before it, on top of the workspace's base files. The base files are in a folder only root can enter. The copy's top folder belongs to the task's user with mode 0700, so no other task can enter it. When the task finishes, its changes become a layer that later tasks see, and undo takes that one layer out.

Stops: a task damaging the original files, a task seeing another running task's changes, and a bad task's changes outliving an undo. See Workspaces and layers.

Secrets live in the vault, /etc/box/vault.env, readable by root only. The box refuses to start if the file is readable by anyone else. A task's environment holds no secret.

When a task needs something done with a key (call a paid API, pay, sign), it asks its broker to run a tool by name. The broker is a Unix socket owned by the task's user with mode 0600. The tool runs inside the core, reads only the vault keys it declares, and returns a result. The task never sees the key.

Stops: a hijacked agent reading keys from files, its environment or memory, because they are not there. See The vault and the broker.

An nftables rule on the box's output path matches every connection by its owner's uid. Task users (30000 to 39999) may connect to 127.0.0.1 on ports 8800 to 8899, the box's proxy, and nothing else. Every other packet is rejected and counted.

The proxy knows which task opened each connection from the uid that owns it, so tasks carry no token. It allows only the hosts in the task's hosts list:

  • example.com allows that host on any port, example.com:8080 only that port, *.example.com its subdomains.
  • It resolves the name once and connects to that address, so the name cannot change between the check and the connection.
  • A name that resolves to the box itself is refused unless the exact host:port is listed, and the box's own ports (the API, SpacetimeDB, the proxy range) are always refused.
  • Every refusal is a net.blocked event in the action log.

A task with an empty hosts list has no network at all.

Stops: sending data to a host the task was not given, going around the proxy, reaching the box's API or SpacetimeDB to approve itself, and reaching services on your local network. See Network policy.

Droid needs a Factory API key for its model calls. A Droid task holds only a placeholder. Its calls go through the box's proxy:

  • Plain API calls go to the proxy as Droid's base URL, and the proxy puts the vault's key on the request.
  • Model calls to *.factory.ai over HTTPS are answered by the proxy with a certificate from the box's own certificate authority. Only Droid tasks are told to trust that authority. The proxy swaps the placeholder for the real key and forwards the call over real TLS.

The authority's private key is in BOX_HOME/ca, root only. Tasks that are not Droid tasks get no Factory access through the proxy.

Stops: the one key every Droid task needs from ever entering a task.

Every tool declares what a call costs. Before a call runs, the broker adds the cost to what the task has spent and to what its other calls in flight have reserved. If the total would pass the task's budget, the task pauses and asks for an OK. The tool runs only after someone approves.

  • One approval per action. A call retried while waiting shares one approval and is charged once.
  • The first answer wins, from any device.
  • A task's pending approvals close when it ends.
  • Agents can also ask for an OK before anything risky.

Stops: a task spending past its budget without a person agreeing, and two payments fired at once together passing the budget. See Approvals and budgets.

Every state change, step, tool call and result, approval and answer, cost and refused connection is a line in the action log, BOX_HOME/log.jsonl. Each line is written and fsynced before the box acts on it. The file is root-only. Tasks cannot read or write it.

Gives you: a record of what each agent did, which person approved what and from which device, and the base for undo and resume. The log is append-only by design but not tamper-evident: root on the box can edit it. See The action log.

On the GurtOS, the system updates as one image. An update must carry a valid ed25519 signature from the build machine's key and match its SHA-256 hash. It is written to the slot that is not running, tried once, and kept only if the box reports healthy within 5 minutes. Otherwise the box starts the previous system again.

Stops: an unsigned or altered image being installed, and a broken update leaving the box unusable. See System updates.

The GurtOS has no login prompt and no user with sudo. Root's password is locked, the sudo binary is absent, and every console login is masked. Release builds have no SSH server. The screen is a full-screen browser that runs as a kiosk user with no password and no shell, showing the portal from the box's own API. The system is built from 40 named packages and their dependencies, without apt, snapd or cloud-init: it changes only by whole-system updates.

Stops: someone at the keyboard, or on the network, logging in to the box. Debug builds add SSH with one key for root and say so in /etc/box-os/release.

Every limit is enforced by the kernel, systemd or the box's own core, outside the agent's process. None depends on the model following instructions. The trust that remains is in the core, the apps installed in it, the machine's root account and the people who can reach the API. The threat model covers those.