agentgovern 2.2.1__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentgovern-2.2.1 → agentgovern-2.3.0}/.gitignore +7 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/CHANGELOG.md +102 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/PKG-INFO +43 -29
- {agentgovern-2.2.1 → agentgovern-2.3.0}/README.md +39 -10
- {agentgovern-2.2.1 → agentgovern-2.3.0}/RELEASING.md +5 -3
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/__init__.py +54 -11
- agentgovern-2.3.0/agentgovern/_version.py +1 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/hook/data/hook.py +30 -5
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/hook/drift.py +22 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/hook/installer.py +124 -43
- {agentgovern-2.2.1 → agentgovern-2.3.0}/pyproject.toml +15 -6
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_hook_v2_context_snapshot.py +22 -4
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_hook_v2_payloads.py +77 -20
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_installer.py +151 -45
- agentgovern-2.3.0/tests/hook/test_matcher_coverage.py +115 -0
- agentgovern-2.3.0/tests/hook/test_tool_map_classification.py +68 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_client.py +40 -4
- agentgovern-2.3.0/tests/test_import_guard.py +222 -0
- agentgovern-2.3.0/tests/test_langchain_1x_agent.py +166 -0
- agentgovern-2.2.1/agentgovern/_version.py +0 -1
- {agentgovern-2.2.1 → agentgovern-2.3.0}/LICENSE +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/__main__.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/client.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/exceptions.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/gate2.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/hook/__init__.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/hook/cli.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/hook/data/__init__.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/hook/settings.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/instrumentors/__init__.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/instrumentors/_base.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/instrumentors/langchain.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/instrumentors/langgraph.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentgovern/types.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentshield/__init__.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentshield/client.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/agentshield/types.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/examples/langgraph_credit_decision_agent.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/__init__.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/__init__.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/conftest.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_bundled_hook.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_drift.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_hook_v2_dispatch.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_hook_v2_outbox_queue.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_hook_v2_payload_extraction.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_hook_v2_remediation_message.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_hook_v2_timing_log.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_hook_v2_transcript_reading.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_hook_v2_turn_index.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_hook_v2_warn_verdict.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/hook/test_settings.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_base_instrumentor.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_evaluate_input.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_gate2_full_path.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_gate2_latency.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_gate2_verdict_handler.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_instrument_langchain.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_langchain.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_langchain_gate1.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_langchain_gate2_callbacks.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_langgraph.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_output_capture_contract.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_payload_schema.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_rate_limit_handling.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_register_contract.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_timestamp.py +0 -0
- {agentgovern-2.2.1 → agentgovern-2.3.0}/tests/test_version_consistency.py +0 -0
|
@@ -32,6 +32,13 @@ test-results/
|
|
|
32
32
|
# Not project config — skills/ and agents/ under .claude/ remain tracked.
|
|
33
33
|
.claude/settings.json
|
|
34
34
|
.claude/settings.local.json
|
|
35
|
+
# Backups of the above, which carry the same content and the same reason to stay
|
|
36
|
+
# untracked. Added 2026-08-21: the exact-filename rules above do not match a
|
|
37
|
+
# `.bak-*` sibling, so a pre-change backup sat untracked-but-not-ignored inside
|
|
38
|
+
# the repo, where a `git add -A` would have staged it — and, with both `ask`
|
|
39
|
+
# arrays now empty, without prompting.
|
|
40
|
+
.claude/settings.json.bak-*
|
|
41
|
+
.claude/settings.local.json.bak-*
|
|
35
42
|
|
|
36
43
|
.vercel
|
|
37
44
|
|
|
@@ -1,5 +1,107 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.3.0 — release date is the `sdk-v2.3.0` tag's (prepared 2026-09-29)
|
|
4
|
+
|
|
5
|
+
### Security — the Claude Code hook now sees every tool call, but only after you reinstall it (NAGT-187)
|
|
6
|
+
|
|
7
|
+
Until this release, `agentgovern hook install` registered the hook for a fixed
|
|
8
|
+
list of tools (ten in 2.2.1: `Bash`, `Write`, `Edit`, `MultiEdit`,
|
|
9
|
+
`NotebookEdit`, `Read`, `Glob`, `Grep`, `WebFetch`, `WebSearch`). **A tool call
|
|
10
|
+
through any other tool never reached the hook, so no policy was applied to
|
|
11
|
+
it.** On Windows, Claude Code's `PowerShell` tool was one such tool: every
|
|
12
|
+
content rule could be bypassed by routing a command through it.
|
|
13
|
+
|
|
14
|
+
- The `PreToolUse` and `PostToolUse` matchers are now `*`: every tool,
|
|
15
|
+
including tools that do not exist yet.
|
|
16
|
+
- `PowerShell` is classified like `Bash` (`execute` / `code_execution`).
|
|
17
|
+
- A tool the hook has no classification for is recorded as
|
|
18
|
+
`action_type="unmapped_tool"`. Before, it was recorded as `"custom"`, the
|
|
19
|
+
value the SDK reserves for your own `track_action()` calls. **A policy that
|
|
20
|
+
relied on `"custom"` as a catch-all for unknown tools needs
|
|
21
|
+
`"unmapped_tool"` added.**
|
|
22
|
+
- The bundled hook script is version 2.3.0.
|
|
23
|
+
|
|
24
|
+
> **Action required for existing installs.** Upgrading the package does not
|
|
25
|
+
> change a hook that is already installed. Until you reinstall it, the hook
|
|
26
|
+
> keeps its old tool list:
|
|
27
|
+
>
|
|
28
|
+
> ```bash
|
|
29
|
+
> pip install --upgrade agentgovern
|
|
30
|
+
> agentgovern hook install --force # repeat --capture-mode=minimal, --matcher or --python-command if you used them
|
|
31
|
+
> agentgovern hook status # no [DRIFT] lines once it is current
|
|
32
|
+
> ```
|
|
33
|
+
>
|
|
34
|
+
> Then restart any running Claude Code session. `--force` writes the current
|
|
35
|
+
> defaults and does not remember options from the earlier install, so pass
|
|
36
|
+
> them again. Without `--capture-mode=minimal`, it turns a minimal install
|
|
37
|
+
> into a full one.
|
|
38
|
+
|
|
39
|
+
### Change — `hook install` tells you when an existing hook is stale (NAGT-200)
|
|
40
|
+
|
|
41
|
+
Before, running `agentgovern hook install` against an existing hook said
|
|
42
|
+
*"already installed"*, even when that hook was out of date. It now adds which
|
|
43
|
+
matcher, timeout or script version differs from what this package would
|
|
44
|
+
write, and says to re-run with `--force`. **It still does not upgrade the
|
|
45
|
+
hook itself.** The exit code and every refusal are unchanged, and `--force`
|
|
46
|
+
is still required to write anything.
|
|
47
|
+
|
|
48
|
+
### Fix — `httpx` and `pydantic` are now required dependencies (NAGT-324)
|
|
49
|
+
|
|
50
|
+
`pip install agentgovern` installed no dependencies at all. The package
|
|
51
|
+
imported cleanly, but 13 of its 23 public names, including `ActionType`,
|
|
52
|
+
`ActionStatus` and `AgentGovernClient`, were bound to `None`. `init()` raised
|
|
53
|
+
a clear `ImportError`, so any documented integration failed loudly at
|
|
54
|
+
startup. But `register_agent()`, `track_action()` and `shutdown()` return
|
|
55
|
+
silently when the SDK is uninitialised, so an integration that skipped or
|
|
56
|
+
swallowed `init()` recorded nothing and reported nothing.
|
|
57
|
+
|
|
58
|
+
This affected every release from 0.2.0 through 2.2.1. LangChain and LangGraph
|
|
59
|
+
users were unaffected in practice: `langchain-core` depends on both `httpx`
|
|
60
|
+
and `pydantic`, so those extras' own dependencies supplied them.
|
|
61
|
+
|
|
62
|
+
- `httpx>=0.27.0` and `pydantic>=2.10.0` are now unconditional dependencies.
|
|
63
|
+
A bare install now also brings in their own dependencies: `anyio`,
|
|
64
|
+
`certifi`, `h11`, `httpcore`, `idna`, `pydantic-core`, `annotated-types`,
|
|
65
|
+
`typing-extensions` and `typing-inspection`.
|
|
66
|
+
- **`python-dotenv` is removed.** No module in the package ever imported it.
|
|
67
|
+
If you relied on `agentgovern[sdk]` to install it, add it directly.
|
|
68
|
+
- The `[sdk]` extra is kept, now empty, so `pip install "agentgovern[sdk]"`,
|
|
69
|
+
`[langchain]` and `[langgraph]` keep working unchanged, with no pip warning.
|
|
70
|
+
- The import guard still catches any `ImportError`, so the hook CLI keeps
|
|
71
|
+
working in a `pip install --no-deps` or half-installed environment. **What
|
|
72
|
+
changed is the message:** `init()` now reports the error that actually
|
|
73
|
+
occurred. Before, it always told you to install the `[sdk]` extra, even
|
|
74
|
+
when that extra was already installed and something else had failed.
|
|
75
|
+
- `init()`'s docstring now says what the fail-silent promise covers
|
|
76
|
+
(AgentGovern being *unreachable* once initialised). It also says what it
|
|
77
|
+
does not cover (the SDK being absent or misconfigured), and why `init()`
|
|
78
|
+
should not be wrapped in a bare `except`.
|
|
79
|
+
|
|
80
|
+
### Docs
|
|
81
|
+
|
|
82
|
+
- The LangChain samples in this README are updated for langchain 1.x, which
|
|
83
|
+
removed `AgentExecutor` and `create_openai_tools_agent`. They now use
|
|
84
|
+
`langchain.agents.create_agent` and pass the handler as
|
|
85
|
+
`config={"callbacks": [handler]}`. A new test runs such an agent and
|
|
86
|
+
confirms the handler's callbacks fire.
|
|
87
|
+
- New README section: *Upgrading an existing install*.
|
|
88
|
+
|
|
89
|
+
### Internal (not part of the installed package)
|
|
90
|
+
|
|
91
|
+
- A new release gate: the wheel is built, its declared dependencies are
|
|
92
|
+
checked, and it is installed into a clean environment and imported before
|
|
93
|
+
it is uploaded. This runs on every pull request and in the publish workflow
|
|
94
|
+
itself.
|
|
95
|
+
- `langchain>=1.0.0` added to the `dev` extra, for the new test.
|
|
96
|
+
- Test fixes that make the suite trustworthy rather than change behaviour:
|
|
97
|
+
- NAGT-280: a test that pinned the hook version to the stale literal
|
|
98
|
+
`2.1.0` is removed; the test that checks the snapshot against the
|
|
99
|
+
module's own version stays;
|
|
100
|
+
- NAGT-326: Stop-hook tests no longer assume the temp directory is inside
|
|
101
|
+
`$HOME`;
|
|
102
|
+
- NAGT-327: the buffer-cap test is deterministic, with no live flush thread
|
|
103
|
+
and no network.
|
|
104
|
+
|
|
3
105
|
## 2.2.1 — 2026-08-13
|
|
4
106
|
|
|
5
107
|
### Fix — `register_agent()` silently no-op'd when called immediately after `init()`, the SDK's own documented call pattern (STORY-109)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentgovern
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Compliance-as-code middleware for agentic AI workflows.
|
|
5
5
|
Project-URL: Homepage, https://agentgovern.zirahn.com
|
|
6
6
|
Project-URL: Repository, https://github.com/ahmedkhan-zirahn/agentgovern
|
|
@@ -20,40 +20,25 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
21
|
Classifier: Topic :: System :: Monitoring
|
|
22
22
|
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Requires-Dist: pydantic>=2.10.0
|
|
23
25
|
Provides-Extra: crewai
|
|
24
26
|
Requires-Dist: crewai>=0.80.0; extra == 'crewai'
|
|
25
|
-
Requires-Dist: httpx>=0.27.0; extra == 'crewai'
|
|
26
|
-
Requires-Dist: pydantic>=2.10.0; extra == 'crewai'
|
|
27
|
-
Requires-Dist: python-dotenv>=1.0.0; extra == 'crewai'
|
|
28
27
|
Provides-Extra: dev
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist: pydantic>=2.10.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: langchain>=1.0.0; extra == 'dev'
|
|
31
29
|
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
32
30
|
Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
|
|
33
31
|
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
34
|
-
Requires-Dist: python-dotenv>=1.0.0; extra == 'dev'
|
|
35
32
|
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
36
33
|
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
37
34
|
Provides-Extra: langchain
|
|
38
|
-
Requires-Dist: httpx>=0.27.0; extra == 'langchain'
|
|
39
35
|
Requires-Dist: langchain-core>=0.3.0; extra == 'langchain'
|
|
40
|
-
Requires-Dist: pydantic>=2.10.0; extra == 'langchain'
|
|
41
|
-
Requires-Dist: python-dotenv>=1.0.0; extra == 'langchain'
|
|
42
36
|
Provides-Extra: langgraph
|
|
43
|
-
Requires-Dist: httpx>=0.27.0; extra == 'langgraph'
|
|
44
37
|
Requires-Dist: langchain-core>=0.3.0; extra == 'langgraph'
|
|
45
38
|
Requires-Dist: langgraph<2.0.0,>=0.2.0; extra == 'langgraph'
|
|
46
|
-
Requires-Dist: pydantic>=2.10.0; extra == 'langgraph'
|
|
47
|
-
Requires-Dist: python-dotenv>=1.0.0; extra == 'langgraph'
|
|
48
39
|
Provides-Extra: openai
|
|
49
|
-
Requires-Dist: httpx>=0.27.0; extra == 'openai'
|
|
50
40
|
Requires-Dist: openai>=1.50.0; extra == 'openai'
|
|
51
|
-
Requires-Dist: pydantic>=2.10.0; extra == 'openai'
|
|
52
|
-
Requires-Dist: python-dotenv>=1.0.0; extra == 'openai'
|
|
53
41
|
Provides-Extra: sdk
|
|
54
|
-
Requires-Dist: httpx>=0.27.0; extra == 'sdk'
|
|
55
|
-
Requires-Dist: pydantic>=2.10.0; extra == 'sdk'
|
|
56
|
-
Requires-Dist: python-dotenv>=1.0.0; extra == 'sdk'
|
|
57
42
|
Description-Content-Type: text/markdown
|
|
58
43
|
|
|
59
44
|
# AgentGovern Python SDK
|
|
@@ -74,7 +59,7 @@ pip install "agentgovern[langchain]"
|
|
|
74
59
|
|
|
75
60
|
```python
|
|
76
61
|
import agentgovern
|
|
77
|
-
from langchain.agents import
|
|
62
|
+
from langchain.agents import create_agent
|
|
78
63
|
from langchain_openai import ChatOpenAI
|
|
79
64
|
|
|
80
65
|
# 1. Initialize once at startup
|
|
@@ -94,12 +79,13 @@ agentgovern.register_agent(
|
|
|
94
79
|
# 3. Get the callback handler — binds to credit-scoring-v2 automatically
|
|
95
80
|
handler = agentgovern.instrument_langchain()
|
|
96
81
|
|
|
97
|
-
# 4.
|
|
98
|
-
|
|
99
|
-
agent = create_openai_tools_agent(llm, tools, prompt)
|
|
100
|
-
executor = AgentExecutor(agent=agent, tools=tools, callbacks=[handler])
|
|
82
|
+
# 4. Build the agent and pass the handler via config — no other changes needed
|
|
83
|
+
agent = create_agent(model=ChatOpenAI(model="gpt-4o"), tools=tools)
|
|
101
84
|
|
|
102
|
-
result =
|
|
85
|
+
result = agent.invoke(
|
|
86
|
+
{"messages": [{"role": "user", "content": "Evaluate loan application for customer #12345"}]},
|
|
87
|
+
config={"callbacks": [handler]},
|
|
88
|
+
)
|
|
103
89
|
```
|
|
104
90
|
|
|
105
91
|
`instrument_langchain()` binds to the most recently registered agent. Every tool call,
|
|
@@ -154,8 +140,11 @@ agentgovern.register_agent("kyc-agent", name="KYC Agent")
|
|
|
154
140
|
handler_fraud = agentgovern.instrument_langchain("fraud-detector")
|
|
155
141
|
handler_kyc = agentgovern.instrument_langchain("kyc-agent")
|
|
156
142
|
|
|
157
|
-
|
|
158
|
-
|
|
143
|
+
fraud_agent = create_agent(model=..., tools=...)
|
|
144
|
+
kyc_agent = create_agent(model=..., tools=...)
|
|
145
|
+
|
|
146
|
+
fraud_agent.invoke(inputs, config={"callbacks": [handler_fraud]})
|
|
147
|
+
kyc_agent.invoke(inputs, config={"callbacks": [handler_kyc]})
|
|
159
148
|
```
|
|
160
149
|
|
|
161
150
|
## Manual instrumentation (all frameworks)
|
|
@@ -206,8 +195,10 @@ Enable policy packs from the [AgentGovern dashboard](https://agentgovern.zirahn.
|
|
|
206
195
|
## Claude Code Hook
|
|
207
196
|
|
|
208
197
|
The SDK ships a built-in CLI for installing the AgentGovern hook into Claude Code. The hook
|
|
209
|
-
intercepts `PreToolUse` events and enforces your tenant policies in real
|
|
210
|
-
|
|
198
|
+
intercepts `PreToolUse` events for every tool call and enforces your tenant policies in real
|
|
199
|
+
time — before the tool executes. A tool AgentGovern doesn't yet have a specific classification
|
|
200
|
+
for is still captured and evaluated, tagged `action_type=unmapped_tool` rather than silently
|
|
201
|
+
skipped.
|
|
211
202
|
|
|
212
203
|
### Install
|
|
213
204
|
|
|
@@ -219,6 +210,29 @@ This deploys the hook script to `~/.agentgovern/hook.py`, merges the correct ent
|
|
|
219
210
|
`~/.claude/settings.json` (with an atomic backup), and prints instructions for any missing
|
|
220
211
|
env vars.
|
|
221
212
|
|
|
213
|
+
### Upgrading an existing install
|
|
214
|
+
|
|
215
|
+
Upgrading the package does **not** update a hook that is already installed. `hook install`
|
|
216
|
+
copies the hook script to `~/.agentgovern/hook.py` and writes its entries into
|
|
217
|
+
`~/.claude/settings.json`, and a plain `agentgovern hook install` against an existing install
|
|
218
|
+
reports it as already installed and changes nothing. After upgrading, reinstall with `--force`:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
pip install --upgrade agentgovern # or the extra you installed with, e.g. "agentgovern[langchain]"
|
|
222
|
+
agentgovern hook install --force
|
|
223
|
+
agentgovern hook status # reports no [DRIFT] lines once the hook is current
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`--force` writes the current defaults. It does not remember options from the earlier install: if
|
|
227
|
+
you installed with `--capture-mode=minimal`, `--matcher` or `--python-command`, pass them again —
|
|
228
|
+
`--force` without `--capture-mode=minimal` turns a minimal install into a full one.
|
|
229
|
+
Your environment variables are not touched. Restart any running Claude Code session afterwards.
|
|
230
|
+
|
|
231
|
+
**If your hook was installed by 2.2.1 or earlier, this step matters for coverage, not just
|
|
232
|
+
freshness.** Those versions wrote a `PreToolUse` matcher naming a fixed list of tools (ten, in 2.2.1), so a call
|
|
233
|
+
through any other tool (`PowerShell`, for example) never reached the hook. Starting with 2.3.0
|
|
234
|
+
the matcher is `*`, every tool — but only once `hook install --force` has rewritten the entry.
|
|
235
|
+
|
|
222
236
|
### Required env vars
|
|
223
237
|
|
|
224
238
|
```bash
|
|
@@ -16,7 +16,7 @@ pip install "agentgovern[langchain]"
|
|
|
16
16
|
|
|
17
17
|
```python
|
|
18
18
|
import agentgovern
|
|
19
|
-
from langchain.agents import
|
|
19
|
+
from langchain.agents import create_agent
|
|
20
20
|
from langchain_openai import ChatOpenAI
|
|
21
21
|
|
|
22
22
|
# 1. Initialize once at startup
|
|
@@ -36,12 +36,13 @@ agentgovern.register_agent(
|
|
|
36
36
|
# 3. Get the callback handler — binds to credit-scoring-v2 automatically
|
|
37
37
|
handler = agentgovern.instrument_langchain()
|
|
38
38
|
|
|
39
|
-
# 4.
|
|
40
|
-
|
|
41
|
-
agent = create_openai_tools_agent(llm, tools, prompt)
|
|
42
|
-
executor = AgentExecutor(agent=agent, tools=tools, callbacks=[handler])
|
|
39
|
+
# 4. Build the agent and pass the handler via config — no other changes needed
|
|
40
|
+
agent = create_agent(model=ChatOpenAI(model="gpt-4o"), tools=tools)
|
|
43
41
|
|
|
44
|
-
result =
|
|
42
|
+
result = agent.invoke(
|
|
43
|
+
{"messages": [{"role": "user", "content": "Evaluate loan application for customer #12345"}]},
|
|
44
|
+
config={"callbacks": [handler]},
|
|
45
|
+
)
|
|
45
46
|
```
|
|
46
47
|
|
|
47
48
|
`instrument_langchain()` binds to the most recently registered agent. Every tool call,
|
|
@@ -96,8 +97,11 @@ agentgovern.register_agent("kyc-agent", name="KYC Agent")
|
|
|
96
97
|
handler_fraud = agentgovern.instrument_langchain("fraud-detector")
|
|
97
98
|
handler_kyc = agentgovern.instrument_langchain("kyc-agent")
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
fraud_agent = create_agent(model=..., tools=...)
|
|
101
|
+
kyc_agent = create_agent(model=..., tools=...)
|
|
102
|
+
|
|
103
|
+
fraud_agent.invoke(inputs, config={"callbacks": [handler_fraud]})
|
|
104
|
+
kyc_agent.invoke(inputs, config={"callbacks": [handler_kyc]})
|
|
101
105
|
```
|
|
102
106
|
|
|
103
107
|
## Manual instrumentation (all frameworks)
|
|
@@ -148,8 +152,10 @@ Enable policy packs from the [AgentGovern dashboard](https://agentgovern.zirahn.
|
|
|
148
152
|
## Claude Code Hook
|
|
149
153
|
|
|
150
154
|
The SDK ships a built-in CLI for installing the AgentGovern hook into Claude Code. The hook
|
|
151
|
-
intercepts `PreToolUse` events and enforces your tenant policies in real
|
|
152
|
-
|
|
155
|
+
intercepts `PreToolUse` events for every tool call and enforces your tenant policies in real
|
|
156
|
+
time — before the tool executes. A tool AgentGovern doesn't yet have a specific classification
|
|
157
|
+
for is still captured and evaluated, tagged `action_type=unmapped_tool` rather than silently
|
|
158
|
+
skipped.
|
|
153
159
|
|
|
154
160
|
### Install
|
|
155
161
|
|
|
@@ -161,6 +167,29 @@ This deploys the hook script to `~/.agentgovern/hook.py`, merges the correct ent
|
|
|
161
167
|
`~/.claude/settings.json` (with an atomic backup), and prints instructions for any missing
|
|
162
168
|
env vars.
|
|
163
169
|
|
|
170
|
+
### Upgrading an existing install
|
|
171
|
+
|
|
172
|
+
Upgrading the package does **not** update a hook that is already installed. `hook install`
|
|
173
|
+
copies the hook script to `~/.agentgovern/hook.py` and writes its entries into
|
|
174
|
+
`~/.claude/settings.json`, and a plain `agentgovern hook install` against an existing install
|
|
175
|
+
reports it as already installed and changes nothing. After upgrading, reinstall with `--force`:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
pip install --upgrade agentgovern # or the extra you installed with, e.g. "agentgovern[langchain]"
|
|
179
|
+
agentgovern hook install --force
|
|
180
|
+
agentgovern hook status # reports no [DRIFT] lines once the hook is current
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`--force` writes the current defaults. It does not remember options from the earlier install: if
|
|
184
|
+
you installed with `--capture-mode=minimal`, `--matcher` or `--python-command`, pass them again —
|
|
185
|
+
`--force` without `--capture-mode=minimal` turns a minimal install into a full one.
|
|
186
|
+
Your environment variables are not touched. Restart any running Claude Code session afterwards.
|
|
187
|
+
|
|
188
|
+
**If your hook was installed by 2.2.1 or earlier, this step matters for coverage, not just
|
|
189
|
+
freshness.** Those versions wrote a `PreToolUse` matcher naming a fixed list of tools (ten, in 2.2.1), so a call
|
|
190
|
+
through any other tool (`PowerShell`, for example) never reached the hook. Starting with 2.3.0
|
|
191
|
+
the matcher is `*`, every tool — but only once `hook install --force` has rewritten the entry.
|
|
192
|
+
|
|
164
193
|
### Required env vars
|
|
165
194
|
|
|
166
195
|
```bash
|
|
@@ -11,15 +11,17 @@ Before the first automated publish, configure trusted publishing on PyPI:
|
|
|
11
11
|
|
|
12
12
|
| Field | Value |
|
|
13
13
|
|-------------------|------------------------------|
|
|
14
|
-
| Owner |
|
|
14
|
+
| Owner | ZirahnAI |
|
|
15
15
|
| Repository name | agentgovern |
|
|
16
16
|
| Workflow filename | publish-sdk.yml |
|
|
17
17
|
| Environment name | pypi |
|
|
18
18
|
|
|
19
|
+
**Note:** the repository moved from `ahmedkhan-zirahn` to `ZirahnAI` in September 2026. A publisher for the old owner existed and was **deleted on 2026-09-14**. A trusted publisher that names a path the organisation no longer controls is a standing binding to a namespace someone else could later claim. See NAGT-314 in `docs/ROADMAP.md`.
|
|
20
|
+
|
|
19
21
|
3. Save. No API token needed after this.
|
|
20
22
|
|
|
21
23
|
Also create the `pypi` environment in GitHub:
|
|
22
|
-
- Go to https://github.com/
|
|
24
|
+
- Go to https://github.com/ZirahnAI/agentgovern/settings/environments
|
|
23
25
|
- Create environment named `pypi` (no protection rules required)
|
|
24
26
|
|
|
25
27
|
## Release steps
|
|
@@ -53,7 +55,7 @@ Also create the `pypi` environment in GitHub:
|
|
|
53
55
|
|
|
54
56
|
If you need to publish without tagging (e.g., to re-publish after a failed run), trigger the workflow manually:
|
|
55
57
|
|
|
56
|
-
1. Go to https://github.com/
|
|
58
|
+
1. Go to https://github.com/ZirahnAI/agentgovern/actions/workflows/publish-sdk.yml
|
|
57
59
|
2. Click **Run workflow** → select branch `main` → **Run workflow**
|
|
58
60
|
|
|
59
61
|
The manual run builds and publishes whatever version is currently in `packages/sdk-python/pyproject.toml`. Make sure the version in `pyproject.toml` and `_version.py` match before triggering.
|
|
@@ -29,9 +29,23 @@ from typing import Any
|
|
|
29
29
|
|
|
30
30
|
from agentgovern._version import __version__
|
|
31
31
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
32
|
+
# httpx and pydantic are REQUIRED dependencies (NAGT-324). A normal
|
|
33
|
+
# `pip install agentgovern` always has them, so this guard is not the
|
|
34
|
+
# optional-extra switch it used to be.
|
|
35
|
+
#
|
|
36
|
+
# It still exists for one case: an install that deliberately suppressed
|
|
37
|
+
# dependency resolution (`pip install --no-deps`), or one that is broken or
|
|
38
|
+
# half-finished. In those, the Claude Code hook CLI -- which is stdlib-only
|
|
39
|
+
# all the way down -- must keep working, and it cannot, because importing
|
|
40
|
+
# `agentgovern.__main__` imports THIS module first.
|
|
41
|
+
#
|
|
42
|
+
# The catch stays BROAD on purpose. Narrowing it to `_exc.name in ("httpx",
|
|
43
|
+
# "pydantic")` was tried and withdrawn: ImportError.name is the INNERMOST
|
|
44
|
+
# missing module, so a broken `pydantic_core` (a compiled wheel, and one of
|
|
45
|
+
# pydantic's most common real failures) reports 'pydantic_core', gets
|
|
46
|
+
# re-raised, and takes the hook CLI down in exactly the case this guard is
|
|
47
|
+
# for. What was actually wrong was the DIAGNOSTIC, not the breadth -- so the
|
|
48
|
+
# exception is recorded and `_require_sdk()` reports what really happened.
|
|
35
49
|
try:
|
|
36
50
|
from agentgovern.client import AgentGovernClient
|
|
37
51
|
from agentgovern.exceptions import (
|
|
@@ -51,8 +65,10 @@ try:
|
|
|
51
65
|
)
|
|
52
66
|
|
|
53
67
|
_SDK_AVAILABLE = True
|
|
54
|
-
|
|
68
|
+
_SDK_IMPORT_ERROR: ImportError | None = None
|
|
69
|
+
except ImportError as _exc:
|
|
55
70
|
_SDK_AVAILABLE = False
|
|
71
|
+
_SDK_IMPORT_ERROR = _exc
|
|
56
72
|
# Provide placeholder names so `from agentgovern import X` fails at call
|
|
57
73
|
# time with a clear message rather than at import time.
|
|
58
74
|
AgentGovernClient = None # type: ignore[assignment,misc]
|
|
@@ -71,12 +87,29 @@ except ImportError:
|
|
|
71
87
|
|
|
72
88
|
|
|
73
89
|
def _require_sdk() -> None:
|
|
90
|
+
"""Raise if the SDK half of this package could not be imported.
|
|
91
|
+
|
|
92
|
+
The message is derived from the exception actually raised, never assumed.
|
|
93
|
+
Before NAGT-324 this always said "install agentgovern[sdk]", which was
|
|
94
|
+
wrong whenever the real cause was anything else -- it told people to
|
|
95
|
+
install a dependency that was, in those cases, already installed.
|
|
96
|
+
"""
|
|
74
97
|
if not _SDK_AVAILABLE:
|
|
98
|
+
_missing = getattr(_SDK_IMPORT_ERROR, "name", None)
|
|
99
|
+
if _missing in ("httpx", "pydantic"):
|
|
100
|
+
raise ImportError(
|
|
101
|
+
f"The AgentGovern SDK requires {_missing}, which is not "
|
|
102
|
+
f"importable. It is a declared dependency, so this usually "
|
|
103
|
+
f"means the install skipped dependency resolution "
|
|
104
|
+
f"(pip install --no-deps) or is incomplete. Reinstall with: "
|
|
105
|
+
f"pip install --force-reinstall agentgovern"
|
|
106
|
+
) from _SDK_IMPORT_ERROR
|
|
75
107
|
raise ImportError(
|
|
76
|
-
"The AgentGovern SDK
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
|
|
108
|
+
f"The AgentGovern SDK failed to load: {_SDK_IMPORT_ERROR!r}. "
|
|
109
|
+
f"This is NOT a missing optional extra -- the installation is "
|
|
110
|
+
f"broken or incomplete. Reinstall with: "
|
|
111
|
+
f"pip install --force-reinstall agentgovern"
|
|
112
|
+
) from _SDK_IMPORT_ERROR
|
|
80
113
|
|
|
81
114
|
|
|
82
115
|
_client: AgentGovernClient | None = None
|
|
@@ -100,8 +133,17 @@ def init(
|
|
|
100
133
|
Initialize the AgentGovern SDK.
|
|
101
134
|
|
|
102
135
|
Must be called once at application startup before any other SDK calls.
|
|
103
|
-
|
|
104
|
-
|
|
136
|
+
|
|
137
|
+
The fail-silent contract covers AgentGovern being UNREACHABLE: once the
|
|
138
|
+
SDK is initialised, network errors, timeouts and policy-engine failures
|
|
139
|
+
are discarded rather than raised into your agent.
|
|
140
|
+
|
|
141
|
+
It does NOT cover AgentGovern being absent or misconfigured. This function
|
|
142
|
+
raises if the SDK's dependencies cannot be imported, and it is the only
|
|
143
|
+
place that failure is reported — do not wrap this call in a bare
|
|
144
|
+
``except``. If it raises, the SDK is not installed correctly and no action
|
|
145
|
+
will be recorded; ``status()`` returns ``{"initialized": False}`` for as
|
|
146
|
+
long as that is true.
|
|
105
147
|
|
|
106
148
|
Args:
|
|
107
149
|
api_key: SDK ingest key from the dashboard.
|
|
@@ -110,7 +152,8 @@ def init(
|
|
|
110
152
|
policy_engine_url: Render policy engine URL (Gate 1 evaluate/input).
|
|
111
153
|
Defaults to ``AGENTGOVERN_POLICY_ENGINE_URL`` env var, then
|
|
112
154
|
``https://agentgovern.onrender.com``.
|
|
113
|
-
fail_silently: If ``True``, SDK errors never raise into your agent
|
|
155
|
+
fail_silently: If ``True``, SDK errors never raise into your agent,
|
|
156
|
+
after initialisation. It does not affect this function.
|
|
114
157
|
"""
|
|
115
158
|
_require_sdk()
|
|
116
159
|
global _client # noqa: PLW0603
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.3.0"
|
|
@@ -30,6 +30,15 @@ invocation, bounded so retry can never meaningfully delay the invocation
|
|
|
30
30
|
that's doing it. Permanently-abandoned events are recorded to
|
|
31
31
|
~/.agentgovern/outbox-dead/ rather than silently dropped.
|
|
32
32
|
|
|
33
|
+
NAGT-187 (v2.3.0): the installer's PreToolUse/PostToolUse matchers are now
|
|
34
|
+
"*" (every tool, including ones this script has no _TOOL_MAP entry for —
|
|
35
|
+
see installer.py). A tool with no _TOOL_MAP entry is classified
|
|
36
|
+
action_type="unmapped_tool", not "custom" — "custom" remains reserved for
|
|
37
|
+
an SDK caller's own deliberate track_action() declaration, and blending an
|
|
38
|
+
unrecognized tool into that bucket would have made it silently
|
|
39
|
+
indistinguishable from an intentional customer choice, undetectable by any
|
|
40
|
+
policy that isn't already scanning "custom" as a catch-all.
|
|
41
|
+
|
|
33
42
|
See README.md for installation instructions.
|
|
34
43
|
"""
|
|
35
44
|
from __future__ import annotations
|
|
@@ -48,7 +57,7 @@ from datetime import datetime, timezone
|
|
|
48
57
|
from pathlib import Path
|
|
49
58
|
from typing import Any
|
|
50
59
|
|
|
51
|
-
__version__ = "2.
|
|
60
|
+
__version__ = "2.3.0"
|
|
52
61
|
|
|
53
62
|
# ── Constants ──────────────────────────────────────────────────────────────────
|
|
54
63
|
|
|
@@ -102,6 +111,10 @@ _VALID_CAPTURE_MODES = {CAPTURE_MODE_MINIMAL, CAPTURE_MODE_ENRICHED}
|
|
|
102
111
|
_TOOL_MAP: dict[str, tuple[str, str]] = {
|
|
103
112
|
"Bash": ("execute", "code_execution"),
|
|
104
113
|
"Execute": ("execute", "code_execution"),
|
|
114
|
+
# NAGT-187: Windows dual-shell environments register PowerShell as a
|
|
115
|
+
# distinct tool from Bash, not a substitute for it. Same capability
|
|
116
|
+
# class as Bash — same classification.
|
|
117
|
+
"PowerShell": ("execute", "code_execution"),
|
|
105
118
|
"Read": ("read", "data_access"),
|
|
106
119
|
"Glob": ("read", "data_access"),
|
|
107
120
|
"Write": ("write", "file_operation"),
|
|
@@ -120,8 +133,18 @@ _TOOL_MAP: dict[str, tuple[str, str]] = {
|
|
|
120
133
|
"PermissionRequest": ("execute", "control_flow"),
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
#
|
|
124
|
-
|
|
136
|
+
# NAGT-187: unknown tools default to execute/unmapped_tool, NOT
|
|
137
|
+
# execute/custom. "custom" is the Agent SDK's own ActionType.CUSTOM — a
|
|
138
|
+
# customer's deliberate declaration for their own action, used today in
|
|
139
|
+
# several policy packs' broad applicable_action_types catch-alls (e.g.
|
|
140
|
+
# interactive-ai-software-dev.yaml's IAID-SECRET-* rules). Routing an
|
|
141
|
+
# unrecognized tool into that same bucket would make "a tool this script
|
|
142
|
+
# has never heard of" indistinguishable from "a customer's intentional
|
|
143
|
+
# custom action" — silently correct only by accident, for exactly as long
|
|
144
|
+
# as every policy author remembers to include 'custom' in their scope.
|
|
145
|
+
# 'unmapped_tool' is its own value precisely so it can be reasoned about,
|
|
146
|
+
# scoped, and alerted on independently of what 'custom' means elsewhere.
|
|
147
|
+
_FALLBACK: tuple[str, str] = ("execute", "unmapped_tool")
|
|
125
148
|
|
|
126
149
|
|
|
127
150
|
# ── Logging ────────────────────────────────────────────────────────────────────
|
|
@@ -197,8 +220,10 @@ def map_tool_to_event(tool_name: str) -> tuple[str, str]:
|
|
|
197
220
|
"""Map a Claude Code tool_name to (tool_event_type, action_type).
|
|
198
221
|
|
|
199
222
|
MCP tools (containing 'mcp__') always map to communicate/api_request.
|
|
200
|
-
Unknown tools
|
|
201
|
-
|
|
223
|
+
Unknown tools (NAGT-187: reachable for real now that the installer's
|
|
224
|
+
matcher is "*", not a fixed tool-name list) fall back to
|
|
225
|
+
execute/unmapped_tool — an explicit, distinct classification a policy
|
|
226
|
+
can target directly, rather than being silently folded into 'custom'.
|
|
202
227
|
"""
|
|
203
228
|
if "mcp__" in tool_name:
|
|
204
229
|
return ("communicate", "api_request")
|
|
@@ -132,6 +132,28 @@ def _read_hook_version(hook_script_path: Path) -> str | None:
|
|
|
132
132
|
This function must never be called with a path derived from network
|
|
133
133
|
input, an untrusted argument, or anything other than a hook-script path
|
|
134
134
|
this installer itself controls.
|
|
135
|
+
|
|
136
|
+
CALLER SET, kept current deliberately (NAGT-200a security review). The
|
|
137
|
+
note above was written when the only reachable entry points were
|
|
138
|
+
`hook status` and `hook test` — commands run to INSPECT. `hook install`
|
|
139
|
+
(non-force, against an existing install) now reaches it too, via
|
|
140
|
+
installer._staleness_note, so the repair command executes the file it is
|
|
141
|
+
about to report on. The path argument is unchanged — still
|
|
142
|
+
_hook_target_path() — so the reasoning above transfers intact; what
|
|
143
|
+
changed is the ORDERING, and `--force`, the path that actually repairs,
|
|
144
|
+
is guarded by `if not args.force:` and never reaches here.
|
|
145
|
+
|
|
146
|
+
Recorded rather than left to be re-derived, because an accepted analysis
|
|
147
|
+
whose scope silently widens is how a correct security note becomes a
|
|
148
|
+
wrong one (NAGT-199). Any future caller added to this function belongs in
|
|
149
|
+
this list.
|
|
150
|
+
|
|
151
|
+
The benignity of executing today's hook.py is a PROPERTY, NOT AN
|
|
152
|
+
INVARIANT — it holds because that module has no side-effectful top-level
|
|
153
|
+
code. `packages/claude-hook/tests/test_hook.py` asserts that
|
|
154
|
+
(test_hook_module_has_no_side_effectful_top_level_code); if it ever fails,
|
|
155
|
+
this function starts triggering real side effects from `hook status` and
|
|
156
|
+
`hook install`, and the acceptance above no longer holds.
|
|
135
157
|
"""
|
|
136
158
|
if not hook_script_path.exists():
|
|
137
159
|
return None
|