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
- HEMRA is a single-operator, local tool. It trusts the operator's configuration and the benchmark's official scorer. It does not trust harness output, artifacts or agent-generated code, and it is not a sandbox for hostile code.
- HEMRA itself makes no network calls and sends no telemetry. Anything that leaves the machine is sent by a harness, by Docker, or by Harbor, under the operator's control.
- The store keeps task inputs, every protocol event, the stderr tail, artifact bytes and Harbor's job files, unredacted. Treat a store and any export from it as being as sensitive as the most sensitive prompt, tool output or file a run touched.
- Environment variable values are never persisted; only their names. But a harness can still leak a credential into an event, its stderr or an artifact, and HEMRA does not redact.
Trust model
| Party | Trusted? | Consequence |
|---|---|---|
Operator (the person running hemra) and their configuration | Yes | Flags, 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 stdout | No | Every 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 writes | No | The runner canonicalizes paths, rejects escapes and symlinks, enforces quotas and computes the digest itself (ADR-0003). |
| Agent-generated code and tool calls | No | Runs with whatever isolation the provider gives (see Isolation). |
| Benchmark's official scorer (Harbor's verifier) | Yes, for scores | Rewards are imported verbatim and never recomputed; HEMRA never derives pass/fail from run.completed (RFC-0003 §4). |
| Other users on the same host | Not considered | v0.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:
| Path | Contents | May contain sensitive data? |
|---|---|---|
hemra.db (SQLite) attempts.manifest_json | The 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_json | The 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, artifacts | Lifecycle history and artifact metadata (path, media type, digest, size). | Artifact paths only |
hemra.db imported_*, scores | Normalized 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, trials | Each 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>.jsonl | Every 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_transitions | The 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
hemra harness checkruns its trial in a throwaway$TMPDIR/hemra-check-*workspace, which is removed when the check ends. With--storethe attempt's events and artifacts are kept in the store; without it, nothing of the trial is kept.hemra runprints its report to stdout (--jsonfor everything). Where that goes is up to the operator's shell.hemra exportwrites a self-contained copy of a run, an attempt (manifest, record,events.jsonl, artifact blobs) or of an imported Harbor job (import.jsonand Harbor's raw files) to a directory. Exports are not redacted; they contain the same sensitive data as the store.
Credentials
What the code does:
- Local harnesses start with an empty environment plus
PATHand the variables the harness file names inenv(local_commandinhemra-engine/src/runner.rs). Host variables such asANTHROPIC_API_KEYorAWS_*are not inherited unless passed explicitly. - Only variable names are persisted. The manifest records
env_keys; values never reach SQLite, the event log or exports (test:docker_manifest_pins_the_image_and_keeps_no_env_values). - Docker harnesses receive values by name, not argv. The
dockerclient runs with a cleared environment except the variables it needs (PATH,HOME,USER,TMPDIR,XDG_CONFIG_HOME,DOCKER_*) plus the harness entries, and the argv carries--env NAMEonly, so values do not appear inpsoutput for thedockerprocess or in the recorded container settings. Names the client itself reads are refused as harness variables. - MCP servers start with an empty environment too (ADR-0026):
PATHplus the names in their[[mcp]]env, read fromhemra's environment; an unset name stops the run before anything starts. Manifests keep the names only. - **
run.startcarries no secrets** by contract (RFC-0002 §13): it holds the task, the run context and runtime paths. - Harbor runs with the operator's full environment.
hemra rundoes not clear the environment of theharborprocess it starts for avia = "harbor"harness, because Harbor's agents read their model API keys from it (for exampleANTHROPIC_API_KEYforanthropic/...models). HEMRA does not write any key intohemra-config.json.
What it does not do:
- No redaction. If a harness echoes a key into a
logevent, a tool result, stderr or an artifact, HEMRA stores it as-is. RFC-0002 §13 treats redaction as defense in depth that is not yet built. - Agent kwargs are persisted. A harness file's
kwargsgo intohemra-config.jsonand into the run spec in the store, which are kept. Never put a key there; use the environment Harbor inherits. - No secret store. There is no keychain, vault or secret-mount integration yet.
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:
- a cleared environment (above) and a working directory set to the trial workspace;
- protocol limits: 1 MiB per line, 64 MiB of events, a 15-minute default deadline, a 30-second handshake timeout, and escalation from
run.canceltoSIGTERMtoSIGKILLon the whole group; - artifact checks: only regular files under the artifact directory, no symlink escapes, quotas, runner-computed SHA-256.
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):
| Control | Default |
|---|---|
| Network | --network none (no egress, no DNS) |
| Privileges | --cap-drop ALL, --security-opt no-new-privileges, never --privileged |
| User | The host's effective uid:gid, not root |
| Resources | 4 GiB memory with no extra swap, up to 2 CPUs, 1024 PIDs |
| Mounts | Only the trial workspace (/hemra/workspace) and artifact directory (/hemra/artifacts); extra mounts (or --docker-mounts) are read-only unless :rw |
| Docker socket | Never mounted |
| Image | Resolved to an immutable image ID before the attempt is recorded; the manifest pins it |
| Cleanup | Every 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.
Modal sandbox ([sandbox] kind = "modal", experimental)
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:
| Sender | When | To where |
|---|---|---|
| A harness (local) | Whenever it chooses | Anywhere: 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 network | Anywhere reachable from that Docker network. |
| A task verifier (Docker) | Only with --verifier-network or [verifier] network | Anywhere 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 flyctl | Each 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 runs | Only 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 always | The image's registry (Docker Hub, GHCR, ...). The image reference is visible to the registry. |
| Harbor and its agents | hemra run with a via = "harbor" harness | Whatever 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 trial | The 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 only | Check 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 work | To 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/sdk | Never on its own | The 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
- Keep stores private. New stores are created
0700; if you point--storeat a directory that already exists,chmod 700it yourself. Do not commit a store or export to a repository. - Run harnesses you did not write in the Docker sandbox (the
hemra rundefault, not--sandbox local), and keepnetwork = "none"unless the harness needs a model API. - Pass credentials only by name, in a harness file's
env, never in task input, harness arguments, agent kwargs or Harbor config files: those are persisted. - Use dedicated, low-limit API keys for evaluation runs; assume anything given to a harness can end up in its transcript.
- Before sharing an export, read
events.jsonland the artifacts. There is no redacted export yet. - To delete one attempt by hand, remove its row from
attempts,attempt_transitionsandartifacts, itsevents/<attempt>.jsonl, and any blob no other row references. Deleting the whole store directory is the reliable way to delete everything. - Runs without
--storeleave$TMPDIR/hemra-*behind on purpose; delete them once you have what you need, or pass--work-dirsomewhere you manage.
Known gaps
Tracked against the RFCs; none of these are built yet.
| Gap | Source |
|---|---|
| Redaction of events and stderr; configurable or disabled raw payload capture for private tasks | RFC-0002 §13 |
| Redacted export for sharing | RFC-0003 §10, ADR-0003 |
| Retention and garbage collection of events and blobs | ADR-0003 |
| Egress allowlist (Docker today is all or nothing) | RFC-0003 §12, RFC-0006 |
| Secret references or mounts instead of environment variables | RFC-0002 §13, RFC-0006 |
| Stronger-than-Docker isolation for untrusted or multi-tenant workloads | RFC-0004 §12, RFC-0006 |