A reply can stop and ask you something

Three features turn out to be one mechanism: a command waiting to be
approved, a question the model wants answered, and "this reply is waiting
for you" are all — stop the generation, put an interactive block in the
bubble, wait for a POST, carry on. So there is one primitive, and the only
thing using it so far is `ask_user`: a model can offer you a few answers
and a box to write your own.

The shell executor is not here yet. This lands first on purpose, because
it is the riskiest machinery in the feature and it is worth having working
before any subprocess exists to complicate it.

Two things about where the pause sits. It pauses a round, not a call: a
round's calls run together under a semaphore, and parking four coroutines
on four separate answers inside that gather would queue them behind each
other invisibly. And Stop had to be taught about it — `cancel` is read
between streamed chunks and there are no chunks while paused, so the
button did nothing at all until `request_stop` learned to resolve the
pause itself.

Also here: a risk class on every tool (read, write, execute), which is
what the four permission modes will be a table over, and the systemd unit
loses ProtectKernelTunables. That last one is not tidying — it
bind-mounts /proc/sys read-only, which stops bubblewrap mounting /proc at
all, and the obvious workaround would expose this process's environment
and with it the encryption key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-01 19:30:44 +02:00
parent ecadb66414
commit 1c659a5640
19 changed files with 1662 additions and 17 deletions
+15 -3
View File
@@ -81,9 +81,21 @@ delivers it in one lump at the end, which is indistinguishable from streaming
being broken. `proxy_read_timeout` is raised to an hour because a model can
think for minutes before the first token.
**Hardening is deliberately moderate.** `ProtectSystem=full`, not `strict`: the
agentic features planned for later need to run commands, and a lockdown that
has to be torn out again is worse than one that was never applied.
**Hardening is deliberately moderate.** `ProtectSystem=full`, not `strict`,
because agent chats run commands. Two lines in the unit are load-bearing and
worth knowing before anyone tidies them:
- **`ProtectKernelTunables` is absent on purpose.** With it, bubblewrap cannot
start at all — it bind-mounts `/proc/sys` read-only, and the kernel then
refuses `mount -t proc` inside a user namespace. The unit says why, and why
the obvious workaround is worse.
- **`TasksMax` and `MemoryMax` bound the whole service**, because a sandbox has
no cgroup of its own and `RLIMIT_NPROC` is counted per uid — the same uid the
server runs as.
Local agent execution is off until an administrator turns it on, and the
sandbox never binds the deployment prefix, so a command cannot read the
database or the encryption key.
**Use a real certificate if this is exposed beyond a trusted LAN.** The
self-signed cert exists so the install works with no external dependencies;
+26 -4
View File
@@ -28,17 +28,39 @@ RestartSec=5
# installer sets to 127.0.0.1: reachable through nginx, never directly.
# --- Hardening -------------------------------------------------------------
# Moderate rather than maximal. The agentic features planned for later need to
# run commands, and a lockdown that has to be torn out again is worse than one
# that was never applied.
# Moderate rather than maximal. The agentic features need to run commands, and
# a lockdown that has to be torn out again is worse than one that was never
# applied.
#
# ProtectKernelTunables is deliberately ABSENT, and putting it back breaks
# agent chats outright. It bind-mounts /proc/sys read-only, which leaves a
# locked submount under /proc; the kernel then refuses `mount -t proc` inside a
# user namespace, and bubblewrap fails with
#
# bwrap: Can't mount proc on /newroot/proc: Operation not permitted
#
# The tempting workaround is worse than the disease: binding the host /proc
# into the sandbox would expose /proc/<pid>/environ of this process, and this
# unit reads LEMBAS_SECRET_KEY out of an EnvironmentFile. The setting only
# guards against a *root* write to /proc/sys, and this service is unprivileged
# with NoNewPrivileges, so little is given up.
#
# NoNewPrivileges is fine alongside bubblewrap because bwrap is not setuid here
# -- it uses an unprivileged user namespace with a single-uid map, which needs
# no /etc/subuid entry for the service account.
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
ReadWritePaths=__PREFIX__
LimitNOFILE=65535
# A sandbox gets no cgroup of its own, and RLIMIT_NPROC is per *uid* -- the
# same uid as this service. Bounding the whole unit is what stops a runaway
# command in an agent chat from taking the server down with it.
TasksMax=2048
MemoryMax=8G
[Install]
WantedBy=multi-user.target