toolgovern-cli 0.1.5__tar.gz → 0.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.
Files changed (57) hide show
  1. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/PKG-INFO +4 -3
  2. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/README.md +2 -1
  3. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/pyproject.toml +2 -2
  4. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/shared/paths.py +18 -3
  5. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_classifier_filesystem_scope.py +31 -0
  6. toolgovern_cli-0.2.0/tests/test_shared_paths.py +43 -0
  7. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/.gitignore +0 -0
  8. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/LICENSE +0 -0
  9. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/__init__.py +0 -0
  10. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/approval/__init__.py +0 -0
  11. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/approval/pending_registry.py +0 -0
  12. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/classifier/__init__.py +0 -0
  13. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/classifier/credential_access.py +0 -0
  14. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/classifier/cross_agent_inheritance.py +0 -0
  15. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/classifier/filesystem_scope.py +0 -0
  16. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/classifier/index.py +0 -0
  17. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/classifier/information_flow.py +0 -0
  18. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/classifier/network_egress.py +0 -0
  19. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/classifier/shell_risk.py +0 -0
  20. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/classifier/util.py +0 -0
  21. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/cli.py +0 -0
  22. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/mcp_server.py +0 -0
  23. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/mcp_trust/__init__.py +0 -0
  24. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/middleware/__init__.py +0 -0
  25. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/middleware/idempotency_cache.py +0 -0
  26. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/middleware/on_tool_call.py +0 -0
  27. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/policy/__init__.py +0 -0
  28. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/policy/load_policy.py +0 -0
  29. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/policy/validate_policy.py +0 -0
  30. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/py.typed +0 -0
  31. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/scoping/__init__.py +0 -0
  32. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/scoping/inheritance_enforcer.py +0 -0
  33. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/scoping/scope_declaration.py +0 -0
  34. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/shared/__init__.py +0 -0
  35. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/trace/__init__.py +0 -0
  36. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/trace/canonical_json.py +0 -0
  37. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/trace/trace_reader.py +0 -0
  38. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/trace/trace_writer.py +0 -0
  39. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/src/toolgovern/types.py +0 -0
  40. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/__init__.py +0 -0
  41. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/conftest.py +0 -0
  42. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_classifier_credential_access.py +0 -0
  43. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_classifier_cross_agent_inheritance.py +0 -0
  44. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_classifier_index.py +0 -0
  45. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_classifier_information_flow.py +0 -0
  46. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_classifier_network_egress.py +0 -0
  47. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_classifier_shell_risk.py +0 -0
  48. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_cli.py +0 -0
  49. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_mcp_trust.py +0 -0
  50. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_middleware_on_tool_call.py +0 -0
  51. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_pending_registry.py +0 -0
  52. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_policy.py +0 -0
  53. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_scoping_inheritance_enforcer.py +0 -0
  54. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_scoping_scope_declaration.py +0 -0
  55. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_trace_canonical_json.py +0 -0
  56. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_trace_reader.py +0 -0
  57. {toolgovern_cli-0.1.5 → toolgovern_cli-0.2.0}/tests/test_trace_writer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: toolgovern-cli
3
- Version: 0.1.5
3
+ Version: 0.2.0
4
4
  Summary: Runtime governance middleware for AI agent tool calls -- gate shell, filesystem, network, and credential access before a tool executes.
5
5
  Project-URL: Homepage, https://github.com/RudrenduPaul/toolgovern
6
6
  Project-URL: Repository, https://github.com/RudrenduPaul/toolgovern
@@ -27,7 +27,7 @@ Classifier: Programming Language :: Python :: 3.13
27
27
  Classifier: Topic :: Security
28
28
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
29
29
  Requires-Python: >=3.9
30
- Requires-Dist: cryptography<49,>=48.0.1
30
+ Requires-Dist: cryptography<51,>=48.0.1
31
31
  Requires-Dist: pyyaml<7,>=6.0
32
32
  Provides-Extra: dev
33
33
  Requires-Dist: build<2,>=1.0; extra == 'dev'
@@ -38,6 +38,7 @@ Requires-Dist: mcp[cli]>=2.0.0; extra == 'mcp'
38
38
  Description-Content-Type: text/markdown
39
39
 
40
40
  <!-- mcp-name: io.github.RudrenduPaul/toolgovern -->
41
+
41
42
  # toolgovern (Python)
42
43
 
43
44
  Gate every tool call an AI agent makes -- shell, filesystem, network, credential access -- before
