Disposable tool worker and approval boundary
Source:
docs/13-disposable-worker.mdat2081f31a099e· docs version 0.4.0
Result
Section titled “Result”tentaflake.worker.agents.<container> creates a per-controller host queue for
untrusted shell, build, and foreign-code jobs. The controller writes only a
small JSON request. A host-side Rust orchestrator captures the request into
private state, takes a bounded workspace snapshot, and creates a short-lived
OCI container with runsc, network=none, numeric non-root user,
cap-drop=ALL, no-new-privileges, read-only root, bounded tmpfs, and
CPU/RAM/swap/PID/runtime limits. Docker workers explicitly select
docker-default AppArmor; Docker/Podman retain their default seccomp policy
and unconfined overrides are never exposed through the request. The worker
receives no agent, provider, Git, backup, or infrastructure credential.
The worker is a second containment boundary. It does not make the controller, OCI daemon, kernel, gVisor, worker toolchain, or submitted code trustworthy. It is optional for a deliberately stopped scaffold, but an automatically started balanced controller fails evaluation without its matching worker.
Enable one worker
Section titled “Enable one worker”The key must exactly match the OCI container generated by the builder. The workspace and numeric UID/GID must also match; evaluation fails otherwise.
tentaflake.worker.agents.hermes-coding = { enable = true; workspace = "/var/lib/hermes-coding/workspace"; containerUid = 10000; containerGid = 10000;
maxSnapshotBytes = 1024 * 1024 * 1024; maxSnapshotEntries = 200000; maxTimeoutSeconds = 900; memory = "2g"; memorySwap = "2g"; cpus = "2.0"; pidsLimit = 512; workspaceTmpfsSize = "2g"; tmpTmpfsSize = "256m";};ZeroClaw uses /var/lib/zeroclaw-<name>/data with UID/GID 65534.
The host service runs with a real group-database entry matching the numeric
container GID. Tentaflake creates one deterministic tfw-gid-<gid> group per
non-nogroup GID and reuses nogroup for GID 65534. If a deployment already
owns a custom container GID, set hostGroup to that existing group; its
explicit host GID must match or evaluation fails.
The default image is built by Nix from the pinned nixpkgs input. It contains an
offline Rust/C/build toolchain. CARGO_NET_OFFLINE=true is set, and the
worker has no network namespace route regardless of tool configuration. A
deployment may replace worker.image and the matching local
worker.imageReference, but the module loads that Nix derivation locally; it
does not pull the reference from a registry.
Submit a job
Section titled “Submit a job”Write one complete file atomically when practical:
{ "version": 1, "id": "cargo_test_1", "action_class": "local-reversible", "argv": ["cargo", "test", "--offline"], "timeout_seconds": 300}The path is:
<workspace>/.tentaflake-worker/inbox/ cargo_test_1.jsonIDs are bounded lowercase ASCII identifiers. The parser rejects unknown fields, oversized requests, empty or oversized argument lists, NUL bytes, unsupported versions, duplicate IDs, and timeouts above policy.
The snapshot walker opens the configured workspace through Linux openat2
with RESOLVE_NO_MAGICLINKS | RESOLVE_NO_SYMLINKS. On kernels or filtered VM
environments that return ENOSYS, it falls back to descriptor-relative,
component-by-component openat calls with O_DIRECTORY | O_NOFOLLOW and
rejects relative paths plus dot or parent components.
It then traverses by directory/file descriptors, does not follow file
symlinks, excludes the queue directory, rejects special files, and stops at
byte and entry ceilings. This prevents an agent-controlled top-level symlink
or rename race from turning the root worker into a reader of an unrelated host
path. The private host snapshot stays root-owned, while the source owner’s read
and execute bits are mirrored to the already-declared capsule GID before the
read-only bind mount. Private workspace entries therefore remain readable
without granting the host worker CAP_CHOWN. Inside the capsule, the job user
recreates the snapshot without preserving host ownership metadata as
additional authority.
Action classes and approval
Section titled “Action classes and approval”| Class | Behavior |
|---|---|
local-reversible |
runs automatically in the offline disposable capsule |
external-reversible |
requires host approval |
irreversible |
requires host approval |
financial |
requires host approval |
production |
requires host approval |
communicative |
requires host approval |
privileged |
requires host approval, but gains no extra worker authority |
forbidden |
always rejected |
The host captures a validated request below
/var/lib/tentaflake-worker-<container>/pending/ before approval. The agent
cannot mutate that private copy. Approve or deny the exact ID from an operator
session:
sudo tentaflake-worker --config \ /etc/tentaflake/workers/hermes-coding.json \ approve message_1
sudo tentaflake-worker --config \ /etc/tentaflake/workers/hermes-coding.json \ deny message_1The root operator command adopts the worker’s declared capsule group before it
creates a snapshot. Direct approval therefore uses the same read-only snapshot
permissions as the systemd queue service and does not require CAP_CHOWN.
Approval is an authorization decision outside the LLM. It does not change the
capsule: approved jobs still have no network, secrets, host devices, runtime
socket, added capabilities, or writable host bind. Therefore an approved
communicative or financial job cannot itself perform that external action.
Such effects require a separate narrow broker; no generic external-side-effect
broker is implemented yet.
Results and cleanup
Section titled “Results and cleanup”The worker captures bounded runtime logs and exit status. Docker capsules use
the local driver with a one-file size cap and compression disabled; Podman
capsules use bounded k8s-file logging. The worker separately caps the
published job.log. A job may place files below artifacts/ in its tmpfs
workspace. After the job command returns, the capsule wrapper records its real
exit status and keeps the container alive briefly. The worker streams a tar
archive through docker exec or podman exec from inside the runsc mount
namespace while the bounded tmpfs is still mounted. Its host importer enforces
the snapshot byte and entry ceilings, accepts only relative regular files and
directories, rejects links and special files, and normalizes result
permissions. The worker rechecks the completion marker to exclude a concurrent
handoff and only then signals the wrapper to exit. The import lands in the
worker’s private result directory and is exposed read-only to the controller:
/run/tentaflake-worker/results/<id>/ result.json job.log artifacts/The controller chooses whether to copy a reviewed artifact into its persistent workspace. The worker never writes an artifact directly back into that workspace, and the capsule receives no writable host bind for this handoff. Result directories expire after 14 days; the prompt-free JSONL audit rotates daily with 14 generations.
The path-activated queue service has a ten-second failure-restart delay but no aggregate systemd start counter. systemd counts successful oneshot activations against that counter as well, which would otherwise disable a healthy queue after a small number of ordinary jobs. Per-request resource, timeout, snapshot, and workspace ceilings remain enforced independently.
Tmpfiles establishes the result root as 02750; the orchestrator preserves
that inherited setgid bit without trying to reapply it inside the hardened
systemd unit. Published result files therefore retain the controller’s exact
numeric group while RestrictSUIDSGID=true remains enforced.
The OCI container and temporary host snapshot are removed after every normal completion, failure, or timeout. A stale container is removed only when its Tentaflake ownership label exactly matches the configured agent.
Verification boundary and remaining risks
Section titled “Verification boundary and remaining risks”Unit tests cover parser/action policy, bounded reads and snapshots, symlink rejection, control-directory exclusion, and log truncation. Module-evaluation tests cover builder matching and fail-closed configuration. The NixOS VM test submits real local, approval-required, and forbidden jobs, checks no network or runtime socket, validates the numeric user and artifacts, and proves container and snapshot cleanup. That runtime test uses Docker; Podman evaluation is not live Podman/AppArmor proof.
A derivation evaluation is not proof that Docker/Podman, runsc, systemd path
units, tmpfs ceilings, or cleanup worked on an activated host. Run the VM check
and then a deployment-specific live drill before relying on it. The module
also cannot transparently intercept every runtime’s built-in terminal tool;
configure the agent/tool policy to submit untrusted execution to this queue.
The posture doctor reports TFSEC-020 when a secure-profile controller has no
worker, but it cannot prove that every model-selected command used it.