Agents run over SSH only; put the hardening back

The local sandbox is dropped before it was built. Every hard problem in it
came from running on the machine that holds the database and the encryption
key: the service user cannot traverse /home, granting it needs ACLs,
RLIMIT_NPROC is counted per uid so a fork bomb starves the server too,
--size only applies to tmpfs so there is no disk quota, and the bind list
is a standing invitation to widen until the sandbox is decoration.

Over SSH, isolation is somebody's considered choice of host -- a throwaway
container with one project mounted into it -- using tools far better at it
than anything that could be built here. It is also the only version that is
honestly multi-user: each person brings their own credentials and their own
machine, and picks a project directory on it.

So ProtectKernelTunables goes back. It was removed for exactly one reason,
that bubblewrap cannot mount /proc without it, and that reason is gone. The
agents settings group loses everything bwrap-shaped with it.

What this costs, and the admin copy has to say so: there was a network:False
switch that made exfiltration from a compromised reply impossible, and over
SSH there is no equivalent, because the network belongs to the far side.
The security of an agent chat is now the security of the host behind its
profile, and LLeMbas cannot tell a scratch container from a live server.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-01 22:12:48 +02:00
parent 4b892054a4
commit c3d6660881
3 changed files with 33 additions and 67 deletions
+13 -32
View File
@@ -46,48 +46,29 @@ def _general_defaults() -> dict[str, Any]:
def _agents_defaults() -> dict[str, Any]:
"""Agentic execution: running commands, on this machine or over SSH.
"""Agentic execution: running commands on a machine reached over SSH.
Local execution is an instance decision rather than a personal one, because
the sandbox runs on this machine and its blast radius is this machine. SSH
profiles belong to whoever made them, but whether SSH exists here at all
does not.
Everything is off until an administrator turns it on. That is not caution
for its own sake: a model reads web pages, files and command output, all of
which are untrusted, so shell access is a capability somebody has to choose
on purpose.
Deliberately never on the machine LLeMbas runs on. Executing here would put
the blast radius on the host holding the database and the encryption key,
and buying it back needs a sandbox, a bind list, a second unix account and
an argument about every one of them. Over SSH, isolation is somebody's
considered choice of host -- a throwaway container with one project mounted
into it, or a VM -- made with tools far better at it than anything that
could be built here.
"""
return {
"local_enabled": False,
"ssh_enabled": False,
"bwrap_path": "bwrap",
# Read-only paths every sandbox sees, on top of /usr and the /lib
# symlinks. The deployment prefix is never here, and a bind containing
# the data directory is refused when the sandbox is built rather than
# trusted to a careful administrator.
"ro_binds": [
"/etc/ssl",
"/etc/ca-certificates",
"/etc/resolv.conf",
# /etc/resolv.conf is a symlink into here on a systemd-resolved box,
# and binding the symlink alone leaves it dangling.
"/run/systemd/resolve",
],
# Off by default, and the single most valuable setting in this group: an
# instruction injected through a file the model read cannot send
# anything anywhere from a sandbox with no network.
"network": False,
# Off until an administrator turns it on. Not caution for its own sake:
# a model reads web pages, files and command output, all of them
# untrusted, so a shell is a capability somebody chooses on purpose.
"enabled": False,
# Per command.
"default_timeout": 60,
"max_timeout": 600,
"max_output_bytes": 64 * 1024,
"ulimit_fsize_mb": 64,
"ulimit_nproc": 128,
# Per reply. See services/agent/policy.py:Limits.
"max_steps": 40,
"max_wall_seconds": 900,
"max_total_output_bytes": 1024 * 1024,
"workspace_max_bytes": 512 * 1024 * 1024,
# How long a reply waits for someone to answer. Clamped on read: a zero
# here would park a background task forever.
"approval_timeout": 900,