@@ -339,7 +340,7 @@ run(args=["validate", "./toolgovern.policy.yml", "--json"])
339
340
  This is a generic subprocess wrapper around the real CLI (not a second implementation of each
340
341
  subcommand), so it stays in sync with `validate`, `audit`, and any future subcommand
341
342
  automatically. This is distinct from toolgovern's `mcp_trust` module, which is a client-side
342
- tool for verifying the trustworthiness of *other* MCP servers an agent connects to -- this
343
+ tool for verifying the trustworthiness of _other_ MCP servers an agent connects to -- this
343
344
  section is about toolgovern-cli exposing its own MCP server for agents to call.
344
345
 
345
346
  ## The signed audit trail
@@ -1,4 +1,5 @@
1
1
  <!-- mcp-name: io.github.RudrenduPaul/toolgovern -->
2
+
2
3
  # toolgovern (Python)
3
4
 
4
5
  Gate every tool call an AI agent makes -- shell, filesystem, network, credential access -- before
@@ -300,7 +301,7 @@ run(args=["validate", "./toolgovern.policy.yml", "--json"])
300
301
  This is a generic subprocess wrapper around the real CLI (not a second implementation of each
301
302
  subcommand), so it stays in sync with `validate`, `audit`, and any future subcommand
302
303
  automatically. This is distinct from toolgovern's `mcp_trust` module, which is a client-side
303
- tool for verifying the trustworthiness of *other* MCP servers an agent connects to -- this
304
+ tool for verifying the trustworthiness of _other_ MCP servers an agent connects to -- this
304
305
  section is about toolgovern-cli exposing its own MCP server for agents to call.
305
306
 
306
307
  ## The signed audit trail
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "toolgovern-cli"
7
- version = "0.1.5"
7
+ version = "0.2.0"
8
8
  description = "Runtime governance middleware for AI agent tool calls -- gate shell, filesystem, network, and credential access before a tool executes."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -31,7 +31,7 @@ classifiers = [
31
31
  ]
32
32
  dependencies = [
33
33
  "PyYAML>=6.0,<7",
34
- "cryptography>=48.0.1,<49",
34
+ "cryptography>=48.0.1,<51",
35
35
  ]
36
36
 
37
37
  [project.optional-dependencies]
@@ -15,8 +15,18 @@ _BRACKETED_HOST_PATTERN = re.compile(r"^\[([^\]]+)\]")
15
15
 
16
16
 
17
17
  def normalize_path(raw_path: str) -> str:
18
- """Collapses ``./``, trailing slashes, and duplicate slashes for stable prefix comparison."""
18
+ """Collapses ``./``, trailing slashes, and duplicate slashes for stable prefix comparison.
19
+
20
+ Backslashes are treated as path separators too -- the same as forward slashes -- regardless
21
+ of the host OS this code happens to run on. A declared scope must hold whether the target
22
+ path is destined for a Windows filesystem (where ``\\`` is the native separator) or is simply
23
+ an attacker-supplied string mixing separators to dodge a ``/``-only prefix check; without
24
+ this, a path like ``/allowed/sub\\..\\..\\..\\secrets`` compares as a literal child of
25
+ ``/allowed`` even though it resolves outside it once backslashes are treated as separators
26
+ downstream.
27
+ """
19
28
  path = raw_path.strip()
29
+ path = path.replace("\\", "/")
20
30
  if path.startswith("./"):
21
31
  path = path[2:]
22
32
  path = re.sub(r"/+", "/", path)
@@ -37,8 +47,13 @@ def is_path_within(candidate: str, prefix: str) -> bool:
37
47
 
38
48
 
39
49
  def contains_path_traversal(raw_path: str) -> bool:
40
- """True if the path contains a ``..`` segment that could escape a scoped prefix via traversal."""
41
- return ".." in raw_path.split("/")
50
+ """True if the path contains a ``..`` segment that could escape a scoped prefix via traversal.
51
+
52
+ Splits on both ``/`` and ``\\`` (see ``normalize_path`` above for why backslash must count as
53
+ a separator here too) so ``sub\\..\\..\\..\\secrets`` is caught exactly like
54
+ ``sub/../../../secrets``.
55
+ """
56
+ return ".." in re.split(r"[/\\]", raw_path)
42
57
 
43
58
 
44
59
  def normalize_host(host_like: str) -> str:
