agentgovern 2.0.2__tar.gz → 2.1.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.
Files changed (63) hide show
  1. {agentgovern-2.0.2 → agentgovern-2.1.0}/.gitignore +10 -0
  2. {agentgovern-2.0.2 → agentgovern-2.1.0}/CHANGELOG.md +38 -1
  3. {agentgovern-2.0.2 → agentgovern-2.1.0}/PKG-INFO +1 -1
  4. agentgovern-2.1.0/agentgovern/_version.py +1 -0
  5. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/exceptions.py +111 -111
  6. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/gate2.py +202 -202
  7. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/hook/cli.py +8 -2
  8. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/hook/data/hook.py +451 -28
  9. agentgovern-2.1.0/agentgovern/hook/drift.py +234 -0
  10. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/hook/installer.py +141 -19
  11. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/hook/settings.py +19 -0
  12. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/instrumentors/__init__.py +4 -4
  13. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/instrumentors/_base.py +545 -545
  14. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/instrumentors/langchain.py +411 -411
  15. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/instrumentors/langgraph.py +684 -684
  16. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/types.py +114 -114
  17. {agentgovern-2.0.2 → agentgovern-2.1.0}/examples/langgraph_credit_decision_agent.py +189 -189
  18. {agentgovern-2.0.2 → agentgovern-2.1.0}/pyproject.toml +1 -1
  19. agentgovern-2.1.0/tests/hook/test_drift.py +235 -0
  20. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/hook/test_hook_v2_context_snapshot.py +3 -2
  21. agentgovern-2.1.0/tests/hook/test_hook_v2_outbox_queue.py +726 -0
  22. agentgovern-2.1.0/tests/hook/test_hook_v2_payload_extraction.py +66 -0
  23. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/hook/test_hook_v2_payloads.py +116 -0
  24. agentgovern-2.1.0/tests/hook/test_hook_v2_remediation_message.py +134 -0
  25. agentgovern-2.1.0/tests/hook/test_hook_v2_timing_log.py +70 -0
  26. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/hook/test_installer.py +229 -7
  27. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_base_instrumentor.py +388 -388
  28. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_client.py +210 -210
  29. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_gate2_full_path.py +255 -255
  30. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_gate2_latency.py +86 -86
  31. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_gate2_verdict_handler.py +258 -258
  32. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_instrument_langchain.py +131 -131
  33. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_langchain.py +193 -193
  34. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_langchain_gate1.py +155 -155
  35. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_langchain_gate2_callbacks.py +293 -293
  36. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_langgraph.py +686 -686
  37. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_output_capture_contract.py +276 -276
  38. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_payload_schema.py +231 -231
  39. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_timestamp.py +81 -81
  40. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_version_consistency.py +34 -34
  41. agentgovern-2.0.2/agentgovern/_version.py +0 -1
  42. {agentgovern-2.0.2 → agentgovern-2.1.0}/LICENSE +0 -0
  43. {agentgovern-2.0.2 → agentgovern-2.1.0}/README.md +0 -0
  44. {agentgovern-2.0.2 → agentgovern-2.1.0}/RELEASING.md +0 -0
  45. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/__init__.py +0 -0
  46. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/__main__.py +0 -0
  47. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/client.py +0 -0
  48. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/hook/__init__.py +0 -0
  49. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentgovern/hook/data/__init__.py +0 -0
  50. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentshield/__init__.py +0 -0
  51. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentshield/client.py +0 -0
  52. {agentgovern-2.0.2 → agentgovern-2.1.0}/agentshield/types.py +0 -0
  53. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/__init__.py +0 -0
  54. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/hook/__init__.py +0 -0
  55. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/hook/conftest.py +0 -0
  56. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/hook/test_bundled_hook.py +0 -0
  57. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/hook/test_hook_v2_dispatch.py +0 -0
  58. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/hook/test_hook_v2_transcript_reading.py +0 -0
  59. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/hook/test_hook_v2_turn_index.py +0 -0
  60. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/hook/test_hook_v2_warn_verdict.py +0 -0
  61. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/hook/test_settings.py +0 -0
  62. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_evaluate_input.py +0 -0
  63. {agentgovern-2.0.2 → agentgovern-2.1.0}/tests/test_rate_limit_handling.py +0 -0
@@ -34,3 +34,13 @@ test-results/
34
34
  .claude/settings.local.json
35
35
 
36
36
  .vercel
37
+
38
+ # Ad-hoc test fixtures with decoy/fake secret-shaped content (never real).
39
+ decoy.env
40
+ decoy_notebook.ipynb
41
+ decoy_multiedit.md
42
+
43
+ .env*
44
+
45
+ # Demo scaffolding (fixtures, decoy secrets for demo scripts) — not product code.
46
+ demo-fixtures/
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
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
+
3
36
  ## 2.0.2 — 2026-07-02
