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.
- {gitvow-0.28.1/src/gitvow.egg-info → gitvow-0.28.2}/PKG-INFO +1 -1
- {gitvow-0.28.1 → gitvow-0.28.2}/pyproject.toml +1 -1
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/default_policy.json +24 -4
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/targets.py +11 -2
- {gitvow-0.28.1 → gitvow-0.28.2/src/gitvow.egg-info}/PKG-INFO +1 -1
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow.egg-info/SOURCES.txt +1 -0
- gitvow-0.28.2/tests/test_git_rules.py +56 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_tiers.py +8 -4
- {gitvow-0.28.1 → gitvow-0.28.2}/LICENSE +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/README.md +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/setup.cfg +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/__init__.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/__main__.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/adapters.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/claims.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/cli.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/collect.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/decisions.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/digest.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/export.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/hooks/__init__.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/install.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/pack.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/paths.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/policy.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/pricing.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/proposals.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/providers.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/recall.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/redact.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/report.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/rules.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/safewrite.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/scan.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/selftest.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/serve.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/snapshots.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/state.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/status.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/sync.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow/transcript.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow.egg-info/dependency_links.txt +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow.egg-info/entry_points.txt +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow.egg-info/requires.txt +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/src/gitvow.egg-info/top_level.txt +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_adapters.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_claims.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_collect.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_command_position.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_commit_window.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_cost.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_decisions.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_digest.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_e2e_globalopt.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_export.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_external_adapter.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_hardening.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_hooks.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_install_cli.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_pack.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_packaging.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_policy.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_proposals.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_providers.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_recall.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_redact.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_report.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_rules.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_scan.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_serve.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_snapshots.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_status.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_sync.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_sync_http.py +0 -0
- {gitvow-0.28.1 → gitvow-0.28.2}/tests/test_targets.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
"
|
|
9
|
+
"program": "git",
|
|
10
|
+
"verbs": [
|
|
11
|
+
"push\\b.*?(?:--force|\\s-f\\b|\\+refs)"
|
|
12
|
+
],
|
|
10
13
|
"reason": "force push"
|
|
11
14
|
},
|
|
12
15
|
{
|
|
13
|
-
"
|
|
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
|
-
"
|
|
52
|
-
"
|
|
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"
|
|
110
|
-
|
|
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.
|
|
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
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|