Skip to content
tentaflake docsv0.4

Disposable tool worker and approval boundary

Source: docs/13-disposable-worker.md at 2081f31a099e · docs version 0.4.0

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.

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.

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.json

IDs 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.

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_1

The 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.

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.

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.