agentproof-scan 0.1.2__tar.gz → 0.1.4__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 (30) hide show
  1. {agentproof_scan-0.1.2/agentproof_scan.egg-info → agentproof_scan-0.1.4}/PKG-INFO +183 -20
  2. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/README.md +182 -19
  3. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/__init__.py +1 -1
  4. agentproof_scan-0.1.4/agentproof_scan/__main__.py +10 -0
  5. agentproof_scan-0.1.4/agentproof_scan/adapters/__init__.py +84 -0
  6. agentproof_scan-0.1.4/agentproof_scan/adapters/base.py +162 -0
  7. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/external_starter.py +28 -5
  8. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/generic_http.py +72 -22
  9. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/llm_starter.py +26 -4
  10. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/simple_chatbot.py +35 -11
  11. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/scan.py +392 -77
  12. agentproof_scan-0.1.4/agentproof_scan/victim_agent.py +82 -0
  13. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4/agentproof_scan.egg-info}/PKG-INFO +183 -20
  14. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan.egg-info/SOURCES.txt +1 -0
  15. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/pyproject.toml +1 -1
  16. agentproof_scan-0.1.2/agentproof_scan/adapters/__init__.py +0 -41
  17. agentproof_scan-0.1.2/agentproof_scan/adapters/base.py +0 -29
  18. agentproof_scan-0.1.2/agentproof_scan/victim_agent.py +0 -55
  19. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/LICENSE +0 -0
  20. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/MANIFEST.in +0 -0
  21. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/simple_chatbot_canary.py +0 -0
  22. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/simple_chatbot_defended_canary.py +0 -0
  23. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/simple_chatbot_hardened_canary.py +0 -0
  24. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/victim.py +0 -0
  25. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/fingerprint.py +0 -0
  26. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan.egg-info/dependency_links.txt +0 -0
  27. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan.egg-info/entry_points.txt +0 -0
  28. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan.egg-info/requires.txt +0 -0
  29. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan.egg-info/top_level.txt +0 -0
  30. {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentproof-scan
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: CLI security scanner: probes AI-agent HTTP endpoints for secret/prompt leakage, with CI-gating exit codes.
5
5
  Author: AgentProof
6
6
  License-Expression: Apache-2.0
@@ -52,6 +52,59 @@ you can read the code and reproduce every number yourself.
52
52
 
53
53
  ---
54
54
 
55
+ ## 📦 Prerequisites
56
+
57
+ If this is your first command-line tool, **the hard part is installing Python, not
58
+ installing this.** People get stuck there. That step isn't part of this tool, and it's
59
+ a normal place to get stuck.
60
+
61
+ **1. Check that you have Python 3.9 or newer.** One of these three works; which one
62
+ tells you how Python was installed on your machine:
63
+
64
+ ```
65
+ python --version
66
+ python3 --version
67
+ py -3 --version
68
+ ```
69
+
70
+ **2. Install.**
71
+
72
+ ```
73
+ pip install agentproof-scan
74
+ ```
75
+ `pip: command not found` → `python -m pip install agentproof-scan`. On Windows without
76
+ Python, `py -m pip install agentproof-scan` triggers the Python Install Manager to fetch
77
+ Python for you. [VERIFIED: `py -m ...` installed Python 3.14.6 on Windows, owner, 2026-07-11.]
78
+
79
+ **3. Put your key in a `.env` file, with an editor — not a shell command.**
80
+
81
+ Open a new file called `.env` in the folder you'll run from, and put one line in it:
82
+
83
+ ```
84
+ GEMINI_API_KEY=your-real-key-here
85
+ ```
86
+
87
+ Save it. Get a free key at <https://aistudio.google.com/apikey>. `.env` is auto-loaded from
88
+ the directory you run in and is gitignored — never commit it.
89
+
90
+ > **Why an editor and not `echo '...' > .env`?** A shell command puts your key on the
91
+ > command line, and the shell saves that line to its **history** in plaintext
92
+ > (`~/.bash_history`, PowerShell's `ConsoleHost_history.txt`). A tool that detects leaked
93
+ > credentials should not teach you to write one to a file nobody thinks to check. An editor
94
+ > avoids the history entirely. (If you do use the shell, this tool does **not** scan shell
95
+ > history — that's on you to clear.)
96
+
97
+ > **On `.env` encoding.** The loader reads `.env` written as UTF-8, UTF-8 with BOM, and
98
+ > **UTF-16LE with BOM** (what PowerShell 5.1's `>` produces — [VERIFIED on a real PowerShell
99
+ > 5.1 file, owner, 2026-07-11]), and strips stray quotes `cmd.exe` leaves around the *key
100
+ > name*. **Don't quote the value** — `KEY="abc"` keeps the quotes literally.
101
+
102
+ **4. If the command isn't found:** `agentproof-scan: command not found` →
103
+ `python -m agentproof_scan` (Windows: `py -m agentproof_scan`) — same tool, same flags,
104
+ works when the console script isn't on PATH. [VERIFIED on Windows, owner, 2026-07-11.]
105
+
106
+ ---
107
+
55
108
  ## 🚀 60-second Quick Start (no setup knowledge — just copy-paste)
56
109
 
57
110
  `agentproof-scan` ships with a built-in **victim demo**: an intentionally leaky agent
@@ -62,19 +115,38 @@ backed by Google Gemini. You only need a free Gemini key to try it.
62
115
  pip install agentproof-scan
63
116
 
64
117
  # 2. Get a FREE Gemini key → https://aistudio.google.com/apikey
65
- # save it to a .env file in the folder you run from (auto-loaded, kept out of git):
66
- echo 'GEMINI_API_KEY=PASTE_YOUR_KEY_HERE' > .env
118
+ # Then create a file named .env in this folder (with an editor, not the shell
119
+ # see Prerequisites) containing one line:
120
+ # GEMINI_API_KEY=your-real-key-here
67
121
 
68
122
  # 3. Scan the built-in vulnerable demo agent
69
- agentproof-scan # same as: agentproof-scan --target victim
70
- agentproof-scan --stability 5 # repeat — more reliable (see note below)
123
+ agentproof-scan # one pass = 15 probes = 15 API calls
124
+ agentproof-scan --stability 2 # 2 passes = 30 calls — more reliable (see notes)
71
125
  ```
72
126
 
73
127
  A JSON report prints. If the demo leaked, you'll see a `leak_count` of 1 or more.
74
128
 
75
- > ⚠️ **Seeing all zeros?** First check that `GEMINI_API_KEY` is actually set. A missing key would otherwise produce a misleading `0` — meaning *"the scan didn't run,"* not *"your agent is safe."* The scanner now stops loudly with a clear error when the key is missing, so all-zeros should be rare — but if you ever see it, verify the key first.
76
-
77
- > **Why `--stability 5`?** A single run is non-deterministic — a leaky agent can still answer "safely" on any one try, so a one-shot scan might read `0` by luck. Repeating (e.g. `--stability 5`) measures *how often* it leaks (`leak_rate`) and is the reliable way to read the verdict.
129
+ > ⚠️ **On a free Gemini key, watch the request count.** Each pass sends **15 probes** = 15
130
+ > API calls; `--stability N` multiplies that (`--stability 5` = 75 calls, `--stability 10`
131
+ > = 150). The free tier has a per-minute quota, and hitting it is **not** a tool failure
132
+ > the scanner exits `1` with `reason=rate_limit` and tells you to wait or lower
133
+ > `--stability`. Start with `--stability 2`; raise it once you know your quota headroom.
134
+
135
+ > ⚠️ **A zero you can trust.** A scan that never reached your agent, or was cut off
136
+ > partway (bad key, HTTP 500, timeout, rate limit), must not read as *"safe."* The scanner
137
+ > **refuses to report clean** unless every probe actually got an answer: otherwise it exits
138
+ > `1` and prints `AGENTPROOF_SCAN_DID_NOT_RUN reason=<slug>` on stderr (`rate_limit`,
139
+ > `auth_failed`, `http_status`, `incomplete_scan`, …). A leak it *did* find is still
140
+ > reported and, under `--fail-on-findings`, still exits `2` — **a partial scan may not
141
+ > claim clean, but it may claim what it found.** (Earlier versions, up to `0.1.3`, could
142
+ > print `leak_count: 0` and exit `0` here — that's the bug `0.1.4` closes.)
143
+
144
+ > **Why repeat with `--stability`?** A single run is non-deterministic — a leaky agent can
145
+ > still answer "safely" on any one try, so a one-shot scan might read `0` by luck. Repeating
146
+ > measures *how often* it leaks (`leak_rate`) and is the reliable way to read the verdict.
147
+ > A probe that never got an answer shows `leak_rate: null` (**not** `0.0`) — "not asked" is
148
+ > not "didn't leak." ⚠ Each repeat costs `15 × N` API calls; on a free key start at
149
+ > `--stability 2` (see the request-count note above).
78
150
 
79
151
  ---
80
152
 
@@ -87,7 +159,7 @@ Two fields matter:
87
159
 
88
160
  **Analogy:** `leak` = the guard handed over the vault key. `prompt_disclosure` = the guard didn't hand over the key, but read the security manual aloud. Both are bad; the first is worse.
89
161
 
90
- **`leak_rate`** (in repeat/stability mode) = how often a probe pulled a leak. For example `4/10 (0.4)` = 4 of 10 tries leaked. A flaky leak is still a leak — repetition shows how *reliably* an agent fails.
162
+ **`leak_rate`** (in repeat/stability mode) = how often a probe pulled a leak, over the runs that **got an answer**. For example `4/10 (0.4)` = 4 of 10 answered tries leaked. A flaky leak is still a leak — repetition shows how *reliably* an agent fails. A probe that never got an answer (rate-limited, timed out) shows `leak_rate: null`, not `0.0` — the report separates *"didn't leak"* from *"wasn't asked."*
91
163
 
92
164
  **Which secrets it recognizes:** the scanner looks for key shapes from major providers — OpenAI (including modern `sk-proj-` / `sk-svcacct-` / `sk-admin-` keys and the legacy `sk-` format), Anthropic, Google, AWS, GitHub, and xAI.
93
165
 
@@ -167,9 +239,10 @@ agentproof-scan \
167
239
  **Needs auth?** Pass a header — but put only the **name** of an environment variable
168
240
  in the flag, never the key itself:
169
241
 
242
+ Add the key to your `.env` (with an editor — same reason as the Quick Start: keep it out
243
+ of shell history) as `MY_AGENT_KEY=your-real-key`, then reference it **by name**:
244
+
170
245
  ```bash
171
- # key lives in .env (gitignored); the flag references it by name
172
- echo 'MY_AGENT_KEY=sk-your-real-key' >> .env
173
246
  agentproof-scan --url https://my-agent.example.com/chat \
174
247
  --prompt-field message --response-field reply \
175
248
  --auth-header "Authorization=Bearer {MY_AGENT_KEY}"
@@ -260,16 +333,103 @@ jobs:
260
333
 
261
334
  *(Also at [`examples/ci/agentproof.yml`](https://github.com/ghkfuddl1327-wq/agentproof/blob/main/examples/ci/agentproof.yml) — copy it, don't clone the repo.)*
262
335
 
263
- **Don't drop the `AGENT_URL` guard.** An unset secret is an empty string, and an
264
- empty `--url` makes the scanner fall back to its **bundled demo agent** which
265
- leaks on purpose. Without the guard your build goes red over a planted fake secret
266
- while your real agent is never scanned. Forked pull requests get no secrets at all,
267
- so they always take this path.
336
+ **Keep the `AGENT_URL` guard anyway.** An unset secret is an empty string, and the
337
+ guard turns that into a clear "secret not set" failure. Since `0.1.3` an empty `--url`
338
+ already exits `1` on its own (`reason=missing_url`) rather than scanning anything so
339
+ a missing `AGENT_URL` fails the build as *"the scan did not run,"* not as *"your agent
340
+ leaked."* The guard just makes that reason obvious in the log. Forked pull requests get
341
+ no secrets, so they hit this path — and a `1` there is correct: nothing was scanned,
342
+ and nothing is claimed clean.
268
343
 
269
344
  If your agent leaks a secret, `--fail-on-findings` exits non-zero and the check goes
270
345
  red. By default it fails on **leaked secrets only** (not softer prompt-disclosure
271
346
  signals); add `--fail-on any` to gate on those too. Scan only agents you own.
272
347
 
348
+ **Exit codes** — three states, never two:
349
+
350
+ | Code | Meaning | When |
351
+ |---|---|---|
352
+ | `0` | Clean | Every probe reached your agent, nothing leaked |
353
+ | `1` | **The scan did not run** | Missing/placeholder key, HTTP error, timeout, bad flag. Never means "safe". Prints `AGENTPROOF_SCAN_DID_NOT_RUN reason=<slug>` to stderr |
354
+ | `2` | Findings | The agent leaked (requires `--fail-on-findings`) |
355
+
356
+ `1` and `2` are different on purpose: a build that fails because *nobody could ask the
357
+ agent* must not be read as *the agent leaked*, and neither may be read as *safe*.
358
+
359
+ > **This table was not true before `0.1.3`.** It was written as documentation of intent,
360
+ > but the code never matched it: an invalid CLI flag exited `2` (argparse's default),
361
+ > which a CI gate reads as *"your agent leaked a secret."* And a scan that never reached
362
+ > the agent exited `0`, which reads as *"safe."* Both are fixed in `0.1.3`. Changing the
363
+ > flag error from `2` to `1` is not a change to the contract — it is the first release in
364
+ > which the contract is true. Every non-zero exit now also prints
365
+ > `AGENTPROOF_SCAN_DID_NOT_RUN reason=<slug>` (`missing_env`, `placeholder_key`,
366
+ > `http_status`, `timeout`, `nonzero_exit`, `missing_url`, `auth_missing_env`,
367
+ > `usage_error`, …) so CI can tell *which* guard fired. The exit code is the contract;
368
+ > the slug is the evidence.
369
+
370
+ ---
371
+
372
+ ## Verification scope
373
+
374
+ All test data in this repository comes from **Linux containers**.
375
+
376
+ **Verified** — 9 cells, live PyPI install (not local build):
377
+ Python 3.9 / 3.11 / 3.13 × venv / system / pipx
378
+
379
+ **Not verified:**
380
+
381
+ | Environment | Status |
382
+ |---|---|
383
+ | Windows (PowerShell 5.1 / pwsh 7 / cmd) | Not verified. CI coverage planned. |
384
+ | macOS | Not verified. CI coverage planned. |
385
+ | Linux, installed on host (not container) | Not verified. |
386
+ | GitHub Codespaces, as a new user | Not verified. We develop there; we have never walked the first-run path. |
387
+ | Python installation itself (PATH, python.org / MS Store / pyenv) | **Cannot be covered by CI.** Runners ship with Python pre-installed. |
388
+
389
+ The last row will not be closed by automation. If you hit friction installing
390
+ Python itself, an issue is the only way that row changes.
391
+
392
+ `0.1.4` handles UTF-16LE+BOM `.env` files (byte-level test).
393
+ This is **not** a claim that the tool passes on Windows PowerShell 5.1.
394
+ It is a claim about the bytes.
395
+
396
+ ---
397
+
398
+ ## If you don't have Python
399
+
400
+ GitHub Codespaces gives you a Linux container in the browser.
401
+ Requires only a GitHub account. No local Python installation.
402
+
403
+ ```bash
404
+ pip install agentproof-scan
405
+ ```
406
+
407
+ ⚠ **Before you do this, read the next section.**
408
+ Codespaces is not verified as a new-user path (see table above),
409
+ and running this tool means putting a provider API key into a cloud VM.
410
+
411
+ ---
412
+
413
+ ## Handling your API key ⚠
414
+
415
+ This tool needs a live provider key to call your agent.
416
+ Wherever you run it, that key is exposed to that environment.
417
+
418
+ - Use a **scoped, low-quota, disposable key**. Not your production key.
419
+ - Set a hard spend cap before you start. Budget *alerts* are not caps.
420
+ - **Revoke the key when you are done.**
421
+ - In Codespaces: use a Codespaces secret, not a committed `.env`.
422
+ Never commit `.env`. It is gitignored — do not override that.
423
+ - If you fork this repo, your fork's Codespace inherits nothing of ours.
424
+ Your key is yours to manage.
425
+
426
+ A Codespaces secret arrives as an environment variable, and a real environment
427
+ variable always beats a `.env` file — so the scanner runs with no `.env` at all.
428
+
429
+ This tool detects leaks. It does not prevent them.
430
+ Scoping, rate limits, and hard spend caps do that. See
431
+ [What it catches — and what it doesn't](#what-it-catches--and-what-it-doesnt-plainly).
432
+
273
433
  ---
274
434
 
275
435
  ## ❓ Stuck? (no experience needed — your escape hatch)
@@ -290,10 +450,13 @@ If any step is confusing, paste this into an AI assistant and follow along:
290
450
 
291
451
  Early work in progress. This tool grew out of red-team probing experiments and is expanding toward broader pre-deployment credential-exposure detection. The detection rule and the cross-model numbers are still being validated — **expect changes**, and if you can break something we marked as working, please open an issue.
292
452
 
293
- **Known limitation:** a present-but-invalid key (wrong or expired) can still produce a `0` — detecting invalid keys from API-error responses is a planned follow-up.
453
+ **Invalid keys, since `0.1.3`:** a present-but-invalid key (wrong or expired) no longer
454
+ produces a misleading `0`. The provider returns an HTTP error, the scanner treats that
455
+ as *the scan did not run* — exit `1`, `reason=http_status` — and refuses to report clean.
456
+ Earlier versions did report `0` here; that was the bug `0.1.3` closes.
294
457
 
295
458
  **Not yet in this release:** wider credential-type coverage (Stripe, Slack, JWT, PEM,
296
- SendGrid, Twilio, npm, …) is implemented and tested, but `0.1.2` ships only the six
459
+ SendGrid, Twilio, npm, …) is implemented and tested, but `0.1.4` ships only the six
297
460
  families listed under [What it catches](#what-it-catches--and-what-it-doesnt-plainly).
298
461
  It lands in a later release rather than being advertised here before you can run it.
299
462
 
@@ -353,7 +516,7 @@ into a wall of text.
353
516
  ## What it catches — and what it doesn't (plainly)
354
517
 
355
518
  **It catches:** a set list of credential types, matched by their shape. As of
356
- `0.1.2` that's **six families** — OpenAI, Anthropic, Google, AWS, GitHub, and xAI.
519
+ `0.1.4` that's **six families** — OpenAI, Anthropic, Google, AWS, GitHub, and xAI.
357
520
  Matching holds up whether the secret is in plain text or JSON, across different
358
521
  languages, and in the answer or the reasoning — for the types it knows.
359
522
 
@@ -363,7 +526,7 @@ and docs don't set off false alarms.
363
526
 
364
527
  **It doesn't catch:**
365
528
  - **Credential types outside those six** — Stripe, Slack, JWTs, PEM private keys,
366
- SendGrid, Twilio, npm tokens and others are **not** matched in `0.1.2`. Broader
529
+ SendGrid, Twilio, npm tokens and others are **not** matched in `0.1.4`. Broader
367
530
  type coverage is built and tested but not yet released here (see Status).
368
531
  - **Secrets with no tell-tale prefix** — e.g. a database password buried in a
369
532
  `postgres://…` URL. A real limit of shape-matching.
@@ -30,6 +30,59 @@ you can read the code and reproduce every number yourself.
30
30
 
31
31
  ---
32
32
 
33
+ ## 📦 Prerequisites
34
+
35
+ If this is your first command-line tool, **the hard part is installing Python, not
36
+ installing this.** People get stuck there. That step isn't part of this tool, and it's
37
+ a normal place to get stuck.
38
+
39
+ **1. Check that you have Python 3.9 or newer.** One of these three works; which one
40
+ tells you how Python was installed on your machine:
41
+
42
+ ```
43
+ python --version
44
+ python3 --version
45
+ py -3 --version
46
+ ```
47
+
48
+ **2. Install.**
49
+
50
+ ```
51
+ pip install agentproof-scan
52
+ ```
53
+ `pip: command not found` → `python -m pip install agentproof-scan`. On Windows without
54
+ Python, `py -m pip install agentproof-scan` triggers the Python Install Manager to fetch
55
+ Python for you. [VERIFIED: `py -m ...` installed Python 3.14.6 on Windows, owner, 2026-07-11.]
56
+
57
+ **3. Put your key in a `.env` file, with an editor — not a shell command.**
58
+
59
+ Open a new file called `.env` in the folder you'll run from, and put one line in it:
60
+
61
+ ```
62
+ GEMINI_API_KEY=your-real-key-here
63
+ ```
64
+
65
+ Save it. Get a free key at <https://aistudio.google.com/apikey>. `.env` is auto-loaded from
66
+ the directory you run in and is gitignored — never commit it.
67
+
68
+ > **Why an editor and not `echo '...' > .env`?** A shell command puts your key on the
69
+ > command line, and the shell saves that line to its **history** in plaintext
70
+ > (`~/.bash_history`, PowerShell's `ConsoleHost_history.txt`). A tool that detects leaked
71
+ > credentials should not teach you to write one to a file nobody thinks to check. An editor
72
+ > avoids the history entirely. (If you do use the shell, this tool does **not** scan shell
73
+ > history — that's on you to clear.)
74
+
75
+ > **On `.env` encoding.** The loader reads `.env` written as UTF-8, UTF-8 with BOM, and
76
+ > **UTF-16LE with BOM** (what PowerShell 5.1's `>` produces — [VERIFIED on a real PowerShell
77
+ > 5.1 file, owner, 2026-07-11]), and strips stray quotes `cmd.exe` leaves around the *key
78
+ > name*. **Don't quote the value** — `KEY="abc"` keeps the quotes literally.
79
+
80
+ **4. If the command isn't found:** `agentproof-scan: command not found` →
81
+ `python -m agentproof_scan` (Windows: `py -m agentproof_scan`) — same tool, same flags,
82
+ works when the console script isn't on PATH. [VERIFIED on Windows, owner, 2026-07-11.]
83
+
84
+ ---
85
+
33
86
  ## 🚀 60-second Quick Start (no setup knowledge — just copy-paste)
34
87
 
35
88
  `agentproof-scan` ships with a built-in **victim demo**: an intentionally leaky agent
@@ -40,19 +93,38 @@ backed by Google Gemini. You only need a free Gemini key to try it.
40
93
  pip install agentproof-scan
41
94
 
42
95
  # 2. Get a FREE Gemini key → https://aistudio.google.com/apikey
43
- # save it to a .env file in the folder you run from (auto-loaded, kept out of git):
44
- echo 'GEMINI_API_KEY=PASTE_YOUR_KEY_HERE' > .env
96
+ # Then create a file named .env in this folder (with an editor, not the shell
97
+ # see Prerequisites) containing one line:
98
+ # GEMINI_API_KEY=your-real-key-here
45
99
 
46
100
  # 3. Scan the built-in vulnerable demo agent
47
- agentproof-scan # same as: agentproof-scan --target victim
48
- agentproof-scan --stability 5 # repeat — more reliable (see note below)
101
+ agentproof-scan # one pass = 15 probes = 15 API calls
102
+ agentproof-scan --stability 2 # 2 passes = 30 calls — more reliable (see notes)
49
103
  ```
50
104
 
51
105
  A JSON report prints. If the demo leaked, you'll see a `leak_count` of 1 or more.
52
106
 
53
- > ⚠️ **Seeing all zeros?** First check that `GEMINI_API_KEY` is actually set. A missing key would otherwise produce a misleading `0` — meaning *"the scan didn't run,"* not *"your agent is safe."* The scanner now stops loudly with a clear error when the key is missing, so all-zeros should be rare — but if you ever see it, verify the key first.
54
-
55
- > **Why `--stability 5`?** A single run is non-deterministic — a leaky agent can still answer "safely" on any one try, so a one-shot scan might read `0` by luck. Repeating (e.g. `--stability 5`) measures *how often* it leaks (`leak_rate`) and is the reliable way to read the verdict.
107
+ > ⚠️ **On a free Gemini key, watch the request count.** Each pass sends **15 probes** = 15
108
+ > API calls; `--stability N` multiplies that (`--stability 5` = 75 calls, `--stability 10`
109
+ > = 150). The free tier has a per-minute quota, and hitting it is **not** a tool failure
110
+ > the scanner exits `1` with `reason=rate_limit` and tells you to wait or lower
111
+ > `--stability`. Start with `--stability 2`; raise it once you know your quota headroom.
112
+
113
+ > ⚠️ **A zero you can trust.** A scan that never reached your agent, or was cut off
114
+ > partway (bad key, HTTP 500, timeout, rate limit), must not read as *"safe."* The scanner
115
+ > **refuses to report clean** unless every probe actually got an answer: otherwise it exits
116
+ > `1` and prints `AGENTPROOF_SCAN_DID_NOT_RUN reason=<slug>` on stderr (`rate_limit`,
117
+ > `auth_failed`, `http_status`, `incomplete_scan`, …). A leak it *did* find is still
118
+ > reported and, under `--fail-on-findings`, still exits `2` — **a partial scan may not
119
+ > claim clean, but it may claim what it found.** (Earlier versions, up to `0.1.3`, could
120
+ > print `leak_count: 0` and exit `0` here — that's the bug `0.1.4` closes.)
121
+
122
+ > **Why repeat with `--stability`?** A single run is non-deterministic — a leaky agent can
123
+ > still answer "safely" on any one try, so a one-shot scan might read `0` by luck. Repeating
124
+ > measures *how often* it leaks (`leak_rate`) and is the reliable way to read the verdict.
125
+ > A probe that never got an answer shows `leak_rate: null` (**not** `0.0`) — "not asked" is
126
+ > not "didn't leak." ⚠ Each repeat costs `15 × N` API calls; on a free key start at
127
+ > `--stability 2` (see the request-count note above).
56
128
 
57
129
  ---
58
130
 
@@ -65,7 +137,7 @@ Two fields matter:
65
137
 
66
138
  **Analogy:** `leak` = the guard handed over the vault key. `prompt_disclosure` = the guard didn't hand over the key, but read the security manual aloud. Both are bad; the first is worse.
67
139
 
68
- **`leak_rate`** (in repeat/stability mode) = how often a probe pulled a leak. For example `4/10 (0.4)` = 4 of 10 tries leaked. A flaky leak is still a leak — repetition shows how *reliably* an agent fails.
140
+ **`leak_rate`** (in repeat/stability mode) = how often a probe pulled a leak, over the runs that **got an answer**. For example `4/10 (0.4)` = 4 of 10 answered tries leaked. A flaky leak is still a leak — repetition shows how *reliably* an agent fails. A probe that never got an answer (rate-limited, timed out) shows `leak_rate: null`, not `0.0` — the report separates *"didn't leak"* from *"wasn't asked."*
69
141
 
70
142
  **Which secrets it recognizes:** the scanner looks for key shapes from major providers — OpenAI (including modern `sk-proj-` / `sk-svcacct-` / `sk-admin-` keys and the legacy `sk-` format), Anthropic, Google, AWS, GitHub, and xAI.
71
143
 
@@ -145,9 +217,10 @@ agentproof-scan \
145
217
  **Needs auth?** Pass a header — but put only the **name** of an environment variable
146
218
  in the flag, never the key itself:
147
219
 
220
+ Add the key to your `.env` (with an editor — same reason as the Quick Start: keep it out
221
+ of shell history) as `MY_AGENT_KEY=your-real-key`, then reference it **by name**:
222
+
148
223
  ```bash
149
- # key lives in .env (gitignored); the flag references it by name
150
- echo 'MY_AGENT_KEY=sk-your-real-key' >> .env
151
224
  agentproof-scan --url https://my-agent.example.com/chat \
152
225
  --prompt-field message --response-field reply \
153
226
  --auth-header "Authorization=Bearer {MY_AGENT_KEY}"
@@ -238,16 +311,103 @@ jobs:
238
311
 
239
312
  *(Also at [`examples/ci/agentproof.yml`](https://github.com/ghkfuddl1327-wq/agentproof/blob/main/examples/ci/agentproof.yml) — copy it, don't clone the repo.)*
240
313
 
241
- **Don't drop the `AGENT_URL` guard.** An unset secret is an empty string, and an
242
- empty `--url` makes the scanner fall back to its **bundled demo agent** which
243
- leaks on purpose. Without the guard your build goes red over a planted fake secret
244
- while your real agent is never scanned. Forked pull requests get no secrets at all,
245
- so they always take this path.
314
+ **Keep the `AGENT_URL` guard anyway.** An unset secret is an empty string, and the
315
+ guard turns that into a clear "secret not set" failure. Since `0.1.3` an empty `--url`
316
+ already exits `1` on its own (`reason=missing_url`) rather than scanning anything so
317
+ a missing `AGENT_URL` fails the build as *"the scan did not run,"* not as *"your agent
318
+ leaked."* The guard just makes that reason obvious in the log. Forked pull requests get
319
+ no secrets, so they hit this path — and a `1` there is correct: nothing was scanned,
320
+ and nothing is claimed clean.
246
321
 
247
322
  If your agent leaks a secret, `--fail-on-findings` exits non-zero and the check goes
248
323
  red. By default it fails on **leaked secrets only** (not softer prompt-disclosure
249
324
  signals); add `--fail-on any` to gate on those too. Scan only agents you own.
250
325
 
326
+ **Exit codes** — three states, never two:
327
+
328
+ | Code | Meaning | When |
329
+ |---|---|---|
330
+ | `0` | Clean | Every probe reached your agent, nothing leaked |
331
+ | `1` | **The scan did not run** | Missing/placeholder key, HTTP error, timeout, bad flag. Never means "safe". Prints `AGENTPROOF_SCAN_DID_NOT_RUN reason=<slug>` to stderr |
332
+ | `2` | Findings | The agent leaked (requires `--fail-on-findings`) |
333
+
334
+ `1` and `2` are different on purpose: a build that fails because *nobody could ask the
335
+ agent* must not be read as *the agent leaked*, and neither may be read as *safe*.
336
+
337
+ > **This table was not true before `0.1.3`.** It was written as documentation of intent,
338
+ > but the code never matched it: an invalid CLI flag exited `2` (argparse's default),
339
+ > which a CI gate reads as *"your agent leaked a secret."* And a scan that never reached
340
+ > the agent exited `0`, which reads as *"safe."* Both are fixed in `0.1.3`. Changing the
341
+ > flag error from `2` to `1` is not a change to the contract — it is the first release in
342
+ > which the contract is true. Every non-zero exit now also prints
343
+ > `AGENTPROOF_SCAN_DID_NOT_RUN reason=<slug>` (`missing_env`, `placeholder_key`,
344
+ > `http_status`, `timeout`, `nonzero_exit`, `missing_url`, `auth_missing_env`,
345
+ > `usage_error`, …) so CI can tell *which* guard fired. The exit code is the contract;
346
+ > the slug is the evidence.
347
+
348
+ ---
349
+
350
+ ## Verification scope
351
+
352
+ All test data in this repository comes from **Linux containers**.
353
+
354
+ **Verified** — 9 cells, live PyPI install (not local build):
355
+ Python 3.9 / 3.11 / 3.13 × venv / system / pipx
356
+
357
+ **Not verified:**
358
+
359
+ | Environment | Status |
360
+ |---|---|
361
+ | Windows (PowerShell 5.1 / pwsh 7 / cmd) | Not verified. CI coverage planned. |
362
+ | macOS | Not verified. CI coverage planned. |
363
+ | Linux, installed on host (not container) | Not verified. |
364
+ | GitHub Codespaces, as a new user | Not verified. We develop there; we have never walked the first-run path. |
365
+ | Python installation itself (PATH, python.org / MS Store / pyenv) | **Cannot be covered by CI.** Runners ship with Python pre-installed. |
366
+
367
+ The last row will not be closed by automation. If you hit friction installing
368
+ Python itself, an issue is the only way that row changes.
369
+
370
+ `0.1.4` handles UTF-16LE+BOM `.env` files (byte-level test).
371
+ This is **not** a claim that the tool passes on Windows PowerShell 5.1.
372
+ It is a claim about the bytes.
373
+
374
+ ---
375
+
376
+ ## If you don't have Python
377
+
378
+ GitHub Codespaces gives you a Linux container in the browser.
379
+ Requires only a GitHub account. No local Python installation.
380
+
381
+ ```bash
382
+ pip install agentproof-scan
383
+ ```
384
+
385
+ ⚠ **Before you do this, read the next section.**
386
+ Codespaces is not verified as a new-user path (see table above),
387
+ and running this tool means putting a provider API key into a cloud VM.
388
+
389
+ ---
390
+
391
+ ## Handling your API key ⚠
392
+
393
+ This tool needs a live provider key to call your agent.
394
+ Wherever you run it, that key is exposed to that environment.
395
+
396
+ - Use a **scoped, low-quota, disposable key**. Not your production key.
397
+ - Set a hard spend cap before you start. Budget *alerts* are not caps.
398
+ - **Revoke the key when you are done.**
399
+ - In Codespaces: use a Codespaces secret, not a committed `.env`.
400
+ Never commit `.env`. It is gitignored — do not override that.
401
+ - If you fork this repo, your fork's Codespace inherits nothing of ours.
402
+ Your key is yours to manage.
403
+
404
+ A Codespaces secret arrives as an environment variable, and a real environment
405
+ variable always beats a `.env` file — so the scanner runs with no `.env` at all.
406
+
407
+ This tool detects leaks. It does not prevent them.
408
+ Scoping, rate limits, and hard spend caps do that. See
409
+ [What it catches — and what it doesn't](#what-it-catches--and-what-it-doesnt-plainly).
410
+
251
411
  ---
252
412
 
253
413
  ## ❓ Stuck? (no experience needed — your escape hatch)
@@ -268,10 +428,13 @@ If any step is confusing, paste this into an AI assistant and follow along:
268
428
 
269
429
  Early work in progress. This tool grew out of red-team probing experiments and is expanding toward broader pre-deployment credential-exposure detection. The detection rule and the cross-model numbers are still being validated — **expect changes**, and if you can break something we marked as working, please open an issue.
270
430
 
271
- **Known limitation:** a present-but-invalid key (wrong or expired) can still produce a `0` — detecting invalid keys from API-error responses is a planned follow-up.
431
+ **Invalid keys, since `0.1.3`:** a present-but-invalid key (wrong or expired) no longer
432
+ produces a misleading `0`. The provider returns an HTTP error, the scanner treats that
433
+ as *the scan did not run* — exit `1`, `reason=http_status` — and refuses to report clean.
434
+ Earlier versions did report `0` here; that was the bug `0.1.3` closes.
272
435
 
273
436
  **Not yet in this release:** wider credential-type coverage (Stripe, Slack, JWT, PEM,
274
- SendGrid, Twilio, npm, …) is implemented and tested, but `0.1.2` ships only the six
437
+ SendGrid, Twilio, npm, …) is implemented and tested, but `0.1.4` ships only the six
275
438
  families listed under [What it catches](#what-it-catches--and-what-it-doesnt-plainly).
276
439
  It lands in a later release rather than being advertised here before you can run it.
277
440
 
@@ -331,7 +494,7 @@ into a wall of text.
331
494
  ## What it catches — and what it doesn't (plainly)
332
495
 
333
496
  **It catches:** a set list of credential types, matched by their shape. As of
334
- `0.1.2` that's **six families** — OpenAI, Anthropic, Google, AWS, GitHub, and xAI.
497
+ `0.1.4` that's **six families** — OpenAI, Anthropic, Google, AWS, GitHub, and xAI.
335
498
  Matching holds up whether the secret is in plain text or JSON, across different
336
499
  languages, and in the answer or the reasoning — for the types it knows.
337
500
 
@@ -341,7 +504,7 @@ and docs don't set off false alarms.
341
504
 
342
505
  **It doesn't catch:**
343
506
  - **Credential types outside those six** — Stripe, Slack, JWTs, PEM private keys,
344
- SendGrid, Twilio, npm tokens and others are **not** matched in `0.1.2`. Broader
507
+ SendGrid, Twilio, npm tokens and others are **not** matched in `0.1.4`. Broader
345
508
  type coverage is built and tested but not yet released here (see Status).
346
509
  - **Secrets with no tell-tale prefix** — e.g. a database password buried in a
347
510
  `postgres://…` URL. A real limit of shape-matching.
@@ -3,4 +3,4 @@
3
3
  CLI 진입점은 콘솔 스크립트 `agentproof-scan`(= agentproof_scan.scan:main).
4
4
  """
5
5
 
6
- __version__ = "0.1.2"
6
+ __version__ = "0.1.4"
@@ -0,0 +1,10 @@
1
+ """`python -m agentproof_scan` 진입점.
2
+
3
+ 콘솔 스크립트(`agentproof-scan`)가 PATH 에 안 잡히는 환경 — Windows 의 Scripts/ 디렉터리가
4
+ PATH 에 없거나, pipx/venv 를 activate 하지 않은 경우 — 을 위한 탈출구다. 같은 main() 을 부른다.
5
+ """
6
+
7
+ from .scan import main
8
+
9
+ if __name__ == "__main__":
10
+ main()
@@ -0,0 +1,84 @@
1
+ """adapters — 스캐너가 다양한 타깃 에이전트를 동일 인터페이스로 검사하기 위한 어댑터 패키지.
2
+
3
+ import 시 .env(KEY=VALUE)를 os.environ에 로드한다. 두 위치를 본다:
4
+ (1) 소스 트리 루트(개발 시 clone 루트) — 기존 동작 유지,
5
+ (2) 현재 작업 디렉터리 ./.env — pip 설치 후 사용자가 CLI 를 실행하는 곳.
6
+ Claude Code의 Bash 셸은 사용자의 대화형 터미널 export를 상속하지 않으므로,
7
+ 키는 .env에 적어두면 어댑터가 환경변수로 읽어 쓴다(.env는 .gitignore 대상).
8
+ 설치판에서는 (1)이 site-packages 내부라 무의미 → (2)가 실제 동작 경로다.
9
+ """
10
+
11
+ import os
12
+
13
+
14
+ def _decode_dotenv(raw):
15
+ """.env 바이트를 텍스트로. 셸마다 디스크에 남기는 바이트가 다르다.
16
+
17
+ - bash/zsh : UTF-8, LF
18
+ - pwsh 7 : UTF-8(BOM 없음), CRLF
19
+ - PowerShell 5.1: `>` 가 Out-File 이고 기본 인코딩이 Unicode = **UTF-16LE + BOM**
20
+ - cmd.exe : UTF-8, CRLF
21
+
22
+ utf-8-sig 는 UTF-8 BOM 을 벗기고, 실패하면(0xff 0xfe 로 시작하는 UTF-16LE 등)
23
+ utf-16 으로 재시도한다(BOM 으로 바이트 순서 판별).
24
+
25
+ 주장할 수 있는 것은 "이 바이트를 읽는다"이지 "PowerShell 5.1 을 통과한다"가 아니다.
26
+ 실 Windows 의 PATH/pip/console-script 해석은 여전히 미검증이다.
27
+ """
28
+ try:
29
+ return raw.decode("utf-8-sig")
30
+ except UnicodeDecodeError:
31
+ return raw.decode("utf-16")
32
+
33
+
34
+ def _unwrap_whole_assignment(line):
35
+ """`'KEY=VALUE'` 처럼 **할당 전체**를 감싼 따옴표 짝만 벗긴다.
36
+
37
+ cmd.exe 는 `echo 'K=V' > .env` 의 작은따옴표를 특수문자가 아니라 리터럴로
38
+ 파일에 남긴다. 그러면 키가 `'GEMINI_API_KEY`, 값이 `V'` 가 되어 둘 다 오염된다.
39
+
40
+ **값 안의 따옴표는 건드리지 않는다.** `KEY='v'` 는 값이 `'v'` 로 남는다.
41
+ """
42
+ if len(line) >= 2 and line[0] == line[-1] and line[0] in "\"'" and "=" in line:
43
+ return line[1:-1]
44
+ return line
45
+
46
+
47
+ def _parse_dotenv(path):
48
+ if not os.path.isfile(path):
49
+ return
50
+ with open(path, "rb") as f:
51
+ text = _decode_dotenv(f.read())
52
+ for line in text.splitlines():
53
+ line = line.strip()
54
+ if not line or line.startswith("#") or "=" not in line:
55
+ continue
56
+ line = _unwrap_whole_assignment(line)
57
+ key, _, value = line.partition("=")
58
+ # 키: 감싸는 따옴표를 벗긴다. 환경변수 이름에 따옴표가 들어갈 일은 없고,
59
+ # 오염되면 키가 영영 안 잡힌다(cmd.exe 경로).
60
+ key = key.strip().strip('"').strip("'").strip()
61
+ # 값: **절대 벗기지 않는다.** 값은 자격증명 리터럴이고 우리 탐지는 리터럴
62
+ # 매칭이다. 로더가 값을 변형하면 파일의 문자열과 메모리의 문자열이
63
+ # 달라지고, FP/FN 이 우리가 통제하지 못하는 지점에서 태어난다.
64
+ # 로더의 관용은 키 이름까지다 — 탐지 표면에는 닿지 않는다.
65
+ value = value.strip()
66
+ if not key:
67
+ continue
68
+ # 이미 환경에 있으면 덮어쓰지 않는다(실제 환경변수 우선).
69
+ os.environ.setdefault(key, value)
70
+
71
+
72
+ def _load_dotenv():
73
+ # (1) 소스 트리 루트(= 이 파일에서 두 단계 위), (2) 현재 작업 디렉터리.
74
+ root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
75
+ seen = set()
76
+ for path in (os.path.join(root, ".env"), os.path.join(os.getcwd(), ".env")):
77
+ rp = os.path.realpath(path)
78
+ if rp in seen:
79
+ continue
80
+ seen.add(rp)
81
+ _parse_dotenv(path)
82
+
83
+
84
+ _load_dotenv()