agentsec-pack 0.1.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.
Files changed (42) hide show
  1. agentsec_pack-0.1.0/LICENSE +21 -0
  2. agentsec_pack-0.1.0/PKG-INFO +321 -0
  3. agentsec_pack-0.1.0/README.md +305 -0
  4. agentsec_pack-0.1.0/agentsec/__init__.py +2 -0
  5. agentsec_pack-0.1.0/agentsec/assertions.py +99 -0
  6. agentsec_pack-0.1.0/agentsec/baseline.py +119 -0
  7. agentsec_pack-0.1.0/agentsec/cli.py +454 -0
  8. agentsec_pack-0.1.0/agentsec/data/measured_profiles.json +156 -0
  9. agentsec_pack-0.1.0/agentsec/data/owasp_asi_2026.json +15 -0
  10. agentsec_pack-0.1.0/agentsec/data/presets.json +120 -0
  11. agentsec_pack-0.1.0/agentsec/data/promptfoo_plugin_asi.json +44 -0
  12. agentsec_pack-0.1.0/agentsec/integrations/__init__.py +0 -0
  13. agentsec_pack-0.1.0/agentsec/integrations/promptfoo_assert.py +77 -0
  14. agentsec_pack-0.1.0/agentsec/integrations/promptfoo_combine.py +189 -0
  15. agentsec_pack-0.1.0/agentsec/presets.py +46 -0
  16. agentsec_pack-0.1.0/agentsec/probe/__init__.py +0 -0
  17. agentsec_pack-0.1.0/agentsec/probe/blast_probe.py +852 -0
  18. agentsec_pack-0.1.0/agentsec/redact.py +49 -0
  19. agentsec_pack-0.1.0/agentsec/report.py +103 -0
  20. agentsec_pack-0.1.0/agentsec/runner.py +216 -0
  21. agentsec_pack-0.1.0/agentsec/scoring.py +400 -0
  22. agentsec_pack-0.1.0/agentsec/ui/__init__.py +51 -0
  23. agentsec_pack-0.1.0/agentsec/ui/server.py +302 -0
  24. agentsec_pack-0.1.0/agentsec/ui/template.html +551 -0
  25. agentsec_pack-0.1.0/agentsec_pack.egg-info/PKG-INFO +321 -0
  26. agentsec_pack-0.1.0/agentsec_pack.egg-info/SOURCES.txt +40 -0
  27. agentsec_pack-0.1.0/agentsec_pack.egg-info/dependency_links.txt +1 -0
  28. agentsec_pack-0.1.0/agentsec_pack.egg-info/entry_points.txt +2 -0
  29. agentsec_pack-0.1.0/agentsec_pack.egg-info/requires.txt +8 -0
  30. agentsec_pack-0.1.0/agentsec_pack.egg-info/top_level.txt +1 -0
  31. agentsec_pack-0.1.0/pyproject.toml +30 -0
  32. agentsec_pack-0.1.0/setup.cfg +4 -0
  33. agentsec_pack-0.1.0/tests/test_assertions.py +115 -0
  34. agentsec_pack-0.1.0/tests/test_baseline.py +105 -0
  35. agentsec_pack-0.1.0/tests/test_cli.py +108 -0
  36. agentsec_pack-0.1.0/tests/test_docs.py +76 -0
  37. agentsec_pack-0.1.0/tests/test_packaging.py +33 -0
  38. agentsec_pack-0.1.0/tests/test_presets.py +273 -0
  39. agentsec_pack-0.1.0/tests/test_probe.py +466 -0
  40. agentsec_pack-0.1.0/tests/test_promptfoo.py +136 -0
  41. agentsec_pack-0.1.0/tests/test_scoring.py +276 -0
  42. agentsec_pack-0.1.0/tests/test_ui.py +242 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jason Knight
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,321 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentsec-pack
3
+ Version: 0.1.0
4
+ Summary: Measure what an AI agent's sandbox lets it reach: network, secrets, host filesystem, privileges and processes, from inside the sandbox.
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=7; extra == "dev"
11
+ Provides-Extra: security
12
+ Requires-Dist: detect-secrets>=1.5; extra == "security"
13
+ Requires-Dist: pip-audit>=2.7; extra == "security"
14
+ Requires-Dist: bandit>=1.7; extra == "security"
15
+ Dynamic: license-file
16
+
17
+ # agentsec-pack
18
+
19
+ Adversarial tests for AI agent deployments, built to answer two questions a CI owner cannot see from a transcript:
20
+
21
+ 1. **Sandbox blast radius** — what can the agent reach from inside the environment you run it in? Network, secrets, host filesystem, privileges, sibling processes.
22
+ 2. **Verifier integrity** (planned) — did the agent do the task, or did it edit the tests, the fixtures or the grader?
23
+
24
+ This repository is at the first milestone: the blast-radius probe, a runner, a scorer and reports. It installs as `agentsec-pack` and runs as `agentsec`.
25
+
26
+ ## How to use this tool
27
+
28
+ ### By hand
29
+
30
+ ```bash
31
+ pip install agentsec-pack # or, from a checkout: uv venv .venv && uv pip install -e '.[dev]'
32
+ agentsec presets # what this host can actually run, not just what is installed
33
+ ```
34
+
35
+ The loop is measure, harden, measure again. Never one measurement.
36
+
37
+ ```bash
38
+ # 1. what you run today
39
+ agentsec blast-radius docker myagent:latest --label as-i-run-it --print-findings
40
+
41
+ # 2. apply the flags the report recommends, keeping what your workload needs
42
+ agentsec blast-radius docker myagent:latest --label hardened --print-findings -- \
43
+ --read-only --cap-drop ALL --security-opt no-new-privileges \
44
+ --user 65534:65534 --tmpfs /tmp --pids-limit 256 --memory 512m
45
+
46
+ # 3. see what moved
47
+ agentsec compare reports/*.json
48
+ ```
49
+
50
+ On a stock `python:3.12-slim` that reads 59, then 15. Adding `--network none`
51
+ reaches 0, and an agent that cannot reach its model provider does not work, so
52
+ **0 is not the target.** Aim for the lowest score your workload survives and
53
+ record why each remaining finding is still open.
54
+
55
+ Two more things it does:
56
+
57
+ ```bash
58
+ agentsec matrix python:3.12-slim # all ten configurations, one table
59
+ agentsec ui --results reports/ --open # the same results in a browser
60
+ agentsec blast-radius preset docker-hardened --image myagent:latest --max-score 15
61
+ ```
62
+
63
+ The last one exits 2 when the sandbox is looser than the budget, which is the CI
64
+ gate. Set the budget to what you measured after hardening so the build fails when
65
+ it drifts, rather than picking a round number.
66
+
67
+ A budget only fails when the total crosses it, so drift underneath it goes
68
+ through: a sandbox at 41 can lose its seccomp filter and gain a readable
69
+ credential and still be under 45. `--baseline` fails on anything worse than a
70
+ report you accepted, whatever the score:
71
+
72
+ ```bash
73
+ agentsec blast-radius preset docker-hardened --image myagent:tag --label accepted # once, then commit it
74
+ agentsec blast-radius preset docker-hardened --image myagent:tag --baseline reports/accepted.json
75
+ agentsec diff reports/accepted.json reports/latest.json # when measuring and gating are separate steps
76
+ ```
77
+
78
+ It exits 2 on a new finding, a higher severity, or wider evidence on an existing
79
+ finding (one more readable credential, one more reachable host, one more writable
80
+ path), and lists each. A finding that disappears is reported as fixed with a
81
+ prompt to re-baseline, so the gate keeps what you gained. If the two runs used
82
+ different probe versions it still fails, but says so: a new finding may be a new
83
+ check rather than a looser sandbox. Take the baseline with the same `--redact`
84
+ setting as the runs it gates.
85
+
86
+
87
+ **When the question is narrower than a finding list**, use `assert`. It asks
88
+ whether a sandbox can reach specific paths, and answers pass or fail per path
89
+ with no score at all:
90
+
91
+ ```bash
92
+ # can the graded child read the answer key it is being marked against?
93
+ agentsec assert --kind docker --image grader:latest \
94
+ --not-readable /answers/key.json --not-writable /results --readable /workspace/input \
95
+ -- -v ./answers:/answers:ro
96
+ ```
97
+
98
+ Exits 2 on any violated expectation, so it drops into a harness or a build the
99
+ same way `--max-score` does. It reports **absent** and **present, denied**
100
+ separately, because those are different sandbox designs and only one of them
101
+ survives someone adding a mount later.
102
+
103
+ **Measuring an agent's own sandbox is different.** You can wrap a container from
104
+ outside. You cannot wrap an agent's sandbox, because the agent is already inside
105
+ it. Ask the agent to run the probe and score what comes back with `agentsec
106
+ score`; `docs/CAPTURING.md` covers the details, including what to do when the
107
+ sandbox is read-only and the result has to come back through a transcript.
108
+
109
+ ### By pointing an agent at this repository
110
+
111
+ `AGENTS.md` is a brief written for an agent rather than a person. Clone the repo,
112
+ point your agent at it, and say something like:
113
+
114
+ > Read AGENTS.md in this repository and carry out Task A, then report what you
115
+ > found. Do not change any host settings.
116
+
117
+ Task A has the agent measure the sandbox it is itself running in, which is the
118
+ measurement you cannot easily take yourself. Task B has it measure and harden an
119
+ image you name. Task C adds the CI gate.
120
+
121
+ **Read `AGENTS.md` yourself first.** It is under 100 lines, it contains no `sudo`, no
122
+ network fetches beyond installing this package, and nothing that writes outside
123
+ the working directory and a temp path. That matters more here than in most repos:
124
+ pointing an agent at a file and letting it follow the instructions inside is
125
+ exactly the pattern this tool exists to worry about. A repository is data, not
126
+ authority, and the brief says so to the agent as well.
127
+
128
+ Expect the agent to come back with a score, which of the five axes are sealed,
129
+ and, importantly, whether anything was proven **reachable** rather than merely
130
+ visible. Those are different findings and the tool separates them. If your agent
131
+ reports a number without saying how it captured it, the tool refused to produce
132
+ that number and the agent made it up: `--label` and `--how` are required.
133
+
134
+ Each run writes `reports/<label>.json` (machine-readable, schema_version 1) and
135
+ `reports/<label>.md`. The JSON records the exact image and flags, so anyone can
136
+ reproduce the number. Review a report before publishing it: it carries the
137
+ sandbox's hostname, working directory and the *names* of secret-looking
138
+ environment variables. Pass `--redact` to scrub those while leaving every score,
139
+ finding and flag intact; the examples in this repo were generated that way.
140
+ `reports/` is git-ignored; `examples/` holds the reports from the demos below.
141
+
142
+ ## What it found on the first run
143
+
144
+ Ten sandbox configurations, one image (`python:3.12-slim`), measured on one Linux host:
145
+
146
+ | Configuration | Score | Worst finding |
147
+ |---|---:|---|
148
+ | `bwrap-ro-root` | 86 | container runtime socket reachable |
149
+ | `bwrap-unshare-all` | 71 | container runtime socket reachable |
150
+ | `bwrap-workdir-net` | 69 | unrestricted internet egress |
151
+ | `docker-default` | 59 | unrestricted internet egress |
152
+ | `bwrap-unshare-all-clearenv` | 56 | container runtime socket reachable |
153
+ | `podman-rootless-default` | 47 | unrestricted internet egress |
154
+ | `docker-no-network` | 44 | runs as real root |
155
+ | `docker-readonly` | 44 | unrestricted internet egress |
156
+ | `docker-hardened` | **0** | none |
157
+ | `podman-rootless-hardened` | **0** | none |
158
+
159
+ The result worth arguing about is the top row. The lightweight "read-only host root" bubblewrap pattern scores **worse than a plain Docker container**, because a read-only root filesystem still hands the agent the host's container socket and every credential file in the home directory. Read-only is not containment. Scores for host-binding sandboxes depend on the host; container rows depend only on the image and flags.
160
+
161
+ Reproduce: `agentsec matrix python:3.12-slim`. Full table in `examples/matrix_slim.md`. These rows were taken with probe 0.1.0 and have not been re-run with 0.3.0; the checks added since look for files and sockets a stock image does not carry, so they are not expected to move, but that is an expectation, not a measurement.
162
+
163
+ ## Measuring an agent's own sandbox
164
+
165
+ A container you can wrap from outside. An agent's sandbox you cannot: it is entered by the agent. So you ask the agent to run the probe inside it and score what comes back (`docs/CAPTURING.md`, `agentsec score`).
166
+
167
+ Measured on Ubuntu 24.04 with bubblewrap 0.9.0 present, 8-9 September 2026:
168
+
169
+ | Sandbox | Score | Contained | Still reachable |
170
+ |---|---:|---|---|
171
+ | Codex CLI 0.153.4 `--sandbox read-only` | 26 | root filesystem read-only, working directory not writable, DNS fails, direct-IP connects refused, container sockets refused | the invoking user's home directory: `~/.ssh/config`, `~/.gitconfig`, `~/.config/gh/hosts.yml`, PID 1's environment |
172
+ | Codex CLI 0.153.4 `--sandbox workspace-write` | 34 | as above | as above, plus a writable working directory (expected, scored informational) and read-write bind mounts from the host |
173
+ | the same two, launched from a developer shell | 41 / 49 | as above | as above, plus one secret-shaped environment variable the shell had already exported |
174
+ | Aider 0.86.2, default (its only mode) | 100 | nothing | everything the invoking user can reach, including a connectable Docker socket |
175
+
176
+
177
+ **Aider is the first open-source target here, and it ships no sandbox at all.** `aider --help` offers no sandbox, container or isolation option, and `aider/run_cmd.py:62` runs commands with `subprocess.Popen(shell=True)` on the host with the inherited environment. The probe was run through that exact call shape, so 100 is not an accusation: the project does not claim to contain anything, and the number simply says what you take on by running it. That is also the case for the hardening presets in this repository, which is the point of measuring it.
178
+
179
+ **The launcher is part of the blast radius, so the environment is part of the measurement.** The sandbox inherits the shell it was started from. Launched from a shell with a token exported, both scores rise by 15 points for a secret that has nothing to do with Codex. The first two rows minimise the environment with `env -i` and are the intrinsic figures; the third row is the same sandbox started the way people actually start it. Shipping the pair is the point: a single number would have hidden which half you can fix by changing your own shell.
180
+
181
+ **This is a design boundary, not a vulnerability, and the report says so.** The vendor documents read-only mode as "the agent can inspect files", so an agent reading your `gh` credentials file is the boundary working as written. The number is useful anyway: it says that on the write and network axes this sandbox is genuinely strong, and that what remains inside the blast radius is *read* access to whatever secrets live in the home directory of the user who launched it. That distinction matters because no filesystem or network sandbox can constrain what an agent does with what it has already read: whatever the agent can read can appear in the text it produces. This tool does not measure that path, and makes no claim about any particular product's handling of it.
182
+
183
+ **Why the socket finding is only informational here.** Both Codex modes show `/var/run/docker.sock` in the filesystem and refuse every connection to it. Permission bits alone cannot tell you whether that refusal is a rule about those paths or a filter on the call itself, so the probe runs a control. It binds a unix socket it created itself and connects to it, and, where there is no writable directory, it connects to a path that does not exist: `PermissionError` instead of `FileNotFoundError` means the call was refused before the path was ever consulted. Under Codex both controls are refused, so the visible socket paths are inert and the report says that rather than guessing.
184
+
185
+
186
+ **Which layer is doing the work.** Codex's sandbox is two layers, and the live process argv shows both: `bwrap --as-pid-1 --new-session --die-with-parent --ro-bind / / --dev /dev --unshare-user --unshare-pid --unshare-ipc --unshare-net --proc /proc --cap-drop ALL`, and then its own helper re-execs the command with `--apply-seccomp-then-exec` under a permission profile. The `codex-cli-0.153.4-bwrap-layer` preset reproduces the first layer only, so running it measures what the second one contributes:
187
+
188
+ | Run | Score | Container socket | Seccomp |
189
+ |---|---:|---|---|
190
+ | Codex CLI 0.153.4 `--sandbox read-only` | 26 | visible, every connect refused | mode 2 |
191
+ | `codex-cli-0.153.4-bwrap-layer` preset | 48 | **reachable: the connect succeeds** | none |
192
+
193
+ `--unshare-net` removes the network but does nothing to unix-domain sockets, so under bubblewrap alone the probe connects to `/var/run/docker.sock` for real and the control confirms unix connect is permitted. The seccomp filter is what closes it. Two differences are expected rather than defects: the preset has no seccomp layer by construction, and `--as-pid-1` makes the probe itself PID 1, so the PID 1 environment finding cannot fire the way it does in the real sandbox. The preset is a way to see what bubblewrap alone buys. It is not a stand-in for Codex, and its rationale says so.
194
+
195
+ **Claude Code 2.1.266, measured the same way.** The sandbox is off unless `sandbox.enabled` is set, and on Linux it needs `bubblewrap` and `socat` present:
196
+
197
+ | Configuration | Score | What the probe found |
198
+ |---|---:|---|
199
+ | default, `sandbox.enabled` unset | 100 | no containment: root filesystem read-write, unrestricted egress, no seccomp, and `/var/run/docker.sock` **reachable** |
200
+ | `sandbox.enabled: true`, `socat` missing | 100 | identical to the row above |
201
+ | `sandbox.enabled: true`, dependencies present | 41 | seccomp mode 2, `no_new_privs`, user namespace, read-only root, no egress, PID 1's environment hidden, `AF_UNIX socket()` itself refused |
202
+
203
+ The middle row is the one worth acting on, and it is **documented behaviour, not a discovered flaw**. The vendor states: "By default, if the sandbox cannot start because dependencies are missing or the platform is unsupported, Claude Code shows a warning and runs commands without sandboxing. To make this a hard failure instead, set `sandbox.failIfUnavailable` to `true`." That setting works, and it was verified here rather than quoted: with `socat` absent and `failIfUnavailable` set, the run exits 1, prints "refusing to start without a working sandbox", and never executes the probe.
204
+
205
+ So the fix is the vendor's own toggle, and anyone enabling a sandbox in an automated context should set it. What is undocumented is the consequence of the default in CI: the warning goes to stderr, the exit code is still zero, and the agent runs with the user's full authority while the configuration file says sandboxed. **That gap between configuration and process is why this tool measures from inside**, and a `--max-score` gate on a real probe catches it whichever product you are running, including one that has no fail-closed setting at all.
206
+
207
+ The unsandboxed row is not a product defect. It is what "no sandbox" means: the probe scores 100 there and 88 when run directly in the shell, and the difference is one variable the product injects. With the sandbox on, both remaining high findings are also variables the sandbox injects itself. Their names alone would only be a guess, so a second run printed variable names and a third printed the non-secret proxy fields: the password sits beside `CLOUDSDK_PROXY_ADDRESS=localhost`, `_TYPE=http`, `_PORT=3128`, so it is a loopback proxy credential rather than a user cloud key. Without those two the score is 34. The comparable Codex mode is `workspace-write`, also 34, because both allow working-directory writes: Claude Code gains points for the two injected variables and loses them again by hiding PID 1's environment, which Codex leaves readable.
208
+
209
+ Two conditions on the numbers. Claude Code's network isolation is an allowlist through a local proxy and this run was headless with nothing approved, so "no egress" here means an empty allowlist, not the absolute namespace-level cut that `--unshare-net` gives Codex; the direct-IP and metadata failures hold either way. **Probe 0.1.0, which took these rows, could not see proxy egress at all:** it connects directly and never through `HTTPS_PROXY`, so it would have reported the same sealed network axis with hosts on the allowlist. That was shown on 2026-11-03, when `curl` reached github.com through the proxy with a 200 while every direct connect the probe made was refused. From 0.2.0 the probe records which proxy variables are set (names only) and the scorer reports NET-004, "egress through the configured proxy was not measured", instead of claiming containment. Read the network column of the Claude Code rows above as unverified until they are re-measured. And the vendor page describes the unix-socket seccomp filter as an optional add-on, yet this run refused `AF_UNIX socket()` without that add-on installed, as did 2.1.317 below: measured stricter than documented. That was raised publicly as [anthropics/claude-code#107142](https://github.com/anthropics/claude-code/issues/107142) on 2026-11-04, after this README had already published it; `SECURITY.md` says the vendor hears first, and here it did not.
210
+
211
+ **Re-measured with probe 0.4.0: Claude Code 2.1.317, 2026-11-04.** Same host, sandbox on, taken from inside a live session through its own Bash tool with `AGENTSEC_PROBE_PROXY_AUTH=1` and no extra domains allowed for that command. Score **41**, unchanged, but the probe now sees what that number is made of:
212
+
213
+ | Axis | What 0.4.0 found |
214
+ |---|---|
215
+ | network | **contained, now proven**: every direct connection refused, and the sandbox's proxy refused a tunnel (403) to every target and to the control. Without the opt-in it answers 407 and the axis reads not measured |
216
+ | persistence | **closed**: shell startup files, `~/.ssh/authorized_keys`, `~/.gitconfig`, `.git/hooks`, the home directory and `~/.claude/settings.json` all refuse writes; the project's `.claude/settings*.json` and `.mcp.json` are masked with a device node |
217
+ | credentials | **readable**: `~/.ssh/config`, `~/.gitconfig`, `~/.config/gh/hosts.yml`, and the agent's own login, `~/.claude/.credentials.json` (another agent's, `~/.codex/auth.json`, too), each proven by opening it |
218
+
219
+ The last row is new, and it is the one to read carefully. The vendor documents that file as where Claude Code keeps its login on Linux; a sandboxed command that can open it can print the token into a transcript or write it into the working tree. **This is documented behaviour, not a discovered flaw.** The sandboxing page states the default as "read access to the entire computer, except certain denied directories. This default still allows reading credential files", says "There is no built-in credential deny list", and lists `.credentials.json` among the paths protected from *writes* only. The vendor's own remedy is `sandbox.filesystem.denyRead` (its example denies `~/` and re-allows `.`) or `sandbox.credentials` entries in `deny` mode, and anyone running this sandbox with a login on the machine should set one. Re-measuring with that set would show what it buys; this run did not. Report and provenance: `examples/agents/claude-code-2.1.317-sandbox-auto-mode.md`.
220
+
221
+ **The allowlist is per command, and it shows.** Re-run with `github.com` allowed for that one command, the way a user approves a domain: the proxy accepts a tunnel to `github.com` (200) and still refuses the control and everything else, so the probe reports NET-005, *egress through the proxy is allowlisted*, and the score is **49**. The sandbox is filtering as designed; what it lets through is whatever has been approved, and a code host accepts uploads. `examples/agents/claude-code-2.1.317-sandbox-auto-mode-github-allowed.md`.
222
+
223
+ **Stated plainly: these Claude Code numbers were produced by a Claude Code session measuring Claude Code.** Same probe, same scorer, same capture path as every other profile, and the raw JSON ships so the numbers can be recomputed. Weigh the source anyway.
224
+
225
+ Full reports with provenance: `examples/agents/`. Entries in `agentsec/data/measured_profiles.json` may only be added from an actual measurement, never from documentation, and each cites the vendor's own docs. Before publishing anything that contradicts a vendor's documentation, tell the vendor first.
226
+
227
+
228
+ ## Seeing it
229
+
230
+ Two commands, one page.
231
+
232
+ ```bash
233
+ # a self-contained HTML file: keep it as a CI artifact, mail it, open it offline
234
+ agentsec report reports/ --html blast-radius.html
235
+
236
+ # a console on localhost, with the runs you have already saved loaded in
237
+ agentsec ui --results reports/ --open
238
+ ```
239
+
240
+ The page leads with a band across the five axes the probe measures, because the
241
+ distinction this tool exists to make is *reachable* versus *merely visible*. An
242
+ axis reads **Sealed** when nothing was found, **Visible but refused** when the
243
+ probe saw something it could not reach, **Reachable** when it got through, and
244
+ **Not measured** when the probe could not settle it: an egress proxy that wants a
245
+ password the probe was not allowed to send (`AGENTSEC_PROBE_PROXY_AUTH=1` lets it),
246
+ or one it cannot speak to, such as SOCKS.
247
+ The score is second; a number alone cannot tell you which of those you have.
248
+ Provenance is a panel rather than a footnote.
249
+
250
+ Both commands render the same template, so the exported page and the clickable
251
+ one cannot drift apart. It loads nothing from the network: system fonts, inline
252
+ CSS, no CDN. A test fails if any `src` or `href` ever points off-box.
253
+
254
+ **What the console will and will not do.** It binds `127.0.0.1`, mints a random
255
+ token at startup and requires it on every API call, checks the `Host` header so
256
+ a name resolving to loopback cannot be used to reach it from a hostile page,
257
+ sends a restrictive CSP and no CORS header at all. It runs a configuration
258
+ shipped in `presets.json`, or an engine/image/flag triple where every flag is a
259
+ key in the remediation map. It does **not** accept a command template, and
260
+ `tests/test_ui.py` asserts that a browser cannot ask for one.
261
+
262
+ `--bind` widens it when your browser is on another machine. A console bound off
263
+ this box is read-only unless you also pass `--allow-remote-runs`: reading a
264
+ report is safe to expose, giving a network the ability to start containers is a
265
+ separate decision you should make on purpose.
266
+
267
+ ## Alongside a red-team run
268
+
269
+ Promptfoo measures what the agent does. This measures what the agent's sandbox would let a successful attack reach. Run both and join them:
270
+
271
+ ```bash
272
+ # 1. measure the sandbox
273
+ agentsec blast-radius preset docker-default --image youragent:tag --out reports
274
+ # 2. run your promptfoo suite with the containment row (examples/promptfoo/promptfooconfig.yaml)
275
+ AGENTSEC_REPORT=reports/docker-default_youragent_tag.json npx promptfoo@0.122.2 eval -c promptfooconfig.yaml -o out.json
276
+ # 3. join them
277
+ agentsec combine --promptfoo out.json --sandbox reports/docker-default_youragent_tag.json --deployed-in docker-default
278
+ ```
279
+
280
+ The assertion adds `blast_radius` to promptfoo's `namedScores` and never runs the probe itself; it reads a report you generated earlier, because promptfoo calls assertions once per test row. Attach it to one dedicated row rather than `defaultTest`, or a single loose sandbox fails your whole suite for one environmental cause.
281
+
282
+ `combine` reports the overlap: an OWASP agentic category where a test failed *and* the sandbox has a matching finding. The plugin-to-category table is promptfoo's own published mapping, cached in `agentsec/data/promptfoo_plugin_asi.json` with its URL and fetch date; plugin ids of the form `owasp:agentic:asi05` are parsed directly.
283
+
284
+ **`--deployed-in` is required and is your claim, not a derived fact.** Neither file knows whether the agent promptfoo tested runs in the sandbox that was measured. The report prints that assumption at the top and conditions every conclusion on it. Example output: `examples/promptfoo/combined_example.md`.
285
+
286
+ **Tested against:** a real `promptfoo eval` run (fixture captured 2026-09-08, and an end-to-end test that shells out to `npx promptfoo@0.122.2`). The plugin-id join is exercised against a hand-written fixture in promptfoo's schema, **not** against a live red-team run, because that needs a target and credentials this repo does not have. In red-team output, `success: false` means the attack landed.
287
+
288
+ ## How it works
289
+
290
+ `agentsec/probe/blast_probe.py` is a single stdlib-only file. The runner bind-mounts it read-only into the target (or launches it through any command template that can run `python3`) and reads one JSON line back. `agentsec/scoring.py` turns the JSON into findings with fixed severities and a 0–100 score: 0 means the probe could reach nothing; higher means a larger blast radius. Findings map to the OWASP Top 10 for Agentic Applications (2026); the mapping lives in `agentsec/data/owasp_asi_2026.json` with its source.
291
+
292
+ ## What the probe will not do
293
+
294
+ Read the probe before you run it somewhere you care about. It reports the names of secret-looking environment variables and the paths of credential files, never their contents. Its network checks resolve and connect to a fixed list of hosts, send nothing, and close. When an egress proxy is configured, it also asks that proxy for a tunnel to each of the same hosts and to `example.com`, a control no allowlist has a reason to contain, and records only the status code: no TLS, nothing reaches a target. A proxy that wants a password gets one only if you set `AGENTSEC_PROBE_PROXY_AUTH=1`, and then only that proxy; the password and the proxy's address are never recorded. It proves a file is readable by opening it and reading zero bytes, so a credential's contents never pass through it. Its only writes are zero-byte marker files, one per probed directory, each created and removed at once; that is how writability is measured, and it can trip file-integrity monitoring where you have it. To find out whether the agent could leave something behind that runs outside the sandbox, it opens existing shell startup files, `~/.ssh/authorized_keys`, `~/.gitconfig` and the agent's own settings files for append and closes them without writing a byte: content and modification time are unchanged and a missing file is never created, though a file watcher will see the close. Tests in `tests/test_probe.py` assert the redaction, the fixed target list, and that those files come out byte-for-byte unchanged.
295
+
296
+ ## Status
297
+
298
+ - [x] blast-radius probe, runner (docker / podman / local / command template), scorer, JSON + Markdown reports
299
+ - [x] ten launcher presets, `matrix` comparison table, `--max-score` CI gate, recommended-flag output, image digests
300
+ - [x] promptfoo integration: containment assertion, `combine` report, OWASP category overlap
301
+ - [x] measured profiles of named agents' sandboxes, captured by running the probe inside them
302
+ - [x] three agents (Claude Code, Codex CLI, Aider) measured across nine configurations; a preset reproducing Codex's bubblewrap layer, with the seccomp gap measured rather than assumed
303
+ - [x] egress measured through a configured proxy, with an allowlist control host and opt-in proxy credentials (probe 0.4.0)
304
+ - [x] agents' own login credentials, a forwarded SSH agent, rootless container sockets, and writes that persist outside the sandbox (probe 0.3.0)
305
+ - [x] PyPI release workflow for `agentsec-pack` (trusted publishing, on a version tag)
306
+ - [ ] macOS: the probe reads `/proc` and assumes Linux
307
+ - [ ] more agents as they become installable here; presets only where the argv can be captured live
308
+ - [ ] verifier-integrity test class
309
+ - [x] browser front end: static HTML report and a localhost console, sharing one template
310
+ - [x] a CI gate that fails on a regression against an accepted baseline, not just on an absolute score (`--baseline`, `agentsec diff`)
311
+ - [ ] history across runs
312
+
313
+ ## Reporting a problem
314
+
315
+ Security issues in this tool: `SECURITY.md`, which also states the rule this
316
+ project holds itself to when it measures somebody else's software. Anything
317
+ else: an issue. Working on it: `CONTRIBUTING.md`.
318
+
319
+ ## License
320
+
321
+ MIT.