agentgovern 2.1.0__tar.gz → 2.2.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.0/CHANGELOG.md +174 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/PKG-INFO +3 -3
- agentgovern-2.2.0/agentgovern/_version.py +1 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/hook/data/hook.py +98 -25
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/hook/installer.py +7 -5
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/hook/settings.py +1 -1
- {agentgovern-2.1.0 → agentgovern-2.2.0}/pyproject.toml +2 -2
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_hook_v2_outbox_queue.py +66 -1
- agentgovern-2.1.0/CHANGELOG.md +0 -104
- agentgovern-2.1.0/agentgovern/_version.py +0 -1
- {agentgovern-2.1.0 → agentgovern-2.2.0}/.gitignore +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/LICENSE +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/README.md +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/RELEASING.md +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/__init__.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/__main__.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/client.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/exceptions.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/gate2.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/hook/__init__.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/hook/cli.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/hook/data/__init__.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/hook/drift.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/instrumentors/__init__.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/instrumentors/_base.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/instrumentors/langchain.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/instrumentors/langgraph.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentgovern/types.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentshield/__init__.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentshield/client.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/agentshield/types.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/examples/langgraph_credit_decision_agent.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/__init__.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/__init__.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/conftest.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_bundled_hook.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_drift.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_hook_v2_context_snapshot.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_hook_v2_dispatch.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_hook_v2_payload_extraction.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_hook_v2_payloads.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_hook_v2_remediation_message.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_hook_v2_timing_log.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_hook_v2_transcript_reading.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_hook_v2_turn_index.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_hook_v2_warn_verdict.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_installer.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/hook/test_settings.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_base_instrumentor.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_client.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_evaluate_input.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_gate2_full_path.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_gate2_latency.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_gate2_verdict_handler.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_instrument_langchain.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_langchain.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_langchain_gate1.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_langchain_gate2_callbacks.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_langgraph.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_output_capture_contract.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_payload_schema.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_rate_limit_handling.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_timestamp.py +0 -0
- {agentgovern-2.1.0 → agentgovern-2.2.0}/tests/test_version_consistency.py +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.2.0 — 2026-08-11
|
|
4
|
+
|
|
5
|
+
### Fix — capture-only hook events (UserPromptSubmit/PostToolUse/Stop) no longer block on a discarded synchronous verdict; async audit-event parity (NAGT-137)
|
|
6
|
+
|
|
7
|
+
`AGENTGOVERN_HOOK_MODE`'s effect is now per-event, not global:
|
|
8
|
+
|
|
9
|
+
- **`UserPromptSubmit`/`PostToolUse`/`Stop`** are capture-only — the hook
|
|
10
|
+
never reads their ingest response for control flow — and now default to
|
|
11
|
+
`evaluation_mode: async` regardless of `AGENTGOVERN_HOOK_MODE`, so they no
|
|
12
|
+
longer pay the policy-engine's synchronous round trip (previously up to
|
|
13
|
+
the full `AGENTGOVERN_HOOK_TIMEOUT_MS`/shell timeout on every turn
|
|
14
|
+
boundary and every tool call). Set `AGENTGOVERN_HOOK_MODE=sync` explicitly
|
|
15
|
+
to force a synchronous verdict on these three as well (debugging).
|
|
16
|
+
- **`PreToolUse` always requests `sync` now** and always enforces the
|
|
17
|
+
verdict it gets back — it is the only event that gates whether a tool
|
|
18
|
+
runs, so its verdict was never safe to discard. **Behavior change:**
|
|
19
|
+
`AGENTGOVERN_HOOK_MODE=async` can no longer make `PreToolUse`
|
|
20
|
+
non-blocking; it could in 2.1.0 and earlier. This was documented (not
|
|
21
|
+
merely latent) — `packages/claude-hook/README.md`'s env-var reference
|
|
22
|
+
table and its rate-limit troubleshooting note both described `async` as
|
|
23
|
+
applying uniformly to all events — both updated in this release to
|
|
24
|
+
reflect the new per-event behavior.
|
|
25
|
+
- **Server-side (`apps/web`):** `evaluateInteractiveAsync` — already the
|
|
26
|
+
default path for all SDK-originated traffic (`evaluation_mode` defaults
|
|
27
|
+
to `async`) — previously never parsed the policy engine's top-level
|
|
28
|
+
`verdict`/`review_id` fields, so any tenant with an enforce-mode policy
|
|
29
|
+
got a generic `interactive_action.failed` audit event with no
|
|
30
|
+
`review_id` and no block-vs-escalate distinction, for every finding.
|
|
31
|
+
`evaluateInteractiveAsync` now fires the same `interactive_action.blocked`
|
|
32
|
+
/`.warned` (with `review_id`) events `evaluateInteractiveSyncAndRespond`
|
|
33
|
+
already did. **Behavior change:** `interactive_action.failed` no longer
|
|
34
|
+
fires for a verdict-bearing block outcome under async — it's retained
|
|
35
|
+
only as the fallback for a malformed/unparseable engine response.
|
|
36
|
+
Consumers filtering the audit-event stream on `.failed` specifically will
|
|
37
|
+
see fewer of them; those findings now surface as `.blocked`.
|
|
38
|
+
- Hook script version (`HOOK_VERSION`) bumped 2.1.0 → 2.2.0 in lockstep.
|
|
39
|
+
|
|
40
|
+
Requires `agentgovern hook install --force` (or a fresh install) to pick up
|
|
41
|
+
the updated hook script — existing deployed hooks do not auto-update.
|
|
42
|
+
|
|
43
|
+
Full design and build record: `docs/audit-reports/nagt-137-hook-sync-blocking-2026-08-03.md`.
|
|
44
|
+
|
|
45
|
+
### Fix — `langgraph` extra had no upper version bound (NAGT-169)
|
|
46
|
+
|
|
47
|
+
`create_react_agent` is deprecated as of LangGraph v1.0 and scheduled for
|
|
48
|
+
removal in v2.0. The `langgraph` extra's dependency (`langgraph>=0.2.0`) had
|
|
49
|
+
no ceiling, so a fresh `pip install agentgovern[langgraph]` after LangGraph
|
|
50
|
+
2.0 ships would have pulled it unpinned — breaking this SDK's own
|
|
51
|
+
`test_langgraph.py` integration tests and the `create_react_agent`-based
|
|
52
|
+
onboarding sample on first run. Pinned to `langgraph>=0.2.0,<2.0.0`. The
|
|
53
|
+
`langchain` extra's `langchain-core` dependency was checked and left
|
|
54
|
+
unbounded — the instrumentor only uses the foundational, non-deprecated
|
|
55
|
+
`BaseCallbackHandler`, and `langchain-core` has no announced 2.0.
|
|
56
|
+
|
|
57
|
+
### Fix — `agentgovern hook status`/`hook test`/`hook install` crash on Windows under the default `cp1252` console codepage (NAGT-090, NAGT-169)
|
|
58
|
+
|
|
59
|
+
Several CLI status/error messages wrote non-ASCII characters (`✓`, `✗`, `→`)
|
|
60
|
+
that `cp1252` — the default console codepage on an unconfigured Windows
|
|
61
|
+
machine — cannot encode, raising `UnicodeEncodeError` in place of the
|
|
62
|
+
intended output. Replaced with ASCII equivalents (`OK`/`MISSING`, `->`) at
|
|
63
|
+
all five confirmed call sites: `hook status`'s per-event detail line and
|
|
64
|
+
`hook test`'s coverage banner (the two originally filed under NAGT-090),
|
|
65
|
+
`hook test`'s invalid-API-key/actor-ID error hints, and the
|
|
66
|
+
`Right-click -> Properties -> Security -> ...` permission-fix hint raised
|
|
67
|
+
when `~/.claude/settings.json` isn't writable — found by an exhaustive
|
|
68
|
+
non-cp1252-character sweep of the installer/CLI/instrumentor surface
|
|
69
|
+
while fixing the two originally-filed sites, not by a new bug report.
|
|
70
|
+
|
|
71
|
+
Full record: `docs/audit-reports/nagt-169-sdk-packaging-hygiene-2026-08-11.md`.
|
|
72
|
+
|
|
73
|
+
## 2.1.0 — 2026-07-20
|
|
74
|
+
|
|
75
|
+
### Feature — durable local outbox retry queue for the Claude Code hook (NAGT-095)
|
|
76
|
+
|
|
77
|
+
Fire-and-forget hook sends (`UserPromptSubmit`/`PostToolUse`/`Stop`) that fail
|
|
78
|
+
at the network level are no longer silently lost. A failed send is now queued
|
|
79
|
+
to a local durable outbox (`~/.agentgovern/outbox/`, one file per event) and
|
|
80
|
+
retried opportunistically on a later hook invocation — bounded so a backlog
|
|
81
|
+
can never meaningfully delay the invocation performing the retry (at most 5
|
|
82
|
+
items per invocation, a 1.5s wall-clock budget, a 60s per-file cooldown).
|
|
83
|
+
Permanently-abandoned events (48h TTL, or a 500-file size cap) are recorded to
|
|
84
|
+
`~/.agentgovern/outbox-dead/` instead of disappearing without a trace.
|
|
85
|
+
|
|
86
|
+
- **Swallowed-exception capture**: the network exception behind a failed send
|
|
87
|
+
is now captured into the queued record itself, turning the next real-world
|
|
88
|
+
occurrence into inspectable diagnostic data instead of a silent, unlogged
|
|
89
|
+
drop.
|
|
90
|
+
- **`client_event_id`**: every fire-and-forget payload now carries a
|
|
91
|
+
client-generated UUID v4, captured (not yet enforced) server-side —
|
|
92
|
+
positions a future dedup fix to act retroactively on rows this release
|
|
93
|
+
ever writes. Delivery remains at-least-once, not exactly-once, until that
|
|
94
|
+
follow-on ships.
|
|
95
|
+
- **Directory hardening**: `~/.agentgovern/outbox(-dead)/` are created
|
|
96
|
+
owner-only (`chmod 0700` on POSIX, an `icacls` ACL lockdown on Windows).
|
|
97
|
+
- Hook script version (`HOOK_VERSION`, distinct from this package version)
|
|
98
|
+
bumped 2.0.0 → 2.1.0 in lockstep.
|
|
99
|
+
|
|
100
|
+
No API or behavior change for installs that never experience a send failure.
|
|
101
|
+
Requires `agentgovern hook install --force` (or a fresh install) to pick up
|
|
102
|
+
the updated hook script — existing deployed hooks do not auto-update.
|
|
103
|
+
|
|
104
|
+
Full design and build record: `docs/audit-reports/nagt-095-async-prompt-drop-2026-07-20.md`.
|
|
105
|
+
|
|
106
|
+
## 2.0.2 — 2026-07-02
|
|
107
|
+
|
|
108
|
+
### Fix — README quickstart correctness (NAGT-019)
|
|
109
|
+
|
|
110
|
+
Two documentation corrections so the published quickstart actually works for a
|
|
111
|
+
first-time customer. No code or API change.
|
|
112
|
+
|
|
113
|
+
- **Install line** now reads `pip install "agentgovern[langchain]"`. The base
|
|
114
|
+
`pip install agentgovern` installs no runtime dependencies (they moved to the
|
|
115
|
+
`[sdk]`/`[langchain]` extras in 0.2.0), so the LangChain quickstart that
|
|
116
|
+
follows it could not run as written.
|
|
117
|
+
- **API-key prefix** in the Claude Code Hook env-var example corrected from
|
|
118
|
+
`as_prod_` to `ag_prod_`, matching the prefix the server actually issues and
|
|
119
|
+
validates (`ag_prod_`/`ag_stg_`/`ag_dev_`).
|
|
120
|
+
|
|
121
|
+
Surfaced by the IAG-007 customer-docs Phase 1 audit (PR #237).
|
|
122
|
+
|
|
123
|
+
## 2.0.1 — 2026-06-29
|
|
124
|
+
|
|
125
|
+
### Fix — Gate 1 wire-shape contract
|
|
126
|
+
|
|
127
|
+
`evaluate_input()` now nests `context` and `metadata` inside the `input` field of
|
|
128
|
+
the request body, matching the engine's `EvaluateInputRequest` schema. Previously
|
|
129
|
+
these were sent at the top level, where the engine silently dropped them — so input
|
|
130
|
+
policies that depend on `context`/`metadata` were evaluated against missing data.
|
|
131
|
+
The public Python signature is unchanged; only the on-the-wire serialization is
|
|
132
|
+
corrected. Surfaced by the SEC-002 pre-flight contract check.
|
|
133
|
+
|
|
134
|
+
## 0.2.0 — 2026-06-11
|
|
135
|
+
|
|
136
|
+
### Breaking change — optional SDK dependencies
|
|
137
|
+
|
|
138
|
+
`httpx`, `pydantic`, and `python-dotenv` are no longer installed by default.
|
|
139
|
+
They have moved to the `[sdk]` optional extra.
|
|
140
|
+
|
|
141
|
+
**Affected users:** anyone who installed `agentgovern` and used the agent SDK
|
|
142
|
+
(`init()`, `track_action()`, `evaluate_input()`, framework instrumentors).
|
|
143
|
+
|
|
144
|
+
**Migration:**
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Before
|
|
148
|
+
pip install agentgovern
|
|
149
|
+
|
|
150
|
+
# After — agent SDK usage
|
|
151
|
+
pip install agentgovern[sdk]
|
|
152
|
+
|
|
153
|
+
# After — LangChain (sdk is pulled in automatically)
|
|
154
|
+
pip install agentgovern[langchain]
|
|
155
|
+
|
|
156
|
+
# Hook-only users (Claude Code governance) — no change needed
|
|
157
|
+
pip install agentgovern
|
|
158
|
+
agentgovern hook install
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Errata (2026-07-10):** this entry originally read "Claude Code / Cursor
|
|
162
|
+
governance" — Cursor was never shipped as a hook target; corrected to
|
|
163
|
+
Claude Code, the only host with a working client.
|
|
164
|
+
|
|
165
|
+
### Why
|
|
166
|
+
|
|
167
|
+
The hook CLI (`agentgovern hook install/uninstall/status/test`) is pure Python
|
|
168
|
+
standard library and does not need httpx or pydantic. Moving these to an
|
|
169
|
+
optional extra reduces the installation footprint for hook-only deployments
|
|
170
|
+
from ~45 MB to ~1 MB.
|
|
171
|
+
|
|
172
|
+
## 0.1.1b15 — prior
|
|
173
|
+
|
|
174
|
+
Initial beta releases. See git history.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: agentgovern
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.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
|
|
@@ -42,7 +42,7 @@ Requires-Dist: python-dotenv>=1.0.0; extra == 'langchain'
|
|
|
42
42
|
Provides-Extra: langgraph
|
|
43
43
|
Requires-Dist: httpx>=0.27.0; extra == 'langgraph'
|
|
44
44
|
Requires-Dist: langchain-core>=0.3.0; extra == 'langgraph'
|
|
45
|
-
Requires-Dist: langgraph
|
|
45
|
+
Requires-Dist: langgraph<2.0.0,>=0.2.0; extra == 'langgraph'
|
|
46
46
|
Requires-Dist: pydantic>=2.10.0; extra == 'langgraph'
|
|
47
47
|
Requires-Dist: python-dotenv>=1.0.0; extra == 'langgraph'
|
|
48
48
|
Provides-Extra: openai
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.2.0"
|
|
@@ -9,6 +9,13 @@ Configuration (environment variables):
|
|
|
9
9
|
AGENTGOVERN_ACTOR_USER_ID Required. Clerk user ID (user_xxxxx).
|
|
10
10
|
AGENTGOVERN_API_URL Default: https://agentgovern.zirahn.com
|
|
11
11
|
AGENTGOVERN_HOOK_MODE Default: sync (sync | async | off)
|
|
12
|
+
NAGT-137: applies to UserPromptSubmit/PostToolUse/Stop only — these
|
|
13
|
+
capture-only events default to async (no blocking) regardless of
|
|
14
|
+
this variable; set 'sync' to force a synchronous verdict on them too
|
|
15
|
+
(debugging). PreToolUse always requests sync and always enforces the
|
|
16
|
+
verdict — this variable can no longer make PreToolUse non-blocking
|
|
17
|
+
(it could prior to v2.2.0; see CHANGELOG). 'off' disables the hook
|
|
18
|
+
entirely, for all events, unchanged.
|
|
12
19
|
AGENTGOVERN_HOOK_VERBOSE Default: 0 (0 | 1)
|
|
13
20
|
AGENTGOVERN_FAIL_ACTION Default: allow (allow | deny)
|
|
14
21
|
AGENTGOVERN_HOOK_TIMEOUT_MS Default: 5000
|
|
@@ -41,7 +48,7 @@ from datetime import datetime, timezone
|
|
|
41
48
|
from pathlib import Path
|
|
42
49
|
from typing import Any
|
|
43
50
|
|
|
44
|
-
__version__ = "2.
|
|
51
|
+
__version__ = "2.2.0"
|
|
45
52
|
|
|
46
53
|
# ── Constants ──────────────────────────────────────────────────────────────────
|
|
47
54
|
|
|
@@ -148,9 +155,28 @@ def _now_iso() -> str:
|
|
|
148
155
|
return _iso_timestamp_utc()
|
|
149
156
|
|
|
150
157
|
|
|
151
|
-
def _get_evaluation_mode() -> str:
|
|
152
|
-
"""Return 'sync' or 'async'
|
|
153
|
-
|
|
158
|
+
def _get_evaluation_mode(event_name: str) -> str:
|
|
159
|
+
"""Return 'sync' or 'async' for this hook event type (NAGT-137).
|
|
160
|
+
|
|
161
|
+
PreToolUse always returns 'sync' — it gates whether the tool runs, so its
|
|
162
|
+
verdict is never discarded, and AGENTGOVERN_HOOK_MODE can no longer make
|
|
163
|
+
it non-blocking (removed deliberately; see the module docstring).
|
|
164
|
+
|
|
165
|
+
The three capture-only events (UserPromptSubmit, PostToolUse, Stop)
|
|
166
|
+
discard the response body regardless of what it contains (_send_to_ingest
|
|
167
|
+
never reads it for control flow) — they default to 'async' so they never
|
|
168
|
+
pay the policy-engine round trip for a verdict nobody reads. Set
|
|
169
|
+
AGENTGOVERN_HOOK_MODE='sync' explicitly to force a synchronous verdict on
|
|
170
|
+
these three as well (debugging escape hatch, preserved from the prior
|
|
171
|
+
single-switch behavior).
|
|
172
|
+
"""
|
|
173
|
+
if event_name == "PreToolUse":
|
|
174
|
+
return "sync"
|
|
175
|
+
# NAGT-137: default is 'async' for these three, the opposite of
|
|
176
|
+
# PreToolUse/the pre-NAGT-137 global default — an unset env var must NOT
|
|
177
|
+
# fall back to 'sync' here, or these events would silently keep blocking
|
|
178
|
+
# exactly as before. Only an explicit 'sync' forces the debug escape hatch.
|
|
179
|
+
hook_mode = os.environ.get("AGENTGOVERN_HOOK_MODE", "").lower()
|
|
154
180
|
return "sync" if hook_mode == "sync" else "async"
|
|
155
181
|
|
|
156
182
|
|
|
@@ -224,13 +250,19 @@ def format_action_name(tool_name: str, tool_input: dict[str, Any]) -> str:
|
|
|
224
250
|
def build_payload(
|
|
225
251
|
hook_event: dict[str, Any],
|
|
226
252
|
actor_user_id: str,
|
|
227
|
-
|
|
253
|
+
evaluation_mode: str,
|
|
228
254
|
) -> dict[str, Any]:
|
|
229
255
|
"""Build the interactiveSchema-compliant PreToolUse payload from a hook event.
|
|
230
256
|
|
|
231
257
|
Preserved from v1 — produces a v1-identical payload (no context_snapshot,
|
|
232
258
|
no turn_index). Used directly by _handle_pre_tool_use in minimal mode.
|
|
233
259
|
In enriched mode, _handle_pre_tool_use augments the result with context_snapshot.
|
|
260
|
+
|
|
261
|
+
evaluation_mode is the already-resolved 'sync'/'async' string (see
|
|
262
|
+
_get_evaluation_mode) — NAGT-137: this used to take the raw
|
|
263
|
+
AGENTGOVERN_HOOK_MODE value and normalize it internally; PreToolUse is
|
|
264
|
+
now the only caller and always resolves to 'sync', so the normalization
|
|
265
|
+
moved to the single call site instead of living here.
|
|
234
266
|
"""
|
|
235
267
|
tool_name: str = hook_event.get("tool_name", "")
|
|
236
268
|
tool_input: dict[str, Any] = hook_event.get("tool_input") or {}
|
|
@@ -238,7 +270,6 @@ def build_payload(
|
|
|
238
270
|
|
|
239
271
|
tool_event_type, action_type = map_tool_to_event(tool_name)
|
|
240
272
|
action_name = format_action_name(tool_name, tool_input)
|
|
241
|
-
evaluation_mode = "sync" if hook_mode == "sync" else "async"
|
|
242
273
|
|
|
243
274
|
payload: dict[str, Any] = {
|
|
244
275
|
"schema_version": "1",
|
|
@@ -515,9 +546,26 @@ def _windows_username_principal() -> str | None:
|
|
|
515
546
|
|
|
516
547
|
def _harden_directory_windows(path: Path) -> None:
|
|
517
548
|
"""Best-effort ACL lockdown via icacls — the same tool and pattern this
|
|
518
|
-
repo's own CLAUDE.md prescribes for ~/.agentgovern/prod.env
|
|
519
|
-
|
|
520
|
-
|
|
549
|
+
repo's own CLAUDE.md prescribes for ~/.agentgovern/prod.env, including
|
|
550
|
+
that file's own explicit SYSTEM grant (CLAUDE.md's own success bar for
|
|
551
|
+
this class of file is "no Everyone/BUILTIN\\Users ACE", not "no
|
|
552
|
+
SYSTEM/Administrators" — SYSTEM can read any file on the box regardless
|
|
553
|
+
of ACLs, and a local admin can always take ownership and reset any ACL,
|
|
554
|
+
so neither principal's presence here grants anything they couldn't
|
|
555
|
+
already reach). `/inheritance:r` only strips ACEs the object *inherited*
|
|
556
|
+
from its parent — on a freshly-created folder near a user-profile root
|
|
557
|
+
(the real ~/.agentgovern/ shape, confirmed by direct testing — see
|
|
558
|
+
docs/audit-reports/nagt-095-async-prompt-drop-2026-07-20.md's
|
|
559
|
+
verification-gap note), SYSTEM and Administrators typically arrive as
|
|
560
|
+
*explicit* ACEs already, which /inheritance:r does not touch and
|
|
561
|
+
/grant:r does not remove for a principal it isn't asked about. The
|
|
562
|
+
explicit SYSTEM grant below doesn't change that end state — it makes it
|
|
563
|
+
intentional and self-documenting instead of an accident of Windows'
|
|
564
|
+
default folder-creation behavior, so the code doesn't silently rely on
|
|
565
|
+
an undocumented default that could change.
|
|
566
|
+
|
|
567
|
+
There is no dependency-free stdlib equivalent to POSIX chmod on Windows
|
|
568
|
+
(adding pywin32 as a new dependency for one directory lockdown is out of
|
|
521
569
|
proportion), so this shells out rather than leaving the directory
|
|
522
570
|
unhardened. If icacls is unavailable, times out, or fails for any reason
|
|
523
571
|
(non-NTFS volume, restricted environment, etc.), this logs verbose and
|
|
@@ -535,7 +583,14 @@ def _harden_directory_windows(path: Path) -> None:
|
|
|
535
583
|
return
|
|
536
584
|
try:
|
|
537
585
|
result = subprocess.run(
|
|
538
|
-
[
|
|
586
|
+
[
|
|
587
|
+
"icacls",
|
|
588
|
+
str(path),
|
|
589
|
+
"/inheritance:r",
|
|
590
|
+
"/grant:r",
|
|
591
|
+
f"{principal}:(OI)(CI)F",
|
|
592
|
+
"NT AUTHORITY\\SYSTEM:(OI)(CI)F",
|
|
593
|
+
],
|
|
539
594
|
capture_output=True,
|
|
540
595
|
text=True,
|
|
541
596
|
timeout=2.0,
|
|
@@ -547,7 +602,10 @@ def _harden_directory_windows(path: Path) -> None:
|
|
|
547
602
|
"directory left at default (inherited) permissions."
|
|
548
603
|
)
|
|
549
604
|
else:
|
|
550
|
-
_vlog(
|
|
605
|
+
_vlog(
|
|
606
|
+
f"[agentgovern] Restricted {path} to current user + SYSTEM + "
|
|
607
|
+
"local Administrators via icacls (excludes other local accounts)."
|
|
608
|
+
)
|
|
551
609
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError) as exc:
|
|
552
610
|
_vlog(
|
|
553
611
|
f"[agentgovern] icacls hardening of {path} raised {exc} — "
|
|
@@ -556,11 +614,14 @@ def _harden_directory_windows(path: Path) -> None:
|
|
|
556
614
|
|
|
557
615
|
|
|
558
616
|
def _ensure_hardened_dir(path: Path) -> None:
|
|
559
|
-
"""Create `path` if missing and
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
617
|
+
"""Create `path` if missing and restrict it (current user + SYSTEM +
|
|
618
|
+
local Administrators on Windows, 0700 on POSIX — excludes other local
|
|
619
|
+
accounts; see _harden_directory_windows for why this isn't "owner-only"),
|
|
620
|
+
once, at creation time only — every later call is a single cheap
|
|
621
|
+
is_dir() stat, never repeated chmod/icacls work, so this never adds
|
|
622
|
+
per-invocation cost to the hot path. Applies to both OUTBOX_DIR and
|
|
623
|
+
DEAD_LETTER_DIR, since both are only ever created via _write_json_atomic
|
|
624
|
+
below."""
|
|
564
625
|
if path.is_dir():
|
|
565
626
|
return
|
|
566
627
|
try:
|
|
@@ -970,6 +1031,10 @@ def _handle_pre_tool_use(
|
|
|
970
1031
|
api_url = os.environ.get("AGENTGOVERN_API_URL", _DEFAULT_API_URL).rstrip("/")
|
|
971
1032
|
hook_mode = os.environ.get("AGENTGOVERN_HOOK_MODE", "sync").lower()
|
|
972
1033
|
fail_action = os.environ.get("AGENTGOVERN_FAIL_ACTION", "allow").lower()
|
|
1034
|
+
# NAGT-137: PreToolUse always requests sync, independent of hook_mode
|
|
1035
|
+
# above — hook_mode is still read for the "off" kill switch and
|
|
1036
|
+
# AGENTGOVERN_FAIL_ACTION handling below, just no longer for this.
|
|
1037
|
+
evaluation_mode = _get_evaluation_mode("PreToolUse")
|
|
973
1038
|
|
|
974
1039
|
try:
|
|
975
1040
|
timeout_seconds = (
|
|
@@ -997,7 +1062,7 @@ def _handle_pre_tool_use(
|
|
|
997
1062
|
)
|
|
998
1063
|
return 0
|
|
999
1064
|
|
|
1000
|
-
payload = build_payload(hook_event, actor_user_id,
|
|
1065
|
+
payload = build_payload(hook_event, actor_user_id, evaluation_mode)
|
|
1001
1066
|
|
|
1002
1067
|
# Enriched mode: add context_snapshot (absent in minimal mode for v1 compat).
|
|
1003
1068
|
if capture_mode == CAPTURE_MODE_ENRICHED:
|
|
@@ -1008,7 +1073,7 @@ def _handle_pre_tool_use(
|
|
|
1008
1073
|
timeout_ms_int = int(timeout_seconds * 1000)
|
|
1009
1074
|
_vlog(
|
|
1010
1075
|
f"[agentgovern] POST {endpoint} tool={tool_name} "
|
|
1011
|
-
f"mode={
|
|
1076
|
+
f"mode={evaluation_mode} timeout={timeout_ms_int}ms"
|
|
1012
1077
|
)
|
|
1013
1078
|
|
|
1014
1079
|
t0 = time.monotonic()
|
|
@@ -1057,10 +1122,18 @@ def _handle_pre_tool_use(
|
|
|
1057
1122
|
)
|
|
1058
1123
|
return 0
|
|
1059
1124
|
|
|
1060
|
-
#
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1125
|
+
# NAGT-137: no async-skip branch here — PreToolUse's evaluation_mode is
|
|
1126
|
+
# always 'sync' now (see above), so this payload always told the server
|
|
1127
|
+
# to evaluate synchronously and this response always has a real verdict
|
|
1128
|
+
# to parse. Before this change, the removed branch (`if hook_mode ==
|
|
1129
|
+
# "async": return 0`) was consistent with the payload actually sent,
|
|
1130
|
+
# since hook_mode drove both. Leaving it keyed on the raw
|
|
1131
|
+
# AGENTGOVERN_HOOK_MODE env var after this change — rather than deleting
|
|
1132
|
+
# it — would have reintroduced exactly that inconsistency in the other
|
|
1133
|
+
# direction: an operator with AGENTGOVERN_HOOK_MODE=async set would still
|
|
1134
|
+
# send 'sync' (per _get_evaluation_mode above) and get a real verdict
|
|
1135
|
+
# back, but this branch would have discarded it unparsed, silently
|
|
1136
|
+
# disabling enforcement. Deleted, not left dead.
|
|
1064
1137
|
|
|
1065
1138
|
# ── Sync mode: parse verdict ──────────────────────────────────────────────
|
|
1066
1139
|
data: dict[str, Any] = response.get("data") or {}
|
|
@@ -1131,7 +1204,7 @@ def _handle_user_prompt_submit(hook_event: dict[str, Any]) -> int:
|
|
|
1131
1204
|
"client_event_id": str(uuid.uuid4()),
|
|
1132
1205
|
"input": {"prompt": prompt_text},
|
|
1133
1206
|
"client_timestamp": _now_iso(),
|
|
1134
|
-
"evaluation_mode": _get_evaluation_mode(),
|
|
1207
|
+
"evaluation_mode": _get_evaluation_mode("UserPromptSubmit"),
|
|
1135
1208
|
"context_snapshot": _build_context_snapshot(hook_event),
|
|
1136
1209
|
}
|
|
1137
1210
|
|
|
@@ -1174,7 +1247,7 @@ def _handle_post_tool_use(hook_event: dict[str, Any]) -> int:
|
|
|
1174
1247
|
"client_event_id": str(uuid.uuid4()),
|
|
1175
1248
|
"input": tool_input,
|
|
1176
1249
|
"client_timestamp": _now_iso(),
|
|
1177
|
-
"evaluation_mode": _get_evaluation_mode(),
|
|
1250
|
+
"evaluation_mode": _get_evaluation_mode("PostToolUse"),
|
|
1178
1251
|
"context_snapshot": _build_context_snapshot(hook_event),
|
|
1179
1252
|
}
|
|
1180
1253
|
|
|
@@ -1277,7 +1350,7 @@ def _handle_stop(hook_event: dict[str, Any]) -> int:
|
|
|
1277
1350
|
"input": {},
|
|
1278
1351
|
"output": {"text": assistant_text_capped},
|
|
1279
1352
|
"client_timestamp": _now_iso(),
|
|
1280
|
-
"evaluation_mode": _get_evaluation_mode(),
|
|
1353
|
+
"evaluation_mode": _get_evaluation_mode("Stop"),
|
|
1281
1354
|
"context_snapshot": _build_context_snapshot(hook_event),
|
|
1282
1355
|
}
|
|
1283
1356
|
|
|
@@ -648,7 +648,7 @@ def status() -> int:
|
|
|
648
648
|
if exit_code == 0 and drift_report.has_drift:
|
|
649
649
|
exit_code = 2
|
|
650
650
|
|
|
651
|
-
# Per-event registration detail — every v2 event marked present (
|
|
651
|
+
# Per-event registration detail — every v2 event marked present (OK) or MISSING,
|
|
652
652
|
# with any matcher/timeout drift shown directly beneath its event.
|
|
653
653
|
matcher_drift_by_event = {md.event_type: md for md in drift_report.matcher_drifts}
|
|
654
654
|
timeout_drift_by_event = {td.event_type: td for td in drift_report.timeout_drifts}
|
|
@@ -665,7 +665,7 @@ def status() -> int:
|
|
|
665
665
|
if entry.get("hooks"):
|
|
666
666
|
cmd = entry["hooks"][0].get("command", "")
|
|
667
667
|
timeout_val = entry["hooks"][0].get("timeout", "")
|
|
668
|
-
print(f" {et:<20}
|
|
668
|
+
print(f" {et:<20} OK matcher={matcher_val} cmd={cmd} timeout={timeout_val}s")
|
|
669
669
|
|
|
670
670
|
md = matcher_drift_by_event.get(et)
|
|
671
671
|
for line in _format_matcher_drift_lines(md) if md is not None else []:
|
|
@@ -753,7 +753,9 @@ def _print_coverage_banner() -> None:
|
|
|
753
753
|
|
|
754
754
|
status_label = _coverage_status_label(marker, registered)
|
|
755
755
|
|
|
756
|
-
marks = " ".join(
|
|
756
|
+
marks = " ".join(
|
|
757
|
+
f"{et} {'OK' if et in registered else 'MISSING'}" for et in _V2_HOOK_EVENT_TYPES
|
|
758
|
+
)
|
|
757
759
|
print("Hook registration coverage:")
|
|
758
760
|
print(f" Registered events: {marks}")
|
|
759
761
|
print(f" Mode: {marker if marker else 'legacy-unknown'}")
|
|
@@ -910,11 +912,11 @@ def _utc_timestamp() -> str:
|
|
|
910
912
|
def _print_http_error_hint(status: int, code: str) -> None:
|
|
911
913
|
if status == 401:
|
|
912
914
|
print("\nYour AGENTGOVERN_API_KEY is invalid or revoked.")
|
|
913
|
-
print("Create a new key in the dashboard: Settings
|
|
915
|
+
print("Create a new key in the dashboard: Settings -> API Keys.")
|
|
914
916
|
elif status == 403 and "ACTOR_NOT_IN_TENANT" in code:
|
|
915
917
|
print(
|
|
916
918
|
"\nYour AGENTGOVERN_ACTOR_USER_ID is not a member of this tenant.\n"
|
|
917
|
-
"Check the user ID in the dashboard: Settings
|
|
919
|
+
"Check the user ID in the dashboard: Settings -> Profile."
|
|
918
920
|
)
|
|
919
921
|
elif status == 429:
|
|
920
922
|
print("\nRate limit exceeded. Try again in a few seconds.")
|
|
@@ -84,7 +84,7 @@ def atomic_write(path: Path, content: dict[str, Any]) -> None:
|
|
|
84
84
|
raise PermissionError(
|
|
85
85
|
f"{check_target} is not writable.\n"
|
|
86
86
|
"On Linux/macOS: chmod u+w " + str(check_target) + "\n"
|
|
87
|
-
"On Windows: Right-click
|
|
87
|
+
"On Windows: Right-click -> Properties -> Security -> "
|
|
88
88
|
"add write permission for your user."
|
|
89
89
|
)
|
|
90
90
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentgovern"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.2.0"
|
|
4
4
|
description = "Compliance-as-code middleware for agentic AI workflows."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -39,7 +39,7 @@ sdk = [
|
|
|
39
39
|
"python-dotenv>=1.0.0",
|
|
40
40
|
]
|
|
41
41
|
langchain = ["agentgovern[sdk]", "langchain-core>=0.3.0"]
|
|
42
|
-
langgraph = ["agentgovern[sdk]", "langgraph>=0.2.0", "langchain-core>=0.3.0"]
|
|
42
|
+
langgraph = ["agentgovern[sdk]", "langgraph>=0.2.0,<2.0.0", "langchain-core>=0.3.0"]
|
|
43
43
|
crewai = ["agentgovern[sdk]", "crewai>=0.80.0"]
|
|
44
44
|
openai = ["agentgovern[sdk]", "openai>=1.50.0"]
|
|
45
45
|
dev = [
|
|
@@ -13,6 +13,9 @@ they never touch the real ~/.agentgovern/outbox(-dead)/.
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
|
+
import os
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
16
19
|
import time
|
|
17
20
|
import uuid
|
|
18
21
|
from datetime import datetime, timedelta, timezone
|
|
@@ -106,9 +109,24 @@ def test_posix_hardening_failure_does_not_raise(
|
|
|
106
109
|
hook_mod._ensure_hardened_dir(tmp_path / "hardened-posix-fail") # must not raise
|
|
107
110
|
|
|
108
111
|
|
|
109
|
-
def
|
|
112
|
+
def test_windows_hardening_invokes_icacls_with_user_and_system_grant(
|
|
110
113
|
hook_mod: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
111
114
|
) -> None:
|
|
115
|
+
"""Verifies the COMMAND SHAPE only — deterministic and cross-platform,
|
|
116
|
+
since icacls itself can't run on the Linux CI runner. This is NOT a
|
|
117
|
+
claim that the resulting ACL is "owner-only": /inheritance:r strips only
|
|
118
|
+
inherited ACEs, and on the real ~/.agentgovern/ shape SYSTEM/
|
|
119
|
+
Administrators typically arrive as explicit ACEs it never touches (see
|
|
120
|
+
the module docstring on _harden_directory_windows and the "verification
|
|
121
|
+
gap" note in docs/audit-reports/nagt-095-async-prompt-drop-2026-07-20.md).
|
|
122
|
+
The explicit SYSTEM grant asserted below is intentional and matches
|
|
123
|
+
CLAUDE.md's own prod.env precedent — it does not remove Administrators
|
|
124
|
+
or change the end state, it just makes the intent visible instead of
|
|
125
|
+
relying on an undocumented Windows default. The real end-state
|
|
126
|
+
verification (no Everyone/Users/Authenticated Users ACE) is
|
|
127
|
+
test_windows_hardening_real_acl_excludes_other_local_accounts below,
|
|
128
|
+
which is Windows-only and skipped in CI.
|
|
129
|
+
"""
|
|
112
130
|
monkeypatch.setattr(hook_mod.os, "name", "nt")
|
|
113
131
|
monkeypatch.setenv("USERDOMAIN", "MACHINE")
|
|
114
132
|
monkeypatch.setenv("USERNAME", "ahmed")
|
|
@@ -134,6 +152,53 @@ def test_windows_hardening_invokes_icacls_with_owner_only_grant(
|
|
|
134
152
|
assert str(target) in cmd
|
|
135
153
|
assert "/inheritance:r" in cmd
|
|
136
154
|
assert any("MACHINE\\ahmed:(OI)(CI)F" in part for part in cmd)
|
|
155
|
+
assert any("NT AUTHORITY\\SYSTEM:(OI)(CI)F" in part for part in cmd)
|
|
156
|
+
# The command itself must never add a broad grant — this is the one
|
|
157
|
+
# thing a mock CAN meaningfully assert about the real success criterion.
|
|
158
|
+
for part in cmd:
|
|
159
|
+
assert "Everyone" not in part
|
|
160
|
+
assert "BUILTIN\\Users" not in part
|
|
161
|
+
assert "Authenticated Users" not in part
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@pytest.mark.skipif(os.name != "nt", reason="icacls only exists on Windows")
|
|
165
|
+
def test_windows_hardening_real_acl_excludes_other_local_accounts(
|
|
166
|
+
hook_mod: ModuleType,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Real (non-mocked) end-to-end check, run only on Windows.
|
|
169
|
+
|
|
170
|
+
This exists because the ORIGINAL verification for Finding 1 ran icacls
|
|
171
|
+
against a scratch directory deep under AppData/Local/Temp, where
|
|
172
|
+
SYSTEM/Administrators/owner all arrive as *inherited* ACEs — so
|
|
173
|
+
/inheritance:r stripped all three and the test (and the build-time
|
|
174
|
+
manual check) saw a misleadingly clean "owner alone" result. The real
|
|
175
|
+
~/.agentgovern/ location is different: a freshly-created folder there
|
|
176
|
+
gets SYSTEM/Administrators as *explicit* ACEs from creation, which
|
|
177
|
+
/inheritance:r never touches. Testing against the wrong directory shape
|
|
178
|
+
is exactly how the false "owner-only" claim survived review — this test
|
|
179
|
+
therefore targets a directory created directly under
|
|
180
|
+
Path.home() / ".agentgovern", the real parent, not a generic tmp_path.
|
|
181
|
+
|
|
182
|
+
The real success criterion (matching CLAUDE.md's own prod.env bar) is
|
|
183
|
+
"no Everyone/BUILTIN\\Users/Authenticated Users ACE" — SYSTEM and
|
|
184
|
+
Administrators are expected and accepted, not a failure.
|
|
185
|
+
"""
|
|
186
|
+
target = Path.home() / ".agentgovern" / f"pytest-acl-verify-{uuid.uuid4().hex}"
|
|
187
|
+
try:
|
|
188
|
+
hook_mod._ensure_hardened_dir(target)
|
|
189
|
+
assert target.is_dir()
|
|
190
|
+
|
|
191
|
+
result = subprocess.run(
|
|
192
|
+
["icacls", str(target)], capture_output=True, text=True, timeout=5.0
|
|
193
|
+
)
|
|
194
|
+
assert result.returncode == 0, result.stderr
|
|
195
|
+
|
|
196
|
+
output = result.stdout
|
|
197
|
+
assert "Everyone" not in output, output
|
|
198
|
+
assert "BUILTIN\\Users" not in output, output
|
|
199
|
+
assert "Authenticated Users" not in output, output
|
|
200
|
+
finally:
|
|
201
|
+
shutil.rmtree(target, ignore_errors=True)
|
|
137
202
|
|
|
138
203
|
|
|
139
204
|
def test_windows_hardening_failure_does_not_raise(
|
agentgovern-2.1.0/CHANGELOG.md
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 2.1.0 — 2026-07-20
|
|
4
|
-
|
|
5
|
-
### Feature — durable local outbox retry queue for the Claude Code hook (NAGT-095)
|
|
6
|
-
|
|
7
|
-
Fire-and-forget hook sends (`UserPromptSubmit`/`PostToolUse`/`Stop`) that fail
|
|
8
|
-
at the network level are no longer silently lost. A failed send is now queued
|
|
9
|
-
to a local durable outbox (`~/.agentgovern/outbox/`, one file per event) and
|
|
10
|
-
retried opportunistically on a later hook invocation — bounded so a backlog
|
|
11
|
-
can never meaningfully delay the invocation performing the retry (at most 5
|
|
12
|
-
items per invocation, a 1.5s wall-clock budget, a 60s per-file cooldown).
|
|
13
|
-
Permanently-abandoned events (48h TTL, or a 500-file size cap) are recorded to
|
|
14
|
-
`~/.agentgovern/outbox-dead/` instead of disappearing without a trace.
|
|
15
|
-
|
|
16
|
-
- **Swallowed-exception capture**: the network exception behind a failed send
|
|
17
|
-
is now captured into the queued record itself, turning the next real-world
|
|
18
|
-
occurrence into inspectable diagnostic data instead of a silent, unlogged
|
|
19
|
-
drop.
|
|
20
|
-
- **`client_event_id`**: every fire-and-forget payload now carries a
|
|
21
|
-
client-generated UUID v4, captured (not yet enforced) server-side —
|
|
22
|
-
positions a future dedup fix to act retroactively on rows this release
|
|
23
|
-
ever writes. Delivery remains at-least-once, not exactly-once, until that
|
|
24
|
-
follow-on ships.
|
|
25
|
-
- **Directory hardening**: `~/.agentgovern/outbox(-dead)/` are created
|
|
26
|
-
owner-only (`chmod 0700` on POSIX, an `icacls` ACL lockdown on Windows).
|
|
27
|
-
- Hook script version (`HOOK_VERSION`, distinct from this package version)
|
|
28
|
-
bumped 2.0.0 → 2.1.0 in lockstep.
|
|
29
|
-
|
|
30
|
-
No API or behavior change for installs that never experience a send failure.
|
|
31
|
-
Requires `agentgovern hook install --force` (or a fresh install) to pick up
|
|
32
|
-
the updated hook script — existing deployed hooks do not auto-update.
|
|
33
|
-
|
|
34
|
-
Full design and build record: `docs/audit-reports/nagt-095-async-prompt-drop-2026-07-20.md`.
|
|
35
|
-
|
|
36
|
-
## 2.0.2 — 2026-07-02
|
|
37
|
-
|
|
38
|
-
### Fix — README quickstart correctness (NAGT-019)
|
|
39
|
-
|
|
40
|
-
Two documentation corrections so the published quickstart actually works for a
|
|
41
|
-
first-time customer. No code or API change.
|
|
42
|
-
|
|
43
|
-
- **Install line** now reads `pip install "agentgovern[langchain]"`. The base
|
|
44
|
-
`pip install agentgovern` installs no runtime dependencies (they moved to the
|
|
45
|
-
`[sdk]`/`[langchain]` extras in 0.2.0), so the LangChain quickstart that
|
|
46
|
-
follows it could not run as written.
|
|
47
|
-
- **API-key prefix** in the Claude Code Hook env-var example corrected from
|
|
48
|
-
`as_prod_` to `ag_prod_`, matching the prefix the server actually issues and
|
|
49
|
-
validates (`ag_prod_`/`ag_stg_`/`ag_dev_`).
|
|
50
|
-
|
|
51
|
-
Surfaced by the IAG-007 customer-docs Phase 1 audit (PR #237).
|
|
52
|
-
|
|
53
|
-
## 2.0.1 — 2026-06-29
|
|
54
|
-
|
|
55
|
-
### Fix — Gate 1 wire-shape contract
|
|
56
|
-
|
|
57
|
-
`evaluate_input()` now nests `context` and `metadata` inside the `input` field of
|
|
58
|
-
the request body, matching the engine's `EvaluateInputRequest` schema. Previously
|
|
59
|
-
these were sent at the top level, where the engine silently dropped them — so input
|
|
60
|
-
policies that depend on `context`/`metadata` were evaluated against missing data.
|
|
61
|
-
The public Python signature is unchanged; only the on-the-wire serialization is
|
|
62
|
-
corrected. Surfaced by the SEC-002 pre-flight contract check.
|
|
63
|
-
|
|
64
|
-
## 0.2.0 — 2026-06-11
|
|
65
|
-
|
|
66
|
-
### Breaking change — optional SDK dependencies
|
|
67
|
-
|
|
68
|
-
`httpx`, `pydantic`, and `python-dotenv` are no longer installed by default.
|
|
69
|
-
They have moved to the `[sdk]` optional extra.
|
|
70
|
-
|
|
71
|
-
**Affected users:** anyone who installed `agentgovern` and used the agent SDK
|
|
72
|
-
(`init()`, `track_action()`, `evaluate_input()`, framework instrumentors).
|
|
73
|
-
|
|
74
|
-
**Migration:**
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
# Before
|
|
78
|
-
pip install agentgovern
|
|
79
|
-
|
|
80
|
-
# After — agent SDK usage
|
|
81
|
-
pip install agentgovern[sdk]
|
|
82
|
-
|
|
83
|
-
# After — LangChain (sdk is pulled in automatically)
|
|
84
|
-
pip install agentgovern[langchain]
|
|
85
|
-
|
|
86
|
-
# Hook-only users (Claude Code governance) — no change needed
|
|
87
|
-
pip install agentgovern
|
|
88
|
-
agentgovern hook install
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
**Errata (2026-07-10):** this entry originally read "Claude Code / Cursor
|
|
92
|
-
governance" — Cursor was never shipped as a hook target; corrected to
|
|
93
|
-
Claude Code, the only host with a working client.
|
|
94
|
-
|
|
95
|
-
### Why
|
|
96
|
-
|
|
97
|
-
The hook CLI (`agentgovern hook install/uninstall/status/test`) is pure Python
|
|
98
|
-
standard library and does not need httpx or pydantic. Moving these to an
|
|
99
|
-
optional extra reduces the installation footprint for hook-only deployments
|
|
100
|
-
from ~45 MB to ~1 MB.
|
|
101
|
-
|
|
102
|
-
## 0.1.1b15 — prior
|
|
103
|
-
|
|
104
|
-
Initial beta releases. See git history.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.1.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
|
|
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
|