| --- |
| license: apache-2.0 |
| library_name: interlock |
| pipeline_tag: text-classification |
| tags: |
| - agent-safety |
| - tool-use |
| - guardrails |
| - ai-safety |
| language: |
| - en |
| --- |
| |
| # Interlock |
|
|
| Interlock judges a *proposed agent tool call* against *what the user actually |
| asked for*. Content shields read the request and ask whether it is harmful. |
| Interlock reads the request/action pair and asks a different question: is this |
| call **more** than what was asked? |
|
|
| It is deliberately blind to where a deviation came from. A widened `path` |
| argument looks the same whether it originated in a prompt injection, a |
| jailbreak, or an ordinary model mistake, and the defence is the same either |
| way. |
|
|
| ## Output |
|
|
| Three-valued, never binary: |
|
|
| | decision | meaning | |
| |-----------|---------| |
| | `allow` | the call stays within what the request asked for | |
| | `confirm` | the call should be put to the user before it runs | |
| | `block` | the call goes materially beyond the request | |
|
|
| The three decisions are `allow`, `confirm`, `block`. Alongside the decision the model |
| predicts a deviation type, one of `none`, `scope_expansion`, `unrequested_action`, `irreversible_unconfirmed`, `target_mismatch`, `out_of_scope`, `privilege_escalation`. |
|
|
| `confirm` is a first-class outcome. Collapsing it into `block` would trade a |
| question for a refusal on exactly the cases where the request is ambiguous. |
|
|
| ## Two tiers |
|
|
| **Tier 1 -- linear (1080 parameters).** A two-headed multinomial |
| logistic regression over 107 structured request/action alignment |
| features. Pure NumPy at inference: no tokeniser, no torch. This is the tier |
| that runs on every agent turn. Shipped as `interlock_linear.npz`, with the |
| fitted operating point in `policy.json`. |
|
|
| **Tier 2 -- encoder (optional).** ModernBERT-base with a decision head and a |
| deviation head, reading a role-annotated text rendering of the same pair. More |
| accurate where the wording rather than the structure carries the signal, and |
| correspondingly slower. Shipped as `interlock.onnx` when it has been trained |
| and exported; the export is verified against the torch model to within 1e-3 on |
| the raw logits before it is published. |
|
|
| A third file, `interlock-linear.gguf`, is a weight container for tier 1 with |
| its feature names and operating point attached. It is not a runtime: llama.cpp |
| has no architecture for this classification head and cannot serve it. The |
| verified runtimes are NumPy for tier 1 and ONNX Runtime for tier 2. |
|
|
| ## Evaluation, and the split that makes it mean something |
|
|
| The train/test split is taken at **tool family** granularity, not at row |
| granularity. Training and validation draw on `fs, shell, mail, calendar, issues`. |
| The held-out split draws on `browser, crm, storage, db` -- every tool schema |
| in it is one the model has never seen. The split is verified against the files |
| on disk, not just asserted in code. |
|
|
| Measured for tier 1: |
|
|
| | split | over-blocking | deviation detection | |
| |-------|---------------|---------------------| |
| | validation (seen families, unseen world) | 2.9% | 88.0% | |
| | held-out families (unseen tool schemas) | 1.8% | 79.3% | |
|
|
| Over-blocking is the fraction of *legitimate* calls that were interrupted, and |
| it is the headline number. A shield that interrupts real work gets switched |
| off, and a switched-off shield detects nothing. Detection is reported second |
| for that reason, not first. |
|
|
| Latency, timing a full check per call including encoding: 0.247 ms mean, 0.663 ms p95 over 400 single-threaded calls on arm64. |
| Timing the model's arithmetic alone would understate what a deployment pays. |
|
|
| ## Intended use |
|
|
| Interlock sits between an agent's proposed tool call and its execution. Given |
| the user's request, the proposed call and the tool's schema, it returns a |
| verdict that a harness can act on: proceed, ask the user, or refuse. It is |
| designed for agent runtimes where tool schemas are declared and where a |
| confirmation prompt is a cheaper failure than an unwanted side effect. |
|
|
| ```python |
| from interlock.model import load_shield |
| |
| shield = load_shield("artifacts") |
| # The request names one file. The call names the directory that contains it. |
| verdict = shield.check( |
| "Archive the Q3 notes at /work/apollo/q3-notes.md", |
| {"tool": "fs.delete_path", "arguments": {"path": "/work/apollo", "recursive": True}}, |
| ) |
| print(verdict.decision, verdict.deviation) # block scope_expansion |
| for reason in verdict.reasons: |
| print(" -", reason) |
| ``` |
|
|
| Nothing about that call reads as dangerous on its own: it is an ordinary delete, |
| on an ordinary path, in the project the user named. It is wrong only relative to |
| the request, which is the only place the difference exists. |
|
|
| A tool the model has never seen still gets a verdict rather than an exception, |
| because an agent can propose anything: |
|
|
| ```python |
| verdict = shield.check( |
| "Rotate the API key for the billing service", |
| {"tool": "acme.rotate_key", "arguments": {"service": "billing"}}, |
| ) |
| print(verdict.decision, verdict.known_tool) # confirm False |
| ``` |
|
|
| It is not a content moderation model, not a prompt-injection detector, and not |
| a substitute for sandboxing or for asking the user. |
|
|
| ## Limitations |
|
|
| - **Synthetic data.** Every episode, tool schema and deviation in training is |
| generated. The tool families model common operation shapes rather than any |
| particular product's API, and no real service was contacted at any point. |
| Performance on production agent traffic is unmeasured here. |
| - **English only.** Requests and tool descriptions are English. Nothing has |
| been measured in any other language. |
| - **One layer among several.** Interlock reads the correspondence between a |
| request and a call. It does not read the content of a payload for harm, does |
| not sandbox anything, and cannot see effects that occur outside the tool |
| call it is shown. |
| - **It does not guarantee safety.** An `allow` verdict means this shield found |
| no boundary deviation, which is a weaker statement than "this call is safe". |
| Deploy it beside content shields, sandboxing and human confirmation, not |
| instead of them. |
| - **Unseen tools cost accuracy.** Detection falls from 88.0% to |
| 79.3% on held-out tool families. The over-blocking rate holds, so |
| the degradation shows up as missed deviations rather than as new |
| interruptions, but it is real. |
| - **The operating point is a choice.** Thresholds were fitted against an |
| explicit over-blocking budget on validation data. A deployment with a |
| different tolerance for interruption should refit them rather than inherit |
| these. |
|
|
| ## Files |
|
|
| | file | what it is | |
| |------|------------| |
| | `interlock_linear.npz` | tier-1 weights, the default serving path | |
| | `policy.json` | fitted decision thresholds | |
| | `train_report_linear.json` | measured metrics for tier 1 | |
| | `interlock.onnx` | tier-2 graph, when the encoder has been trained | |
| | `interlock-linear.gguf` | tier-1 weight container, not a runtime | |
| | `latency.json` | measured per-call latency and peak RSS | |
|
|
| Card generated 2026-08-30T01:30:45+00:00 for `NagaYu/interlock`. |
|
|