agentgovern 2.7.0__tar.gz → 2.7.3__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.
- {agentgovern-2.7.0 → agentgovern-2.7.3}/CHANGELOG.md +45 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/PKG-INFO +22 -15
- {agentgovern-2.7.0 → agentgovern-2.7.3}/README.md +20 -12
- {agentgovern-2.7.0 → agentgovern-2.7.3}/RELEASING.md +8 -0
- agentgovern-2.7.3/agentgovern/_version.py +1 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/hook/cli.py +2 -1
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/hook/data/hook.py +88 -8
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/hook/installer.py +23 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/pyproject.toml +2 -3
- agentgovern-2.7.3/tests/hook/fixtures/action_name_cuts.json +35 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/fixtures/hook_2_4_0_golden_outputs.json +0 -32
- agentgovern-2.7.3/tests/hook/test_action_name_cuts_fixture.py +50 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_hook_v2_nagt400.py +13 -9
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_hook_v2_nagt400_compat.py +6 -2
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_installer.py +62 -0
- agentgovern-2.7.0/agentgovern/_version.py +0 -1
- {agentgovern-2.7.0 → agentgovern-2.7.3}/.gitignore +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/LICENSE +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/__init__.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/__main__.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/client.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/exceptions.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/gate2.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/hook/__init__.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/hook/data/__init__.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/hook/drift.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/hook/settings.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/instrumentors/__init__.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/instrumentors/_base.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/instrumentors/langchain.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/instrumentors/langgraph.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentgovern/types.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentshield/__init__.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentshield/client.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/agentshield/types.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/examples/langgraph_credit_decision_agent.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/__init__.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/conftest.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/__init__.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/conftest.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/fixtures/claude_code_pretooluse_output_keys.json +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_bundled_hook.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_drift.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_hook_v2_context_snapshot.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_hook_v2_dispatch.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_hook_v2_outbox_queue.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_hook_v2_payload_extraction.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_hook_v2_payloads.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_hook_v2_remediation_message.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_hook_v2_system_message.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_hook_v2_timing_log.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_hook_v2_transcript_reading.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_hook_v2_turn_index.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_hook_v2_warn_verdict.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_matcher_coverage.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_settings.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/test_tool_map_classification.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_base_instrumentor.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_client.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_evaluate_input.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_gate2_full_path.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_gate2_latency.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_gate2_verdict_handler.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_import_guard.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_instrument_langchain.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_langchain.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_langchain_1x_agent.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_langchain_gate1.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_langchain_gate2_callbacks.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_langgraph.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_nagt380_enforce_propagation.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_no_localhost_3000_config.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_no_localhost_3000_traffic.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_output_capture_contract.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_payload_schema.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_policy_engine_url.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_rate_limit_handling.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_register_contract.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_timestamp.py +0 -0
- {agentgovern-2.7.0 → agentgovern-2.7.3}/tests/test_version_consistency.py +0 -0
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.7.3 — release date is the `sdk-v2.7.3` tag's (prepared 2026-10-22)
|
|
4
|
+
|
|
5
|
+
**This release bundles Claude Code hook 2.5.1** (it was 2.5.0), and **`agentgovern hook install` registers the Python that runs it, by absolute path, instead of a bare `python3`.** The dependencies and the Python requirement (3.10 or later; the hook itself runs on 3.9 or later) are unchanged from 2.7.2. After upgrading, run `agentgovern hook install --force`: it installs hook 2.5.1 and registers the Python by absolute path. The install snippet already passes the absolute path from 2026-10-22. The hook's behaviour changes only if you set `AGENTGOVERN_FAIL_ACTION=deny`.
|
|
6
|
+
|
|
7
|
+
### Fix — `AGENTGOVERN_FAIL_ACTION=deny` blocks every tool call AgentGovern didn't check (NAGT-484)
|
|
8
|
+
|
|
9
|
+
- **Before (hook 2.5.0 and earlier):** `deny` blocked a tool call only when the hook got no answer at all (its own 5-second timeout, or a network error). Two other unchecked cases still let the call run:
|
|
10
|
+
- **an error answer** from AgentGovern (any HTTP 4xx or 5xx, a 429 rate limit included);
|
|
11
|
+
- **a `timeout` verdict:** AgentGovern answered, but its policy engine could not evaluate the call. The dashboard shows these calls as "Evaluation Failed".
|
|
12
|
+
- **Now:** with `deny`, the hook blocks in those two cases as well, so every call it couldn't get checked is blocked. The reason Claude Code shows is the hook's own text and, for an error, the HTTP status. Nothing from AgentGovern's response is shown.
|
|
13
|
+
- **With a 429 under `deny`,** the call is blocked, and the "rate limit reached, allowed" notice from 2.5.0 is not shown, since it would no longer be true.
|
|
14
|
+
- **The default (`allow`) is unchanged:** an unchecked call still runs.
|
|
15
|
+
- **Not changed:** a verdict the hook doesn't recognise still lets the call run, so a newer server can't stop an older hook; prompt, tool-result and Stop events never block.
|
|
16
|
+
- **Also blocked under `deny`: a tool call while `AGENTGOVERN_API_KEY` or `AGENTGOVERN_ACTOR_USER_ID` is unset or empty (NAGT-500).** Up to 2.5.0 the hook printed "… not set — governance unavailable, proceeding unaudited." and let the call run, whatever the setting. Now Claude Code shows: *"AgentGovern isn't set up in this shell: AGENTGOVERN_API_KEY is not set, so this tool call was not checked. AGENTGOVERN_FAIL_ACTION=deny: blocking it. To fix it, paste your install snippet again from Settings → Developers in the AgentGovern dashboard, or set AGENTGOVERN_API_KEY and open a new terminal."* It names only the variables that are missing, and never shows any value. Under `allow` the message and behaviour are unchanged. `AGENTGOVERN_HOOK_MODE=off` still turns the hook off.
|
|
17
|
+
|
|
18
|
+
### Fix — the hook runs with the Python you installed, whatever Claude Code's PATH is (NAGT-490)
|
|
19
|
+
|
|
20
|
+
- **What it did:** `hook install` wrote `"command": "python3"` (`python` on Windows) into `~/.claude/settings.json`. Claude Code looks that name up on its own PATH when it runs the hook, and that need not be your shell's: an app started from the Dock gets `/usr/bin:/bin:/usr/sbin:/sbin`.
|
|
21
|
+
- **So the hook could run under a different Python from the one you installed with,** and on a Mac without the Command Line Tools, `/usr/bin/python3` is a stub that only asks you to install them. There the hook never ran, and every tool call went ahead unchecked.
|
|
22
|
+
- **Now:** it writes the absolute path of the Python running `agentgovern hook install`: in a virtual environment, that environment's own `python`. It falls back to the bare name only if Python can't say where it is.
|
|
23
|
+
- **`--python-command` still overrides it.**
|
|
24
|
+
|
|
25
|
+
## 2.7.2 — release date is the `sdk-v2.7.2` tag's (prepared 2026-10-19)
|
|
26
|
+
|
|
27
|
+
**`agentgovern hook uninstall` now leaves Claude Code's settings exactly as they were before the install.** The bundled Claude Code hook (2.5.0), the dependencies and the Python requirement (3.10 or later) are unchanged from 2.7.1. There is no need to reinstall the hook.
|
|
28
|
+
|
|
29
|
+
### Fix — the uninstall leaves nothing behind in settings.json (NAGT-391 F8)
|
|
30
|
+
|
|
31
|
+
- **The install-mode marker (`_agentgovern_install_mode`) is removed with the hook entries.** It was left in `~/.claude/settings.json`.
|
|
32
|
+
- **The backup the uninstall writes (`settings.json.bak`) is removed once the new settings are written.** Left behind, it was a copy of the settings that still registered the hook, and restoring it would have pointed Claude Code at a hook script the uninstall steps delete. If the write fails, the backup stays.
|
|
33
|
+
- Found by the install rehearsal workflow (`.github/workflows/guard-install-rehearsal.yml`), which installs and uninstalls on clean Linux, macOS and Windows runners and compares the home folder before and after.
|
|
34
|
+
|
|
35
|
+
## 2.7.1 — release date is the `sdk-v2.7.1` tag's (prepared 2026-10-16)
|
|
36
|
+
|
|
37
|
+
**Documentation and package metadata only. The code, its dependencies and the bundled Claude Code hook (2.5.0) are unchanged from 2.7.0.** There is no need to reinstall the hook.
|
|
38
|
+
|
|
39
|
+
### Fix — the links on PyPI work (NAGT-391 F2)
|
|
40
|
+
|
|
41
|
+
- **The project's Repository and Issues links pointed at a GitHub repository that is not public,** so they returned "not found" for everyone outside Zirahn. They are replaced by a **Support** link, https://zirahn.com/contact. The README's own links are corrected the same way.
|
|
42
|
+
|
|
43
|
+
### Fix — the README says which Python you need (NAGT-391 F2)
|
|
44
|
+
|
|
45
|
+
- **The install line now needs Python 3.10 or later and asks for `agentgovern>=2.7.0`.** On Python 3.9, such as the macOS system Python, an unpinned `pip install agentgovern` installed 2.5.0 without a word; the pinned line fails with "No matching distribution found" instead.
|
|
46
|
+
- **The Claude Code hook section says what each policy mode does,** and that a new organization starts with nothing in enforce mode. It said the hook enforces your policies, which read as if it always blocked.
|
|
47
|
+
|
|
3
48
|
## 2.7.0 — release date is the `sdk-v2.7.0` tag's (prepared 2026-10-16)
|
|
4
49
|
|
|
5
50
|
**The Claude Code hook bundled with this release is hook 2.5.0** (it was 2.4.0). Install it with `agentgovern hook install --force` after upgrading; see "Upgrading an existing install" in the README. Until you do, the hook you have keeps working exactly as before, and the AgentGovern server shows it a once-per-session notice that a newer hook is available.
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentgovern
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.3
|
|
4
4
|
Summary: Compliance-as-code middleware for agentic AI workflows.
|
|
5
5
|
Project-URL: Homepage, https://agentgovern.zirahn.com
|
|
6
|
-
Project-URL:
|
|
7
|
-
Project-URL: Issues, https://github.com/ahmedkhan-zirahn/agentgovern/issues
|
|
6
|
+
Project-URL: Support, https://zirahn.com/contact
|
|
8
7
|
Author-email: Ahmed Khan <ahmed.khan@zirahn.com>
|
|
9
8
|
License: MIT
|
|
10
9
|
License-File: LICENSE
|
|
@@ -44,14 +43,16 @@ Description-Content-Type: text/markdown
|
|
|
44
43
|
|
|
45
44
|
**Compliance-as-code for agentic AI workflows.**
|
|
46
45
|
|
|
47
|
-
> **Beta** — API may change before 1.0.
|
|
46
|
+
> **Beta** — API may change before 1.0. Report issues through [zirahn.com/contact](https://zirahn.com/contact).
|
|
48
47
|
|
|
49
48
|
AgentGovern intercepts AI agent actions, evaluates them against configurable compliance policies (the EU AI Act today; NIST AI RMF and ISO 42001 are on the roadmap — see the table below), and generates audit-ready evidence in near real-time. Tracking runs in a background thread and fails open. An `enforce`-mode block raises in your code (see *Enforcement modes* below for exactly what it stops). *(Corrected again 2026-10-03: the first correction said "the SDK blocks only if you enable Gate 2 `enforce` mode, where a block verdict raises `OutputBlocked`", which reads as if enforce mode halts an agent; through the integrations it does not — NAGT-380.)* This SDK auto-instruments LangChain and LangGraph; any other framework (CrewAI, OpenAI Agents and the rest) can report actions with `track_action()`. *(Corrected 2026-10-03: this said NIST AI RMF and ISO 42001 were supported, that the SDK never blocks, and that it instruments CrewAI and OpenAI Agents code.)*
|
|
50
49
|
|
|
51
50
|
## Install
|
|
52
51
|
|
|
52
|
+
Needs **Python 3.10 or later**. The macOS system Python is 3.9, which is too old: install a newer one first, from python.org or with Homebrew (`brew install python@3.12`). On 3.9 this command fails with "No matching distribution found" rather than installing an old release.
|
|
53
|
+
|
|
53
54
|
```bash
|
|
54
|
-
pip install "agentgovern[langchain]"
|
|
55
|
+
python3 -m pip install "agentgovern[langchain]>=2.7.0"
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
## Quickstart — LangChain
|
|
@@ -212,19 +213,25 @@ Enable policy packs from the [AgentGovern dashboard](https://agentgovern.zirahn.
|
|
|
212
213
|
**Policy engine address (2.4.0).** The default engine address is now the stable name
|
|
213
214
|
`https://engine.zirahn.com`. Versions 2.3.0 and earlier call `https://agentgovern.onrender.com`
|
|
214
215
|
directly. That address keeps working, so nothing breaks if you stay on an older version, but only
|
|
215
|
-
the stable name can follow the engine if it moves. Upgrade with `pip install --upgrade agentgovern`.
|
|
216
|
+
the stable name can follow the engine if it moves. Upgrade with `python3 -m pip install --upgrade "agentgovern>=2.7.0"`.
|
|
216
217
|
If you set `policy_engine_url` or `AGENTGOVERN_POLICY_ENGINE_URL` yourself, it still takes precedence.
|
|
217
218
|
|
|
218
219
|
## Claude Code Hook
|
|
219
220
|
|
|
220
221
|
The SDK ships a built-in CLI for installing the AgentGovern hook into Claude Code. The hook
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
222
|
+
checks every tool call against your tenant's policies before the tool runs. A policy in
|
|
223
|
+
**enforce** mode blocks the call and Claude Code shows the reason; **warn** lets it run and
|
|
224
|
+
shows a warning; **log** only records it. A new organization starts with no policy in enforce
|
|
225
|
+
mode. A tool AgentGovern doesn't yet have a specific classification for is still captured and
|
|
226
|
+
evaluated, tagged `action_type=unmapped_tool` rather than silently skipped.
|
|
225
227
|
|
|
226
228
|
### Install
|
|
227
229
|
|
|
230
|
+
**The simplest way is the dashboard:** on **Settings → Developers**, **Add Developer** gives an
|
|
231
|
+
install snippet for bash and PowerShell that checks for Python 3.10 or later, installs this
|
|
232
|
+
package into its own virtual environment, saves your two settings for new terminals, and runs
|
|
233
|
+
the command below. To do it by hand:
|
|
234
|
+
|
|
228
235
|
```bash
|
|
229
236
|
agentgovern hook install
|
|
230
237
|
```
|
|
@@ -242,7 +249,7 @@ reports it as already installed and changes nothing. Upgrade, read what is insta
|
|
|
242
249
|
reinstall with `--force`:
|
|
243
250
|
|
|
244
251
|
```bash
|
|
245
|
-
python3 -m pip install --upgrade agentgovern # or the extra you installed with, e.g. "agentgovern[langchain]"
|
|
252
|
+
python3 -m pip install --upgrade "agentgovern>=2.7.0" # or the extra you installed with, e.g. "agentgovern[langchain]>=2.7.0"
|
|
246
253
|
agentgovern hook status # note each line's cmd= value and the "Install mode" line
|
|
247
254
|
agentgovern hook install --force # plus the flags below, if status calls for them
|
|
248
255
|
agentgovern hook status # reports no [DRIFT] lines once the hook is current
|
|
@@ -268,7 +275,7 @@ Your environment variables are not touched. Restart any running Claude Code sess
|
|
|
268
275
|
|
|
269
276
|
```bash
|
|
270
277
|
python3 -m venv ~/.agentgovern-venv
|
|
271
|
-
~/.agentgovern-venv/bin/python -m pip install --upgrade agentgovern
|
|
278
|
+
~/.agentgovern-venv/bin/python -m pip install --upgrade "agentgovern>=2.7.0"
|
|
272
279
|
~/.agentgovern-venv/bin/agentgovern hook status
|
|
273
280
|
~/.agentgovern-venv/bin/agentgovern hook install --force # plus the flags status called for
|
|
274
281
|
```
|
|
@@ -315,7 +322,7 @@ In PowerShell (the variable is removed again when claude exits):
|
|
|
315
322
|
try { $env:AGENTGOVERN_HOOK_MODE='off'; claude } finally { Remove-Item Env:AGENTGOVERN_HOOK_MODE }
|
|
316
323
|
```
|
|
317
324
|
|
|
318
|
-
|
|
325
|
+
`agentgovern hook install --help` lists the install options, and `agentgovern hook status` shows what is installed. The environment variables are in *Required env vars* and *Bypass* above.
|
|
319
326
|
|
|
320
327
|
## Design guarantees
|
|
321
328
|
|
|
@@ -327,8 +334,8 @@ See `packages/claude-hook/README.md` for the full option reference.
|
|
|
327
334
|
## Links
|
|
328
335
|
|
|
329
336
|
- **Dashboard:** https://agentgovern.zirahn.com
|
|
330
|
-
- **Documentation:**
|
|
331
|
-
- **
|
|
337
|
+
- **Documentation:** this README
|
|
338
|
+
- **Support and issues:** https://zirahn.com/contact
|
|
332
339
|
|
|
333
340
|
## License
|
|
334
341
|
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
**Compliance-as-code for agentic AI workflows.**
|
|
4
4
|
|
|
5
|
-
> **Beta** — API may change before 1.0.
|
|
5
|
+
> **Beta** — API may change before 1.0. Report issues through [zirahn.com/contact](https://zirahn.com/contact).
|
|
6
6
|
|
|
7
7
|
AgentGovern intercepts AI agent actions, evaluates them against configurable compliance policies (the EU AI Act today; NIST AI RMF and ISO 42001 are on the roadmap — see the table below), and generates audit-ready evidence in near real-time. Tracking runs in a background thread and fails open. An `enforce`-mode block raises in your code (see *Enforcement modes* below for exactly what it stops). *(Corrected again 2026-10-03: the first correction said "the SDK blocks only if you enable Gate 2 `enforce` mode, where a block verdict raises `OutputBlocked`", which reads as if enforce mode halts an agent; through the integrations it does not — NAGT-380.)* This SDK auto-instruments LangChain and LangGraph; any other framework (CrewAI, OpenAI Agents and the rest) can report actions with `track_action()`. *(Corrected 2026-10-03: this said NIST AI RMF and ISO 42001 were supported, that the SDK never blocks, and that it instruments CrewAI and OpenAI Agents code.)*
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
+
Needs **Python 3.10 or later**. The macOS system Python is 3.9, which is too old: install a newer one first, from python.org or with Homebrew (`brew install python@3.12`). On 3.9 this command fails with "No matching distribution found" rather than installing an old release.
|
|
12
|
+
|
|
11
13
|
```bash
|
|
12
|
-
pip install "agentgovern[langchain]"
|
|
14
|
+
python3 -m pip install "agentgovern[langchain]>=2.7.0"
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
## Quickstart — LangChain
|
|
@@ -170,19 +172,25 @@ Enable policy packs from the [AgentGovern dashboard](https://agentgovern.zirahn.
|
|
|
170
172
|
**Policy engine address (2.4.0).** The default engine address is now the stable name
|
|
171
173
|
`https://engine.zirahn.com`. Versions 2.3.0 and earlier call `https://agentgovern.onrender.com`
|
|
172
174
|
directly. That address keeps working, so nothing breaks if you stay on an older version, but only
|
|
173
|
-
the stable name can follow the engine if it moves. Upgrade with `pip install --upgrade agentgovern`.
|
|
175
|
+
the stable name can follow the engine if it moves. Upgrade with `python3 -m pip install --upgrade "agentgovern>=2.7.0"`.
|
|
174
176
|
If you set `policy_engine_url` or `AGENTGOVERN_POLICY_ENGINE_URL` yourself, it still takes precedence.
|
|
175
177
|
|
|
176
178
|
## Claude Code Hook
|
|
177
179
|
|
|
178
180
|
The SDK ships a built-in CLI for installing the AgentGovern hook into Claude Code. The hook
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
checks every tool call against your tenant's policies before the tool runs. A policy in
|
|
182
|
+
**enforce** mode blocks the call and Claude Code shows the reason; **warn** lets it run and
|
|
183
|
+
shows a warning; **log** only records it. A new organization starts with no policy in enforce
|
|
184
|
+
mode. A tool AgentGovern doesn't yet have a specific classification for is still captured and
|
|
185
|
+
evaluated, tagged `action_type=unmapped_tool` rather than silently skipped.
|
|
183
186
|
|
|
184
187
|
### Install
|
|
185
188
|
|
|
189
|
+
**The simplest way is the dashboard:** on **Settings → Developers**, **Add Developer** gives an
|
|
190
|
+
install snippet for bash and PowerShell that checks for Python 3.10 or later, installs this
|
|
191
|
+
package into its own virtual environment, saves your two settings for new terminals, and runs
|
|
192
|
+
the command below. To do it by hand:
|
|
193
|
+
|
|
186
194
|
```bash
|
|
187
195
|
agentgovern hook install
|
|
188
196
|
```
|
|
@@ -200,7 +208,7 @@ reports it as already installed and changes nothing. Upgrade, read what is insta
|
|
|
200
208
|
reinstall with `--force`:
|
|
201
209
|
|
|
202
210
|
```bash
|
|
203
|
-
python3 -m pip install --upgrade agentgovern # or the extra you installed with, e.g. "agentgovern[langchain]"
|
|
211
|
+
python3 -m pip install --upgrade "agentgovern>=2.7.0" # or the extra you installed with, e.g. "agentgovern[langchain]>=2.7.0"
|
|
204
212
|
agentgovern hook status # note each line's cmd= value and the "Install mode" line
|
|
205
213
|
agentgovern hook install --force # plus the flags below, if status calls for them
|
|
206
214
|
agentgovern hook status # reports no [DRIFT] lines once the hook is current
|
|
@@ -226,7 +234,7 @@ Your environment variables are not touched. Restart any running Claude Code sess
|
|
|
226
234
|
|
|
227
235
|
```bash
|
|
228
236
|
python3 -m venv ~/.agentgovern-venv
|
|
229
|
-
~/.agentgovern-venv/bin/python -m pip install --upgrade agentgovern
|
|
237
|
+
~/.agentgovern-venv/bin/python -m pip install --upgrade "agentgovern>=2.7.0"
|
|
230
238
|
~/.agentgovern-venv/bin/agentgovern hook status
|
|
231
239
|
~/.agentgovern-venv/bin/agentgovern hook install --force # plus the flags status called for
|
|
232
240
|
```
|
|
@@ -273,7 +281,7 @@ In PowerShell (the variable is removed again when claude exits):
|
|
|
273
281
|
try { $env:AGENTGOVERN_HOOK_MODE='off'; claude } finally { Remove-Item Env:AGENTGOVERN_HOOK_MODE }
|
|
274
282
|
```
|
|
275
283
|
|
|
276
|
-
|
|
284
|
+
`agentgovern hook install --help` lists the install options, and `agentgovern hook status` shows what is installed. The environment variables are in *Required env vars* and *Bypass* above.
|
|
277
285
|
|
|
278
286
|
## Design guarantees
|
|
279
287
|
|
|
@@ -285,8 +293,8 @@ See `packages/claude-hook/README.md` for the full option reference.
|
|
|
285
293
|
## Links
|
|
286
294
|
|
|
287
295
|
- **Dashboard:** https://agentgovern.zirahn.com
|
|
288
|
-
- **Documentation:**
|
|
289
|
-
- **
|
|
296
|
+
- **Documentation:** this README
|
|
297
|
+
- **Support and issues:** https://zirahn.com/contact
|
|
290
298
|
|
|
291
299
|
## License
|
|
292
300
|
|
|
@@ -93,4 +93,12 @@ To re-run a publish after a failed run, trigger the workflow manually **from the
|
|
|
93
93
|
- its bundled `hook.py` is hook 2.4.0, byte-identical to the tag commit's;
|
|
94
94
|
- each file's PyPI attestation names publisher `ZirahnAI/agentgovern`, workflow `publish-sdk.yml`, environment `pypi`, with a subject digest equal to the file's;
|
|
95
95
|
- an unpinned install resolves 2.6.0 on Python 3.12 and 2.5.0 on Python 3.9.6 (`/usr/bin/python3`, macOS 15.8.1).
|
|
96
|
+
- **2026-10-16:** 2.7.0 was published by run `38849510745` from tag `sdk-v2.7.0` at `90a5d6db` (#947's merge). **The tag was pushed by Claude Code under Ahmed's explicit, one-time authorisation for this tag**; the push printed the tag ruleset's "Cannot create ref due to creations being restricted" line and created the tag (the admin bypass). Build 04:13:32–04:13:52Z, Publish 04:13:56–04:14:27Z; files uploaded 04:14:22Z and 04:14:23Z. Verified afterwards, read-only:
|
|
97
|
+
- the wheel and sdist sha256 match PyPI's digests (`57105511…`, `e266e7e9…`), compared with the run's own `dist` artifact;
|
|
98
|
+
- the wheel's `METADATA` says `Version: 2.7.0`, `Requires-Python: >=3.10`;
|
|
99
|
+
- its bundled `hook.py` is **hook 2.5.0**, sha256 `f43c3c95010120dd34e8a91982de11516d9d3c582b5e2fd4bb7791c8fa6ffec1`, byte-identical to the tag commit's;
|
|
100
|
+
- each file's PyPI attestation names publisher `ZirahnAI/agentgovern`, workflow `publish-sdk.yml`, environment `pypi`;
|
|
101
|
+
- a fresh Python 3.12 venv installing `agentgovern==2.7.0` from PyPI reports `__version__` 2.7.0, and `hook install` into a temporary HOME writes hook 2.5.0 with all four events OK (`hook test` not run: it calls production);
|
|
102
|
+
- Python 3.9.6 (`/usr/bin/python3`) resolves `agentgovern-2.5.0`;
|
|
103
|
+
- the hook version changed, so the released-hook manifest moved to 2.5.0 after the tag (NAGT-400 (c), #953, `b748b1f7`).
|
|
96
104
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.7.3"
|
|
@@ -57,7 +57,8 @@ def add_hook_subparsers(hook_parser: argparse.ArgumentParser) -> None:
|
|
|
57
57
|
default=None,
|
|
58
58
|
help=(
|
|
59
59
|
"Override the python command written to settings.json "
|
|
60
|
-
"(default:
|
|
60
|
+
"(default: the absolute path of the Python running this command; "
|
|
61
|
+
"'python' on Windows or 'python3' elsewhere if it is unknown)."
|
|
61
62
|
),
|
|
62
63
|
)
|
|
63
64
|
install.add_argument(
|
|
@@ -18,6 +18,10 @@ Configuration (environment variables):
|
|
|
18
18
|
entirely, for all events, unchanged.
|
|
19
19
|
AGENTGOVERN_HOOK_VERBOSE Default: 0 (0 | 1)
|
|
20
20
|
AGENTGOVERN_FAIL_ACTION Default: allow (allow | deny)
|
|
21
|
+
What a PreToolUse does when AgentGovern did not check the call. 'deny'
|
|
22
|
+
blocks it: on no answer at all, on an HTTP error answer, and (since
|
|
23
|
+
v2.5.1, NAGT-484) on a `timeout` verdict, the server's "Evaluation
|
|
24
|
+
Failed". 'allow' lets it run.
|
|
21
25
|
AGENTGOVERN_HOOK_TIMEOUT_MS Default: 5000
|
|
22
26
|
AGENTGOVERN_CAPTURE_MODE Default: enriched (enriched | minimal)
|
|
23
27
|
enriched — all 4 events (UserPromptSubmit, PreToolUse, PostToolUse, Stop)
|
|
@@ -49,6 +53,12 @@ policies_violated entry whose enforcement_mode matches the verdict
|
|
|
49
53
|
falling back to the first entry; and async events dropped on an HTTP error
|
|
50
54
|
are counted per session and reported once per session in verbose mode.
|
|
51
55
|
|
|
56
|
+
NAGT-484 (v2.5.1), docs/audit-reports/nagt-484-deny-on-errors-2026-10-22.md:
|
|
57
|
+
AGENTGOVERN_FAIL_ACTION=deny also blocks a PreToolUse that gets an HTTP error
|
|
58
|
+
back (a 429 included) or a `timeout` verdict, not only one that gets no answer.
|
|
59
|
+
The deny reason is the hook's own text and the HTTP status; nothing from the
|
|
60
|
+
response body reaches it. With the default (allow), nothing changes.
|
|
61
|
+
|
|
52
62
|
See README.md for installation instructions.
|
|
53
63
|
"""
|
|
54
64
|
from __future__ import annotations
|
|
@@ -70,7 +80,7 @@ from datetime import datetime, timezone
|
|
|
70
80
|
from pathlib import Path
|
|
71
81
|
from typing import Any
|
|
72
82
|
|
|
73
|
-
__version__ = "2.5.
|
|
83
|
+
__version__ = "2.5.1"
|
|
74
84
|
|
|
75
85
|
# ── Constants ──────────────────────────────────────────────────────────────────
|
|
76
86
|
|
|
@@ -487,6 +497,35 @@ def format_warn_message(evaluation: dict[str, Any]) -> str:
|
|
|
487
497
|
)
|
|
488
498
|
|
|
489
499
|
|
|
500
|
+
def _unchecked_deny_reason(what: str) -> str:
|
|
501
|
+
"""NAGT-484: the deny reason when AGENTGOVERN_FAIL_ACTION=deny blocks an unchecked call.
|
|
502
|
+
|
|
503
|
+
`what` is the hook's own words (and at most an HTTP status number): nothing
|
|
504
|
+
from the response body, which can come from an edge proxy, reaches Claude.
|
|
505
|
+
"""
|
|
506
|
+
return (
|
|
507
|
+
f"AgentGovern {what}, so this tool call was not checked. "
|
|
508
|
+
"AGENTGOVERN_FAIL_ACTION=deny: blocking it."
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _missing_config_deny_reason(missing: list[str]) -> str:
|
|
513
|
+
"""NAGT-500: the deny reason when AGENTGOVERN_FAIL_ACTION=deny blocks a call
|
|
514
|
+
the hook can't send for checking, because its configuration is missing.
|
|
515
|
+
|
|
516
|
+
`missing` holds variable NAMES only; their values are empty by definition,
|
|
517
|
+
and no other environment value is included.
|
|
518
|
+
"""
|
|
519
|
+
names = " and ".join(missing)
|
|
520
|
+
verb = "is" if len(missing) == 1 else "are"
|
|
521
|
+
return (
|
|
522
|
+
f"AgentGovern isn't set up in this shell: {names} {verb} not set, "
|
|
523
|
+
"so this tool call was not checked. AGENTGOVERN_FAIL_ACTION=deny: blocking it. "
|
|
524
|
+
"To fix it, paste your install snippet again from Settings → Developers "
|
|
525
|
+
f"in the AgentGovern dashboard, or set {names} and open a new terminal."
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
490
529
|
def render_deny_output(reason: str) -> str:
|
|
491
530
|
"""Return the JSON string for a PreToolUse deny decision."""
|
|
492
531
|
return json.dumps({
|
|
@@ -657,10 +696,11 @@ def _claim_session_notice(session_id: Any, kind: str = "stale_hook") -> bool:
|
|
|
657
696
|
|
|
658
697
|
|
|
659
698
|
# NAGT-400 (b): a PreToolUse HTTP 429 means the tenant's ingest rate limit was
|
|
660
|
-
# reached, so this tool call was not evaluated and is being allowed (
|
|
661
|
-
#
|
|
662
|
-
# only the debug log; this is the
|
|
663
|
-
# hook's own: nothing from the 429's
|
|
699
|
+
# reached, so this tool call was not evaluated and is being allowed (with
|
|
700
|
+
# AGENTGOVERN_FAIL_ACTION=deny it is blocked instead, NAGT-484, and this message
|
|
701
|
+
# is not shown). The stderr line reaches only the debug log; this is the
|
|
702
|
+
# developer-visible half. The text is the hook's own: nothing from the 429's
|
|
703
|
+
# body is shown.
|
|
664
704
|
_RATE_LIMIT_MESSAGE = (
|
|
665
705
|
"AgentGovern: your team reached Guard's rate limit, so this tool call was "
|
|
666
706
|
"not checked by Guard and was allowed. Please tell your team's AgentGovern admin."
|
|
@@ -1555,6 +1595,26 @@ def _handle_pre_tool_use(
|
|
|
1555
1595
|
_vlog("[agentgovern] Mode=off — skipping.")
|
|
1556
1596
|
return 0
|
|
1557
1597
|
|
|
1598
|
+
# NAGT-500: under deny, a call the hook can't send for checking (no key, no
|
|
1599
|
+
# actor id) is as unchecked as one AgentGovern didn't answer, so it is
|
|
1600
|
+
# blocked too. Under allow (the default) both cases below are unchanged.
|
|
1601
|
+
missing = [
|
|
1602
|
+
name
|
|
1603
|
+
for name, value in (
|
|
1604
|
+
("AGENTGOVERN_API_KEY", api_key),
|
|
1605
|
+
("AGENTGOVERN_ACTOR_USER_ID", actor_user_id),
|
|
1606
|
+
)
|
|
1607
|
+
if not value
|
|
1608
|
+
]
|
|
1609
|
+
if missing and fail_action == "deny":
|
|
1610
|
+
_err(
|
|
1611
|
+
f"[agentgovern] {' and '.join(missing)} not set — "
|
|
1612
|
+
"blocking (AGENTGOVERN_FAIL_ACTION=deny)."
|
|
1613
|
+
)
|
|
1614
|
+
sys.stdout.write(render_deny_output(_missing_config_deny_reason(missing)))
|
|
1615
|
+
sys.stdout.flush()
|
|
1616
|
+
return 0
|
|
1617
|
+
|
|
1558
1618
|
if not api_key:
|
|
1559
1619
|
_err(
|
|
1560
1620
|
"[agentgovern] AGENTGOVERN_API_KEY not set — "
|
|
@@ -1623,15 +1683,27 @@ def _handle_pre_tool_use(
|
|
|
1623
1683
|
if isinstance(err_obj, dict):
|
|
1624
1684
|
code = err_obj.get("code", "")
|
|
1625
1685
|
detail = f" ({code})" if code else ""
|
|
1686
|
+
if fail_action == "deny":
|
|
1687
|
+
# NAGT-484: an error answer leaves the call as unchecked as no
|
|
1688
|
+
# answer does. Only the status number is used, never the body.
|
|
1689
|
+
_err(f"[agentgovern] HTTP {status}{detail} from governance endpoint — blocking.")
|
|
1690
|
+
what = (
|
|
1691
|
+
"reached your team's rate limit (HTTP 429)"
|
|
1692
|
+
if status == 429
|
|
1693
|
+
else f"answered with an error (HTTP {status if isinstance(status, int) else '?'})"
|
|
1694
|
+
)
|
|
1695
|
+
sys.stdout.write(render_deny_output(_unchecked_deny_reason(what)))
|
|
1696
|
+
sys.stdout.flush()
|
|
1697
|
+
return 0
|
|
1626
1698
|
_err(
|
|
1627
1699
|
f"[agentgovern] HTTP {status}{detail} from governance endpoint "
|
|
1628
1700
|
"— proceeding unaudited."
|
|
1629
1701
|
)
|
|
1630
1702
|
if status == 429:
|
|
1631
1703
|
# NAGT-400 (b): the developer-visible half, at most once a minute
|
|
1632
|
-
# per session. systemMessage only, never a permissionDecision;
|
|
1633
|
-
# call is still allowed
|
|
1634
|
-
#
|
|
1704
|
+
# per session. systemMessage only, never a permissionDecision; with
|
|
1705
|
+
# the default fail action the call is still allowed, as every HTTP
|
|
1706
|
+
# error is. Fail-open inside.
|
|
1635
1707
|
_surface_rate_limited(hook_event.get("session_id"))
|
|
1636
1708
|
return 0
|
|
1637
1709
|
|
|
@@ -1670,6 +1742,14 @@ def _handle_pre_tool_use(
|
|
|
1670
1742
|
return 0
|
|
1671
1743
|
|
|
1672
1744
|
if verdict == "timeout":
|
|
1745
|
+
if fail_action == "deny":
|
|
1746
|
+
# NAGT-484: the server could not evaluate the call (it is marked
|
|
1747
|
+
# "Evaluation Failed"), so it was not checked.
|
|
1748
|
+
_err("[agentgovern] Policy evaluation timed out server-side — blocking.")
|
|
1749
|
+
reason = _unchecked_deny_reason("couldn't evaluate this tool call")
|
|
1750
|
+
sys.stdout.write(render_deny_output(reason))
|
|
1751
|
+
sys.stdout.flush()
|
|
1752
|
+
return 0
|
|
1673
1753
|
_err(
|
|
1674
1754
|
"[agentgovern] Policy evaluation timed out server-side "
|
|
1675
1755
|
"— tool call proceeding unaudited."
|
|
@@ -103,6 +103,19 @@ def _hook_target_path() -> Path:
|
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
def _detect_python_command() -> str:
|
|
106
|
+
"""The interpreter the hook entry runs: the one running this installer.
|
|
107
|
+
|
|
108
|
+
NAGT-490: a bare `python3` is looked up on the PATH Claude Code has when it
|
|
109
|
+
runs the hook, which need not be the shell's. Started from the Dock, it is
|
|
110
|
+
/usr/bin:/bin:..., where a Mac without the Command Line Tools has only a stub
|
|
111
|
+
python3 that asks to install them and runs nothing. sys.executable is the
|
|
112
|
+
absolute path of a Python that has this package, so 3.10 or later: in a
|
|
113
|
+
venv, the venv's own python. Bare names remain the fallback for an
|
|
114
|
+
interpreter that cannot say where it is.
|
|
115
|
+
"""
|
|
116
|
+
exe = sys.executable
|
|
117
|
+
if exe and os.path.isabs(exe) and os.path.isfile(exe):
|
|
118
|
+
return exe
|
|
106
119
|
return "python" if sys.platform == "win32" else "python3"
|
|
107
120
|
|
|
108
121
|
|
|
@@ -703,6 +716,10 @@ def uninstall() -> int:
|
|
|
703
716
|
print("AgentGovern hook is not installed (no entry found in settings.json).")
|
|
704
717
|
return 0
|
|
705
718
|
|
|
719
|
+
# NAGT-391 F8: the install-mode marker is Guard's own key, written by the
|
|
720
|
+
# install; it goes with the hook, so the settings are left as they were.
|
|
721
|
+
new_settings.pop(INSTALL_MODE_KEY, None)
|
|
722
|
+
|
|
706
723
|
if settings_path.exists():
|
|
707
724
|
backup(settings_path)
|
|
708
725
|
|
|
@@ -712,6 +729,12 @@ def uninstall() -> int:
|
|
|
712
729
|
print(f"Error: {exc}", file=sys.stderr)
|
|
713
730
|
return 1
|
|
714
731
|
|
|
732
|
+
# NAGT-391 F8: the backup only guards the write above, which succeeded. Left
|
|
733
|
+
# behind, it is a copy of the settings that still registers Guard's hook, and
|
|
734
|
+
# restoring it would point Claude Code at a hook script the uninstall
|
|
735
|
+
# snippet then deletes. (Found by the install rehearsal workflow.)
|
|
736
|
+
settings_path.with_suffix(".json.bak").unlink(missing_ok=True)
|
|
737
|
+
|
|
715
738
|
print(f"AgentGovern hook removed from {settings_path} (all event types).")
|
|
716
739
|
print(f"The hook script at {hook_target} was not deleted.")
|
|
717
740
|
return 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentgovern"
|
|
3
|
-
version = "2.7.
|
|
3
|
+
version = "2.7.3"
|
|
4
4
|
description = "Compliance-as-code middleware for agentic AI workflows."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -64,8 +64,7 @@ agentgovern = "agentgovern.__main__:main"
|
|
|
64
64
|
|
|
65
65
|
[project.urls]
|
|
66
66
|
Homepage = "https://agentgovern.zirahn.com"
|
|
67
|
-
|
|
68
|
-
Issues = "https://github.com/ahmedkhan-zirahn/agentgovern/issues"
|
|
67
|
+
Support = "https://zirahn.com/contact"
|
|
69
68
|
|
|
70
69
|
[build-system]
|
|
71
70
|
requires = ["hatchling"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "NAGT-403: action names made by the Guard hook's own functions (packages/claude-hook/agentgovern_hook.py: _truncate_for_action_name for prompts and replies, format_action_name for Bash). {GH} stands for a fake GitHub token, ghp_ followed by 36 'a', and {GHP} for its ghp_ prefix where a name cut it short; each test substitutes both (literal fragments would trip the repository's secret scan). test_action_name_cuts_fixture.py regenerates every name from the hook and fails if one differs.",
|
|
3
|
+
"cases": [
|
|
4
|
+
{
|
|
5
|
+
"id": "prompt-cut-through-token",
|
|
6
|
+
"kind": "prompt",
|
|
7
|
+
"source": "Create a .env file in this folder with the lines DEBUG=true and GITHUB_TOKEN={GH} and nothing else.",
|
|
8
|
+
"hook_action_name": "Create a .env file in this folder with the lines DEBUG=true and GITHUB_TOKEN={GHP}aaaaaaaaaaaaaaaa..."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "prompt-newlines-flattened",
|
|
12
|
+
"kind": "prompt",
|
|
13
|
+
"source": "Please set up the project.\nUse these settings:\nDEBUG=true\nAPI token for GitHub is GITHUB_TOKEN={GH}\nThanks",
|
|
14
|
+
"hook_action_name": "Please set up the project. Use these settings: DEBUG=true API token for GitHub is GITHUB_TOKEN=gh..."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "reply-cut-through-token",
|
|
18
|
+
"kind": "reply",
|
|
19
|
+
"source": "I created `/Users/someone/project/.env` with two lines. The second sets GITHUB_TOKEN={GH} as you asked.",
|
|
20
|
+
"hook_action_name": "I created `/Users/someone/project/.env` with two lines. The second sets GITHUB_TOKEN={GHP}aaaaaaaa..."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "bash-cut-at-200-through-token",
|
|
24
|
+
"kind": "bash",
|
|
25
|
+
"source": "echo 'configuring the deployment environment for the staging cluster, then writing the token into the local env file so that the gh CLI can authenticate' && export GITHUB_TOKEN={GH} && gh auth status",
|
|
26
|
+
"hook_action_name": "Bash: echo 'configuring the deployment environment for the staging cluster, then writing the token into the local env file so that the gh CLI can authenticate' && export GITHUB_TOKEN={GHP}aaaaaaaaaaaaa"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "prompt-not-cut",
|
|
30
|
+
"kind": "prompt",
|
|
31
|
+
"source": "Use GITHUB_TOKEN={GH} please",
|
|
32
|
+
"hook_action_name": "Use GITHUB_TOKEN={GH} please"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -324,38 +324,6 @@
|
|
|
324
324
|
"stdout": ""
|
|
325
325
|
}
|
|
326
326
|
],
|
|
327
|
-
"http-503-deny-mode": [
|
|
328
|
-
{
|
|
329
|
-
"rc": 0,
|
|
330
|
-
"sent": [
|
|
331
|
-
{
|
|
332
|
-
"action_name": "Bash: ls -la",
|
|
333
|
-
"action_type": "code_execution",
|
|
334
|
-
"actor_user_id": "user_test",
|
|
335
|
-
"client_timestamp": "<TS>",
|
|
336
|
-
"command_executed": "ls -la",
|
|
337
|
-
"context_snapshot": {
|
|
338
|
-
"claude_code_version": "unknown",
|
|
339
|
-
"cwd": null,
|
|
340
|
-
"git_branch": null,
|
|
341
|
-
"git_commit_sha": null,
|
|
342
|
-
"hook_version": "<HOOK_VERSION>"
|
|
343
|
-
},
|
|
344
|
-
"conversation_id": "sess-1",
|
|
345
|
-
"evaluation_mode": "sync",
|
|
346
|
-
"input": {
|
|
347
|
-
"command": "ls -la"
|
|
348
|
-
},
|
|
349
|
-
"schema_version": "1",
|
|
350
|
-
"source_type": "interactive_cli",
|
|
351
|
-
"source_version": "agentgovern-claude-hook/<HOOK_VERSION>",
|
|
352
|
-
"tool_event_type": "execute"
|
|
353
|
-
}
|
|
354
|
-
],
|
|
355
|
-
"stderr": "[agentgovern] Response in <N>ms\n[agentgovern] HTTP 503 from governance endpoint — proceeding unaudited.\n",
|
|
356
|
-
"stdout": ""
|
|
357
|
-
}
|
|
358
|
-
],
|
|
359
327
|
"minimal-block": [
|
|
360
328
|
{
|
|
361
329
|
"rc": 0,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""NAGT-403: the action-name cut fixture matches the hook that makes the names.
|
|
2
|
+
|
|
3
|
+
The web app's ingest masking (apps/web/lib/secret-masking/ingest.ts) is tested
|
|
4
|
+
against fixtures/action_name_cuts.json: prompts, replies and Bash commands with a
|
|
5
|
+
fake GitHub token, and the action name the Guard hook builds from each. This test
|
|
6
|
+
rebuilds every name with the hook's own functions, so a change to how the hook
|
|
7
|
+
cuts names fails here instead of leaving the web tests on a stale copy.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
|
|
16
|
+
FIXTURE = Path(__file__).parent / "fixtures" / "action_name_cuts.json"
|
|
17
|
+
GH = "ghp_" + "a" * 36
|
|
18
|
+
PLACEHOLDER = "{GH}"
|
|
19
|
+
PREFIX_PLACEHOLDER = "{GHP}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _expand(text: str) -> str:
|
|
23
|
+
return text.replace(PLACEHOLDER, GH).replace(PREFIX_PLACEHOLDER, "ghp_")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _name(hook_mod: ModuleType, kind: str, source: str) -> str:
|
|
27
|
+
if kind == "bash":
|
|
28
|
+
return hook_mod.format_action_name("Bash", {"command": source})
|
|
29
|
+
return hook_mod._truncate_for_action_name(source)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_every_fixture_name_is_what_the_hook_builds(hook_mod: ModuleType) -> None:
|
|
33
|
+
cases = json.loads(FIXTURE.read_text())["cases"]
|
|
34
|
+
assert {c["kind"] for c in cases} == {"prompt", "reply", "bash"}
|
|
35
|
+
for case in cases:
|
|
36
|
+
built = _name(hook_mod, case["kind"], _expand(case["source"]))
|
|
37
|
+
assert built == _expand(case["hook_action_name"]), case["id"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_the_cut_cases_really_cut_the_token_short(hook_mod: ModuleType) -> None:
|
|
41
|
+
"""Each *-cut-* case leaves a token fragment the masking pattern cannot match
|
|
42
|
+
({20,} after the prefix), which is the defect NAGT-403 fixes."""
|
|
43
|
+
cases = json.loads(FIXTURE.read_text())["cases"]
|
|
44
|
+
cut = [c for c in cases if "-cut-" in c["id"]]
|
|
45
|
+
assert len(cut) == 3
|
|
46
|
+
for case in cut:
|
|
47
|
+
assert PLACEHOLDER not in case["hook_action_name"], case["id"]
|
|
48
|
+
name = _expand(case["hook_action_name"])
|
|
49
|
+
tail = name.removesuffix("...").rsplit("ghp_", 1)[1]
|
|
50
|
+
assert 0 <= len(tail) < 20, case["id"]
|
|
@@ -135,9 +135,9 @@ def _only_system_message(stdout: str) -> str:
|
|
|
135
135
|
|
|
136
136
|
|
|
137
137
|
def test_version_is_2_5_0(env):
|
|
138
|
-
assert env.__version__ == "2.5.
|
|
139
|
-
assert env.HOOK_VERSION == "2.5.
|
|
140
|
-
assert env._SOURCE_VERSION == "agentgovern-claude-hook/2.5.
|
|
138
|
+
assert env.__version__ == "2.5.1"
|
|
139
|
+
assert env.HOOK_VERSION == "2.5.1"
|
|
140
|
+
assert env._SOURCE_VERSION == "agentgovern-claude-hook/2.5.1"
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
# ── 1. usage_limit notice ─────────────────────────────────────────────────────
|
|
@@ -373,13 +373,17 @@ def test_rate_limit_marker_that_is_not_a_regular_file_means_do_not_show(env, tmp
|
|
|
373
373
|
assert abs(directory.stat().st_mtime - old) < 1
|
|
374
374
|
|
|
375
375
|
|
|
376
|
-
def
|
|
376
|
+
def test_429_in_deny_mode_blocks_without_the_rate_limit_message(env, monkeypatch, capsys):
|
|
377
|
+
# NAGT-484 (hook 2.5.1): deny blocks an unchecked call, a 429 included,
|
|
378
|
+
# and the deny is the only output (no systemMessage beside it).
|
|
377
379
|
monkeypatch.setenv("AGENTGOVERN_FAIL_ACTION", "deny")
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
380
|
+
for _ in range(2):
|
|
381
|
+
rc, out, err = _pre(env, monkeypatch, capsys, _R429)
|
|
382
|
+
obj = json.loads(out)
|
|
383
|
+
assert rc == 0 and set(obj) == {"hookSpecificOutput"}
|
|
384
|
+
assert obj["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
385
|
+
assert "rate limit (HTTP 429)" in obj["hookSpecificOutput"]["permissionDecisionReason"]
|
|
386
|
+
assert "HTTP 429" in err
|
|
383
387
|
|
|
384
388
|
|
|
385
389
|
@pytest.mark.parametrize("status", [400, 401, 403, 404, 413, 428, 430, 500, 502, 503, 504])
|
|
@@ -18,6 +18,11 @@ Deliberately NOT in this table, because 2.5.0 changes them on purpose: an HTTP
|
|
|
18
18
|
429 on PreToolUse (now a systemMessage), a "usage_limit" notice (now shown),
|
|
19
19
|
entries carrying `enforcement_mode` (now selected by mode), and verbose mode on
|
|
20
20
|
a Stop event after a dropped async event (now reported).
|
|
21
|
+
|
|
22
|
+
Removed in 2.5.1 (NAGT-484), because it changes on purpose: an HTTP error on
|
|
23
|
+
PreToolUse with AGENTGOVERN_FAIL_ACTION=deny (the "http-503-deny-mode" case,
|
|
24
|
+
now a deny; packages/claude-hook/tests/test_deny_on_errors.py). Every other
|
|
25
|
+
case still matches 2.4.0 byte for byte.
|
|
21
26
|
"""
|
|
22
27
|
|
|
23
28
|
from __future__ import annotations
|
|
@@ -135,7 +140,6 @@ CASES: dict[str, list[tuple[dict[str, str], dict, Any]]] = {
|
|
|
135
140
|
"http-503": [({}, _pre(), _http(503, "SERVICE_UNAVAILABLE"))],
|
|
136
141
|
"http-403": [({}, _pre(), _http(403, "ACTOR_NOT_IN_TENANT"))],
|
|
137
142
|
"http-400-no-body": [({}, _pre(), _http(400))],
|
|
138
|
-
"http-503-deny-mode": [({"AGENTGOVERN_FAIL_ACTION": "deny"}, _pre(), _http(503))],
|
|
139
143
|
"network-allow": [({}, _pre(), None)],
|
|
140
144
|
"network-deny": [({"AGENTGOVERN_FAIL_ACTION": "deny"}, _pre(), None)],
|
|
141
145
|
"minimal-block": [({"AGENTGOVERN_CAPTURE_MODE": "minimal"}, _pre(), _ok("block", [_P1]))],
|
|
@@ -229,6 +233,6 @@ def test_golden_file_covers_exactly_these_cases() -> None:
|
|
|
229
233
|
|
|
230
234
|
def test_2_4_0_style_exchanges_are_byte_identical_to_2_4_0(hook_mod: ModuleType) -> None:
|
|
231
235
|
golden = json.loads(GOLDEN.read_text(encoding="utf-8"))["cases"]
|
|
232
|
-
assert hook_mod.__version__ == "2.5.
|
|
236
|
+
assert hook_mod.__version__ == "2.5.1"
|
|
233
237
|
for name, steps in CASES.items():
|
|
234
238
|
assert run_case(hook_mod, steps) == golden[name], name
|
|
@@ -92,10 +92,40 @@ def test_install_writes_absolute_path_to_args(
|
|
|
92
92
|
assert "%USERPROFILE%" not in args_val
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
def test_install_registers_the_running_python_by_absolute_path(
|
|
96
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
97
|
+
) -> None:
|
|
98
|
+
"""NAGT-490: the interpreter running `hook install` (a venv's python), not a bare name."""
|
|
99
|
+
home = _fake_home(tmp_path, monkeypatch)
|
|
100
|
+
venv_python = tmp_path / "my venv" / "bin" / "python"
|
|
101
|
+
venv_python.parent.mkdir(parents=True)
|
|
102
|
+
venv_python.write_text("")
|
|
103
|
+
monkeypatch.setattr(sys, "executable", str(venv_python))
|
|
104
|
+
for platform in ("darwin", "linux", "win32"):
|
|
105
|
+
monkeypatch.setattr(sys, "platform", platform)
|
|
106
|
+
installer.install(_default_args(force=True))
|
|
107
|
+
cfg = json.loads(_settings_path(home).read_text())
|
|
108
|
+
for event in ("PreToolUse", "UserPromptSubmit", "PostToolUse", "Stop"):
|
|
109
|
+
assert cfg["hooks"][event][0]["hooks"][0]["command"] == str(venv_python), event
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.mark.parametrize("executable", ["", "python3", "/no/such/python"])
|
|
113
|
+
def test_install_falls_back_to_a_bare_name_when_the_python_is_unknown(
|
|
114
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, executable: str
|
|
115
|
+
) -> None:
|
|
116
|
+
"""sys.executable can be empty, relative or gone; then the 2.7.2 default."""
|
|
117
|
+
monkeypatch.setattr(sys, "executable", executable)
|
|
118
|
+
monkeypatch.setattr(sys, "platform", "darwin")
|
|
119
|
+
assert installer._detect_python_command() == "python3"
|
|
120
|
+
monkeypatch.setattr(sys, "platform", "win32")
|
|
121
|
+
assert installer._detect_python_command() == "python"
|
|
122
|
+
|
|
123
|
+
|
|
95
124
|
def test_install_uses_python3_on_non_windows(
|
|
96
125
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
97
126
|
) -> None:
|
|
98
127
|
home = _fake_home(tmp_path, monkeypatch)
|
|
128
|
+
monkeypatch.setattr(sys, "executable", "")
|
|
99
129
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
100
130
|
monkeypatch.setattr(installer, "_python_on_path", lambda cmd: True)
|
|
101
131
|
installer.install(_default_args())
|
|
@@ -106,6 +136,7 @@ def test_install_uses_python3_on_non_windows(
|
|
|
106
136
|
|
|
107
137
|
def test_install_uses_python_on_windows(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
108
138
|
home = _fake_home(tmp_path, monkeypatch)
|
|
139
|
+
monkeypatch.setattr(sys, "executable", "")
|
|
109
140
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
110
141
|
monkeypatch.setattr(installer, "_python_on_path", lambda cmd: True)
|
|
111
142
|
installer.install(_default_args())
|
|
@@ -601,6 +632,37 @@ def test_uninstall_preserves_other_config(tmp_path: Path, monkeypatch: pytest.Mo
|
|
|
601
632
|
assert cfg.get("theme") == "dark"
|
|
602
633
|
|
|
603
634
|
|
|
635
|
+
def test_uninstall_leaves_the_settings_exactly_as_before_the_install(
|
|
636
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
637
|
+
) -> None:
|
|
638
|
+
"""NAGT-391 F8: no install-mode marker and no backup are left behind."""
|
|
639
|
+
home = _fake_home(tmp_path, monkeypatch)
|
|
640
|
+
before = {"theme": "dark", "permissions": {"allow": ["Bash(ls:*)"]}}
|
|
641
|
+
_write_json(_settings_path(home), before)
|
|
642
|
+
installer.install(_default_args())
|
|
643
|
+
assert _MARKER_KEY in json.loads(_settings_path(home).read_text())
|
|
644
|
+
assert _settings_path(home).with_suffix(".json.bak").exists()
|
|
645
|
+
assert installer.uninstall() == 0
|
|
646
|
+
assert json.loads(_settings_path(home).read_text()) == before
|
|
647
|
+
assert not _settings_path(home).with_suffix(".json.bak").exists()
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def test_uninstall_keeps_the_backup_when_the_write_fails(
|
|
651
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
652
|
+
) -> None:
|
|
653
|
+
"""The backup is removed only after a successful write."""
|
|
654
|
+
home = _fake_home(tmp_path, monkeypatch)
|
|
655
|
+
_write_json(_settings_path(home), {"theme": "dark"})
|
|
656
|
+
installer.install(_default_args())
|
|
657
|
+
|
|
658
|
+
def refuse(*_args, **_kwargs): # type: ignore[no-untyped-def]
|
|
659
|
+
raise PermissionError("read-only")
|
|
660
|
+
|
|
661
|
+
monkeypatch.setattr(installer, "atomic_write", refuse)
|
|
662
|
+
assert installer.uninstall() == 1
|
|
663
|
+
assert _settings_path(home).with_suffix(".json.bak").exists()
|
|
664
|
+
|
|
665
|
+
|
|
604
666
|
def test_uninstall_not_installed_exits_zero(
|
|
605
667
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
|
|
606
668
|
) -> None:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.7.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentgovern-2.7.0 → agentgovern-2.7.3}/tests/hook/fixtures/claude_code_pretooluse_output_keys.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|