HEMRA

Trust and privacy

What HEMRA v0.1 trusts, what it writes to disk, how it handles credentials, how agent runs are isolated, and what leaves the machine. This is the "documented trust assumptions, privacy boundaries and operational limits" item of RFC-0001 MVP 5. It describes the code on main, not the RFCs' intended end state; where the two differ, the gap is listed under Known gaps.

Summary

Trust model

PartyTrusted?Consequence
Operator (the person running hemra) and their configurationYesFlags, run files, harness files, task files, mounts, the environment variables harness files name, and Harbor configs are taken as given (RFC-0003 §12).
Harness process and everything it emits on stdoutNoEvery frame is size-limited, parsed and checked by SessionValidator before it is recorded. Violations end the trial with an RFC-0002 §11 code.
Artifacts the harness writesNoThe runner canonicalizes paths, rejects escapes and symlinks, enforces quotas and computes the digest itself (ADR-0003).
Agent-generated code and tool callsNoRuns with whatever isolation the provider gives (see Isolation).
Benchmark's official scorer (Harbor's verifier)Yes, for scoresRewards are imported verbatim and never recomputed; HEMRA never derives pass/fail from run.completed (RFC-0003 §4).
Other users on the same hostNot consideredv0.1 has no access control; the store relies on file-system permissions.

Out of scope for v0.1 (RFC-0001 non-goals, RFC-0003 §3): multi-tenant or public runners, and hostile code that tries to escape Docker. RFC-0004 and RFC-0006 cover those later and require VM-grade isolation first.

What HEMRA stores

The store (--store, default .hemra, or $HEMRA_STORE)

Layout from crates/hemra-storage/src/lib.rs:

PathContentsMay contain sensitive data?
hemra.db (SQLite) attempts.manifest_jsonThe resolved manifest: task ID and full task input, run context, harness program and args, names of passed environment variables, provider settings (Docker image ID, network, limits, host paths of bind mounts), limits, HEMRA version.Yes: task input and argv are stored as given.
hemra.db attempts.record_jsonThe trial record: status, error, harness info, final output and summary, harness error messages verbatim, exit info, last 64 KiB of stderr, artifact index, container report.Yes
hemra.db attempt_transitions, artifactsLifecycle history and artifact metadata (path, media type, digest, size).Artifact paths only
hemra.db imported_*, scoresNormalized Harbor jobs and their scored trials: rewards, exception classes and messages, agent and model names, task names.Error messages can quote agent output
hemra.db runs, trialsEach run's spec (ADR-0021): the resolved datasets and task checksums, the harness records with their digests (including Harbor agent kwargs and models), the sandbox settings and required capabilities, and the planned trials. Old benches/<run-id>.json records are migrated into it and stay on disk.Only what the run file and harness files contain
events/<attempt>.jsonlEvery accepted protocol event, streamed as it arrives (ADR-0005): message.* text, model.request/model.response, tool.call/tool.result arguments and results, log lines, usage. Capped at 64 MiB per trial by default. With MCP servers it also holds the proxy's own tool.call/tool.result events, with arguments and results unless the profile sets capture = "metadata" (ADR-0026).Yes: this is the transcript. Prompts, model output, tool I/O and anything a tool read.
blobs/sha256/..Artifact bytes (≤ 512 MiB each, 2 GiB per trial by default) and, for Harbor, every regular file of the job directory byte-for-byte, including agent trajectories, logs and verifier output.Yes
adapter-runs/<job>/Harbor's working directory: hemra-config.json (HEMRA's override layer, including --agent-version and --agent-kwarg values), stdout.log, stderr.log and Harbor's jobs/ tree.Yes: whatever Harbor and its agents print
hemra.db jobs, job_transitionsThe durable queue (ADR-0009): each job's spec (task ID and full task input, harness program and args, names of environment variables, timeout, retry budget) and its history. Values of the named variables are read from the scheduler's environment when the job starts and never queued.Yes: task input and argv are stored as given.
work/<run_id>/<attempt_id>/{workspace,artifacts}Workspace and artifact directories of attempts that hemra run works itself (work/<attempt_id>/ for hemra serve, unless --work-dir points elsewhere). Kept after the attempt; artifacts are also captured as blobs.Yes: whatever the harness wrote
locks/Lock files for live attempts.No

