gitvow 0.28.1__tar.gz → 0.28.2__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 (76) hide show
  1. {gitvow-0.28.1/src/gitvow.egg-info → gitvow-0.28.2}/PKG-INFO +1 -1
  2. {gitvow-0.28.1 → gitvow-0.28.2}/pyproject.toml +1 -1
  3. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/default_policy.json +24 -4
  4. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/targets.py +11 -2
  5. {gitvow-0.28.1 → gitvow-0.28.2/src/gitvow.egg-info}/PKG-INFO +1 -1
  6. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow.egg-info/SOURCES.txt +1 -0
  7. gitvow-0.28.2/tests/test_git_rules.py +56 -0
  8. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_tiers.py +8 -4
  9. {gitvow-0.28.1 → gitvow-0.28.2}/LICENSE +0 -0
  10. {gitvow-0.28.1 → gitvow-0.28.2}/README.md +0 -0
  11. {gitvow-0.28.1 → gitvow-0.28.2}/setup.cfg +0 -0
  12. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/__init__.py +0 -0
  13. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/__main__.py +0 -0
  14. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/adapters.py +0 -0
  15. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/claims.py +0 -0
  16. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/cli.py +0 -0
  17. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/collect.py +0 -0
  18. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/decisions.py +0 -0
  19. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/digest.py +0 -0
  20. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/export.py +0 -0
  21. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/hooks/__init__.py +0 -0
  22. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/install.py +0 -0
  23. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/pack.py +0 -0
  24. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/paths.py +0 -0
  25. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/policy.py +0 -0
  26. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/pricing.py +0 -0
  27. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/proposals.py +0 -0
  28. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/providers.py +0 -0
  29. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/recall.py +0 -0
  30. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/redact.py +0 -0
  31. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/report.py +0 -0
  32. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/rules.py +0 -0
  33. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/safewrite.py +0 -0
  34. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/scan.py +0 -0
  35. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/selftest.py +0 -0
  36. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/serve.py +0 -0
  37. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/snapshots.py +0 -0
  38. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/state.py +0 -0
  39. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/status.py +0 -0
  40. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/sync.py +0 -0
  41. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/transcript.py +0 -0
  42. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow.egg-info/dependency_links.txt +0 -0
  43. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow.egg-info/entry_points.txt +0 -0
  44. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow.egg-info/requires.txt +0 -0
  45. {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow.egg-info/top_level.txt +0 -0
  46. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_adapters.py +0 -0
  47. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_claims.py +0 -0
  48. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_collect.py +0 -0
  49. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_command_position.py +0 -0
  50. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_commit_window.py +0 -0
  51. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_cost.py +0 -0
  52. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_decisions.py +0 -0
  53. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_digest.py +0 -0
  54. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_e2e_globalopt.py +0 -0
  55. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_export.py +0 -0
  56. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_external_adapter.py +0 -0
  57. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_hardening.py +0 -0
  58. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_hooks.py +0 -0
  59. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_install_cli.py +0 -0
  60. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_pack.py +0 -0
  61. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_packaging.py +0 -0
  62. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_policy.py +0 -0
  63. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_proposals.py +0 -0
  64. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_providers.py +0 -0
  65. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_recall.py +0 -0
  66. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_redact.py +0 -0
  67. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_report.py +0 -0
  68. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_rules.py +0 -0
  69. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_scan.py +0 -0
  70. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_serve.py +0 -0
  71. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_snapshots.py +0 -0
  72. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_status.py +0 -0
  73. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_sync.py +0 -0
  74. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_sync_http.py +0 -0
  75. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_targets.py +0 -0
  76. {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_transcript_formats.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitvow
3
- Version: 0.28.1
3
+ Version: 0.28.2
4
4
  Summary: Provenance and policy gate for AI-agent coding sessions: session trailers on commits, redacted session notes in git, a tool-call gate, and a local ledger. No runtime dependencies.
5
5
  Author-email: Nikhil Bora <nikhil@wirevow.com>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gitvow"
7
- version = "0.28.1"
7
+ version = "0.28.2"
8
8
  description = "Provenance and policy gate for AI-agent coding sessions: session trailers on commits, redacted session notes in git, a tool-call gate, and a local ledger. No runtime dependencies."
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -6,11 +6,19 @@
6
6
  "reason": "recursive delete of root/home"
7
7
  },
8
8
  {
9
- "pattern": "\\bgit\\s+push\\b.*(--force|-f\\b|\\+refs)",
9
+ "program": "git",
10
+ "verbs": [
11
+ "push\\b.*?(?:--force|\\s-f\\b|\\+refs)"
12
+ ],
10
13
  "reason": "force push"
11
14
  },
12
15
  {
13
- "pattern": "\\bgit\\s+(reset\\s+--hard|clean\\s+-fd|branch\\s+-D)\\b",
16
+ "program": "git",
17
+ "verbs": [
18
+ "reset\\s+--hard",
19
+ "clean\\s+-fd",
20
+ "branch\\s+-D"
21
+ ],
14
22
  "reason": "destructive git history/worktree operation"
15
23
  },
16
24
  {
@@ -48,8 +56,20 @@
48
56
  ],
49
57
  "bash_confirm": [
50
58
  {
51
- "pattern": "\\bgit\\s+push\\b",
52
- "reason": "pushing to a remote"
59
+ "program": "git",
60
+ "verbs": [
61
+ "push"
62
+ ],
63
+ "target": "branch=(main|master|production|release[^ ]*)$",
64
+ "reason": "pushing to a protected branch"
65
+ },
66
+ {
67
+ "program": "git",
68
+ "verbs": [
69
+ "push"
70
+ ],
71
+ "reason": "pushing to a remote",
72
+ "when": "observe"
53
73
  },
54
74
  {
55
75
  "program": "kubectl",
@@ -106,8 +106,17 @@ def resolve(prog: str, args: list[str], cwd: str | None, env: dict[str, str] | N
106
106
  return kubectl(args, env)
107
107
  if prog == "helm":
108
108
  return helm(args, env)
109
- if prog == "git" and args and args[0] == "push":
110
- return git_push(args[1:], cwd)
109
+ if prog == "git":
110
+ # step over git's own options (`-C dir`, `-c k=v`, `--git-dir=…`) to the subcommand
111
+ i = 0
112
+ while i < len(args) and args[i].startswith("-"):
113
+ i += 2 if args[i] in ("-C", "-c", "--git-dir", "--work-tree", "--namespace") else 1
114
+ if i < len(args) and args[i] == "push":
115
+ repo_dir = None
116
+ for j in range(0, i):
117
+ if args[j] == "-C" and j + 1 < len(args):
118
+ repo_dir = args[j + 1]
119
+ return git_push(args[i + 1 :], repo_dir if repo_dir and os.path.isdir(repo_dir) else cwd)
111
120
  if prog in ("terraform", "tofu"):
112
121
  return terraform(args, cwd)
113
122
  except Exception: # a target resolver must never take the gate down; None asks
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitvow
3
- Version: 0.28.1
3
+ Version: 0.28.2
4
4
  Summary: Provenance and policy gate for AI-agent coding sessions: session trailers on commits, redacted session notes in git, a tool-call gate, and a local ledger. No runtime dependencies.
5
5
  Author-email: Nikhil Bora <nikhil@wirevow.com>
6
6
  License: Apache-2.0
@@ -50,6 +50,7 @@ tests/test_digest.py
50
50
  tests/test_e2e_globalopt.py
51
51
  tests/test_export.py
52
52
  tests/test_external_adapter.py
53
+ tests/test_git_rules.py
53
54
  tests/test_hardening.py
54
55
  tests/test_hooks.py
55
56
  tests/test_install_cli.py
@@ -0,0 +1,56 @@
1
+ """0.28.2: the git rules are program rules, so `git -C <dir> push` and `git -c k=v push` are seen.
2
+
3
+ Found by dogfooding: every push made through `git -C <repo> push` on 2026-09-22 walked past the `pattern` rule
4
+ `\\bgit\\s+push\\b`, which needed the two words adjacent. Same bug class the kubectl rules had until 0.17.1.
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ from gitvow.policy import DEFAULT_POLICY_PATH, evaluate
13
+ from tests.conftest import git
14
+
15
+ C = "/Users/someone/work/repo"
16
+
17
+
18
+ @pytest.fixture
19
+ def pol():
20
+ return json.loads(Path(DEFAULT_POLICY_PATH).read_text())
21
+
22
+
23
+ @pytest.mark.parametrize(
24
+ "cmd",
25
+ [
26
+ "git push --force origin main",
27
+ "git push -f",
28
+ f"git -C {C} push -q -f origin v0.26",
29
+ f"git -C {C} push --force-with-lease origin main",
30
+ "git -c user.name=x push +refs/heads/main",
31
+ f"git -C {C} reset --hard HEAD~1",
32
+ f"git -C {C} clean -fd",
33
+ "git branch -D feature",
34
+ ],
35
+ )
36
+ def test_destructive_git_is_denied_whatever_sits_between_git_and_the_verb(pol, cmd):
37
+ assert evaluate(pol, "Bash", {"command": cmd}).outcome == "deny", cmd
38
+
39
+
40
+ def test_a_push_to_a_protected_branch_asks_and_other_pushes_are_observed(repo, home, pol):
41
+ git(repo, "remote", "add", "origin", "git@example.com:acme/ledger.git")
42
+ d = evaluate(pol, "Bash", {"command": f"git -C {repo} push -q origin main"}, str(repo))
43
+ assert d.outcome == "confirm" and d.when == "immediate" and d.reason == "pushing to a protected branch"
44
+ assert d.findings[0]["finding"].endswith("@ remote=git@example.com:acme/ledger.git branch=main")
45
+ d = evaluate(pol, "Bash", {"command": "git push origin feature/x"}, str(repo))
46
+ assert d.outcome == "confirm" and d.when == "observe" and d.reason == "pushing to a remote"
47
+ d = evaluate(pol, "Bash", {"command": "git push origin HEAD:refs/heads/release-2"}, str(repo))
48
+ assert d.when == "immediate"
49
+ # a push whose destination cannot be read (no such remote) is asked about
50
+ d = evaluate(pol, "Bash", {"command": "git push nowhere feature/x"}, str(repo))
51
+ assert d.when == "immediate"
52
+
53
+
54
+ def test_mentions_of_push_inside_arguments_are_not_pushes(pol):
55
+ assert evaluate(pol, "Bash", {"command": 'git commit -m "docs: how to git push safely"'}).outcome == "allow"
56
+ assert evaluate(pol, "Bash", {"command": "echo 'git push --force is denied' >> notes.md"}).outcome == "allow"
@@ -105,10 +105,14 @@ def test_immediate_confirm_is_answered_once_per_session(repo, home, payload, tra
105
105
  session_start(payload("SessionStart"), str(home))
106
106
  push = payload("PreToolUse", "Bash", {"command": "git push origin feat"}, transcript)
107
107
  code, msg = pre_tool_use(push, str(home))
108
- assert code == 2 and msg.startswith("CONFIRMATION REQUIRED (pushing to a remote)")
108
+ assert code == 2 and msg.startswith("CONFIRMATION REQUIRED (pushing to a protected branch)")
109
109
  assert "gitvow decide 1 accept --scope session" in msg and "gitvow decide 1 decline" in msg
110
110
  fs = dec.open_findings(str(repo))
111
- assert fs[0]["finding"] == "run git (pushing to a remote)" and fs[0]["immediate"] and fs[0]["decision"] is None
111
+ assert (
112
+ fs[0]["finding"] == "run git (pushing to a protected branch)"
113
+ and fs[0]["immediate"]
114
+ and fs[0]["decision"] is None
115
+ )
112
116
  # asked and never answered: not on the card, not owed
113
117
  assert dec.undecided(str(repo)) == []
114
118
  assert "No open findings" in dec.card(str(repo), for_agent=False)
@@ -130,7 +134,7 @@ def test_immediate_confirm_is_answered_once_per_session(repo, home, payload, tra
130
134
  pre_tool_use(payload("PreToolUse", "Bash", {"command": "git commit -m x"}, transcript), str(home))
131
135
  git(repo, "commit", "-qam", "after the push was allowed")
132
136
  body = git(repo, "log", "-1", "--format=%B")
133
- assert "Gitvow-Accepted: run git (pushing to a remote) by t scope=session: release branch" in body
137
+ assert "Gitvow-Accepted: run git (pushing to a protected branch) by t scope=session: release branch" in body
134
138
  post_tool_use(payload("PostToolUse", "Bash", {"command": "git commit -m x"}, transcript), str(home))
135
139
  from gitvow.rules import derive
136
140
 
@@ -154,7 +158,7 @@ def test_declined_immediate_confirm_stays_blocked_for_the_session(repo, home, pa
154
158
  (repo / "a.txt").write_text("d\n")
155
159
  pre_tool_use(payload("PreToolUse", "Bash", {"command": "git commit -m x"}, transcript), str(home))
156
160
  git(repo, "commit", "-qam", "carries the decline")
157
- assert "Gitvow-Declined: run git (pushing to a remote) by t: not from this branch" in git(
161
+ assert "Gitvow-Declined: run git (pushing to a protected branch) by t: not from this branch" in git(
158
162
  repo, "log", "-1", "--format=%B"
159
163
  )
160
164
 
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
File without changes
File without changes