aegis-kernel 0.4.0__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.
- aegis_kernel-0.4.0/LICENSE +21 -0
- aegis_kernel-0.4.0/PKG-INFO +665 -0
- aegis_kernel-0.4.0/README.md +630 -0
- aegis_kernel-0.4.0/aegis/__init__.py +20 -0
- aegis_kernel-0.4.0/aegis/__main__.py +6 -0
- aegis_kernel-0.4.0/aegis/adapters/__init__.py +5 -0
- aegis_kernel-0.4.0/aegis/adapters/mcp.py +429 -0
- aegis_kernel-0.4.0/aegis/adapters/mcp_client.py +344 -0
- aegis_kernel-0.4.0/aegis/audit.py +130 -0
- aegis_kernel-0.4.0/aegis/conformance/__init__.py +11 -0
- aegis_kernel-0.4.0/aegis/conformance/cli.py +386 -0
- aegis_kernel-0.4.0/aegis/conformance/drift.py +145 -0
- aegis_kernel-0.4.0/aegis/conformance/export.py +213 -0
- aegis_kernel-0.4.0/aegis/conformance/fixtures.py +128 -0
- aegis_kernel-0.4.0/aegis/conformance/invariants.py +285 -0
- aegis_kernel-0.4.0/aegis/conformance/loopholes.py +648 -0
- aegis_kernel-0.4.0/aegis/conformance/mcp_checks.py +261 -0
- aegis_kernel-0.4.0/aegis/conformance/report.py +169 -0
- aegis_kernel-0.4.0/aegis/conformance/report_html.py +184 -0
- aegis_kernel-0.4.0/aegis/conformance/runner.py +177 -0
- aegis_kernel-0.4.0/aegis/conformance/scaffold.py +45 -0
- aegis_kernel-0.4.0/aegis/conformance/spec.py +95 -0
- aegis_kernel-0.4.0/aegis/constitution.py +226 -0
- aegis_kernel-0.4.0/aegis/constitution.yaml +69 -0
- aegis_kernel-0.4.0/aegis/corpus/payloads.yaml +110 -0
- aegis_kernel-0.4.0/aegis/decision.py +67 -0
- aegis_kernel-0.4.0/aegis/grant.py +220 -0
- aegis_kernel-0.4.0/aegis/guards/__init__.py +14 -0
- aegis_kernel-0.4.0/aegis/guards/base.py +36 -0
- aegis_kernel-0.4.0/aegis/guards/budget.py +19 -0
- aegis_kernel-0.4.0/aegis/guards/capability.py +47 -0
- aegis_kernel-0.4.0/aegis/guards/data.py +216 -0
- aegis_kernel-0.4.0/aegis/guards/spawn.py +51 -0
- aegis_kernel-0.4.0/aegis/kernel.py +272 -0
- aegis_kernel-0.4.0/aegis/observe.py +82 -0
- aegis_kernel-0.4.0/aegis/policy.py +360 -0
- aegis_kernel-0.4.0/aegis/py.typed +0 -0
- aegis_kernel-0.4.0/aegis/registry.py +69 -0
- aegis_kernel-0.4.0/aegis/runtime.py +106 -0
- aegis_kernel-0.4.0/aegis/templates/baseline.yaml +68 -0
- aegis_kernel-0.4.0/aegis/templates/policy.yaml +67 -0
- aegis_kernel-0.4.0/aegis/templates/restricted.yaml +28 -0
- aegis_kernel-0.4.0/aegis/templates/suite.yaml +204 -0
- aegis_kernel-0.4.0/aegis/templates/workflow.yml +47 -0
- aegis_kernel-0.4.0/aegis_kernel.egg-info/PKG-INFO +665 -0
- aegis_kernel-0.4.0/aegis_kernel.egg-info/SOURCES.txt +60 -0
- aegis_kernel-0.4.0/aegis_kernel.egg-info/dependency_links.txt +1 -0
- aegis_kernel-0.4.0/aegis_kernel.egg-info/entry_points.txt +2 -0
- aegis_kernel-0.4.0/aegis_kernel.egg-info/requires.txt +6 -0
- aegis_kernel-0.4.0/aegis_kernel.egg-info/top_level.txt +1 -0
- aegis_kernel-0.4.0/pyproject.toml +67 -0
- aegis_kernel-0.4.0/setup.cfg +4 -0
- aegis_kernel-0.4.0/tests/test_async.py +287 -0
- aegis_kernel-0.4.0/tests/test_cli.py +183 -0
- aegis_kernel-0.4.0/tests/test_confidence.py +156 -0
- aegis_kernel-0.4.0/tests/test_conformance.py +238 -0
- aegis_kernel-0.4.0/tests/test_corpus.py +147 -0
- aegis_kernel-0.4.0/tests/test_examples.py +46 -0
- aegis_kernel-0.4.0/tests/test_governance.py +193 -0
- aegis_kernel-0.4.0/tests/test_mcp_audit.py +353 -0
- aegis_kernel-0.4.0/tests/test_mcp_live.py +204 -0
- aegis_kernel-0.4.0/tests/test_observe.py +159 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aegis contributors
|
|
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,665 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aegis-kernel
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Capability-based constraint enforcement for AI agents, with a regression suite that hunts its own loopholes and an MCP server auditor.
|
|
5
|
+
Author: Aegis contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Aditya31398/aegis
|
|
8
|
+
Project-URL: Documentation, https://github.com/Aditya31398/aegis#readme
|
|
9
|
+
Project-URL: Issues, https://github.com/Aditya31398/aegis/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Aditya31398/aegis/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Security, https://github.com/Aditya31398/aegis/security/policy
|
|
12
|
+
Keywords: ai-agents,mcp,model-context-protocol,security,guardrails,policy,authorization,llm-security,sarif
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: PyYAML>=6.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
33
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# Aegis — constraint enforcement for agent systems
|
|
37
|
+
|
|
38
|
+
[](https://github.com/Aditya31398/aegis/actions/workflows/ci.yml)
|
|
39
|
+
[](https://pypi.org/project/aegis-kernel/)
|
|
40
|
+
[](https://pypi.org/project/aegis-kernel/)
|
|
41
|
+
[](LICENSE)
|
|
42
|
+
|
|
43
|
+
Aegis does two jobs:
|
|
44
|
+
|
|
45
|
+
1. **Runtime enforcement.** A kernel mediates every tool call an agent makes
|
|
46
|
+
against a declarative YAML policy: capability allowlists, argument
|
|
47
|
+
constraints, spend and time budgets, PII/taint egress rules, and bounded
|
|
48
|
+
agent spawning. Denied calls never execute.
|
|
49
|
+
2. **Regression and audit.** A conformance framework proves the constraints
|
|
50
|
+
still hold after a change, detects when a policy is quietly weakened, hunts
|
|
51
|
+
for loopholes in your own policies, and audits third-party MCP servers.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install aegis-kernel # Python 3.10+, one dependency (PyYAML)
|
|
57
|
+
aegis --version
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or run the container, no Python needed:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
docker run --rm -v "$PWD:/work" ghcr.io/aditya31398/aegis --help
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quickstart: audit an MCP server
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# a live server (only initialize + tools/list are ever sent; no tool is called)
|
|
70
|
+
aegis mcp --server https://mcp.example.com/mcp --bearer-env MCP_TOKEN --out audit-out
|
|
71
|
+
|
|
72
|
+
# or a saved manifest / claude_desktop_config.json
|
|
73
|
+
aegis mcp --manifest claude_desktop_config.json --out audit-out
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`audit-out/` gets a Markdown report, `audit.json`, `audit.sarif`, and a
|
|
77
|
+
`hardened-policy.yaml` you can adopt.
|
|
78
|
+
|
|
79
|
+
## Quickstart: enforce a policy on your own agents
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
aegis init . --ci # policy, adversarial scenarios, baseline, GitHub workflow
|
|
83
|
+
aegis ratify --policy policies/base.yaml
|
|
84
|
+
aegis verify --suites suites --policy policies/base.yaml --require-coverage
|
|
85
|
+
aegis audit --policy policies/base.yaml
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The scaffold passes every check out of the box, so the first failure you see
|
|
89
|
+
is one you caused. Then, in code:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from aegis import Agent, PolicyViolation, ToolRegistry, build_kernel, load_policy
|
|
93
|
+
|
|
94
|
+
registry = ToolRegistry()
|
|
95
|
+
|
|
96
|
+
@registry.tool("fs.read", effects={"read"}, classification="internal")
|
|
97
|
+
def read_file(path: str) -> str:
|
|
98
|
+
return open(path).read()
|
|
99
|
+
|
|
100
|
+
kernel, root = build_kernel(load_policy("quickstart-policy.yaml"), registry)
|
|
101
|
+
agent = Agent(root, kernel)
|
|
102
|
+
|
|
103
|
+
agent.tools.fs__read(path="/workspace/notes.md") # allowed
|
|
104
|
+
agent.tools.fs__read(path="/etc/passwd") # PolicyViolation: never executed
|
|
105
|
+
await agent.atools.fs__read(path="/workspace/a.md") # async runtimes too
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Runnable version: [`examples/quickstart.py`](examples/quickstart.py) with its
|
|
109
|
+
one-tool [policy](examples/quickstart-policy.yaml). `build_kernel` refuses to
|
|
110
|
+
start an unconstitutional policy — including one that grants a tool you never
|
|
111
|
+
registered — and every decision, allowed or denied, lands in a hash-chained
|
|
112
|
+
audit log (`kernel.audit`).
|
|
113
|
+
|
|
114
|
+
## Using it in CI
|
|
115
|
+
|
|
116
|
+
**GitHub Actions** — pin to a release tag:
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
permissions:
|
|
120
|
+
contents: read
|
|
121
|
+
security-events: write # only if upload-sarif is on
|
|
122
|
+
|
|
123
|
+
steps:
|
|
124
|
+
- uses: actions/checkout@v7
|
|
125
|
+
- uses: Aditya31398/aegis@v0.4.0
|
|
126
|
+
with:
|
|
127
|
+
manifest: mcp-servers.json # and/or server: https://…/mcp
|
|
128
|
+
baseline: aegis-baseline.yaml
|
|
129
|
+
fail-on: high
|
|
130
|
+
upload-sarif: "true" # findings appear in the Security tab
|
|
131
|
+
comment-on-pr: "true"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Anywhere else** — `aegis` is a normal CLI with a stable contract:
|
|
135
|
+
|
|
136
|
+
| | |
|
|
137
|
+
|---|---|
|
|
138
|
+
| Exit codes | `0` pass · `1` findings/violations at or above `--fail-on` · `2` bad input or usage · `3` internal error |
|
|
139
|
+
| Formats | `--format text\|json\|sarif`, `--output FILE` (text stays on stdout for the log) |
|
|
140
|
+
| JSON schema | `aegis.audit/v1`; fields are only ever added |
|
|
141
|
+
| Fingerprints | stable per finding; SARIF `partialFingerprints` so dashboards dedupe across runs |
|
|
142
|
+
| Accepted risk | baselined findings stay in the output as SARIF *suppressions* with the written reason |
|
|
143
|
+
|
|
144
|
+
A broken policy file is always exit `2`, never `1`, so a pipeline can tell
|
|
145
|
+
"your policy has holes" from "your policy file is malformed".
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
aegis ratify --policy policies/base.yaml
|
|
149
|
+
aegis verify --suites suites --policy policies/base.yaml --require-coverage
|
|
150
|
+
aegis fuzz --policy policies/base.yaml --iterations 20 --async
|
|
151
|
+
aegis audit --policy policies/base.yaml --format sarif --output aegis.sarif
|
|
152
|
+
aegis drift --baseline main-base.yaml --candidate policies/base.yaml
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Observability: pairing with an APM
|
|
156
|
+
|
|
157
|
+
Aegis decides what an agent *may* do; it deliberately ships no dashboard. Two hooks
|
|
158
|
+
let an observability tool see every decision without being able to influence one:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from aegis.observe import register_context_provider
|
|
162
|
+
|
|
163
|
+
register_context_provider(lambda: {"run_id": current_run_id()}) # -> details.ctx on every record
|
|
164
|
+
kernel.audit.subscribe(lambda rec: ship(rec)) # after the record is chained
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Model calls are not tools, but they spend the same budget. Reserve before the
|
|
168
|
+
request and settle after it, and the ledger becomes a hard gate on model spend:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
r = kernel.reserve_spend(grant, usd=estimate, tokens=max_tokens) # BudgetExhausted -> call never made
|
|
172
|
+
kernel.settle_spend(r, usd=actual_cost, tokens=actual_tokens)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
[AgentDynamics](https://github.com/Aditya31398/agentdynamics) uses exactly these
|
|
176
|
+
hooks (`agentdynamics.integrations.aegis`): denials land in the task they happened
|
|
177
|
+
in, model calls are gated by the Aegis budget, a watchdog revokes grants that keep
|
|
178
|
+
probing a boundary, and observed behaviour is turned back into a tighter policy
|
|
179
|
+
that `aegis ratify` and `aegis drift` verify.
|
|
180
|
+
|
|
181
|
+
## Supply chain
|
|
182
|
+
|
|
183
|
+
Releases are built once in CI from a tag, published to PyPI through trusted
|
|
184
|
+
publishing (no long-lived token exists), and carry signed build provenance:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
gh attestation verify aegis_kernel-0.4.0-py3-none-any.whl --repo Aditya31398/aegis
|
|
188
|
+
gh attestation verify oci://ghcr.io/aditya31398/aegis:0.4.0 --repo Aditya31398/aegis
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The container runs as a non-root user and ships an SBOM. The kernel has one
|
|
192
|
+
runtime dependency (PyYAML) and is small enough to vendor.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
The rest of this document explains how it works and why it is built this way.
|
|
197
|
+
|
|
198
|
+
## The load-bearing idea
|
|
199
|
+
|
|
200
|
+
**Prompt rules are advisory. Only a mediating kernel is enforceable.**
|
|
201
|
+
|
|
202
|
+
An agent never receives a callable. It receives a `ToolProxy` bound to
|
|
203
|
+
`(kernel, grant, tool_name)`. The only code path from an agent to a real
|
|
204
|
+
implementation runs through `Kernel.invoke`, which runs the guard chain first.
|
|
205
|
+
`tests/test_conformance.py::test_kernel_is_the_only_execution_path` parses the
|
|
206
|
+
AST of every file in `aegis/` and fails the build if a second call site to a
|
|
207
|
+
tool implementation ever appears.
|
|
208
|
+
|
|
209
|
+
The same holds for async runtimes. `await kernel.ainvoke(grant, tool, **args)`
|
|
210
|
+
(or `await agent.atools.fs__read(path=...)`) goes through the identical guard
|
|
211
|
+
chain, ledger and audit log; coroutine tools are awaited, blocking tools run
|
|
212
|
+
off the event loop, and calling a coroutine tool through the sync `invoke` is
|
|
213
|
+
denied as `kernel.async_tool_requires_ainvoke` rather than leaking an
|
|
214
|
+
unmediated awaitable.
|
|
215
|
+
|
|
216
|
+
So the guarantee is scoped honestly: **an agent can attempt anything; it cannot
|
|
217
|
+
*cause* anything outside its grant.** No framework can stop a model from
|
|
218
|
+
generating a bad tool call. This one stops the call from executing.
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
Agent ──> ToolProxy ──> Kernel.invoke
|
|
222
|
+
│
|
|
223
|
+
├─ guards (fail-closed, first DENY wins)
|
|
224
|
+
│ capability → spawn → budget → data
|
|
225
|
+
├─ budget charge (reserved before execution)
|
|
226
|
+
├─ _execute / _aexecute ← THE ONLY CALL SITES
|
|
227
|
+
├─ post-guards (classification ceiling, taint)
|
|
228
|
+
└─ audit record (hash-chained)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Grants and attenuation
|
|
232
|
+
|
|
233
|
+
A `Grant` is the only thing that authorises an effect. It can only ever be
|
|
234
|
+
*attenuated*. `Grant.attenuate()` raises on any request for authority the
|
|
235
|
+
parent doesn't hold, so "a spawned agent can never exceed its parent" is a
|
|
236
|
+
structural property, not a convention:
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
child.tools ⊆ parent.tools ∩ policy.spawn.allow_tools
|
|
240
|
+
child.budget ≤ parent.remaining × min(requested, child_budget_fraction)
|
|
241
|
+
child.depth = parent.depth + 1 ≤ max_depth
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Budgets are **hierarchical** — a child's spend debits every ancestor's ledger,
|
|
245
|
+
so N children cannot collectively outspend the root even though each is
|
|
246
|
+
individually within its own limit.
|
|
247
|
+
|
|
248
|
+
Revocation is total: revoking a grant disables its entire subtree immediately.
|
|
249
|
+
|
|
250
|
+
## The four constraint classes
|
|
251
|
+
|
|
252
|
+
| Class | Mechanism | Example rule ids |
|
|
253
|
+
|---|---|---|
|
|
254
|
+
| Tool / side effect | default-deny allowlist + per-argument regex, prefix, enum, length, forbidden-pattern | `capability.not_granted`, `capability.arg_prefix`, `capability.unexpected_arg` |
|
|
255
|
+
| Spend / time | hierarchical ledger, pre-flight admission | `budget.usd_exceeded`, `budget.tool_calls_exceeded`, `budget.deadline_exceeded` |
|
|
256
|
+
| Data / PII egress | classification ceiling on reads, taint propagation, PII scan at declared sinks | `data.classification_exceeded`, `data.taint_egress_blocked`, `data.pii_egress_blocked` |
|
|
257
|
+
| Spawning | depth, fan-out, whole-tree descendant cap, delegable-tool list | `spawn.max_depth_exceeded`, `spawn.max_fanout_exceeded`, `spawn.privilege_escalation` |
|
|
258
|
+
|
|
259
|
+
Rule ids are the stable contract. Tests pin the id, never the prose, so
|
|
260
|
+
reworded messages don't break the suite but a rule that stops firing does.
|
|
261
|
+
|
|
262
|
+
## Policy
|
|
263
|
+
|
|
264
|
+
Policies are YAML data, never code — so they can be diffed, signed, pinned, and
|
|
265
|
+
cannot be authored at runtime by an agent. `extends:` derives a tighter profile;
|
|
266
|
+
the loader rejects a derived policy that raises a budget.
|
|
267
|
+
|
|
268
|
+
```yaml
|
|
269
|
+
tools:
|
|
270
|
+
allow:
|
|
271
|
+
- name: db.query
|
|
272
|
+
require_args: [sql]
|
|
273
|
+
args:
|
|
274
|
+
sql:
|
|
275
|
+
matches: "(?is)^\\s*select\\b.*"
|
|
276
|
+
forbid_matches: "(?i)\\b(drop|delete|update|insert|alter)\\b"
|
|
277
|
+
spawn:
|
|
278
|
+
max_depth: 2
|
|
279
|
+
max_fanout: 3
|
|
280
|
+
child_budget_fraction: 0.4
|
|
281
|
+
allow_tools: [kb.search, fs.read, http.get, db.query, agent.spawn]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
# The regression framework
|
|
287
|
+
|
|
288
|
+
Four independent layers. All four are required CI checks.
|
|
289
|
+
|
|
290
|
+
### 1. Scenario conformance (`suites/*.yaml`)
|
|
291
|
+
|
|
292
|
+
Adversarial cases — path traversal, SSRF, SQL mutation, argument smuggling, PII
|
|
293
|
+
exfiltration, spawn bombs, privilege escalation, budget exhaustion. Each step
|
|
294
|
+
pins the expected verdict *and* the rule id.
|
|
295
|
+
|
|
296
|
+
Each denial asserts the **strong** form: the tool implementation was never
|
|
297
|
+
entered. Fixtures record every execution, so a test proves "nothing happened",
|
|
298
|
+
not merely "an error was returned".
|
|
299
|
+
|
|
300
|
+
```yaml
|
|
301
|
+
- id: deny-path-traversal
|
|
302
|
+
steps:
|
|
303
|
+
- invoke: {tool: fs.read, args: {path: "/etc/passwd"}}
|
|
304
|
+
expect: deny
|
|
305
|
+
rule: capability.arg_prefix
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Severity is not confidence
|
|
309
|
+
|
|
310
|
+
Severity answers "how bad if real". Confidence answers "how sure", and the two
|
|
311
|
+
come apart constantly — an architectural risk can be high severity and low
|
|
312
|
+
confidence at the same time. Every finding carries both:
|
|
313
|
+
|
|
314
|
+
| Confidence | Evidence behind it |
|
|
315
|
+
|---|---|
|
|
316
|
+
| `confirmed` | the input went through the real guard chain and was admitted, or the schema/description literally says so |
|
|
317
|
+
| `likely` | two independent signals, or one fact plus an inference step that could be wrong (inferred effects, for instance) |
|
|
318
|
+
| `possible` | a structural pattern that depends on context this audit cannot see |
|
|
319
|
+
|
|
320
|
+
Confidence never hides anything. It changes presentation, and it gates what
|
|
321
|
+
may *fail* a build:
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
aegis audit --policy policies/base.yaml --fail-on high --min-confidence confirmed
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
That reports every finding exactly as before and fails only on the ones we can
|
|
328
|
+
prove. SARIF keeps the axes apart too: `security-severity` for how bad, `rank`
|
|
329
|
+
for how sure, so a dashboard can sort on either. Confidence is deliberately not
|
|
330
|
+
part of a fingerprint — re-grading a check must never renumber a baseline.
|
|
331
|
+
|
|
332
|
+
### The payload corpus is data
|
|
333
|
+
|
|
334
|
+
The payloads the probe engine pushes through the guard chain live in
|
|
335
|
+
`aegis/corpus/payloads.yaml` (`schema: aegis.corpus/v1`, with a `version`),
|
|
336
|
+
not in the code. A newer corpus drops in without upgrading the package:
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
aegis audit --policy policies/base.yaml --corpus corpus-2026-09.yaml
|
|
340
|
+
AEGIS_CORPUS=corpus-2026-09.yaml aegis mcp --manifest servers.json --out out
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Which corpus produced a run is recorded in `audit.json` under
|
|
344
|
+
`tool.corpus_version`. A corpus that fails validation is a usage error (exit
|
|
345
|
+
`2`), never a quiet pass with fewer probes — a silently-skipped payload looks
|
|
346
|
+
exactly like a clean audit.
|
|
347
|
+
|
|
348
|
+
The property that makes refreshes safe: a payload finding is fingerprinted on
|
|
349
|
+
the **(tool, argument) pair it reached**, not on the payload string that
|
|
350
|
+
happened to arrive first. So adding payloads can reveal a new hole, and can
|
|
351
|
+
never invalidate an accepted entry in someone's baseline.
|
|
352
|
+
`test_adding_payloads_never_changes_existing_fingerprints` inserts a payload at
|
|
353
|
+
the front of every kind — the worst case for witness selection — and asserts
|
|
354
|
+
every prior fingerprint survives.
|
|
355
|
+
|
|
356
|
+
### 2. Invariants under fuzz (`aegis/conformance/invariants.py`)
|
|
357
|
+
|
|
358
|
+
A random workload generator drives the kernel with thousands of arbitrary
|
|
359
|
+
call/spawn/revoke sequences, half hostile payloads and half well-formed calls
|
|
360
|
+
the policy admits. Seven invariants are re-checked after *every* operation:
|
|
361
|
+
|
|
362
|
+
`attenuation` · `depth_bound` · `budget_conservation` · `no_effect_on_deny` ·
|
|
363
|
+
`every_effect_was_charged` · `audit_chain` · `revocation_is_total`
|
|
364
|
+
|
|
365
|
+
`fuzz --async` drives the same invariants through `ainvoke`/`aspawn`: each
|
|
366
|
+
round launches a batch concurrently, cancels some calls mid-flight, and a
|
|
367
|
+
watcher task re-checks every invariant at each scheduling point, so the kernel
|
|
368
|
+
is observed *during* calls, not only between them. Its negative control is a
|
|
369
|
+
planted kernel that checks the budget, awaits the tool, then charges — a
|
|
370
|
+
check-then-act race the fuzzer must catch.
|
|
371
|
+
|
|
372
|
+
Writing that control exposed a weakness in the fuzzer itself: the workload was
|
|
373
|
+
almost entirely hostile, so nearly every call died at the guards, budgets were
|
|
374
|
+
never exhausted, and the budget invariants were green because the ledger barely
|
|
375
|
+
moved. The planted race was caught on 1 seed in 4. Adding well-formed calls
|
|
376
|
+
raised that to 4 in 6, and `test_fuzz_workload_reaches_budget_exhaustion` now
|
|
377
|
+
fails if the workload ever stops reaching an exhausted budget.
|
|
378
|
+
|
|
379
|
+
Any exception that isn't a `PolicyViolation` is a framework bug and fails the
|
|
380
|
+
run. This layer found a real one during development: `agent.spawn` routed
|
|
381
|
+
through the tool path crashed rather than denying.
|
|
382
|
+
|
|
383
|
+
### 3. Privilege drift (`aegis/conformance/drift.py`)
|
|
384
|
+
|
|
385
|
+
The subtle regression isn't broken enforcement — the suite catches that. It's
|
|
386
|
+
someone quietly *loosening the policy*, after which every test still passes
|
|
387
|
+
because the tests now agree with the weaker rules.
|
|
388
|
+
|
|
389
|
+
So the policy is diffed against the pinned baseline and any widening fails the
|
|
390
|
+
build unless explicitly waived by code:
|
|
391
|
+
|
|
392
|
+
```
|
|
393
|
+
+ [tools.constraint_relaxed] fs.read.path: prefix '/workspace/' -> '/'
|
|
394
|
+
+ [budget.usd_raised] usd 5.0 -> 50.0
|
|
395
|
+
+ [spawn.max_depth_raised] max_depth 2 -> 6
|
|
396
|
+
RESULT: FAIL — policy widens agent authority.
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The differ is deliberately conservative: a regex change it can't *prove* is a
|
|
400
|
+
tightening counts as widening.
|
|
401
|
+
|
|
402
|
+
### 4. Structural and fail-closed checks
|
|
403
|
+
|
|
404
|
+
- AST check that the kernel remains the sole execution path
|
|
405
|
+
- crashing guard → `guard.internal_error` DENY, never an allow
|
|
406
|
+
- policy-allowed but unregistered tool → `registry.unknown_tool` DENY
|
|
407
|
+
- tampering with an audit record breaks `verify()`
|
|
408
|
+
- a sibling swarm with `budget_fraction: 1.0` each still can't outspend the root
|
|
409
|
+
- coverage: every granted tool must be exercised by some scenario
|
|
410
|
+
|
|
411
|
+
## Adding a tool — the checklist
|
|
412
|
+
|
|
413
|
+
1. `registry.register(...)` with its `effects`, `classification`, `cost_usd`
|
|
414
|
+
2. add it to `policies/base.yaml` with the *tightest* argument constraints
|
|
415
|
+
3. decide whether it is an egress sink and whether it is delegable to children
|
|
416
|
+
4. write at least one allow case and one deny case per constraint in `suites/`
|
|
417
|
+
5. run `drift` — the new grant shows as `tools.added`, waive it in the PR
|
|
418
|
+
|
|
419
|
+
Step 5 is the point: widening authority is always a deliberate, reviewed act.
|
|
420
|
+
|
|
421
|
+
## What this does not do
|
|
422
|
+
|
|
423
|
+
- It can't stop the model from *trying*. It stops attempts from having effects.
|
|
424
|
+
- Constraints are only as good as the policy. A tool registered with loose
|
|
425
|
+
regexes is a hole the kernel will faithfully honour.
|
|
426
|
+
- The PII scanner is a backstop, not the primary control — the primary control
|
|
427
|
+
is that the agent never holds the raw callable.
|
|
428
|
+
- Prompt injection is out of scope as an *input* problem; it is in scope as an
|
|
429
|
+
*effect* problem, since an injected instruction still has to pass the guards.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
# Law, not just locks
|
|
434
|
+
|
|
435
|
+
The framework is layered the way a legal system is, because the failure modes
|
|
436
|
+
are the same ones legal systems evolved to handle.
|
|
437
|
+
|
|
438
|
+
| Layer | File | Amended by | Waivable? |
|
|
439
|
+
|---|---|---|---|
|
|
440
|
+
| **Constitution** | `aegis/constitution.yaml` | editing the document | **no** — no flag, no config, no override |
|
|
441
|
+
| **Statute** | `policies/*.yaml` | a PR that widens | yes, `--waive <code>` with review |
|
|
442
|
+
| **Case law** | `suites/*.yaml` | adding scenarios | n/a — precedents accumulate |
|
|
443
|
+
| **Accepted holes** | `loopholes.baseline.yaml` | adding a fingerprint | yes, with a written reason and an owner |
|
|
444
|
+
|
|
445
|
+
### Constitution
|
|
446
|
+
|
|
447
|
+
Seven clauses that every policy must satisfy to be **ratified**. `build_kernel`
|
|
448
|
+
ratifies before it returns, so an unconstitutional policy cannot start:
|
|
449
|
+
|
|
450
|
+
```
|
|
451
|
+
C1 Bounded authority — no budget axis may be unbounded
|
|
452
|
+
C2 Every egress path screened — an outward-capable tool must be a declared sink
|
|
453
|
+
C3 No unconstrained argument — effectful args carry at least one constraint
|
|
454
|
+
C4 Delegation attenuates — child fraction < 1.0, depth bounded
|
|
455
|
+
C5 Reading is not exporting — egress ceiling strictly below read ceiling
|
|
456
|
+
C6 Rules must be enforceable — every named PII kind has a working detector
|
|
457
|
+
C7 No phantom grants — a granted tool must actually exist
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
There is deliberately no waiver path. The only route past a clause is to edit
|
|
461
|
+
`aegis/constitution.yaml`, which is a loud diff in review rather than a flag buried
|
|
462
|
+
in a CI invocation. `tests/test_governance.py` asserts every clause can
|
|
463
|
+
actually fire — a clause that cannot fail is decoration.
|
|
464
|
+
|
|
465
|
+
```
|
|
466
|
+
aegis ratify --policy policies/base.yaml
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Loophole hunting
|
|
470
|
+
|
|
471
|
+
The conformance suite answers *"do the rules I wrote still work?"*. The hunter
|
|
472
|
+
answers *"what gets through that I never thought to test?"* — three techniques:
|
|
473
|
+
|
|
474
|
+
1. **Static** — structural analysis of policy and registry *together*: rules
|
|
475
|
+
that cannot fire, exits nobody screens, delegation shapes that recombine
|
|
476
|
+
authority, unanchored or wildcard-bearing patterns.
|
|
477
|
+
2. **Probe** — pushes a corpus of known-dangerous payloads (traversal,
|
|
478
|
+
SSRF, metadata endpoints, host-suffix confusion, file-reading SELECTs,
|
|
479
|
+
encoded PII) through the real guard chain in decision-only mode. Anything
|
|
480
|
+
ALLOWED is reported with the exact witness string.
|
|
481
|
+
3. **Metamorphic** — takes every step the suite expects to be DENIED, mutates
|
|
482
|
+
the arguments in meaning-preserving ways (case, percent-encoding,
|
|
483
|
+
zero-width joiners, SQL comments, null bytes, doubled separators) and
|
|
484
|
+
re-runs. A mutation that flips DENY to ALLOW is a bypass.
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
aegis audit --policy policies/base.yaml --fail-on high
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**On the first run against the policy shipped in this repo it found 23 holes**
|
|
491
|
+
— 7 critical, 9 high — including:
|
|
492
|
+
|
|
493
|
+
- `SELECT pg_read_file('/etc/passwd')` and `SELECT … INTO OUTFILE` passed the
|
|
494
|
+
read-only SQL guard, because "starts with SELECT and contains no DROP" does
|
|
495
|
+
not mean "cannot write or read files"
|
|
496
|
+
- `SELECT dblink_exec(…)` opened an outbound connection from inside a
|
|
497
|
+
"read-only" tool
|
|
498
|
+
- `/workspace/%2e%2e/etc/passwd` and a null-byte path defeated the traversal check
|
|
499
|
+
- every PII pattern fell to zero-width joiners, homoglyph separators and base64
|
|
500
|
+
- taint does not cross agents, so a reader child's output can reach a sink sibling
|
|
501
|
+
|
|
502
|
+
Fixes: Unicode normalisation (NFKC + invisible-character stripping +
|
|
503
|
+
confusables) and base64 decoding in the scanner, a much broader SQL denylist,
|
|
504
|
+
and encoded-traversal patterns on paths. **23 → 6.** The remaining six are
|
|
505
|
+
architectural, and they live in `loopholes.baseline.yaml` with a written reason
|
|
506
|
+
and an owner rather than being quietly dropped.
|
|
507
|
+
|
|
508
|
+
### The regression property
|
|
509
|
+
|
|
510
|
+
Findings carry a stable fingerprint. Accepted ones sit in the baseline; anything
|
|
511
|
+
new at high or above fails CI. So **the known-hole set can shrink silently but
|
|
512
|
+
never grow silently** — and a baseline entry whose finding has disappeared also
|
|
513
|
+
fails, so stale acceptances can't hide a hole that was already closed.
|
|
514
|
+
|
|
515
|
+
The three checks catch different directions of failure and none subsumes
|
|
516
|
+
another:
|
|
517
|
+
|
|
518
|
+
- `ratify` — is the policy *structurally* sound? (a constraint exists)
|
|
519
|
+
- `audit` — is the policy *substantively* sound? (the constraint holds)
|
|
520
|
+
- `drift` — is the policy moving in the *wrong direction*? (it got weaker)
|
|
521
|
+
|
|
522
|
+
A weakened `fs.read` prefix still ratifies, still passes every scenario, and is
|
|
523
|
+
caught by `audit` and `drift`. That is the point of having all three.
|
|
524
|
+
|
|
525
|
+
## Prior art
|
|
526
|
+
|
|
527
|
+
This overlaps with real work; see the chat discussion for the comparison. In
|
|
528
|
+
short: NeMo Guardrails, Guardrails AI and Fiddler sit at the content layer;
|
|
529
|
+
OPA/Rego, Cedar, Oso, Cerbos and OpenFGA are mature policy engines but are not
|
|
530
|
+
tool-call-shaped; AgentSpec (ICSE '26) is the closest academic relative for
|
|
531
|
+
runtime enforcement. The part that is genuinely thin in all of them is the
|
|
532
|
+
*regression* half — adversarial conformance, privilege-drift detection and
|
|
533
|
+
automated loophole discovery. If you adopt an existing engine, port
|
|
534
|
+
`aegis/conformance/` onto it rather than rebuilding it.
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
# Auditing somebody else's agents
|
|
539
|
+
|
|
540
|
+
Everything above points at policies you wrote. `aegis/adapters/mcp.py` points
|
|
541
|
+
the same machinery at an MCP server you did not write, which is what makes this
|
|
542
|
+
usable as a service rather than a library.
|
|
543
|
+
|
|
544
|
+
```
|
|
545
|
+
aegis mcp --manifest server-manifest.json \
|
|
546
|
+
--out audit-out --client "Acme"
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Accepts a `tools/list` response, a `claude_desktop_config.json`, or a bundle of
|
|
550
|
+
several servers. Or skip the export and point it at the live server:
|
|
551
|
+
|
|
552
|
+
```
|
|
553
|
+
aegis mcp --server https://mcp.example.com/mcp --bearer-env MCP_TOKEN --out audit-out
|
|
554
|
+
aegis mcp --server-cmd "npx -y @modelcontextprotocol/server-filesystem /tmp" --out audit-out
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
The live client performs the real MCP handshake (Streamable HTTP with JSON or
|
|
558
|
+
SSE replies, or stdio) and follows `tools/list` pagination. It is structurally
|
|
559
|
+
incapable of calling a tool: any method other than `initialize`,
|
|
560
|
+
`notifications/initialized` and `tools/list` raises before reaching the wire.
|
|
561
|
+
What it fetched is saved to `audit-out/manifest.json` (env values redacted) so
|
|
562
|
+
the audit can be replayed and baselined offline with identical fingerprints.
|
|
563
|
+
|
|
564
|
+
Auth becomes evidence rather than a guess: a remote server that answers
|
|
565
|
+
`tools/list` with no credential is reported as a **critical** with a witness,
|
|
566
|
+
and a server that demands and receives a token is not flagged at all.
|
|
567
|
+
`--server-cmd` runs the given command on your machine; only use it on servers
|
|
568
|
+
you would run anyway. Three moves:
|
|
569
|
+
|
|
570
|
+
1. **Ingest** — normalise the manifest.
|
|
571
|
+
2. **Synthesise** — derive a policy from the declared JSON Schemas. This is
|
|
572
|
+
what the server *currently* permits. Every schema field with no `enum`,
|
|
573
|
+
`pattern` or `maxLength` becomes a door the probe engine walks through.
|
|
574
|
+
3. **Harden** — emit a tightened policy the customer can adopt.
|
|
575
|
+
|
|
576
|
+
Nothing in the audit executes a tool. The registry is built from inert doubles
|
|
577
|
+
that raise if called.
|
|
578
|
+
|
|
579
|
+
## MCP-specific checks
|
|
580
|
+
|
|
581
|
+
### Effects are inferred from the surface, not the name
|
|
582
|
+
|
|
583
|
+
A tool called `sync_workspace` tells you nothing; a `path` argument beside a
|
|
584
|
+
`content` argument tells you it writes files. Effects come from the schema
|
|
585
|
+
shape first (a `url` plus a body is egress; a `command` argument is compute; a
|
|
586
|
+
`confirm` boolean implies something worth braking), then the declared MCP
|
|
587
|
+
annotations, and only then tool-name keywords. `EffectInference.sources`
|
|
588
|
+
records which signal decided, so a severity can be traced back.
|
|
589
|
+
|
|
590
|
+
Annotations (`readOnlyHint`, `destructiveHint`, `openWorldHint`) are claims
|
|
591
|
+
made by the party being audited, so they may only ever *widen* the inferred
|
|
592
|
+
effects. Honouring a narrowing claim would let any server opt out of scrutiny
|
|
593
|
+
by asserting its own innocence — and clients that auto-approve tools marked
|
|
594
|
+
read-only would run it unattended. A read-only claim on a surface that
|
|
595
|
+
demonstrably mutates is reported as `annotation_contradicts_surface`, with
|
|
596
|
+
both signals in the witness.
|
|
597
|
+
|
|
598
|
+
Beyond that: **omnibus tools** (one handler taking a free-form
|
|
599
|
+
string and dispatching many operations), **tool-name shadowing** across
|
|
600
|
+
servers, **description injection** (model-directed imperatives and invisible
|
|
601
|
+
Unicode in tool descriptions, which the model reads verbatim and a reviewer
|
|
602
|
+
does not), **irreversible tools with no `confirm`/`dry_run`**, **plaintext
|
|
603
|
+
credentials** in client config, and **unauthenticated remote transports**.
|
|
604
|
+
|
|
605
|
+
### On false positives
|
|
606
|
+
|
|
607
|
+
Published YARA-based MCP scanners run around a **78% false-positive rate**,
|
|
608
|
+
because they flag tool descriptions that merely describe normal functionality.
|
|
609
|
+
Two design rules here:
|
|
610
|
+
|
|
611
|
+
- Every check either produces a **reproducing witness** or requires **two
|
|
612
|
+
independent signals** before firing. `test_omnibus_requires_two_signals`
|
|
613
|
+
asserts a well-scoped tool with one free-form argument is *not* flagged.
|
|
614
|
+
- Findings are **consolidated**. Twelve traversal payloads across three
|
|
615
|
+
filesystem tools is not twelve problems, it is three unconstrained path
|
|
616
|
+
arguments. On the sample manifest this takes **101 raw findings down to 32**.
|
|
617
|
+
It also keeps fingerprints stable as the payload corpus grows, which matters
|
|
618
|
+
because the baseline file is keyed on them.
|
|
619
|
+
|
|
620
|
+
## What a run looks like
|
|
621
|
+
|
|
622
|
+
On `examples/sample_mcp_manifest.json` (3 servers, 8 tools):
|
|
623
|
+
|
|
624
|
+
```
|
|
625
|
+
32 findings — 8 critical, 16 high, 8 medium
|
|
626
|
+
critical description_injection helpdesk.escalate "you must always"
|
|
627
|
+
critical payload_admitted analytics.query.sql SELECT pg_read_file('/etc/passwd')
|
|
628
|
+
critical payload_admitted filesystem.*.path /workspace/sub/../../etc/passwd
|
|
629
|
+
high tool_shadowing read_file filesystem, helpdesk
|
|
630
|
+
high omnibus_tool analytics.query.sql "Accepts raw SQL"
|
|
631
|
+
high irreversible_no_brake filesystem.delete_file
|
|
632
|
+
high annotation_contradicts_surface filesystem.sync_workspace readOnlyHint vs schema:path+content
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Apply the generated `hardened-policy.yaml` and re-probe: **0 critical, 1 high**
|
|
636
|
+
— and the residual is `taint_laundering`, which is architectural and cannot be
|
|
637
|
+
fixed by a policy file. `test_hardened_policy_closes_the_critical_findings`
|
|
638
|
+
asserts exactly this, so the claim the report makes to a customer is itself
|
|
639
|
+
under regression test.
|
|
640
|
+
|
|
641
|
+
Writing this found a bug in the hardener: its generated SQL denylist covered
|
|
642
|
+
`pg_read_file` and `dblink` but not `pg_shadow`, so `SeLeCt 1 FROM pg_shadow`
|
|
643
|
+
survived hardening.
|
|
644
|
+
|
|
645
|
+
## Deliverables
|
|
646
|
+
|
|
647
|
+
`audit-out/audit-report.md` — severity summary, surface inventory, every
|
|
648
|
+
finding with where, what, a reproducing input and a fix, then a prioritised
|
|
649
|
+
top-five.
|
|
650
|
+
|
|
651
|
+
`audit-out/audit-report.html` — the same content as one self-contained file:
|
|
652
|
+
no scripts, no remote fonts, no build step. It opens from a mail attachment or
|
|
653
|
+
a shared drive, which is what the person approving the work actually does.
|
|
654
|
+
Every value in it is escaped and the page declares a restrictive CSP, because a
|
|
655
|
+
witness string is an attacker-shaped input by construction and the reviewer
|
|
656
|
+
must be able to read the attack without running it.
|
|
657
|
+
|
|
658
|
+
`audit-out/audit.json` and `audit-out/audit.sarif` — the machine-readable pair.
|
|
659
|
+
|
|
660
|
+
`audit-out/hardened-policy.yaml` — an adoptable policy. Placeholders are
|
|
661
|
+
shouted in capitals on purpose; a generated policy that looks finished is more
|
|
662
|
+
dangerous than one that obviously needs a human.
|
|
663
|
+
|
|
664
|
+
Findings carry stable fingerprints, so `--baseline` plus `--fail-on high` turns
|
|
665
|
+
a one-off audit into a CI gate the customer keeps running after you leave.
|