@@ -121,6 +121,37 @@ class TestPathTraversal:
121
121
  decision, ids = _fired(ctx)
122
122
  assert "TG02-path-traversal" in ids
123
123
 
124
+ # 2026-08-24 security fix: `contains_path_traversal`/`is_path_within`
125
+ # (python/src/toolgovern/shared/paths.py) used to split only on "/", so a backslash-delimited
126
+ # ".." segment (e.g. "sub\..\..\..\secrets") was never recognized as traversal -- the same
127
+ # string compared as a literal in-scope child of the declared prefix because it happened to
128
+ # start with the right characters, letting a sub-agent escape its declared filesystem scope
129
+ # undetected. Identical bug in the TypeScript and .NET ports; all three share this fix.
130
+ def test_fires_on_backslash_delimited_dotdot(self, ctx_factory):
131
+ ctx = ctx_factory(
132
+ {"path": "/workspace/sub\\..\\..\\..\\secrets", "operation": "write"},
133
+ scope=ScopeDeclaration(filesystem=["/workspace"]),
134
+ )
135
+ decision, ids = _fired(ctx)
136
+ assert decision == "deny"
137
+ assert "TG02-path-traversal" in ids
138
+
139
+ def test_fires_on_bare_backslash_traversal(self, ctx_factory):
140
+ ctx = ctx_factory(
141
+ {"path": "..\\..\\secrets", "operation": "read"},
142
+ scope=ScopeDeclaration(filesystem=["/workspace"]),
143
+ )
144
+ decision, ids = _fired(ctx)
145
+ assert "TG02-path-traversal" in ids
146
+
147
+ def test_fires_on_mixed_separator_traversal(self, ctx_factory):
148
+ ctx = ctx_factory(
149
+ {"path": "/workspace/sub/..\\../etc/passwd", "operation": "write"},
150
+ scope=ScopeDeclaration(filesystem=["/workspace"]),
151
+ )
152
+ decision, ids = _fired(ctx)
153
+ assert "TG02-path-traversal" in ids
154
+
124
155
 
125
156
  class TestSymlinkEscape:
126
157
  def test_fires_outside_scope(self, ctx_factory):
@@ -0,0 +1,43 @@
1
+ """Unit tests for toolgovern.shared.paths -- 2026-08-24 security fix.
2
+
3
+ Previously ``normalize_path``/``contains_path_traversal`` only split on "/", so a backslash-
4
+ delimited ".." segment was invisible to traversal detection, and a scope prefix like "/allowed"
5
+ would treat any candidate that merely started with the literal string "/allowed/" as in-scope even
6
+ when the remainder was pure backslash-delimited traversal
7
+ (``/allowed/sub\\..\\..\\..\\secrets``). This undercut the "sub-agent can't reach outside its
8
+ declared scope" guarantee identically across the TypeScript, Python, and .NET ports.
9
+ """
10
+
11
+ from toolgovern.shared.paths import contains_path_traversal, is_path_within, normalize_path
12
+
13
+
14
+ def test_normalize_path_converts_backslashes_to_forward_slashes():
15
+ assert normalize_path("workspace\\sub\\file.txt") == "workspace/sub/file.txt"
16
+
17
+
18
+ def test_normalize_path_collapses_mixed_separators_consistently():
19
+ assert normalize_path("./workspace\\sub/../file.txt") == "workspace/sub/../file.txt"
20
+
21
+
22
+ def test_contains_path_traversal_detects_backslash_delimited_dotdot():
23
+ assert contains_path_traversal("sub\\..\\..\\..\\secrets") is True
24
+
25
+
26
+ def test_contains_path_traversal_detects_mixed_separator_dotdot():
27
+ assert contains_path_traversal("sub/..\\../secrets") is True
28
+
29
+
30
+ def test_contains_path_traversal_false_for_clean_backslash_path():
31
+ assert contains_path_traversal("workspace\\sub\\file.txt") is False
32
+
33
+
34
+ def test_contains_path_traversal_still_detects_forward_slash_case():
35
+ assert contains_path_traversal("../../etc/passwd") is True
36
+
37
+
38
+ def test_is_path_within_still_recognizes_nested_path_as_in_scope():
39
+ assert is_path_within("/allowed/sub/dir/file.txt", "/allowed") is True
40
+
41
+
42
+ def test_is_path_within_still_rejects_unrelated_path():
43
+ assert is_path_within("/other/dir/file.txt", "/allowed") is False
File without changes