Security model

Assume the model-facing side is compromised.

Airlock is useful only if the requester node cannot turn text, state, or transport access into trusted execution. The integration is designed around that assumption.

Threat model

Untrusted by location, content, and intent

The harness, skill text, MCP process, requester service, capability labels, request descriptions, and model-generated fields all live on or originate from the untrusted side. Signed catalog metadata proves origin and integrity; it does not turn descriptive text into instructions.

Core rule: model-visible text can request authority. It can never grant authority.

Model-facing surface

Exactly three tools

  • airlock_capabilities
    Discover sanitized signed capability entries before asking for credentials or inventing a bypass.
  • airlock_create_request
    Submit only the strict typed fields required by an exact matching capability.
  • airlock_requests
    Read sanitized request and receipt state without trusted action text or reusable approvals.

There are no hooks, provider clients, shell commands, command strings, trusted-node URLs, secret headers, or direct execution methods in the model surface.

Requester transport

Loopback, explicit, and boring

  • Requester URLs must be explicit http://127.0.0.1:<port> values.
  • Credentials, URL user-info, query strings, fragments, redirects, and inherited proxies are refused.
  • The MCP server can advertise tools while the requester is offline, but returns sanitized unavailable errors for live operations.
  • The requester never connects directly to an external provider on the agent's behalf.

Trusted side

Revalidate and reconstruct locally

The trusted node pulls requests rather than accepting inbound trusted action text. It verifies the signed catalog context, request digest, expiry, capability identity, and a versioned local adapter's constraints. Request IDs are idempotent and cannot be reused with a different bound digest.

Only then does it render a locally reconstructed plan for human review. The configured absolute GitHub CLI and exact argv are passed directly with a fixed minimal environment: no shell, PATH lookup, inherited token/proxy environment, or generic command field exists.

Linux local control uses an owner-private Unix socket and verifies SO_PEERCRED for every accepted connection before HTTP reads it. The peer UID must equal the daemon's effective UID; filesystem modes are defense-in-depth, not an authentication substitute. Unsupported platforms fail closed instead of exposing this control plane without peer credentials.

Human boundary

One trusted human approval before direct execution

  1. Decision: inspect the validated request and exact plan, then choose Approve and execute or deny.
  2. Attempt: Airlock persists approval plus a running reservation, then executes outside its store lock. Failures are bounded and retries are explicit.

Requester/MCP/Hermes surfaces do not bridge this action. See the approval timing diagram for the full sequence.

Installer integrity

Fail closed around user-owned files

  • The binary source must be an explicit executable and is installed content-addressably.
  • Only exact airlock@airlock plugin identity is considered installer-owned.
  • Instruction blocks require both managed markers and matching path/digest state.
  • Unrecorded, edited, malformed, missing, or symlinked instruction targets are refused.
  • A failed Hermes config read aborts before plugin, MCP, file, or ownership state removal.
  • Default install and update do not read or back up global instruction files.

Evidence semantics

Receipts describe workflow state, not external truth

approved_for_execution proves a trusted reviewer reservation. executed proves only that the trusted child returned success and its completion persisted. Neither proves the provider accepted the action or that the intended state exists.

Every success claim therefore ends with an independent ordinary read-only verification path.