wardn 0.1.0.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. wardn-0.1.0.dev0/.gitattributes +2 -0
  2. wardn-0.1.0.dev0/.gitignore +12 -0
  3. wardn-0.1.0.dev0/LICENSE +21 -0
  4. wardn-0.1.0.dev0/PKG-INFO +213 -0
  5. wardn-0.1.0.dev0/README.md +187 -0
  6. wardn-0.1.0.dev0/TODO.md +403 -0
  7. wardn-0.1.0.dev0/docs/ci.md +92 -0
  8. wardn-0.1.0.dev0/docs/concepts.md +179 -0
  9. wardn-0.1.0.dev0/docs/embedding.md +97 -0
  10. wardn-0.1.0.dev0/docs/ledger-format.md +220 -0
  11. wardn-0.1.0.dev0/docs/policy-format.md +102 -0
  12. wardn-0.1.0.dev0/examples/flagship/README.md +77 -0
  13. wardn-0.1.0.dev0/examples/flagship/act1_enforcement.py +82 -0
  14. wardn-0.1.0.dev0/examples/flagship/act2_ci_gate.py +70 -0
  15. wardn-0.1.0.dev0/examples/flagship/agent.py +254 -0
  16. wardn-0.1.0.dev0/pyproject.toml +59 -0
  17. wardn-0.1.0.dev0/spike/policy.json +37 -0
  18. wardn-0.1.0.dev0/spike/spike.py +442 -0
  19. wardn-0.1.0.dev0/src/wardn/__init__.py +32 -0
  20. wardn-0.1.0.dev0/src/wardn/__main__.py +6 -0
  21. wardn-0.1.0.dev0/src/wardn/_version.py +1 -0
  22. wardn-0.1.0.dev0/src/wardn/approvals.py +103 -0
  23. wardn-0.1.0.dev0/src/wardn/cli.py +149 -0
  24. wardn-0.1.0.dev0/src/wardn/diff.py +154 -0
  25. wardn-0.1.0.dev0/src/wardn/engine.py +105 -0
  26. wardn-0.1.0.dev0/src/wardn/ledger.py +237 -0
  27. wardn-0.1.0.dev0/src/wardn/matchers.py +62 -0
  28. wardn-0.1.0.dev0/src/wardn/orchestrator.py +122 -0
  29. wardn-0.1.0.dev0/src/wardn/policy.py +313 -0
  30. wardn-0.1.0.dev0/src/wardn/proxy.py +478 -0
  31. wardn-0.1.0.dev0/src/wardn/py.typed +1 -0
  32. wardn-0.1.0.dev0/src/wardn/revert.py +101 -0
  33. wardn-0.1.0.dev0/src/wardn/types.py +49 -0
  34. wardn-0.1.0.dev0/tests/conftest.py +147 -0
  35. wardn-0.1.0.dev0/tests/fake_server.py +115 -0
  36. wardn-0.1.0.dev0/tests/test_cli_replay_revert.py +76 -0
  37. wardn-0.1.0.dev0/tests/test_diff.py +103 -0
  38. wardn-0.1.0.dev0/tests/test_dogfood_filesystem.py +166 -0
  39. wardn-0.1.0.dev0/tests/test_engine.py +107 -0
  40. wardn-0.1.0.dev0/tests/test_import_purity.py +32 -0
  41. wardn-0.1.0.dev0/tests/test_ledger_replay.py +94 -0
  42. wardn-0.1.0.dev0/tests/test_multi_server.py +202 -0
  43. wardn-0.1.0.dev0/tests/test_policy.py +90 -0
  44. wardn-0.1.0.dev0/tests/test_proxy_e2e.py +184 -0
  45. wardn-0.1.0.dev0/tests/test_revert.py +59 -0
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,12 @@
1
+ # spike outputs are regenerable (and byte-deterministic — run spike/spike.py)
2
+ spike/out/
3
+
4
+ __pycache__/
5
+ *.py[cod]
6
+ .venv/
7
+ dist/
8
+ .pytest_cache/
9
+ *.egg-info/
10
+
11
+ # flagship example outputs are regenerable
12
+ examples/flagship/runs/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pi-Wi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,213 @@
1
+ Metadata-Version: 2.4
2
+ Name: wardn
3
+ Version: 0.1.0.dev0
4
+ Summary: Bounded, auditable action governor for LLM agents — a policy gate between an MCP client and its servers.
5
+ Project-URL: Repository, https://github.com/Pi-Wi/warden
6
+ Project-URL: Documentation, https://github.com/Pi-Wi/warden/tree/main/docs
7
+ Author: Pi-Wi
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,audit,governance,ledger,llm,mcp,model-context-protocol,policy
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Classifier: Topic :: System :: Logging
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # wardn
28
+
29
+ Bounded, auditable action governor for LLM agents — a policy gate between an
30
+ MCP client and its servers. Every tool call is allowed, denied, or
31
+ quarantined by a rule you declared; run-wide budgets cap total side-effects
32
+ with loud refusals; and every attempt lands in an append-only,
33
+ timestamp-free, byte-deterministic ledger that replays offline and diffs in
34
+ CI.
35
+
36
+ wardn bounds and audits. It does not sandbox, and it is not a content-safety
37
+ product.
38
+
39
+ ## Status
40
+
41
+ **Pre-release — M2 (adoptable, CI-integrable) is built.** The proxy governs
42
+ one or more real MCP servers over stdio; `wardn review` decides quarantined
43
+ calls from any terminal; `wardn replay` reproduces every decision offline;
44
+ `wardn revert` undoes declared-reversible effects from the ledger file
45
+ alone; and `wardn diff` turns two ledgers into a CI verdict — exit 4 the
46
+ moment governed activity drifts. Two identical runs emit byte-identical
47
+ ledgers, including against the real
48
+ `@modelcontextprotocol/server-filesystem`.
49
+
50
+ Not yet on PyPI — install from a clone (`pip install .`). The roadmap,
51
+ locked decisions, and spike record live in [TODO.md](TODO.md).
52
+
53
+ ## The gap
54
+
55
+ Permission systems exist (per-client allow/ask/deny rules), approval
56
+ services exist, security gateways exist, eval-time diffs exist. But none of
57
+ them owns the middle: side-effect governance that is *declared* (a policy
58
+ file, not scattered client settings), *bounded* ("at most 2 writes", not
59
+ just "ask"), and *auditable* — emitted as a single deterministic artifact a
60
+ CI job can diff with an exit code.
61
+
62
+ That artifact is the whole point of wardn. A permission prompt leaves no
63
+ record. An observability stream isn't deterministic. wardn's ledger is
64
+ both: the decision log and the drift detector, one file.
65
+
66
+ ## The core idea: one ledger, three uses
67
+
68
+ Every attempt — allowed, denied, quarantined, refused by a budget — is one
69
+ recorded event stream: what was attempted, which rule matched, how the call
70
+ was classified (and by what authority), what was decided, why, and what came
71
+ back.
72
+
73
+ - **Read it forward** → the audit report. Every side-effect, explained.
74
+ - **Replay it** (`wardn replay`) → the decisions, reproduced offline from
75
+ policy + file alone. A tampered ledger or a swapped policy refuses loudly.
76
+ - **Replay it inverted** (`wardn revert`) → the undo. Declared-reversible
77
+ effects are reverted from the file alone — the undo call was resolved and
78
+ recorded at capture time, so revert needs no policy, only the ledger and a
79
+ live server. If the world drifted since the run, revert refuses rather
80
+ than guesses.
81
+
82
+ And because the file is byte-deterministic — no timestamps, no PIDs, no
83
+ nonces — `wardn diff yesterday.jsonl today.jsonl` is a CI gate: same
84
+ policy, same attempts, same decisions, or exit 4.
85
+
86
+ ## Quick look
87
+
88
+ A policy is an ordered list of rules, first match wins, with a mandatory
89
+ explicit default — a policy that doesn't say what happens to unmatched
90
+ calls is invalid:
91
+
92
+ ```json
93
+ {
94
+ "format": "wardn-policy/1",
95
+ "default": "deny",
96
+ "budget": {"max_calls": 20, "max_writes": 5, "max_spend": 1.0},
97
+ "approval_timeout_s": 300,
98
+ "rules": [
99
+ {
100
+ "id": "allow-reads",
101
+ "match": {"server": "filesystem", "tool": "read_text_file"},
102
+ "effect": "read",
103
+ "disposition": "allow",
104
+ "reason": "reads are harmless here"
105
+ },
106
+ {
107
+ "id": "bound-writes",
108
+ "match": {"server": "filesystem", "tool": "write_file",
109
+ "args": {"path": {"path_prefix": "/work/out/"}}},
110
+ "effect": "write",
111
+ "disposition": "allow",
112
+ "bound": {"max_calls": 2},
113
+ "cost_per_call": 0.1,
114
+ "capture": {"tool": "read_text_file", "args_from": {"path": "path"}},
115
+ "inverse": {"tool": "write_file",
116
+ "args": {"path": "$args.path", "content": "$capture.value"},
117
+ "expect": "$args.content"},
118
+ "reason": "writes capped at 2, under out/ only, pre-state captured"
119
+ },
120
+ {
121
+ "id": "quarantine-moves",
122
+ "match": {"server": "filesystem", "tool": "move_file"},
123
+ "effect": "destructive",
124
+ "disposition": "quarantine",
125
+ "reason": "moves need a human"
126
+ }
127
+ ]
128
+ }
129
+ ```
130
+
131
+ Put wardn between your client and the server — the client config points at
132
+ wardn, wardn spawns the real server:
133
+
134
+ ```json
135
+ {
136
+ "mcpServers": {
137
+ "filesystem": {
138
+ "command": "wardn",
139
+ "args": ["proxy", "--policy", "policy.json", "--ledger", "run.jsonl",
140
+ "--", "npx", "-y", "@modelcontextprotocol/server-filesystem",
141
+ "/work"]
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ The agent now reads freely, gets its third write refused loudly (an error
148
+ result naming the rule and the bound — the agent can read why and adapt),
149
+ and blocks on `move_file` until someone, in any terminal, runs:
150
+
151
+ ```console
152
+ $ wardn review list --queue run.jsonl.queue.json
153
+ q6 move_file {"source":"/work/report.txt","destination":"/work/old.txt"} rule=quarantine-moves — moves need a human
154
+
155
+ $ wardn review approve q6 --queue run.jsonl.queue.json --by phiwir
156
+ q6: approve by phiwir
157
+ ```
158
+
159
+ Afterwards, the run is a file:
160
+
161
+ ```console
162
+ $ wardn replay --policy policy.json --ledger run.jsonl
163
+ replayed 8 attempt(s) — every decision reproduced by this policy
164
+
165
+ $ wardn revert --ledger run.jsonl -- npx -y @modelcontextprotocol/server-filesystem /work
166
+ applied 2 undo(s) — revert ledger: run.jsonl.revert.jsonl
167
+
168
+ $ wardn diff approved/run.jsonl run.jsonl && echo "no drift"
169
+ ```
170
+
171
+ Several servers can sit behind one gate — `wardn proxy --servers
172
+ servers.json` takes a file in the same shape as an MCP client config, and
173
+ rules match on the server's name. See [docs/concepts.md](docs/concepts.md)
174
+ for what multi-server mode honestly does and doesn't proxy.
175
+
176
+ ## What wardn is not
177
+
178
+ - **Not a sandbox.** wardn is a policy gate in front of cooperative
179
+ transports. A hostile server, or a client that bypasses the proxy, is out
180
+ of scope — and argument constraints are string constraints:
181
+ `path_prefix: "out/"` happily matches `out/../secret.txt`. wardn does not
182
+ resolve paths; that is a sandbox's job. Never treat a wardn policy as an
183
+ isolation boundary.
184
+ - **Not content safety.** wardn governs side-effects, not text. Prompt
185
+ injection defense is a different layer.
186
+ - **No LLM anywhere in the enforcement path.** A decision is a pure
187
+ function of policy + request + recorded approvals. Ever.
188
+ - **No measured spend.** `max_spend` refuses on arithmetic over *declared*
189
+ costs (`cost_per_call`), never on billing APIs — wardn cannot observe
190
+ real spend and doesn't pretend to.
191
+
192
+ ## Docs
193
+
194
+ - [docs/concepts.md](docs/concepts.md) — rules, dispositions, bounds,
195
+ classification, quarantine, capture/revert, and the determinism claim
196
+ stated precisely (including its honest asterisks).
197
+ - [docs/policy-format.md](docs/policy-format.md) — `wardn-policy/1`,
198
+ normative.
199
+ - [docs/ledger-format.md](docs/ledger-format.md) — `wardn-ledger/1`, the
200
+ compatibility contract. The ledger format and the proxy CLI are the two
201
+ things wardn promises stability for.
202
+ - [docs/ci.md](docs/ci.md) — the CI story: budgets as drift tripwires,
203
+ `wardn diff` as the gate.
204
+ - [docs/embedding.md](docs/embedding.md) — the engine API for non-MCP
205
+ embeddings: `govern_call` in front of any tool-calling boundary.
206
+
207
+ ## Family
208
+
209
+ Sibling to [agentrec](https://github.com/Pi-Wi/agent-rec) (records what
210
+ models *say*) and [mendr](https://github.com/Pi-Wi/mendr) (repairs what data
211
+ *got wrong*) — wardn governs what agents *do*. Same ethos: declared,
212
+ bounded, recorded; the record is the product; no LLM anywhere in the
213
+ enforcement path.
@@ -0,0 +1,187 @@
1
+ # wardn
2
+
3
+ Bounded, auditable action governor for LLM agents — a policy gate between an
4
+ MCP client and its servers. Every tool call is allowed, denied, or
5
+ quarantined by a rule you declared; run-wide budgets cap total side-effects
6
+ with loud refusals; and every attempt lands in an append-only,
7
+ timestamp-free, byte-deterministic ledger that replays offline and diffs in
8
+ CI.
9
+
10
+ wardn bounds and audits. It does not sandbox, and it is not a content-safety
11
+ product.
12
+
13
+ ## Status
14
+
15
+ **Pre-release — M2 (adoptable, CI-integrable) is built.** The proxy governs
16
+ one or more real MCP servers over stdio; `wardn review` decides quarantined
17
+ calls from any terminal; `wardn replay` reproduces every decision offline;
18
+ `wardn revert` undoes declared-reversible effects from the ledger file
19
+ alone; and `wardn diff` turns two ledgers into a CI verdict — exit 4 the
20
+ moment governed activity drifts. Two identical runs emit byte-identical
21
+ ledgers, including against the real
22
+ `@modelcontextprotocol/server-filesystem`.
23
+
24
+ Not yet on PyPI — install from a clone (`pip install .`). The roadmap,
25
+ locked decisions, and spike record live in [TODO.md](TODO.md).
26
+
27
+ ## The gap
28
+
29
+ Permission systems exist (per-client allow/ask/deny rules), approval
30
+ services exist, security gateways exist, eval-time diffs exist. But none of
31
+ them owns the middle: side-effect governance that is *declared* (a policy
32
+ file, not scattered client settings), *bounded* ("at most 2 writes", not
33
+ just "ask"), and *auditable* — emitted as a single deterministic artifact a
34
+ CI job can diff with an exit code.
35
+
36
+ That artifact is the whole point of wardn. A permission prompt leaves no
37
+ record. An observability stream isn't deterministic. wardn's ledger is
38
+ both: the decision log and the drift detector, one file.
39
+
40
+ ## The core idea: one ledger, three uses
41
+
42
+ Every attempt — allowed, denied, quarantined, refused by a budget — is one
43
+ recorded event stream: what was attempted, which rule matched, how the call
44
+ was classified (and by what authority), what was decided, why, and what came
45
+ back.
46
+
47
+ - **Read it forward** → the audit report. Every side-effect, explained.
48
+ - **Replay it** (`wardn replay`) → the decisions, reproduced offline from
49
+ policy + file alone. A tampered ledger or a swapped policy refuses loudly.
50
+ - **Replay it inverted** (`wardn revert`) → the undo. Declared-reversible
51
+ effects are reverted from the file alone — the undo call was resolved and
52
+ recorded at capture time, so revert needs no policy, only the ledger and a
53
+ live server. If the world drifted since the run, revert refuses rather
54
+ than guesses.
55
+
56
+ And because the file is byte-deterministic — no timestamps, no PIDs, no
57
+ nonces — `wardn diff yesterday.jsonl today.jsonl` is a CI gate: same
58
+ policy, same attempts, same decisions, or exit 4.
59
+
60
+ ## Quick look
61
+
62
+ A policy is an ordered list of rules, first match wins, with a mandatory
63
+ explicit default — a policy that doesn't say what happens to unmatched
64
+ calls is invalid:
65
+
66
+ ```json
67
+ {
68
+ "format": "wardn-policy/1",
69
+ "default": "deny",
70
+ "budget": {"max_calls": 20, "max_writes": 5, "max_spend": 1.0},
71
+ "approval_timeout_s": 300,
72
+ "rules": [
73
+ {
74
+ "id": "allow-reads",
75
+ "match": {"server": "filesystem", "tool": "read_text_file"},
76
+ "effect": "read",
77
+ "disposition": "allow",
78
+ "reason": "reads are harmless here"
79
+ },
80
+ {
81
+ "id": "bound-writes",
82
+ "match": {"server": "filesystem", "tool": "write_file",
83
+ "args": {"path": {"path_prefix": "/work/out/"}}},
84
+ "effect": "write",
85
+ "disposition": "allow",
86
+ "bound": {"max_calls": 2},
87
+ "cost_per_call": 0.1,
88
+ "capture": {"tool": "read_text_file", "args_from": {"path": "path"}},
89
+ "inverse": {"tool": "write_file",
90
+ "args": {"path": "$args.path", "content": "$capture.value"},
91
+ "expect": "$args.content"},
92
+ "reason": "writes capped at 2, under out/ only, pre-state captured"
93
+ },
94
+ {
95
+ "id": "quarantine-moves",
96
+ "match": {"server": "filesystem", "tool": "move_file"},
97
+ "effect": "destructive",
98
+ "disposition": "quarantine",
99
+ "reason": "moves need a human"
100
+ }
101
+ ]
102
+ }
103
+ ```
104
+
105
+ Put wardn between your client and the server — the client config points at
106
+ wardn, wardn spawns the real server:
107
+
108
+ ```json
109
+ {
110
+ "mcpServers": {
111
+ "filesystem": {
112
+ "command": "wardn",
113
+ "args": ["proxy", "--policy", "policy.json", "--ledger", "run.jsonl",
114
+ "--", "npx", "-y", "@modelcontextprotocol/server-filesystem",
115
+ "/work"]
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ The agent now reads freely, gets its third write refused loudly (an error
122
+ result naming the rule and the bound — the agent can read why and adapt),
123
+ and blocks on `move_file` until someone, in any terminal, runs:
124
+
125
+ ```console
126
+ $ wardn review list --queue run.jsonl.queue.json
127
+ q6 move_file {"source":"/work/report.txt","destination":"/work/old.txt"} rule=quarantine-moves — moves need a human
128
+
129
+ $ wardn review approve q6 --queue run.jsonl.queue.json --by phiwir
130
+ q6: approve by phiwir
131
+ ```
132
+
133
+ Afterwards, the run is a file:
134
+
135
+ ```console
136
+ $ wardn replay --policy policy.json --ledger run.jsonl
137
+ replayed 8 attempt(s) — every decision reproduced by this policy
138
+
139
+ $ wardn revert --ledger run.jsonl -- npx -y @modelcontextprotocol/server-filesystem /work
140
+ applied 2 undo(s) — revert ledger: run.jsonl.revert.jsonl
141
+
142
+ $ wardn diff approved/run.jsonl run.jsonl && echo "no drift"
143
+ ```
144
+
145
+ Several servers can sit behind one gate — `wardn proxy --servers
146
+ servers.json` takes a file in the same shape as an MCP client config, and
147
+ rules match on the server's name. See [docs/concepts.md](docs/concepts.md)
148
+ for what multi-server mode honestly does and doesn't proxy.
149
+
150
+ ## What wardn is not
151
+
152
+ - **Not a sandbox.** wardn is a policy gate in front of cooperative
153
+ transports. A hostile server, or a client that bypasses the proxy, is out
154
+ of scope — and argument constraints are string constraints:
155
+ `path_prefix: "out/"` happily matches `out/../secret.txt`. wardn does not
156
+ resolve paths; that is a sandbox's job. Never treat a wardn policy as an
157
+ isolation boundary.
158
+ - **Not content safety.** wardn governs side-effects, not text. Prompt
159
+ injection defense is a different layer.
160
+ - **No LLM anywhere in the enforcement path.** A decision is a pure
161
+ function of policy + request + recorded approvals. Ever.
162
+ - **No measured spend.** `max_spend` refuses on arithmetic over *declared*
163
+ costs (`cost_per_call`), never on billing APIs — wardn cannot observe
164
+ real spend and doesn't pretend to.
165
+
166
+ ## Docs
167
+
168
+ - [docs/concepts.md](docs/concepts.md) — rules, dispositions, bounds,
169
+ classification, quarantine, capture/revert, and the determinism claim
170
+ stated precisely (including its honest asterisks).
171
+ - [docs/policy-format.md](docs/policy-format.md) — `wardn-policy/1`,
172
+ normative.
173
+ - [docs/ledger-format.md](docs/ledger-format.md) — `wardn-ledger/1`, the
174
+ compatibility contract. The ledger format and the proxy CLI are the two
175
+ things wardn promises stability for.
176
+ - [docs/ci.md](docs/ci.md) — the CI story: budgets as drift tripwires,
177
+ `wardn diff` as the gate.
178
+ - [docs/embedding.md](docs/embedding.md) — the engine API for non-MCP
179
+ embeddings: `govern_call` in front of any tool-calling boundary.
180
+
181
+ ## Family
182
+
183
+ Sibling to [agentrec](https://github.com/Pi-Wi/agent-rec) (records what
184
+ models *say*) and [mendr](https://github.com/Pi-Wi/mendr) (repairs what data
185
+ *got wrong*) — wardn governs what agents *do*. Same ethos: declared,
186
+ bounded, recorded; the record is the product; no LLM anywhere in the
187
+ enforcement path.