A store directory that HEMRA creates is owner-only (0700); an existing directory keeps the permissions the operator gave it. Blobs are written read-only. Nothing is encrypted at rest.

Attempts are append-only: there is no command that edits or deletes a stored attempt, event log or blob, and no retention or garbage collection yet. Deleting data means deleting files from the store by hand (see Operating guidance).

Outside the store

Credentials

What the code does:

What it does not do:

Isolation of agent runs

Local provider (default)

The harness is a child process in its own process group, running as the operator's user, in the operator's file system and network namespace. HEMRA gives it:

It does not restrict file access, network access or system calls. A local harness can read ~/.ssh, ~/.aws, browser profiles or anything else the operator can read, and can talk to any host. Use the local provider only for harnesses and agents you trust as much as your own shell.

Docker provider (the hemra run default, or --docker-image)

hemra run runs program harnesses in the Docker sandbox unless given --sandbox local, with the image from the run file's [sandbox] table; hemra harness check --docker-image does the same for one trial. The same protocol session runs inside docker run -i with the RFC-0003 §12 defaults (hemra-engine/src/docker.rs, ADR-0006):

ControlDefault
Network--network none (no egress, no DNS)
Privileges--cap-drop ALL, --security-opt no-new-privileges, never --privileged
UserThe host's effective uid:gid, not root
Resources4 GiB memory with no extra swap, up to 2 CPUs, 1024 PIDs
MountsOnly the trial workspace (/hemra/workspace) and artifact directory (/hemra/artifacts); extra mounts (or --docker-mounts) are read-only unless :rw
Docker socketNever mounted
ImageResolved to an immutable image ID before the attempt is recorded; the manifest pins it
CleanupEvery container is labelled dev.hemra.owned, removed with its volumes before artifacts are read; hemra doctor --prune removes ones a killed runner left behind

Each loosening (network, memory = "0", :rw mounts in [sandbox], or --docker-network, --docker-memory 0, --docker-user on harness check) is an explicit operator setting and is recorded in the manifest, so a stored attempt shows the isolation it actually ran with.

Docker shares the host kernel. It is a reasonable boundary against accidents and against agents doing something unexpected, not against code deliberately trying to escape (RFC-0001 invariants, RFC-0004 §12). A harness that needs a model API must be given network access (for example network = "bridge"), and from then on it can reach any host, not just the model provider.

hemra run can run program harnesses in a Modal Sandbox (ADR-0044) through the hemra-sandbox-modal bridge. The harness runs as root inside a gVisor sandbox on Modal's machines, with no network unless network = "open", and CPU and memory limits. What leaves the machine: the trial's workspace, every [sandbox] mounts directory (copied, regular files only), the task input in run.start, and the values of the harness's environment variables, which Modal holds for the exec. Artifacts come back as regular files only, checked by name and within the trial's limits, before the runner verifies them. Modal's credentials (MODAL_TOKEN_ID, MODAL_TOKEN_SECRET or ~/.modal.toml) are passed to the bridge only, never to the harness, and a harness variable with a Modal or bridge name is refused. A sandbox HEMRA could not stop is stopped by Modal when its lifetime (the trial's timeout plus 15 minutes) runs out.

Fly.io Machine ([sandbox] kind = "fly", experimental)

Program harnesses can also run in a Fly Machine (ADR-0045): a Firecracker microVM in the Fly app named by [sandbox] app, running as root inside it, always with outbound network (Fly cannot block it, so the run file must say network = "open" and --require network_block refuses). The same things leave the machine as for Modal, sent to Fly: the workspace, mounts directories, task input and the harness's variable values. The values travel in a script written into the Machine over SSH and deleted as it starts, never in a command line. FLY_API_TOKEN is passed to the bridge and flyctl only, never to the harness. Use a deploy token scoped to the sandbox app (fly tokens create deploy -a <app>), not an organization token. A Machine HEMRA could not destroy destroys itself when its lifetime runs out.

Fly Sprite ([sandbox] kind = "sprites", experimental)

