Production checklist¶
A guard that works on your laptop can still fail you in production. It might be switched off by accident, block legitimate work, or leave you without a record of what happened. Work through this list before you rely on interlock for real traffic. Each item links to the page with the details.
Before you deploy¶
- Rules are installed at startup, and you check they're there. With no rules, interlock allows everything. Fail loudly instead:
import interlock
from my_service.policies import RULES
engine = interlock.install(rules=RULES)
assert len(engine) > 0, "interlock has no rules installed"
With scopes, a call made outside any span(principal=...)
matches no scope and is blocked. Set default_scope=("root",) to apply your
root policy to such calls instead.
-
Rules are in a sensible order. A block from any rule wins, but later rules see earlier rewrites, and budgets only count calls that reach them (ordering).
-
Every detector you rely on actually sees your tools. Check each one's "Scoped to" column in the catalog, and pass your tool names or wrap it with
only_for/as_action(detectors guide). -
Your policy has tests, for both what it blocks and what it lets through (writing rules). Run them in CI.
-
Every agent run is tagged. Wrap each run in
span(principal="tenant/agent")so decisions and receipts say who did what.
Roll out in monitor mode first¶
New rules always surprise someone. Run them in monitor mode on real traffic, read what they would have blocked, and enforce once the results look right. Driving the mode from configuration keeps that a deploy-time switch:
import os
from interlock import guard
ENFORCEMENT = os.environ.get("INTERLOCK_ENFORCEMENT", "blocking") # "monitor" to observe only
@guard(enforcement=ENFORCEMENT)
def send_email(to: str, subject: str, body: str) -> str: ...
INTERLOCK_ENFORCEMENT is just a name for your own setting; interlock doesn't
read it. Only the exact value "monitor" turns enforcement off. Anything else,
including a typo, enforces, so a misconfiguration fails safe.
Decisions are logged at DEBUG on the interlock logger:
interlock action=send_email verdict=BLOCK reason=external email span=2b4a2ac5a5b2433c817329101259e728 observed=True attributed_to=None
interlock mcp result action=send_email verdict=ALLOW observed=True attributed_to=None
observed=True means monitor mode recorded it without acting. Route this logger
wherever your logs go.
Handle refusals in the agent¶
-
A blocked call doesn't crash the agent. Catch
Blockedin your tool loop and hand the reason back to the model as the tool's result, so it can try something else (example). -
Hand-written rules can't crash the call path in surprising ways. A rule that raises stops the call, but the caller gets the rule's exception, not
Blocked. Wrap risky checks indeny_when, or catchExceptionaround tool calls. -
Rewritten arguments are what you use.
enforce_tool_callreturns the arguments to call the tool with. Calling the tool with the original arguments throws the rewrite away.
Keep a record¶
-
Receipts are on, with a stable key. Without
key=, a random key is made per process, and nobody else can verify the log (audit log). -
One writer per audit file. A sink reopened after a restart continues the file's chain, but two processes appending to the same file at once fork it, and
verify()reports the fork as a break. Give each process its own file. -
You actually verify logs, and can tell if the end was cut off. Record
sink.headsomewhere the agent's host can't change, then runverify_file(path, key=KEY, head=recorded_head)when you archive a file. Alert if it returns anything butNone.
Contain what you can't inspect¶
- Code the agent writes or downloads runs in the sandbox, not just behind argument checks (sandbox).
- You refuse weak sandboxes before anything runs. Build the sandbox
with the guarantees you need,
Sandbox(profile, require=("net", "fs-rw")), andrun()raisesIsolationUnavailablewithout starting the process on a host that can't deliver them. Checkingresult.enforcedafterrun()is too late: the code has already run (sandbox guide). - Memory limits fit your platform. CPU and memory limits apply on every
backend, but macOS can't cap memory, so a profile with
memory_mbraisesLimitUnsupportedthere. Keepmemory_mbfor Linux hosts.
Node.js agents¶
- The sidecar runs as a supervised service (systemd, a container sidecar, or your process manager), started before the agent.
- The socket lives in a directory the service owns, not directly in
/tmp(sidecar reference). -
interlock-sidecar --rules ... --checkruns in CI, so a broken or empty rule set fails the build rather than the deploy. - No
RemoteEnginedisables its timeout. The default is 5 seconds; withtimeout: nulla hung daemon hangs every guarded call. - You've accepted the availability trade-off. If the daemon is down, every guarded call is denied.
Performance¶
Rules run in your process as ordinary Python, so a call costs roughly what your
rules cost. There's no network hop. Scope lookups are cached per scope. There's no
published benchmark for the Python side yet (one is on the
roadmap), so measure with your own rules. The Node.js client
adds one local socket round trip per guarded call. Its smoke benchmark measured
tens of microseconds per call on a development machine (npm run bench in js/).
Upgrades¶
interlock isn't on PyPI yet, so you install it from Git. Pin a commit or tag so an upgrade is a deliberate change:
Next¶
- Security model: what's protected and what isn't.
- Audit log
- Sidecar reference