airlock-sandbox

A Hermes Agent terminal backend. Terminal, execute_code, and file-tool commands
leave Hermes through this plugin, hit an Airlock
HTTP API that I configure myself, and come back as BLOCKED, ALLOWED, or
VERIFIED. Airlock's policy engine decides allow or deny. Allowed code runs in
Airlock's disposable Docker sandbox, not on the Hermes host. Every decision includes
a signed receipt (RFC 8785 JCS, SHA-256, Ed25519). VERIFIED means that single
receipt's hash and signature checked out under a public key I configured locally.
This is a loadable plugin (plugin.yaml plus register(ctx)). It is not a prompt
wrapper or a skill. It hooks Hermes through the official terminal environment
provider seam: ctx.register_terminal_environment_provider(...).
Demo (opt-in)
A short recording against my public Airlock demo. The demo host and signing key are
not plugin defaults — you must set base_url and public_key_hex yourself.
Open the demo on GitHub
to play it (~30 seconds). The voiceover is TTS, not my voice.
Install
hermes plugins install /path/to/hermes-airlock-plugin
# or from git after I publish a pin:
# hermes plugins install https://github.com/justrunme/hermes-airlock-plugin
hermes plugins enable airlock-sandbox
hermes config set terminal.backend airlock_sandbox
Requires Hermes >=0.21.6 (terminal environment provider API).
Configure (required trust material)
Settings live under plugins.entries.airlock-sandbox.settings (also editable in
the Desktop Plugins form from config_schema).
| Key | Default | Purpose |
|-----|---------|---------|
| base_url | (empty, required) | HTTPS Airlock API base. No built-in host. |
| public_key_hex | (empty, required when verifying) | Trusted Ed25519 key (64 hex chars), obtained out-of-band |
| timeout_s | 60 | HTTP timeout |
| verify_receipts | true | Fail closed on bad hash/signature; requires public_key_hex |
| language | bash | Arena language (bash or python) |
| api_token (secret) | unset | Optional AIRLOCK_API_TOKEN for a private Airlock |
Example (replace with your own endpoint and key):
hermes config set plugins.entries.airlock-sandbox.settings.base_url "https://airlock.example.internal"
hermes config set plugins.entries.airlock-sandbox.settings.public_key_hex "<64-hex-chars>"
Opt-in sample against my public demo (labelled sample only — not defaults):
# DEMO ONLY — review the key yourself before trusting it
hermes config set plugins.entries.airlock-sandbox.settings.base_url "https://45-77-141-253.sslip.io"
hermes config set plugins.entries.airlock-sandbox.settings.public_key_hex \
"d8c2acc2972692ee205a7e056d8ae2791906ba1e17f9bdf2cee14baf53377748"
I read the optional token only through agent.secret_scope.get_secret("AIRLOCK_API_TOKEN").
I never open .env or Hermes auth.json myself. I never fetch the verifying key
from Airlock during verification — a server that could forge receipts could also
serve a matching key.
How execution is hooked
- Hermes loads this plugin and calls
register(ctx). - I register
AirlockSandboxProviderwith
ctx.register_terminal_environment_provider. - When I set
terminal.backend: airlock_sandbox, Hermes routesterminal,
execute_code, and file tools through myAirlockEnvironment.execute(). - Before any network call I require a clean HTTPS origin
base_urland (when
verifying) a localpublic_key_hex. Missing,http://, userinfo, query,
fragment, or bad ports fail closed. - Each command is posted to Airlock
POST /api/arena. On allow, Airlock runs it
in its sandbox. On deny, nothing starts there. Redirects are refused so an
HTTPS POST body cannot be replayed onto another origin or HTTP. - I verify the receipt under my local key, then bind signed
decision.verdict
andexecution.{exit_code,stdout_sha256,stderr_sha256}to the outer arena
fields before printingBLOCKED/ALLOWED/VERIFIED. Both digests must
be present exact hex (schema-optionalstderr_sha256absent/null refuses
VERIFIED);exit_codemust be an exact JSON integer (no float/bool/string
coercion). Signature-OK alone does not authorize unbound outer fields. - Hermes dangerous-command approvals stay on (
skip_container_guards = False).
Airlock is an extra fail-closed gate, not a bypass.
Limitations (honest)
- Airlock
/api/arenahas no process-stdin field (input_textbecomes a sandbox
fileinput.txt). Hermes file-tool writes pipe content viastdin_data; this
plugin refuses those calls instead of reporting a false empty write. - Airlock arena has no cwd field and each job is a fresh sandbox. Non-default
cwdvalues are refused. - The signed receipt does not include the command text.
VERIFIEDbinds
decision/execution digests under my pinned key; it does not prove which command
string was evaluated. When the unsigned outeraction.codeis present I still
require it to match the command I sent (consistency only). - Airlock marks
stderr_sha256optional in schema. This plugin still requires
a valid signed stderr digest before returning stderr underVERIFIED(empty
stderr is fine when the signed digest is the hash of""). Absent/null digests
refuse rather than display unbound outer stderr. - Truncated or
report_invalidexecutions cannot be bound to signed hashes and
are refused forVERIFIED. prev_hashon a receipt is shown as informational metadata. This plugin does
not verify hash-chain linkage across calls (a shared Airlock interleaves
other clients). Use Airlock's offlineverify-chainfor chain audits.
Disclosure (catalog rule 13)
- Network: HTTPS calls to the configured Airlock base URL only:
GET /healthz,POST /api/arena,GET /api/receipt/{id}. - Shell: This plugin does not spawn a host shell. Commands run only inside
Airlock when policy allows. - Credentials: Optional
AIRLOCK_API_TOKENvia Hermesconfig_schemasecret /
secret scope. Listed instrip_env_keysso model-authored commands cannot read it. - No binaries shipped. Python deps:
httpx,cryptography,jcs.
Fail-closed behaviour
| Case | Result |
|------|--------|
| Missing / blank base_url | BLOCKED, nothing sent |
| Non-HTTPS / userinfo / query / fragment / bad port | BLOCKED, nothing sent |
| Missing / malformed public_key_hex (verify on) | BLOCKED, nothing sent |
| HTTPS→HTTP (or any) redirect | refused; body not forwarded |
| Airlock down / bad /healthz | BLOCKED, non-zero exit, nothing on host |
| Policy deny | BLOCKED (+ VERIFIED if receipt checks out under local key) |
| Tampered / wrong-key receipt | BLOCKED / INVALID RECEIPT, never VERIFIED |
| Outer verdict/stdout/exit disagree with signed fields | BINDING FAILED, never VERIFIED |
| Allow without execution / status=failed | refused (no fake exit 0) |
| stdin_data present (incl. empty write) | BLOCKED unsupported |
| Non-default cwd | BLOCKED unsupported |
Tests
pip install -e .
pip install 'pytest>=8,<9'
pytest
hermes plugins validate . --install-deps
Sample catalog entry (SHA placeholder): catalog/airlock-sandbox.yaml.
License
MIT. See LICENSE.
README from the reviewed commit a35cf83; it updates when the author re-pins.
justrunme