Program harnesses can also run in a Fly Sprite (ADR-0047): a Firecracker microVM from Fly's own base image, running as the Sprite's unprivileged user. Its network policy is set before anything is copied in. By default it denies all outbound traffic. With [sandbox] egress = ["github.com", ...] it allows only those domains, and network = "open" sets no policy. The same things leave the machine as for Fly Machines: the workspace, mounts directories, task input and the harness's variable values. The values are written in a script through the Sprites filesystem API and deleted as the harness starts, never put in a URL or a command line. SPRITES_TOKEN is passed to the bridge only, never to the harness. A Sprite has no lifetime. One that HEMRA could not delete (HEMRA killed mid-trial) stays asleep with its disk, including the workspace copy, until you delete it. Every such Sprite is named hemra-... and labelled hemra-owned.

MCP servers ([[mcp]] in a harness file)

HEMRA runs each declared MCP server itself, on the runner's host, outside any sandbox, and the harness reaches it only through a Unix socket in a private temporary directory (mounted read-only into a Docker container) (ADR-0026). The server has the host access of the user running hemra, so an agent in a --network none container can still act on the network or the filesystem through the tools it was given. List only servers you would run yourself. A server's arguments and results are recorded like any tool call.

Task verifiers (driver harnesses in the Docker sandbox)

The task's verifier runs in its own container on a snapshot of the agent's, with the same hardening, --network none and only the literal values of the task's [verifier.env] (ADR-0012, ADR-0019). Verifiers that call models (LLM judges) need the operator to open them per run: [verifier] network and env in the run file, or --verifier-network and --verifier-env NAME (ADR-0036). Only the named host variables are read, their values reach the container as --env NAME through the docker client's environment, and the store records the names, never the values. Opening the network lets the task's test code reach any host with the keys you named, so do that only for datasets whose tests you would run yourself. A multi-step task runs each step in a container started from the snapshot of the step before it. When a verifier grades with a rubric (Harbor's rewardkit), its reward-details.json is kept with the trial's files, and the store keeps each criterion's score, the judges' model names and their reasoning, cut to 600 characters (ADR-0042). That reasoning is model output about the agent's work: it can quote the work, and it is shown as text only.

