Manifest-only STDIO mode (v0.5.7+)¶
Module: agent_airlock.mcp_spec.manifest_only_mode
The OX Security 2026-04-15 deep dive established that arbitrary
strings reaching StdioServerParameters.command is the
agent-supply-chain class of bug. Anthropic confirmed the SDK
validates nothing — that "sanitization is the developer's
responsibility." Manifest-only mode is the design that makes
runtime sanitization unnecessary because argv never originates
from runtime input.
The shape¶
- Pre-register a
StdioManifestwith a fixedcommandtuple under a stablemanifest_id. Registration requires an HMAC-SHA256 signing key. - At runtime, callers ask
launch_from_manifestfor the manifest by ID. The function rejects any attempt to overridecommand,cwd, orenvoutside the manifest's allowlist.
There is no surface a runtime caller can use to inject argv.
Modes¶
SecurityPolicy.stdio_mode is a Literal["allowlist", "manifest_only", "disabled"]:
| Mode | Behaviour |
|---|---|
"allowlist" (default) |
v0.5.1 behaviour — runtime argv goes through validate_stdio_command |
"manifest_only" |
Only launch_from_manifest may spawn STDIO subprocesses |
"disabled" |
No STDIO subprocesses, period |
Allowlist mode remains the default so existing v0.5.1 callers keep working. Manifest-only mode is opt-in for hardening-conscious deployments.
HMAC key storage¶
The signing key is loaded from AIRLOCK_MANIFEST_SIGNING_KEY. The
guard refuses to start with a key shorter than 32 bytes:
export AIRLOCK_MANIFEST_SIGNING_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(48))')"
Rotate the key by re-registering manifests with the new key. A
cross-key resolve raises ManifestSignatureError so stale
registries are caught at first call.
Quick start¶
from agent_airlock import (
ManifestRegistry,
StdioManifest,
launch_from_manifest,
)
registry = ManifestRegistry()
manifest = StdioManifest(
manifest_id="local-fs",
command=("uvx", "mcp-server-everything"),
env_allowlist=frozenset({"PATH", "HOME"}),
cwd="/var/repos/local",
signer="sre-team",
)
registry.register(manifest, signing_key=key)
# Later, anywhere — no argv flows from runtime input:
proc = launch_from_manifest(
"local-fs",
registry,
runtime_env=os.environ,
allowed_cwd_prefixes=("/var/repos/",),
)
A runtime caller passing command=["evil"] (or any kwarg outside
manifest_id / runtime_env) raises
ManifestRuntimeOverrideAttempted. That's the load-bearing
guarantee.
Errors¶
All four subclass AirlockError and are top-level re-exports:
| Error | Raised when |
|---|---|
ManifestNotRegisteredError |
resolve() called with unknown manifest_id |
ManifestSignatureError |
Stored signature does not verify against the supplied key |
ManifestRuntimeOverrideAttempted |
Runtime caller tried to pass argv / cwd / command |
ManifestSigningKeyError |
Signing key shorter than 32 bytes |
Migration from allowlist mode¶
For a controlled rollout:
- Keep
stdio_mode="allowlist"while you enumerate every STDIO server your agents currently use. - Register each as a
StdioManifest. The v0.5.7 perf benchmark shows resolve+verify latency at ~3 µs median — invisible to any human-driven workflow. - Flip to
stdio_mode="manifest_only"after the manifest list stabilises.
Dynamic-argv use cases (e.g. LangGraph agents that compose argv at runtime) cannot adopt manifest-only — they should stay on allowlist mode.
Observability¶
Every clean launch emits airlock.stdio.manifest.launch (OTel)
with attributes:
airlock.stdio.manifest.signerairlock.stdio.manifest.sha256
Spans are best-effort — a misconfigured OTel provider never breaks the audit path.