4
37
 
5
38
  ### Fix — README quickstart correctness (NAGT-019)
@@ -50,11 +83,15 @@ pip install agentgovern[sdk]
50
83
  # After — LangChain (sdk is pulled in automatically)
51
84
  pip install agentgovern[langchain]
52
85
 
53
- # Hook-only users (Claude Code / Cursor governance) — no change needed
86
+ # Hook-only users (Claude Code governance) — no change needed
54
87
  pip install agentgovern
55
88
  agentgovern hook install
56
89
  ```
57
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
+
58
95
  ### Why
59
96
 
60
97
  The hook CLI (`agentgovern hook install/uninstall/status/test`) is pure Python
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentgovern
3
- Version: 2.0.2
3
+ Version: 2.1.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
@@ -0,0 +1 @@
1
+ __version__ = "2.1.0"
@@ -1,111 +1,111 @@
1
- """AgentGovern SDK exceptions."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import TYPE_CHECKING
6
-
7
- if TYPE_CHECKING:
8
- from agentgovern.types import InputEvaluation
9
-
10
-
11
- # noqa: N818 — name is part of stable SDK public API; renaming would break import contracts for existing users.
12
- class PolicyViolation(Exception): # noqa: N818
13
- """
14
- Raised by Gate 1 when enforcement_mode=enforce and the input is blocked.
15
-
16
- Gate 2 (async ingest) never raises — it fails silently per the fail-open guarantee.
17
- """
18
-
19
- def __init__(self, evaluation: InputEvaluation) -> None:
20
- first = evaluation.violations[0] if evaluation.violations else None
21
- msg = first.reason if first else "Input blocked by compliance policy."
22
- super().__init__(msg)
23
- self.evaluation = evaluation
24
-
25
- @property
26
- def rule_code(self) -> str | None:
27
- return self.evaluation.violations[0].rule_code if self.evaluation.violations else None
28
-
29
- @property
30
- def reason(self) -> str:
31
- return str(self.args[0])
32
-
33
- @property
34
- def framework_ref(self) -> str | None:
35
- return self.evaluation.violations[0].framework_ref if self.evaluation.violations else None
36
-
37
- def __repr__(self) -> str:
38
- return (
39
- f"PolicyViolation(rule_code={self.rule_code!r}, "
40
- f"reason={self.reason!r}, "
41
- f"framework_ref={self.framework_ref!r})"
42
- )
43
-
44
-
45
- # noqa: N818 — name is part of stable SDK public API; renaming would break import contracts for existing users.
46
- class GateUnavailable(Exception): # noqa: N818
47
- """
48
- Raised when Gate 1 is unreachable and fail_closed=True.
49
-
50
- With the default fail_closed=False, Gate 1 errors are silently treated as
51
- allowed=True so the agent continues unaffected.
52
- """
53
-
54
-
55
- class OutputBlocked(Exception): # noqa: N818
56
- """
57
- Raised by Gate 2 when the agent's output is blocked in enforce mode.
58
-
59
- The output is never returned to the caller. Catch this to handle a blocked
60
- response (e.g. show an error message, fall back to a safe default, log to
61
- your own audit trail).
62
- """
63
-
64
- def __init__(self, reason: str, *, evaluation_id: str | None = None) -> None:
65
- super().__init__(reason)
66
- self.reason = reason
67
- self.evaluation_id = evaluation_id
68
-
69
- def __repr__(self) -> str:
70
- return (
71
- f"OutputBlocked(reason={self.reason!r}, "
72
- f"evaluation_id={self.evaluation_id!r})"
73
- )
74
-
75
-
76
- class OutputEscalated(Exception): # noqa: N818
77
- """
78
- Raised by Gate 2 when the agent's output is escalated for human review.
79
-
80
- A review_queue row has been created. The output is held until a human
81
- approves or rejects it. Catch this to notify the caller that a review
82
- is in progress and surface the review_id so they can poll for a decision.
83
- """
84
-
85
- def __init__(self, review_id: str, *, evaluation_id: str | None = None) -> None:
86
- super().__init__(f"Output escalated for human review: {review_id}")
87
- self.review_id = review_id
88
- self.evaluation_id = evaluation_id
89
-
90
- def __repr__(self) -> str:
91
- return (
92
- f"OutputEscalated(review_id={self.review_id!r}, "
93
- f"evaluation_id={self.evaluation_id!r})"
94
- )
95
-
96
-
97
- class ClientError(Exception):
98
- """
99
- Raised on 4xx responses from the AgentGovern API.
100
-
101
- This always propagates regardless of fail_silently — a 4xx indicates a
102
- configuration problem (wrong API key, invalid payload) that the caller
103
- should fix, not a transient network issue.
104
- """
105
-
106
- def __init__(self, message: str, *, status_code: int) -> None:
107
- super().__init__(message)
108
- self.status_code = status_code
109
-
110
- def __repr__(self) -> str:
111
- return f"ClientError(status_code={self.status_code}, message={str(self.args[0])!r})"
1
+ """AgentGovern SDK exceptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from agentgovern.types import InputEvaluation
9
+
10
+
11
+ # noqa: N818 — name is part of stable SDK public API; renaming would break import contracts for existing users.
12
+ class PolicyViolation(Exception): # noqa: N818
13
+ """
14
+ Raised by Gate 1 when enforcement_mode=enforce and the input is blocked.
15
+
16
+ Gate 2 (async ingest) never raises — it fails silently per the fail-open guarantee.
17
+ """
18
+
19
+ def __init__(self, evaluation: InputEvaluation) -> None:
20
+ first = evaluation.violations[0] if evaluation.violations else None
21
+ msg = first.reason if first else "Input blocked by compliance policy."
22
+ super().__init__(msg)
23
+ self.evaluation = evaluation
24
+
25
+ @property
26
+ def rule_code(self) -> str | None:
27
+ return self.evaluation.violations[0].rule_code if self.evaluation.violations else None
28
+
29
+ @property
30
+ def reason(self) -> str:
31
+ return str(self.args[0])
32
+
33
+ @property
34
+ def framework_ref(self) -> str | None:
35
+ return self.evaluation.violations[0].framework_ref if self.evaluation.violations else None
36
+
37
+ def __repr__(self) -> str:
38
+ return (
39
+ f"PolicyViolation(rule_code={self.rule_code!r}, "
40
+ f"reason={self.reason!r}, "
41
+ f"framework_ref={self.framework_ref!r})"
42
+ )
43
+
44
+
45
+ # noqa: N818 — name is part of stable SDK public API; renaming would break import contracts for existing users.
46
+ class GateUnavailable(Exception): # noqa: N818
47
+ """
48
+ Raised when Gate 1 is unreachable and fail_closed=True.
49
+
50
+ With the default fail_closed=False, Gate 1 errors are silently treated as
51
+ allowed=True so the agent continues unaffected.
52
+ """
53
+
54
+
55
+ class OutputBlocked(Exception): # noqa: N818
56
+ """
57
+ Raised by Gate 2 when the agent's output is blocked in enforce mode.
58
+
59
+ The output is never returned to the caller. Catch this to handle a blocked
60
+ response (e.g. show an error message, fall back to a safe default, log to
61
+ your own audit trail).
62
+ """
63
+
64
+ def __init__(self, reason: str, *, evaluation_id: str | None = None) -> None:
65
+ super().__init__(reason)
66
+ self.reason = reason
67
+ self.evaluation_id = evaluation_id
68
+
69
+ def __repr__(self) -> str:
70
+ return (
71
+ f"OutputBlocked(reason={self.reason!r}, "
72
+ f"evaluation_id={self.evaluation_id!r})"
73
+ )
74
+
75
+
76
+ class OutputEscalated(Exception): # noqa: N818
77
+ """
78
+ Raised by Gate 2 when the agent's output is escalated for human review.
79
+
80
+ A review_queue row has been created. The output is held until a human
81
+ approves or rejects it. Catch this to notify the caller that a review
82
+ is in progress and surface the review_id so they can poll for a decision.
83
+ """
84
+
85
+ def __init__(self, review_id: str, *, evaluation_id: str | None = None) -> None:
86
+ super().__init__(f"Output escalated for human review: {review_id}")
87
+ self.review_id = review_id
88
+ self.evaluation_id = evaluation_id
89
+
90
+ def __repr__(self) -> str:
91
+ return (
92
+ f"OutputEscalated(review_id={self.review_id!r}, "
93
+ f"evaluation_id={self.evaluation_id!r})"
94
+ )
95
+
96
+
97
+ class ClientError(Exception):
98
+ """
99
+ Raised on 4xx responses from the AgentGovern API.
100
+
101
+ This always propagates regardless of fail_silently — a 4xx indicates a
102
+ configuration problem (wrong API key, invalid payload) that the caller
103
+ should fix, not a transient network issue.
104
+ """
105
+
106
+ def __init__(self, message: str, *, status_code: int) -> None:
107
+ super().__init__(message)
108
+ self.status_code = status_code
109
+
110
+ def __repr__(self) -> str:
111
+ return f"ClientError(status_code={self.status_code}, message={str(self.args[0])!r})"