A HEMRA judge (judges/<name>/, applied with [[judge]] or --judge, ADR-0043) runs after the task's verifier, in its own container from HEMRA's judge runtime image, with --network none, a read-only root and the same hardening. It sees only what its judge.toml reads (the agent's last message, its artifacts, a trajectory built from its events, a copy of its working directory) and the instruction. It reaches its model only through HEMRA's model gateway, a Unix socket served by the host that forwards only the provider's model-call paths to the judge's base_url, adds the key there, and refuses calls beyond max_calls. The key is never in the container, the store or the transcripts. Each call's request and response bodies (the agent's work, the rubric, the judge's answer) are kept with the trial's files under trials/<attempt>/judges/<name>/calls/, and the score records each call's model, status, duration and tokens.

Requiring isolation (--require)

Each provider reports the isolation it enforces as configured (ADR-0013): Docker offers network_block only with --network none and resource_limit only with memory, CPU and PID limits all set, and the local provider offers neither. hemra run --require network_block,resource_limit refuses to start, recording nothing, when the run's sandbox does not offer every flag, and a stored manifest lists the flags that were required. hemra doctor --json prints each provider's capabilities and a one-line statement of its isolation.

Harbor (hemra run with via = "harbor" harnesses)

HEMRA supervises the harbor CLI as an ordinary local process: its own process group, a timeout, SIGINT/SIGTERM/SIGKILL escalation on cancel. Isolation of the agent and verifier is Harbor's, configured through Harbor's own environment settings; HEMRA does not add Docker flags to Harbor's containers. HEMRA retains Harbor's output without following symlinks, since the job directory can hold agent-written files.

What is sent off-machine

HEMRA's own crates send nothing on their own: no telemetry, crash reporting or update checks. The one exception is the signed desktop app's update check (below). Off-machine traffic comes only from the components below, each started by an operator command or an opt-in setting:

SenderWhenTo where
A harness (local)Whenever it choosesAnywhere: typically the model provider, with the prompts, task input, repository contents and tool output it sends. HEMRA cannot see or limit this.
A harness (Docker)Only if the operator enabled a networkAnywhere reachable from that Docker network.
A task verifier (Docker)Only with --verifier-network or [verifier] networkAnywhere reachable from that network: an LLM judge sends the agent's work and the task's rubric to its model provider.
The Modal bridge (hemra-sandbox-modal)Each trial of a run with [sandbox] kind = "modal"Modal's API (api.modal.com, or MODAL_SERVER_URL): the workspace, uploads, task input and the harness's variable values; Modal pulls the image from its registry. Artifacts come back from Modal.
The Fly bridge (hemra-sandbox-fly) and flyctlEach trial of a run with [sandbox] kind = "fly"The Machines API (api.machines.dev, or FLY_API_HOSTNAME) and the app's Machines over Fly's WireGuard network: the workspace, uploads, task input and the harness's variable values. Fly pulls the image from its registry. Artifacts come back over the same SSH sessions.
The Sprites bridge (hemra-sandbox-sprites)Each trial of a run with [sandbox] kind = "sprites"The Sprites API (api.sprites.dev, or SPRITES_API_URL): the workspace, uploads, task input and the harness's variable values. Artifacts come back from the same API.
A HEMRA judge's model gateway (host)When a run names a judge ([[judge]], --judge) or hemra judge check runsOnly the judge's base_url (its provider's API by default), with what the judge reads: the instruction and the agent's output, artifacts, trajectory or workspace, and the rubric.
Docker--docker-pull missing (default) for an absent image, or alwaysThe image's registry (Docker Hub, GHCR, ...). The image reference is visible to the registry.
Harbor and its agentshemra run with a via = "harbor" harnessWhatever Harbor does: pulling task images, downloading datasets, and the agents' model API calls with task content. See Harbor's own documentation for any upload features; HEMRA does not enable any.
Harbor's installed agents (via = "harbor" harnesses such as Claude Code and Pi)Each trialThe task container installs the harness (npm, the Claude Code release bucket) and calls the model provider, so the task containers need network. The keyless CI path instead points ANTHROPIC_BASE_URL at a local scripted model (tests/bench/scripted_model.py), and nothing reaches a real provider.
The desktop app, signed release builds onlyCheck for updates on Checks, or at launch if "Check for updates when the app opens" is on (off by default). Development and unsigned builds never check.A GET of latest.json from the endpoint built into the app (GitHub releases by default), then, if you choose to install, a download of the update archive. GitHub sees your IP address and user agent. Nothing from the store is sent. The archive installs only if its signature verifies against the key built into the app and was made for the version announced (ADR-0033).
The worker controller: hemra serve --workers-listen, or the desktop app with remote workers turned on (Checks, off by default, confirmed in a dialog)Whenever a registered hemra worker asks for workTo each worker that presents the enrollment token: queued jobs without a Docker sandbox, with their task input, harness command and the names of their environment variables (values come from the worker's own environment). Workers send back events, artifacts and results, which are stored here. Off loopback the listener needs TLS (ADR-0014, ADR-0035). The app keeps the token in worker-token (mode 0600) in its config directory.
@hemra/sdkNever on its ownThe SDK only speaks the protocol on stdin/stdout.

hemra import, hemra dataset (except dataset add harbor: and swebench:, which download through Harbor or the datasets library, and hf:, which downloads from HF_ENDPOINT or huggingface.co and its CDN, sending HF_TOKEN to the endpoint only and never recording it), hemra runs, hemra show, hemra export, hemra cancel, hemra compare and hemra doctor work entirely on local files, the local Docker daemon and --version probes of local tools. hemra serve sends nothing out itself, though the queued attempts it runs reach whatever their harness reaches; it listens on 127.0.0.1:7878 by default, has no authentication, and refuses a non-loopback address unless given --expose (ADR-0010). Anyone who can reach it can read the whole store, including transcripts and artifacts.

Operating guidance

Known gaps

Tracked against the RFCs; none of these are built yet.

GapSource
Redaction of events and stderr; configurable or disabled raw payload capture for private tasksRFC-0002 §13
Redacted export for sharingRFC-0003 §10, ADR-0003
Retention and garbage collection of events and blobsADR-0003
Egress allowlist (Docker today is all or nothing)RFC-0003 §12, RFC-0006
Secret references or mounts instead of environment variablesRFC-0002 §13, RFC-0006
Stronger-than-Docker isolation for untrusted or multi-tenant workloadsRFC-0004 §12, RFC-0006

Edit this page on GitHub