agentgovern 2.7.2__tar.gz → 2.8.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.7.2 → agentgovern-2.8.0}/CHANGELOG.md +61 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/PKG-INFO +82 -18
- {agentgovern-2.7.2 → agentgovern-2.8.0}/README.md +81 -17
- {agentgovern-2.7.2 → agentgovern-2.8.0}/RELEASING.md +12 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/__main__.py +15 -0
- agentgovern-2.8.0/agentgovern/_fit.py +158 -0
- agentgovern-2.8.0/agentgovern/_version.py +1 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/client.py +38 -15
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/exceptions.py +31 -11
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/gate2.py +136 -15
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/hook/cli.py +2 -1
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/hook/data/hook.py +193 -21
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/hook/installer.py +51 -21
- agentgovern-2.8.0/agentgovern/hook/setup.py +721 -0
- agentgovern-2.8.0/agentgovern/hook/urls.py +62 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/instrumentors/_base.py +134 -2
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/instrumentors/langchain.py +36 -40
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/instrumentors/langgraph.py +39 -43
- {agentgovern-2.7.2 → agentgovern-2.8.0}/examples/langgraph_credit_decision_agent.py +5 -5
- {agentgovern-2.7.2 → agentgovern-2.8.0}/pyproject.toml +1 -1
- agentgovern-2.8.0/tests/hook/fixtures/action_name_cuts.json +35 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/fixtures/hook_2_4_0_golden_outputs.json +0 -32
- agentgovern-2.8.0/tests/hook/test_action_name_cuts_fixture.py +50 -0
- agentgovern-2.8.0/tests/hook/test_hook_v2_deny_deadline.py +219 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_hook_v2_dispatch.py +8 -2
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_hook_v2_nagt400.py +13 -9
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_hook_v2_nagt400_compat.py +25 -3
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_installer.py +138 -7
- agentgovern-2.8.0/tests/hook/test_setup.py +766 -0
- agentgovern-2.8.0/tests/hook/test_setup_unreachable_deny.py +217 -0
- agentgovern-2.8.0/tests/hook/test_urls.py +123 -0
- agentgovern-2.8.0/tests/test_fit_body.py +255 -0
- agentgovern-2.8.0/tests/test_gate1_llm_paths.py +293 -0
- agentgovern-2.8.0/tests/test_gate1_window.py +296 -0
- agentgovern-2.8.0/tests/test_gate2_size_cap.py +423 -0
- agentgovern-2.8.0/tests/test_nagt607_enforce_claims.py +315 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_no_localhost_3000_config.py +2 -2
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_register_contract.py +16 -2
- agentgovern-2.7.2/agentgovern/_version.py +0 -1
- {agentgovern-2.7.2 → agentgovern-2.8.0}/.gitignore +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/LICENSE +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/__init__.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/hook/__init__.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/hook/data/__init__.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/hook/drift.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/hook/settings.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/instrumentors/__init__.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentgovern/types.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentshield/__init__.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentshield/client.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/agentshield/types.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/__init__.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/conftest.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/__init__.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/conftest.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/fixtures/claude_code_pretooluse_output_keys.json +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_bundled_hook.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_drift.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_hook_v2_context_snapshot.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_hook_v2_outbox_queue.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_hook_v2_payload_extraction.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_hook_v2_payloads.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_hook_v2_remediation_message.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_hook_v2_system_message.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_hook_v2_timing_log.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_hook_v2_transcript_reading.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_hook_v2_turn_index.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_hook_v2_warn_verdict.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_matcher_coverage.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_settings.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/hook/test_tool_map_classification.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_base_instrumentor.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_client.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_evaluate_input.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_gate2_full_path.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_gate2_latency.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_gate2_verdict_handler.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_import_guard.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_instrument_langchain.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_langchain.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_langchain_1x_agent.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_langchain_gate1.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_langchain_gate2_callbacks.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_langgraph.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_nagt380_enforce_propagation.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_no_localhost_3000_traffic.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_output_capture_contract.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_payload_schema.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_policy_engine_url.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_rate_limit_handling.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_timestamp.py +0 -0
- {agentgovern-2.7.2 → agentgovern-2.8.0}/tests/test_version_consistency.py +0 -0
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.8.0 — release date is the `sdk-v2.8.0` tag's (prepared 2026-10-29)
|
|
4
|
+
|
|
5
|
+
**New: `agentgovern setup` and `agentgovern remove`, so Guard's install snippet can be four short lines (NAGT-557).** The dependencies and the Python requirement (3.10 or later) are unchanged. `agentgovern hook install` and its flags are unchanged, so install snippets copied before this release keep working with it.
|
|
6
|
+
|
|
7
|
+
**This release also bundles Claude Code hook 2.5.2** (it was 2.5.1; NAGT-554). With `AGENTGOVERN_FAIL_ACTION=deny` and AgentGovern unreachable, hook 2.5.1 could run past the 6 seconds Claude Code gives it (6.3–6.9 s measured with events queued to resend), and Claude Code then ran the tool call unchecked. In hook 2.5.2 a `PreToolUse` no longer first resends queued events (the other events still do), and its check is given only what is left of 5 seconds from when the hook's code starts, including a name lookup that never answers, so the verdict is printed in time. `AGENTGOVERN_HOOK_TIMEOUT_MS` above that is capped on `PreToolUse`. `agentgovern hook install` takes its 6-second timeout from the bundled hook; the value it writes is unchanged. **Upgrading the package doesn't change an installed hook:** `agentgovern hook install --force` moves an existing install to 2.5.2 (`agentgovern setup` does too, since it installs with `--force`). With the default (`allow`), a call that gets no answer still goes ahead; what changes is how long a `PreToolUse` waits, and that it no longer resends queued events first.
|
|
8
|
+
|
|
9
|
+
### `agentgovern setup --actor-user-id <id>`
|
|
10
|
+
|
|
11
|
+
What the install snippet used to do after `pip install`, in order:
|
|
12
|
+
|
|
13
|
+
1. **Checks the developer id**, then **asks for the API key** without showing it as you paste, so the key is never in the snippet or your shell's history. A shorter value, such as the key reference, is refused and asked again (three tries). If `AGENTGOVERN_API_KEY` already holds a whole key, Return keeps it. Without a terminal (a scripted install, including stdin from `NUL` on Windows), it reads `AGENTGOVERN_API_KEY` instead. Nothing is saved until both checks pass.
|
|
14
|
+
2. **Checks both with AgentGovern** (`POST /api/v1/ingest/heartbeat`). A refused key, or a developer id that isn't in the key's organization, stops here with nothing saved. If AgentGovern can't be reached, setup goes on and says that until it can, Guard lets tool calls run unchecked, or blocks them instead if `AGENTGOVERN_FAIL_ACTION=deny` is set (unless `AGENTGOVERN_HOOK_MODE=off`). It refuses an `AGENTGOVERN_API_URL` that isn't https, other than plain http to `localhost`, `127.0.0.1` or `::1`, and follows no redirect, so the key goes nowhere else.
|
|
15
|
+
3. **Saves both settings for new terminals**: on macOS and Linux, the same two lines ending in `# added by AgentGovern Guard`, in the profile of the shell you ran it from (`${ZDOTDIR:-~}/.zshrc` for zsh, `~/.bash_profile` for bash on macOS, `~/.bashrc` on Linux; `--profile` to choose). Running it again replaces them. On Windows, the two user environment variables.
|
|
16
|
+
4. **Installs the hook** with the Python running setup, by absolute path, replacing only Guard's own entry.
|
|
17
|
+
|
|
18
|
+
It ends with **"Done. Open a new terminal, then start Claude Code."** It can't change the terminal it runs in. `agentgovern setup --check` re-runs only step 2, with the current terminal's settings.
|
|
19
|
+
|
|
20
|
+
### `agentgovern remove`
|
|
21
|
+
|
|
22
|
+
Removes the hook entry, Guard's profile lines (from every profile the install could have used) or user variables, and Guard's files under `~/.agentgovern` (then the folder, only if it is empty). It doesn't delete `~/.venvs/agentgovern`, which it runs from; the uninstall line does that afterwards. It also removes an install made by an older snippet.
|
|
23
|
+
|
|
24
|
+
### Security — `agentgovern hook test` no longer sends your API key to an unsafe `AGENTGOVERN_API_URL` (NAGT-559)
|
|
25
|
+
|
|
26
|
+
- **Before:** `hook install` and `hook test` checked the URL by its first characters, so `http://localhost.example.com` and `http://localhost@example.com` (whose host is `example.com`) counted as this machine. `hook test` warned on an unsafe URL and sent the key anyway.
|
|
27
|
+
- **Now:** one parsed check for `hook install`, `hook test` and `setup`: https, or plain http to `localhost`, `127.0.0.1` or `[::1]`, with no `user@` before the host. `hook test` refuses any other URL and sends nothing; `hook install` warns, as before. Plain http to a LAN or Docker host now fails `hook test`.
|
|
28
|
+
|
|
29
|
+
### Fix — Gate 2 evaluates an output over the policy engine's 256 KB limit, instead of the engine refusing it (NAGT-545)
|
|
30
|
+
|
|
31
|
+
- **Before:** an output whose Gate 2 request was over the engine's 256 KB body limit got a 413 and was never evaluated. With `gate_2_failure_mode="fail_closed"` it was blocked; otherwise it was allowed unchecked.
|
|
32
|
+
- **Now:** the output and metadata are cut to fit (every string over one length is cut to it, keeping its start; keys, numbers and short strings are kept), and the part that fits is evaluated. `VerdictResult.partial`, or `.partial` on `OutputBlocked` / `OutputEscalated`, says so, with sizes only.
|
|
33
|
+
- **A block, escalate or redact found in that part raises.** A redact blocks rather than redacts, since it can't be applied to the whole output. When no policy applies, the output is allowed, as it would be whole. Anything else is unchecked: `fail_closed` blocks it, as before; otherwise it is allowed.
|
|
34
|
+
- **Not changed:** an output under the limit is sent byte for byte as before. Warn mode never raises. LangGraph's own 64 K-character cut of graph output is unchanged.
|
|
35
|
+
|
|
36
|
+
### Fix — Gate 1 checks completion-style LLMs, and the newest input (NAGT-555)
|
|
37
|
+
|
|
38
|
+
- **Completion-style (non-chat) LLMs are now checked by Gate 1** in the LangChain and LangGraph integrations, through `on_llm_start`, with the same blocking and fail-open behaviour as chat models. Before, they were never checked. With the default `auto_evaluate_input=True`, such a call now makes a synchronous Gate 1 request (5-second timeout) before the model runs, and an Enforce policy can stop it.
|
|
39
|
+
- **Gate 1 now checks the last 4,000 characters of a call's input instead of the first 4,000.** In a longer conversation the first 4,000 were the system prompt and early history, so the newest message went unchecked. Now the newest message is checked, with as much history before it as fits; anything earlier is not checked in that call. A Gate 1 request is no larger than before.
|
|
40
|
+
- If you subclassed the handler and overrode `on_llm_start`, chat calls no longer pass through it.
|
|
41
|
+
|
|
42
|
+
## 2.7.3 — release date is the `sdk-v2.7.3` tag's (prepared 2026-10-22)
|
|
43
|
+
|
|
44
|
+
**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`.
|
|
45
|
+
|
|
46
|
+
### Fix — `AGENTGOVERN_FAIL_ACTION=deny` blocks every tool call AgentGovern didn't check (NAGT-484)
|
|
47
|
+
|
|
48
|
+
- **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:
|
|
49
|
+
- **an error answer** from AgentGovern (any HTTP 4xx or 5xx, a 429 rate limit included);
|
|
50
|
+
- **a `timeout` verdict:** AgentGovern answered, but its policy engine could not evaluate the call. The dashboard shows these calls as "Evaluation Failed".
|
|
51
|
+
- **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.
|
|
52
|
+
- **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.
|
|
53
|
+
- **The default (`allow`) is unchanged:** an unchecked call still runs.
|
|
54
|
+
- **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.
|
|
55
|
+
- **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.
|
|
56
|
+
|
|
57
|
+
### Fix — the hook runs with the Python you installed, whatever Claude Code's PATH is (NAGT-490)
|
|
58
|
+
|
|
59
|
+
- **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`.
|
|
60
|
+
- **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.
|
|
61
|
+
- **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.
|
|
62
|
+
- **`--python-command` still overrides it.**
|
|
63
|
+
|
|
3
64
|
## 2.7.2 — release date is the `sdk-v2.7.2` tag's (prepared 2026-10-19)
|
|
4
65
|
|
|
5
66
|
**`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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentgovern
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.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: Support, https://zirahn.com/contact
|
|
@@ -66,7 +66,7 @@ from langchain_openai import ChatOpenAI
|
|
|
66
66
|
agentgovern.init(
|
|
67
67
|
api_key="ag_prod_...", # from https://agentgovern.zirahn.com/settings/api-keys
|
|
68
68
|
base_url="https://agentgovern.zirahn.com",
|
|
69
|
-
environment="
|
|
69
|
+
environment="production", # your API key's environment: "production" | "staging" | "development"
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
# 2. Register your agent
|
|
@@ -94,14 +94,15 @@ and visible in the dashboard.
|
|
|
94
94
|
|
|
95
95
|
## Enforcement modes
|
|
96
96
|
|
|
97
|
-
AgentGovern supports
|
|
97
|
+
AgentGovern supports five enforcement modes per policy:
|
|
98
98
|
|
|
99
99
|
| Mode | Behavior |
|
|
100
100
|
|----------|------------------------------------------------------------|
|
|
101
101
|
| warn | Records the violation; the agent continues |
|
|
102
102
|
| log | Records the violation silently; the agent continues |
|
|
103
|
-
|
|
|
104
|
-
|
|
|
103
|
+
| alert | Same as warn today: records the violation; the agent continues |
|
|
104
|
+
| disabled | Still evaluated, and failures are recorded; the agent continues |
|
|
105
|
+
| enforce | Records the violation, and **raises** in your code where the check runs before the action (below) |
|
|
105
106
|
|
|
106
107
|
### What enforce stops, through the LangChain and LangGraph integrations
|
|
107
108
|
|
|
@@ -109,18 +110,60 @@ When an enforce-mode policy blocks, the exception reaches the code that called
|
|
|
109
110
|
`invoke()`, `ainvoke()`, `stream()` or `astream()`:
|
|
110
111
|
|
|
111
112
|
- **Gate 1 (input) raises `PolicyViolation` before the model call runs.** This is the
|
|
112
|
-
control that stops an agent before it acts.
|
|
113
|
+
control that stops an agent before it acts. By default (`auto_evaluate_input=True`) it
|
|
114
|
+
runs before every model call the handler sees, chat models and completion-style LLMs
|
|
115
|
+
alike, and checks the last 4,000 characters of the call's input against your
|
|
116
|
+
policies' `input` rules: the newest message, and as much history before it as fits.
|
|
117
|
+
Anything earlier is not checked in that call, and a single message longer than
|
|
118
|
+
4,000 characters is checked only on its last 4,000.
|
|
119
|
+
*(Changed in SDK 2.8.0, NAGT-555. Earlier releases checked chat models only, and
|
|
120
|
+
only the first 4,000 characters, so a long conversation's newest message went
|
|
121
|
+
unchecked.)*
|
|
113
122
|
- **Gate 2 (output) raises `OutputBlocked` (block) or `OutputEscalated` (escalate)**
|
|
114
|
-
when the run finishes
|
|
123
|
+
when the run finishes, if you pass `gate_2_enforcement_mode="enforce"` to
|
|
124
|
+
`instrument_langchain()` or `instrument_langgraph()`. It is off by default.
|
|
125
|
+
Gate 2 stops `invoke()` from **returning** a blocked answer.
|
|
115
126
|
**It does not undo anything that already happened:** tool calls have already run, a
|
|
116
127
|
LangGraph checkpointer has already saved the final state, and a `stream()` caller has
|
|
117
128
|
already received the streamed output. On LangChain, a run whose LLM streamed tokens skips the synchronous Gate 2 entirely.
|
|
118
129
|
- **Redact** replaces the answer on LangChain; LangGraph does not apply it.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
- **An output over the policy engine's 256 KB limit** is cut to fit, and the part that
|
|
131
|
+
fits is evaluated; the result's `partial` (or the exception's) says so. A block,
|
|
132
|
+
escalate or redact found in that part raises `OutputBlocked` / `OutputEscalated` (a
|
|
133
|
+
redact can't be applied to the whole answer). When no policy applies to the action, it
|
|
134
|
+
is allowed, as it would be whole. Anything else is treated as unchecked: with
|
|
135
|
+
`gate_2_failure_mode="fail_closed"` it raises `OutputBlocked`, as the engine's refusal
|
|
136
|
+
did before; otherwise it is allowed. *(SDK 2.8.0, NAGT-545. Before, such an
|
|
137
|
+
output was never evaluated at all.)* On LangGraph, see the last item below.
|
|
138
|
+
|
|
139
|
+
**What enforce does not stop:**
|
|
140
|
+
|
|
141
|
+
- **A tool call.** The SDK does not check a tool call before it runs. A policy on tool
|
|
142
|
+
calls is evaluated on the recorded action after the tool has run, so in enforce mode
|
|
143
|
+
it records the failure but cannot prevent the call. (Guard's Claude Code hook, below,
|
|
144
|
+
does check each tool call before it runs.)
|
|
145
|
+
- **An agent AgentGovern can't find.** Gate 1 allows the call, and records no
|
|
146
|
+
evaluation, when the agent isn't registered yet or is registered under a different
|
|
147
|
+
environment from the one `init()` names. `register_agent()` sends the registration in
|
|
148
|
+
the background, so a new agent's first model calls can pass this way.
|
|
149
|
+
- **An input that also fails a warn, log, alert or disabled policy, until AgentGovern's
|
|
150
|
+
policy engine is updated (NAGT-555).** Gate 1 acts on the first `input` rule the input
|
|
151
|
+
fails, and that rule's policy decides. So when an input fails rules in two policies,
|
|
152
|
+
one set to enforce and one set to warn or disabled, it can be allowed instead of
|
|
153
|
+
blocked. The engine is updated separately from this SDK; after that update the
|
|
154
|
+
strictest failing rule decides, whatever SDK version you run.
|
|
155
|
+
- **The rest of a long LangGraph output.** The LangGraph integration cuts the graph's
|
|
156
|
+
output to its first 65,536 characters before Gate 2 and evaluates that part as if it
|
|
157
|
+
were the whole output: it is not marked partial, and Gate 2 does not check the rest.
|
|
158
|
+
|
|
159
|
+
**What never stops your agent:** warn, log, alert and allow verdicts, and any error
|
|
160
|
+
inside the SDK itself. If Gate 1 can't complete its check (AgentGovern can't be reached,
|
|
161
|
+
doesn't answer within 5 seconds, or returns a server error), the model call goes ahead,
|
|
162
|
+
unless you pass `fail_open=False`, which makes Gate 1 raise `GateUnavailable`. A Gate 1
|
|
163
|
+
request AgentGovern refuses (a 4xx, such as a wrong API key) lets the call go ahead
|
|
164
|
+
either way. If Gate 2 can't complete its check, the output is returned, unless you pass
|
|
165
|
+
both `gate_2_enforcement_mode="enforce"` and `gate_2_failure_mode="fail_closed"`, which
|
|
166
|
+
makes Gate 2 raise `OutputBlocked`.
|
|
124
167
|
|
|
125
168
|
**Pass AgentGovern's handler last.** When an enforce block raises, LangChain can stop
|
|
126
169
|
delivering that event to the handlers after ours in the `callbacks` list (on synchronous
|
|
@@ -130,21 +173,42 @@ Put AgentGovern's handler at the end of the list:
|
|
|
130
173
|
`config={"callbacks": [your_tracer, agentgovern_handler]}`.
|
|
131
174
|
|
|
132
175
|
**How:** an instrumentor sets LangChain's `raise_error` when an enforce gate is
|
|
133
|
-
configured (Gate 2 `enforce`, or Gate 1 auto-evaluation on), and lets
|
|
134
|
-
verdict exceptions above
|
|
176
|
+
configured (Gate 2 `enforce`, or Gate 1 auto-evaluation on), and lets out of its
|
|
177
|
+
callbacks only the three verdict exceptions above and, when you pass `fail_open=False`,
|
|
178
|
+
`GateUnavailable`.
|
|
135
179
|
|
|
136
180
|
*(Changed in SDK 2.6.0, NAGT-380. Earlier releases recorded enforce-mode violations
|
|
137
181
|
but did not stop the agent: LangChain caught the exception.)*
|
|
138
182
|
|
|
139
|
-
You can also check input yourself before invoking an agent
|
|
183
|
+
You can also check input yourself before invoking an agent. `evaluate_input()` raises
|
|
184
|
+
`PolicyViolation` when a policy set to enforce blocks the input, and otherwise returns
|
|
185
|
+
the result:
|
|
140
186
|
|
|
141
187
|
```python
|
|
142
|
-
|
|
143
|
-
|
|
188
|
+
from agentgovern import PolicyViolation
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
agentgovern.evaluate_input(agent_external_id="my-agent", prompt=user_prompt)
|
|
192
|
+
except PolicyViolation:
|
|
144
193
|
raise HTTPException(status_code=403, detail="Prompt blocked by compliance policy")
|
|
145
194
|
agent.invoke({"input": user_prompt})
|
|
146
195
|
```
|
|
147
196
|
|
|
197
|
+
It sends the whole prompt, not 4,000 characters. Unlike the integrations, it raises
|
|
198
|
+
`ClientError` when AgentGovern refuses the request (a 4xx). If AgentGovern can't be
|
|
199
|
+
reached, doesn't answer within 5 seconds, or returns a server error, it returns an allowed
|
|
200
|
+
result, unless you pass `fail_closed=True`, which makes it raise `GateUnavailable`.
|
|
201
|
+
|
|
202
|
+
*(Corrected 2026-10-31, NAGT-607: the table listed four modes, without alert, and said
|
|
203
|
+
a disabled policy is not evaluated. This section did not say that Gate 2 is off unless
|
|
204
|
+
you turn it on, that tool calls are not checked before they run, that an agent
|
|
205
|
+
AgentGovern can't find passes Gate 1, that an input failing a warn, log, alert or
|
|
206
|
+
disabled policy first is not blocked until the engine is updated, or that the
|
|
207
|
+
LangGraph integration's Gate 2 checks only the first 65,536 characters of an output.
|
|
208
|
+
It said a Gate 1 error stops the call with `fail_open=False`, which a 4xx does not. Its
|
|
209
|
+
example checked the returned result's `action_taken` for a block, a branch that never
|
|
210
|
+
runs, because `evaluate_input()` raises on a block instead of returning.)*
|
|
211
|
+
|
|
148
212
|
## Multiple agents in one process
|
|
149
213
|
|
|
150
214
|
If you run more than one agent in the same process, pass the agent ID explicitly to
|
|
@@ -206,7 +270,7 @@ Enable policy packs from the [AgentGovern dashboard](https://agentgovern.zirahn.
|
|
|
206
270
|
|-----------|---------|-------------|
|
|
207
271
|
| `api_key` | required | SDK ingest key from the dashboard |
|
|
208
272
|
| `base_url` | `https://agentgovern.zirahn.com` | API endpoint |
|
|
209
|
-
| `environment` | `"production"` | `"production"` \| `"staging"` \| `"development"
|
|
273
|
+
| `environment` | `"production"` | `"production"` \| `"staging"` \| `"development"`. Use your API key's environment. Actions are recorded under the key's environment whatever this says, but Gate 1 and Gate 2 look your agent up under this one, and an agent they can't find there goes unchecked |
|
|
210
274
|
| `policy_engine_url` | `https://engine.zirahn.com` | Policy engine endpoint for Gate 1 (`evaluate_input`) and Gate 2 checks. Override with this argument or the `AGENTGOVERN_POLICY_ENGINE_URL` env var |
|
|
211
275
|
| `fail_silently` | `True` | If `True`, SDK errors never raise into your agent |
|
|
212
276
|
|
|
@@ -25,7 +25,7 @@ from langchain_openai import ChatOpenAI
|
|
|
25
25
|
agentgovern.init(
|
|
26
26
|
api_key="ag_prod_...", # from https://agentgovern.zirahn.com/settings/api-keys
|
|
27
27
|
base_url="https://agentgovern.zirahn.com",
|
|
28
|
-
environment="
|
|
28
|
+
environment="production", # your API key's environment: "production" | "staging" | "development"
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
# 2. Register your agent
|
|
@@ -53,14 +53,15 @@ and visible in the dashboard.
|
|
|
53
53
|
|
|
54
54
|
## Enforcement modes
|
|
55
55
|
|
|
56
|
-
AgentGovern supports
|
|
56
|
+
AgentGovern supports five enforcement modes per policy:
|
|
57
57
|
|
|
58
58
|
| Mode | Behavior |
|
|
59
59
|
|----------|------------------------------------------------------------|
|
|
60
60
|
| warn | Records the violation; the agent continues |
|
|
61
61
|
| log | Records the violation silently; the agent continues |
|
|
62
|
-
|
|
|
63
|
-
|
|
|
62
|
+
| alert | Same as warn today: records the violation; the agent continues |
|
|
63
|
+
| disabled | Still evaluated, and failures are recorded; the agent continues |
|
|
64
|
+
| enforce | Records the violation, and **raises** in your code where the check runs before the action (below) |
|
|
64
65
|
|
|
65
66
|
### What enforce stops, through the LangChain and LangGraph integrations
|
|
66
67
|
|
|
@@ -68,18 +69,60 @@ When an enforce-mode policy blocks, the exception reaches the code that called
|
|
|
68
69
|
`invoke()`, `ainvoke()`, `stream()` or `astream()`:
|
|
69
70
|
|
|
70
71
|
- **Gate 1 (input) raises `PolicyViolation` before the model call runs.** This is the
|
|
71
|
-
control that stops an agent before it acts.
|
|
72
|
+
control that stops an agent before it acts. By default (`auto_evaluate_input=True`) it
|
|
73
|
+
runs before every model call the handler sees, chat models and completion-style LLMs
|
|
74
|
+
alike, and checks the last 4,000 characters of the call's input against your
|
|
75
|
+
policies' `input` rules: the newest message, and as much history before it as fits.
|
|
76
|
+
Anything earlier is not checked in that call, and a single message longer than
|
|
77
|
+
4,000 characters is checked only on its last 4,000.
|
|
78
|
+
*(Changed in SDK 2.8.0, NAGT-555. Earlier releases checked chat models only, and
|
|
79
|
+
only the first 4,000 characters, so a long conversation's newest message went
|
|
80
|
+
unchecked.)*
|
|
72
81
|
- **Gate 2 (output) raises `OutputBlocked` (block) or `OutputEscalated` (escalate)**
|
|
73
|
-
when the run finishes
|
|
82
|
+
when the run finishes, if you pass `gate_2_enforcement_mode="enforce"` to
|
|
83
|
+
`instrument_langchain()` or `instrument_langgraph()`. It is off by default.
|
|
84
|
+
Gate 2 stops `invoke()` from **returning** a blocked answer.
|
|
74
85
|
**It does not undo anything that already happened:** tool calls have already run, a
|
|
75
86
|
LangGraph checkpointer has already saved the final state, and a `stream()` caller has
|
|
76
87
|
already received the streamed output. On LangChain, a run whose LLM streamed tokens skips the synchronous Gate 2 entirely.
|
|
77
88
|
- **Redact** replaces the answer on LangChain; LangGraph does not apply it.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
- **An output over the policy engine's 256 KB limit** is cut to fit, and the part that
|
|
90
|
+
fits is evaluated; the result's `partial` (or the exception's) says so. A block,
|
|
91
|
+
escalate or redact found in that part raises `OutputBlocked` / `OutputEscalated` (a
|
|
92
|
+
redact can't be applied to the whole answer). When no policy applies to the action, it
|
|
93
|
+
is allowed, as it would be whole. Anything else is treated as unchecked: with
|
|
94
|
+
`gate_2_failure_mode="fail_closed"` it raises `OutputBlocked`, as the engine's refusal
|
|
95
|
+
did before; otherwise it is allowed. *(SDK 2.8.0, NAGT-545. Before, such an
|
|
96
|
+
output was never evaluated at all.)* On LangGraph, see the last item below.
|
|
97
|
+
|
|
98
|
+
**What enforce does not stop:**
|
|
99
|
+
|
|
100
|
+
- **A tool call.** The SDK does not check a tool call before it runs. A policy on tool
|
|
101
|
+
calls is evaluated on the recorded action after the tool has run, so in enforce mode
|
|
102
|
+
it records the failure but cannot prevent the call. (Guard's Claude Code hook, below,
|
|
103
|
+
does check each tool call before it runs.)
|
|
104
|
+
- **An agent AgentGovern can't find.** Gate 1 allows the call, and records no
|
|
105
|
+
evaluation, when the agent isn't registered yet or is registered under a different
|
|
106
|
+
environment from the one `init()` names. `register_agent()` sends the registration in
|
|
107
|
+
the background, so a new agent's first model calls can pass this way.
|
|
108
|
+
- **An input that also fails a warn, log, alert or disabled policy, until AgentGovern's
|
|
109
|
+
policy engine is updated (NAGT-555).** Gate 1 acts on the first `input` rule the input
|
|
110
|
+
fails, and that rule's policy decides. So when an input fails rules in two policies,
|
|
111
|
+
one set to enforce and one set to warn or disabled, it can be allowed instead of
|
|
112
|
+
blocked. The engine is updated separately from this SDK; after that update the
|
|
113
|
+
strictest failing rule decides, whatever SDK version you run.
|
|
114
|
+
- **The rest of a long LangGraph output.** The LangGraph integration cuts the graph's
|
|
115
|
+
output to its first 65,536 characters before Gate 2 and evaluates that part as if it
|
|
116
|
+
were the whole output: it is not marked partial, and Gate 2 does not check the rest.
|
|
117
|
+
|
|
118
|
+
**What never stops your agent:** warn, log, alert and allow verdicts, and any error
|
|
119
|
+
inside the SDK itself. If Gate 1 can't complete its check (AgentGovern can't be reached,
|
|
120
|
+
doesn't answer within 5 seconds, or returns a server error), the model call goes ahead,
|
|
121
|
+
unless you pass `fail_open=False`, which makes Gate 1 raise `GateUnavailable`. A Gate 1
|
|
122
|
+
request AgentGovern refuses (a 4xx, such as a wrong API key) lets the call go ahead
|
|
123
|
+
either way. If Gate 2 can't complete its check, the output is returned, unless you pass
|
|
124
|
+
both `gate_2_enforcement_mode="enforce"` and `gate_2_failure_mode="fail_closed"`, which
|
|
125
|
+
makes Gate 2 raise `OutputBlocked`.
|
|
83
126
|
|
|
84
127
|
**Pass AgentGovern's handler last.** When an enforce block raises, LangChain can stop
|
|
85
128
|
delivering that event to the handlers after ours in the `callbacks` list (on synchronous
|
|
@@ -89,21 +132,42 @@ Put AgentGovern's handler at the end of the list:
|
|
|
89
132
|
`config={"callbacks": [your_tracer, agentgovern_handler]}`.
|
|
90
133
|
|
|
91
134
|
**How:** an instrumentor sets LangChain's `raise_error` when an enforce gate is
|
|
92
|
-
configured (Gate 2 `enforce`, or Gate 1 auto-evaluation on), and lets
|
|
93
|
-
verdict exceptions above
|
|
135
|
+
configured (Gate 2 `enforce`, or Gate 1 auto-evaluation on), and lets out of its
|
|
136
|
+
callbacks only the three verdict exceptions above and, when you pass `fail_open=False`,
|
|
137
|
+
`GateUnavailable`.
|
|
94
138
|
|
|
95
139
|
*(Changed in SDK 2.6.0, NAGT-380. Earlier releases recorded enforce-mode violations
|
|
96
140
|
but did not stop the agent: LangChain caught the exception.)*
|
|
97
141
|
|
|
98
|
-
You can also check input yourself before invoking an agent
|
|
142
|
+
You can also check input yourself before invoking an agent. `evaluate_input()` raises
|
|
143
|
+
`PolicyViolation` when a policy set to enforce blocks the input, and otherwise returns
|
|
144
|
+
the result:
|
|
99
145
|
|
|
100
146
|
```python
|
|
101
|
-
|
|
102
|
-
|
|
147
|
+
from agentgovern import PolicyViolation
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
agentgovern.evaluate_input(agent_external_id="my-agent", prompt=user_prompt)
|
|
151
|
+
except PolicyViolation:
|
|
103
152
|
raise HTTPException(status_code=403, detail="Prompt blocked by compliance policy")
|
|
104
153
|
agent.invoke({"input": user_prompt})
|
|
105
154
|
```
|
|
106
155
|
|
|
156
|
+
It sends the whole prompt, not 4,000 characters. Unlike the integrations, it raises
|
|
157
|
+
`ClientError` when AgentGovern refuses the request (a 4xx). If AgentGovern can't be
|
|
158
|
+
reached, doesn't answer within 5 seconds, or returns a server error, it returns an allowed
|
|
159
|
+
result, unless you pass `fail_closed=True`, which makes it raise `GateUnavailable`.
|
|
160
|
+
|
|
161
|
+
*(Corrected 2026-10-31, NAGT-607: the table listed four modes, without alert, and said
|
|
162
|
+
a disabled policy is not evaluated. This section did not say that Gate 2 is off unless
|
|
163
|
+
you turn it on, that tool calls are not checked before they run, that an agent
|
|
164
|
+
AgentGovern can't find passes Gate 1, that an input failing a warn, log, alert or
|
|
165
|
+
disabled policy first is not blocked until the engine is updated, or that the
|
|
166
|
+
LangGraph integration's Gate 2 checks only the first 65,536 characters of an output.
|
|
167
|
+
It said a Gate 1 error stops the call with `fail_open=False`, which a 4xx does not. Its
|
|
168
|
+
example checked the returned result's `action_taken` for a block, a branch that never
|
|
169
|
+
runs, because `evaluate_input()` raises on a block instead of returning.)*
|
|
170
|
+
|
|
107
171
|
## Multiple agents in one process
|
|
108
172
|
|
|
109
173
|
If you run more than one agent in the same process, pass the agent ID explicitly to
|
|
@@ -165,7 +229,7 @@ Enable policy packs from the [AgentGovern dashboard](https://agentgovern.zirahn.
|
|
|
165
229
|
|-----------|---------|-------------|
|
|
166
230
|
| `api_key` | required | SDK ingest key from the dashboard |
|
|
167
231
|
| `base_url` | `https://agentgovern.zirahn.com` | API endpoint |
|
|
168
|
-
| `environment` | `"production"` | `"production"` \| `"staging"` \| `"development"
|
|
232
|
+
| `environment` | `"production"` | `"production"` \| `"staging"` \| `"development"`. Use your API key's environment. Actions are recorded under the key's environment whatever this says, but Gate 1 and Gate 2 look your agent up under this one, and an agent they can't find there goes unchecked |
|
|
169
233
|
| `policy_engine_url` | `https://engine.zirahn.com` | Policy engine endpoint for Gate 1 (`evaluate_input`) and Gate 2 checks. Override with this argument or the `AGENTGOVERN_POLICY_ENGINE_URL` env var |
|
|
170
234
|
| `fail_silently` | `True` | If `True`, SDK errors never raise into your agent |
|
|
171
235
|
|
|
@@ -101,4 +101,16 @@ To re-run a publish after a failed run, trigger the workflow manually **from the
|
|
|
101
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
102
|
- Python 3.9.6 (`/usr/bin/python3`) resolves `agentgovern-2.5.0`;
|
|
103
103
|
- the hook version changed, so the released-hook manifest moved to 2.5.0 after the tag (NAGT-400 (c), #953, `b748b1f7`).
|
|
104
|
+
- **2026-10-23:** 2.7.3 was published by run `39881523976` from tag `sdk-v2.7.3` at `ccbbcd6d` (#1043's merge; the release carries #1042 (NAGT-484, NAGT-500) and #1043 (NAGT-490)). **Ahmed pushed the tag.** Build 01:00:53–01:01:10Z, Publish 01:01:14–01:01:38Z; files uploaded 01:01:32Z and 01:01:34Z. Verified afterwards, read-only:
|
|
105
|
+
- the wheel and sdist sha256 match PyPI's digests (`11f01051…`, `8a5b51fd…`), compared with the run's own `dist` artifact;
|
|
106
|
+
- the wheel's `METADATA` says `Version: 2.7.3`, `Requires-Python: >=3.10`;
|
|
107
|
+
- its bundled `hook.py` is **hook 2.5.1**, sha256 `96e1ed3bdf2cc78932a8dd5e348e3b23c777723e2c9cb36b36a651bbeb40f327`, byte-identical to the tag commit's;
|
|
108
|
+
- each file's PyPI attestation names publisher `ZirahnAI/agentgovern`, workflow `publish-sdk.yml`, environment `pypi`, with a subject digest equal to the file's;
|
|
109
|
+
- Python 3.9.6 (`/usr/bin/python3`) resolves `agentgovern-2.5.0`;
|
|
110
|
+
- **Ahmed's fresh install from PyPI on his Mac, in a temp HOME** (`docs/runbooks/sdk-2.7.3-release-2026-10-23.md` §6), passed:
|
|
111
|
+
- `2.7.3 3.13.15`;
|
|
112
|
+
- all four hooks registered with the venv's own `…/.venvs/agentgovern/bin/python3.13`;
|
|
113
|
+
- hook `2.5.1`;
|
|
114
|
+
- under `deny`, an HTTP 501 is blocked and a missing `AGENTGOVERN_API_KEY` is blocked with the reinstall reason.
|
|
115
|
+
- the hook version changed, so the released-hook manifest moved to 2.5.1 in the release-record PR (#1050).
|
|
104
116
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""AgentGovern CLI entry point.
|
|
2
2
|
|
|
3
3
|
Dispatches to subcommands. Currently supported:
|
|
4
|
+
agentgovern setup [--actor-user-id ID] [--check] [--profile PATH]
|
|
5
|
+
agentgovern remove
|
|
4
6
|
agentgovern hook [install|uninstall|status|test]
|
|
5
7
|
"""
|
|
6
8
|
|
|
@@ -28,12 +30,25 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
28
30
|
|
|
29
31
|
add_hook_subparsers(hook_parser)
|
|
30
32
|
|
|
33
|
+
# `agentgovern setup` / `agentgovern remove` (NAGT-557): the install snippet's steps
|
|
34
|
+
from agentgovern.hook.setup import add_setup_subparsers
|
|
35
|
+
|
|
36
|
+
add_setup_subparsers(subparsers)
|
|
37
|
+
|
|
31
38
|
args = parser.parse_args(argv)
|
|
32
39
|
|
|
33
40
|
if args.command == "hook":
|
|
34
41
|
from agentgovern.hook.cli import dispatch
|
|
35
42
|
|
|
36
43
|
return dispatch(args)
|
|
44
|
+
if args.command == "setup":
|
|
45
|
+
from agentgovern.hook.setup import run_setup
|
|
46
|
+
|
|
47
|
+
return run_setup(args)
|
|
48
|
+
if args.command == "remove":
|
|
49
|
+
from agentgovern.hook.setup import run_remove
|
|
50
|
+
|
|
51
|
+
return run_remove(args)
|
|
37
52
|
|
|
38
53
|
parser.error(f"Unknown command: {args.command}")
|
|
39
54
|
return 2 # unreachable
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NAGT-545: fit a Gate 2 evaluate body under the policy engine's cap by shortening
|
|
3
|
+
its longest strings, so a large output is evaluated on what fits rather than
|
|
4
|
+
refused with a 413.
|
|
5
|
+
|
|
6
|
+
The Python twin of apps/web/lib/eval/fit-evaluate-body.ts (NAGT-527, NAGT-540).
|
|
7
|
+
Only string values inside the parts given are shortened (the output payload and
|
|
8
|
+
the metadata), every one to the same maximum length (the largest that fits),
|
|
9
|
+
keeping its start. Shorter strings, keys, numbers and structure are unchanged.
|
|
10
|
+
|
|
11
|
+
The result of an evaluation of a cut body is partial: content past a cut was not
|
|
12
|
+
seen. agentgovern/gate2.py never presents it as a full pass.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from collections.abc import Callable, Mapping
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
# The engine is the authority: apps/policy-engine/app/config.py's
|
|
23
|
+
# `engine_max_body_bytes` default, which app/main.py applies to /api/v1/evaluate/.
|
|
24
|
+
# The engine refuses a body with `len(body) > max_bytes` (app/middleware.py), so
|
|
25
|
+
# a body of exactly this many bytes is admitted. tests/test_fit_body.py reads the
|
|
26
|
+
# engine's source and fails if this copy differs. A lower ENGINE_MAX_BODY_BYTES
|
|
27
|
+
# override on the engine is not visible here; it is still safe: the engine
|
|
28
|
+
# answers 413 and Gate 2's existing error path runs.
|
|
29
|
+
ENGINE_EVALUATE_MAX_BODY_BYTES = 262_144
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def encode_body(body: Any) -> bytes:
|
|
33
|
+
"""The exact bytes Gate 2 sends: httpx 0.28's `json=` encoding.
|
|
34
|
+
|
|
35
|
+
Measuring and sending the same bytes means no margin is needed.
|
|
36
|
+
"""
|
|
37
|
+
return json.dumps(
|
|
38
|
+
body, ensure_ascii=False, separators=(",", ":"), allow_nan=False
|
|
39
|
+
).encode("utf-8")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class FitResult:
|
|
44
|
+
"""`kind` is "whole", "partial" or "unfittable".
|
|
45
|
+
|
|
46
|
+
"unfittable": over the cap even with every string emptied; sent whole, as before.
|
|
47
|
+
`partial` is the record of the cut, set only when `kind` is "partial".
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
kind: str
|
|
51
|
+
body: bytes
|
|
52
|
+
partial: dict[str, Any] | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _cut_strings(value: Any, max_len: int) -> tuple[Any, int]:
|
|
56
|
+
"""`value` with every string longer than `max_len` cut, and how many were."""
|
|
57
|
+
count = 0
|
|
58
|
+
|
|
59
|
+
def walk(v: Any) -> Any:
|
|
60
|
+
nonlocal count
|
|
61
|
+
if isinstance(v, str):
|
|
62
|
+
if len(v) <= max_len:
|
|
63
|
+
return v
|
|
64
|
+
count += 1
|
|
65
|
+
return v[:max_len]
|
|
66
|
+
if isinstance(v, (list, tuple)):
|
|
67
|
+
return [walk(x) for x in v]
|
|
68
|
+
if isinstance(v, Mapping):
|
|
69
|
+
return {k: walk(child) for k, child in v.items()}
|
|
70
|
+
return v
|
|
71
|
+
|
|
72
|
+
return walk(value), count
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _longest_string(value: Any) -> int:
|
|
76
|
+
if isinstance(value, str):
|
|
77
|
+
return len(value)
|
|
78
|
+
if isinstance(value, (list, tuple)):
|
|
79
|
+
return max((_longest_string(x) for x in value), default=0)
|
|
80
|
+
if isinstance(value, Mapping):
|
|
81
|
+
return max((_longest_string(x) for x in value.values()), default=0)
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _size(value: Any) -> int:
|
|
86
|
+
return len(encode_body(value))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def fit_body(
|
|
90
|
+
build: Callable[[dict[str, Any]], Any],
|
|
91
|
+
parts: dict[str, Any],
|
|
92
|
+
cap_bytes: int,
|
|
93
|
+
) -> FitResult:
|
|
94
|
+
"""The encoded body for `parts`, under `cap_bytes`.
|
|
95
|
+
|
|
96
|
+
`build` makes the whole engine body from the parts, so everything around them
|
|
97
|
+
is counted exactly as it will be sent. Each part is cut; the record has a side
|
|
98
|
+
for each, under the part's name in `parts`.
|
|
99
|
+
"""
|
|
100
|
+
whole = encode_body(build(parts))
|
|
101
|
+
if len(whole) <= cap_bytes:
|
|
102
|
+
return FitResult(kind="whole", body=whole)
|
|
103
|
+
try:
|
|
104
|
+
return _cut_to_fit(build, parts, cap_bytes, whole)
|
|
105
|
+
except RecursionError:
|
|
106
|
+
# The walkers recurse once per nesting level and can overflow on a deeply
|
|
107
|
+
# nested body that json.dumps still encodes. Sent whole, such a body takes
|
|
108
|
+
# the existing 413 path, as any body that can't be cut to fit does.
|
|
109
|
+
return FitResult(kind="unfittable", body=whole)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _cut_parts(parts: dict[str, Any], max_len: int) -> tuple[dict[str, Any], dict[str, int]]:
|
|
113
|
+
cut: dict[str, Any] = {}
|
|
114
|
+
counts: dict[str, int] = {}
|
|
115
|
+
for name, value in parts.items():
|
|
116
|
+
cut[name], counts[name] = _cut_strings(value, max_len)
|
|
117
|
+
return cut, counts
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _cut_to_fit(
|
|
121
|
+
build: Callable[[dict[str, Any]], Any],
|
|
122
|
+
parts: dict[str, Any],
|
|
123
|
+
cap_bytes: int,
|
|
124
|
+
whole: bytes,
|
|
125
|
+
) -> FitResult:
|
|
126
|
+
def fits_at(max_len: int) -> bool:
|
|
127
|
+
return _size(build(_cut_parts(parts, max_len)[0])) <= cap_bytes
|
|
128
|
+
|
|
129
|
+
if not fits_at(0):
|
|
130
|
+
return FitResult(kind="unfittable", body=whole)
|
|
131
|
+
|
|
132
|
+
# The largest max_len that fits. fits_at(0) holds; fits_at(longest) does not,
|
|
133
|
+
# since at that length nothing is cut and the whole body is over the cap.
|
|
134
|
+
lo = 0
|
|
135
|
+
hi = max((_longest_string(v) for v in parts.values()), default=0)
|
|
136
|
+
while hi - lo > 1:
|
|
137
|
+
mid = (lo + hi) // 2
|
|
138
|
+
if fits_at(mid):
|
|
139
|
+
lo = mid
|
|
140
|
+
else:
|
|
141
|
+
hi = mid
|
|
142
|
+
|
|
143
|
+
cut, counts = _cut_parts(parts, lo)
|
|
144
|
+
body = encode_body(build(cut))
|
|
145
|
+
partial: dict[str, Any] = {
|
|
146
|
+
"cap_bytes": cap_bytes,
|
|
147
|
+
"body_bytes_before": len(whole),
|
|
148
|
+
"body_bytes_after": len(body),
|
|
149
|
+
# In code points (Python str); the web's is in UTF-16 code units.
|
|
150
|
+
"max_string_length": lo,
|
|
151
|
+
}
|
|
152
|
+
for name in parts:
|
|
153
|
+
partial[name] = {
|
|
154
|
+
"strings_cut": counts[name],
|
|
155
|
+
"bytes_before": _size(parts[name]),
|
|
156
|
+
"bytes_after": _size(cut[name]),
|
|
157
|
+
}
|
|
158
|
+
return FitResult(kind="partial", body=body, partial=partial)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.8.0"
|