gitvow 0.2.1__tar.gz → 0.2.3__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.2.1/src/gitvow.egg-info → gitvow-0.2.3}/PKG-INFO +1 -1
- {gitvow-0.2.1 → gitvow-0.2.3}/pyproject.toml +1 -1
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow/hooks/__init__.py +13 -4
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow/install.py +3 -1
- {gitvow-0.2.1 → gitvow-0.2.3/src/gitvow.egg-info}/PKG-INFO +1 -1
- {gitvow-0.2.1 → gitvow-0.2.3}/tests/test_hooks.py +43 -3
- {gitvow-0.2.1 → gitvow-0.2.3}/LICENSE +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/README.md +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/setup.cfg +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow/__init__.py +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow/__main__.py +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow/cli.py +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow/collect.py +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow/default_policy.json +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow/policy.py +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow/redact.py +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow/selftest.py +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow/state.py +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow/transcript.py +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow.egg-info/SOURCES.txt +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow.egg-info/dependency_links.txt +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow.egg-info/entry_points.txt +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow.egg-info/requires.txt +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/src/gitvow.egg-info/top_level.txt +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/tests/test_collect.py +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/tests/test_install_cli.py +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/tests/test_packaging.py +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/tests/test_policy.py +0 -0
- {gitvow-0.2.1 → gitvow-0.2.3}/tests/test_redact.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitvow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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.2.
|
|
7
|
+
version = "0.2.3"
|
|
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"}
|
|
@@ -29,15 +29,17 @@ def notes_ref(session_id: str | None) -> str:
|
|
|
29
29
|
def session_start(h: dict[str, Any], home: str | None = None) -> tuple[int, str]:
|
|
30
30
|
cwd = h.get("cwd") or os.getcwd()
|
|
31
31
|
st = load_state(cwd)
|
|
32
|
+
same_session = st.get("session_id") == h.get("session_id") # a resumed session keeps its counters
|
|
32
33
|
st.update(
|
|
33
34
|
{
|
|
34
35
|
"session_id": h.get("session_id"),
|
|
35
36
|
"transcript_path": h.get("transcript_path"),
|
|
36
|
-
"started": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
37
|
-
"steps": st.get("steps", 0),
|
|
38
|
-
"agent_blobs": st.get("agent_blobs", {}) if
|
|
37
|
+
"started": st.get("started") if same_session and st.get("started") else time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
38
|
+
"steps": st.get("steps", 0) if same_session else 0,
|
|
39
|
+
"agent_blobs": st.get("agent_blobs", {}) if same_session else {},
|
|
39
40
|
}
|
|
40
41
|
)
|
|
42
|
+
st.pop("pending_commit", None)
|
|
41
43
|
save_state(cwd, st)
|
|
42
44
|
log_event(cwd, "session_start", {"session_id": h.get("session_id")})
|
|
43
45
|
return 0, ""
|
|
@@ -75,6 +77,7 @@ def pre_tool_use(h: dict[str, Any], home: str | None = None) -> tuple[int, str]:
|
|
|
75
77
|
st["session_id"] = h.get("session_id") or st.get("session_id")
|
|
76
78
|
st["transcript_path"] = h.get("transcript_path") or st.get("transcript_path")
|
|
77
79
|
st["steps"] = st.get("steps", 0) + 1
|
|
80
|
+
st["pending_commit"] = time.time() # the git hook adds trailers only while this is fresh
|
|
78
81
|
save_state(cwd, st)
|
|
79
82
|
if rules is None:
|
|
80
83
|
log_event(cwd, "allowed", {"tool": tool})
|
|
@@ -162,14 +165,20 @@ def post_tool_use(h: dict[str, Any], home: str | None = None) -> tuple[int, str]
|
|
|
162
165
|
return 0, ""
|
|
163
166
|
if tool != "Bash" or not COMMIT_RE.search(inp.get("command", "")):
|
|
164
167
|
return 0, ""
|
|
168
|
+
st = load_state(cwd)
|
|
169
|
+
if "pending_commit" in st:
|
|
170
|
+
del st["pending_commit"]
|
|
171
|
+
save_state(cwd, st)
|
|
165
172
|
rc, head, _ = git(["rev-parse", "HEAD"], cwd)
|
|
166
173
|
if rc != 0:
|
|
167
174
|
return 0, ""
|
|
168
175
|
rules, warn = _rules_or_none(cwd, home)
|
|
169
176
|
if rules is None:
|
|
170
177
|
return 0, warn
|
|
171
|
-
st = load_state(cwd)
|
|
172
178
|
summ = summarize(h.get("transcript_path") or st.get("transcript_path"), rules=rules)
|
|
179
|
+
_, head_msg, _ = git(["log", "-1", "--format=%B", head], cwd)
|
|
180
|
+
if "Gitvow-Session:" not in head_msg:
|
|
181
|
+
return 0, "" # the commit did not go through (or was not the agent's): no note
|
|
173
182
|
_, files, _ = git(["show", "--stat", "--format=", head], cwd)
|
|
174
183
|
attribution = _attribution(cwd, head, st.get("agent_blobs", {}), summ["files_written"])
|
|
175
184
|
session_id = h.get("session_id") or st.get("session_id")
|
|
@@ -14,7 +14,9 @@ GIT_HOOK = """#!/bin/sh
|
|
|
14
14
|
# gitvow: append session trailers when a session is active in this repo; chain to the repo's own hook if present.
|
|
15
15
|
GD="$(git rev-parse --git-dir)"; STATE="$GD/gitvow-session.json"
|
|
16
16
|
if [ -f "$STATE" ]; then
|
|
17
|
-
|
|
17
|
+
# Trailers go only on commits the agent itself runs: the PreToolUse gate sets pending_commit when the
|
|
18
|
+
# agent invokes git commit and PostToolUse clears it. A human commit in a terminal gets no trailer.
|
|
19
|
+
SID=$(python3 -c "import json,time;s=json.load(open('$STATE'));p=s.get('pending_commit') or 0;print(s.get('session_id') or '' if time.time()-p<300 else '')" 2>/dev/null)
|
|
18
20
|
STEP=$(python3 -c "import json;print(json.load(open('$STATE')).get('steps') or 0)" 2>/dev/null)
|
|
19
21
|
if [ -n "$SID" ] && ! grep -q "^Gitvow-Session:" "$1"; then printf "\\nGitvow-Session: %s\\nGitvow-Step: %s\\n" "$SID" "$STEP" >> "$1"; fi
|
|
20
22
|
fi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitvow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
+
import time
|
|
3
4
|
|
|
4
5
|
from gitvow.hooks import post_tool_use, pre_tool_use, session_start, stop
|
|
5
6
|
from gitvow.install import _write_git_hook
|
|
@@ -51,12 +52,32 @@ def test_commit_flow_trailer_note_and_attribution(repo, home, payload, transcrip
|
|
|
51
52
|
assert att["files"][0]["agent_wrote"] is True and "agent_blob" not in att["files"][0]
|
|
52
53
|
|
|
53
54
|
|
|
54
|
-
def test_human_commit_has_no_trailer(repo, home):
|
|
55
|
+
def test_human_commit_has_no_trailer(repo, home, payload):
|
|
55
56
|
_write_git_hook(str(repo / ".gitvow" / "git-hooks"))
|
|
56
57
|
git(repo, "config", "core.hooksPath", ".gitvow/git-hooks")
|
|
57
58
|
(repo / "a.txt").write_text("h\n")
|
|
58
|
-
git(repo, "commit", "-qam", "human")
|
|
59
|
+
git(repo, "commit", "-qam", "human, no session at all")
|
|
59
60
|
assert "Gitvow-Session" not in git(repo, "log", "-1", "--format=%B")
|
|
61
|
+
session_start(payload("SessionStart"), str(home)) # session active, but the agent did not run git commit
|
|
62
|
+
(repo / "a.txt").write_text("h2\n")
|
|
63
|
+
git(repo, "commit", "-qam", "human, alongside a live session")
|
|
64
|
+
assert "Gitvow-Session" not in git(repo, "log", "-1", "--format=%B")
|
|
65
|
+
st = json.loads((repo / ".git" / "gitvow-session.json").read_text())
|
|
66
|
+
st["pending_commit"] = 1 # a stale flag from long ago
|
|
67
|
+
(repo / ".git" / "gitvow-session.json").write_text(json.dumps(st))
|
|
68
|
+
(repo / "a.txt").write_text("h3\n")
|
|
69
|
+
git(repo, "commit", "-qam", "human, stale flag")
|
|
70
|
+
assert "Gitvow-Session" not in git(repo, "log", "-1", "--format=%B")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_pending_flag_is_cleared_after_note_and_no_note_without_trailer(repo, home, payload):
|
|
74
|
+
session_start(payload("SessionStart"), str(home))
|
|
75
|
+
pre_tool_use(payload("PreToolUse", "Bash", {"command": "git commit -m x"}), str(home))
|
|
76
|
+
assert "pending_commit" in json.loads((repo / ".git" / "gitvow-session.json").read_text())
|
|
77
|
+
# the agent's commit failed (nothing to commit); PostToolUse still runs
|
|
78
|
+
code, msg = post_tool_use(payload("PostToolUse", "Bash", {"command": "git commit -m x"}), str(home))
|
|
79
|
+
assert (code, msg) == (0, "") and git(repo, "for-each-ref", "refs/notes/") == ""
|
|
80
|
+
assert "pending_commit" not in json.loads((repo / ".git" / "gitvow-session.json").read_text())
|
|
60
81
|
|
|
61
82
|
|
|
62
83
|
def test_git_hook_chains_to_repo_hook(repo, home):
|
|
@@ -65,7 +86,9 @@ def test_git_hook_chains_to_repo_hook(repo, home):
|
|
|
65
86
|
local = repo / ".git" / "hooks" / "prepare-commit-msg"
|
|
66
87
|
local.write_text('#!/bin/sh\necho LOCAL >> "$1"\n')
|
|
67
88
|
os.chmod(local, 0o700)
|
|
68
|
-
(repo / ".git" / "gitvow-session.json").write_text(
|
|
89
|
+
(repo / ".git" / "gitvow-session.json").write_text(
|
|
90
|
+
json.dumps({"session_id": "s", "steps": 2, "pending_commit": time.time()})
|
|
91
|
+
)
|
|
69
92
|
(repo / "a.txt").write_text("c\n")
|
|
70
93
|
git(repo, "commit", "-qam", "chain")
|
|
71
94
|
body = git(repo, "log", "-1", "--format=%B")
|
|
@@ -193,3 +216,20 @@ def test_invalid_rules_fail_closed_nothing_written(repo, home, payload, transcri
|
|
|
193
216
|
log = (repo / ".git" / "gitvow-hooks.log").read_text()
|
|
194
217
|
assert '"kind": "redaction_unavailable"' in log
|
|
195
218
|
assert "detail" not in [k for ln in log.splitlines() for k in json.loads(ln) if json.loads(ln)["kind"] == "allowed"]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_new_session_resets_steps_and_blobs_but_resume_keeps_them(repo, home, payload):
|
|
222
|
+
session_start(payload("SessionStart"), str(home))
|
|
223
|
+
pre_tool_use(payload("PreToolUse", "Bash", {"command": "git commit -m x"}), str(home))
|
|
224
|
+
(repo / "a.txt").write_text("b\n")
|
|
225
|
+
post_tool_use(payload("PostToolUse", "Edit", {"file_path": "a.txt"}), str(home))
|
|
226
|
+
st = json.loads((repo / ".git" / "gitvow-session.json").read_text())
|
|
227
|
+
assert st["steps"] == 1 and st["agent_blobs"] and "pending_commit" in st
|
|
228
|
+
session_start(payload("SessionStart"), str(home)) # same id: resume
|
|
229
|
+
st = json.loads((repo / ".git" / "gitvow-session.json").read_text())
|
|
230
|
+
assert st["steps"] == 1 and st["agent_blobs"] and "pending_commit" not in st
|
|
231
|
+
p = payload("SessionStart")
|
|
232
|
+
p["session_id"] = "sess-2"
|
|
233
|
+
session_start(p, str(home)) # a new session in the same repo starts from zero
|
|
234
|
+
st = json.loads((repo / ".git" / "gitvow-session.json").read_text())
|
|
235
|
+
assert st["session_id"] == "sess-2" and st["steps"] == 0 and st["agent_blobs"] == {}
|
|
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
|