File size: 6,972 Bytes
7065c0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acba802
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7065c0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acba802
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
---
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`.