tripwire-git 0.2.0__tar.gz → 0.2.1__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.
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/PKG-INFO +8 -4
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/README.md +7 -3
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/pyproject.toml +1 -1
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/__init__.py +1 -1
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/cli.py +8 -1
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire_git.egg-info/PKG-INFO +8 -4
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/tests/test_aiguard.py +148 -136
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/LICENSE +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/setup.cfg +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/__main__.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/aiguard.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/approvals.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/blocks.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/config.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/engine.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/gitea.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/github.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/gitio.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/gitlab.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/httpjson.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/models.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire/review.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire_git.egg-info/SOURCES.txt +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire_git.egg-info/dependency_links.txt +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire_git.egg-info/entry_points.txt +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire_git.egg-info/requires.txt +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/src/tripwire_git.egg-info/top_level.txt +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/tests/test_engine.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/tests/test_git.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/tests/test_gitea.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/tests/test_github.py +0 -0
- {tripwire_git-0.2.0 → tripwire_git-0.2.1}/tests/test_gitlab.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tripwire-git
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Protect blocks of code in Git: changes need approval from an owner, an approver or a leader.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: git,code-owners,approval,pre-receive,ci
|
|
@@ -108,7 +108,9 @@ The agent acts as its own identity (`TRIPWIRE_AI_ACTOR`, default `ai-agent`),
|
|
|
108
108
|
which is not an owner unless you list it. When it tries to `Edit`, `MultiEdit`
|
|
109
109
|
or `Write` a protected block, the call is refused with a message naming who may
|
|
110
110
|
approve it, and the request is recorded. A human then approves that exact
|
|
111
|
-
change **from an interactive terminal
|
|
111
|
+
change **from an interactive terminal**; `grant` shows the request and makes
|
|
112
|
+
the person type the approver's name to confirm (a pseudo-terminal alone is not
|
|
113
|
+
trusted):
|
|
112
114
|
|
|
113
115
|
```bash
|
|
114
116
|
tripwire pending # what was blocked
|
|
@@ -125,8 +127,10 @@ Limits: shell commands that rewrite files (`sed -i`, redirects, scripts) are not
|
|
|
125
127
|
inspected, so keep the pre-commit hook and the server / CI check as backstops.
|
|
126
128
|
`grant` trusts whoever runs it in a terminal; a person with shell access can
|
|
127
129
|
also edit `.tripwire/approvals.json` by hand. Verified with simulated hook
|
|
128
|
-
payloads
|
|
129
|
-
|
|
130
|
+
payloads and end to end with a live Claude Code 2.1 session: the edit was
|
|
131
|
+
refused with the message shown to the agent, a human `grant` let the identical
|
|
132
|
+
retry through, and the agent's own attempt to run `tripwire grant` did not
|
|
133
|
+
approve anything.
|
|
130
134
|
|
|
131
135
|
## Platforms
|
|
132
136
|
|
|
@@ -89,7 +89,9 @@ The agent acts as its own identity (`TRIPWIRE_AI_ACTOR`, default `ai-agent`),
|
|
|
89
89
|
which is not an owner unless you list it. When it tries to `Edit`, `MultiEdit`
|
|
90
90
|
or `Write` a protected block, the call is refused with a message naming who may
|
|
91
91
|
approve it, and the request is recorded. A human then approves that exact
|
|
92
|
-
change **from an interactive terminal
|
|
92
|
+
change **from an interactive terminal**; `grant` shows the request and makes
|
|
93
|
+
the person type the approver's name to confirm (a pseudo-terminal alone is not
|
|
94
|
+
trusted):
|
|
93
95
|
|
|
94
96
|
```bash
|
|
95
97
|
tripwire pending # what was blocked
|
|
@@ -106,8 +108,10 @@ Limits: shell commands that rewrite files (`sed -i`, redirects, scripts) are not
|
|
|
106
108
|
inspected, so keep the pre-commit hook and the server / CI check as backstops.
|
|
107
109
|
`grant` trusts whoever runs it in a terminal; a person with shell access can
|
|
108
110
|
also edit `.tripwire/approvals.json` by hand. Verified with simulated hook
|
|
109
|
-
payloads
|
|
110
|
-
|
|
111
|
+
payloads and end to end with a live Claude Code 2.1 session: the edit was
|
|
112
|
+
refused with the message shown to the agent, a human `grant` let the identical
|
|
113
|
+
retry through, and the agent's own attempt to run `tripwire grant` did not
|
|
114
|
+
approve anything.
|
|
111
115
|
|
|
112
116
|
## Platforms
|
|
113
117
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tripwire-git"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Protect blocks of code in Git: changes need approval from an owner, an approver or a leader."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -185,7 +185,7 @@ def cmd_pending(args, root: str) -> int:
|
|
|
185
185
|
def cmd_grant(args, root: str) -> int:
|
|
186
186
|
from .aiguard import load_pending, save_pending
|
|
187
187
|
|
|
188
|
-
if not sys.stdin.isatty()
|
|
188
|
+
if not sys.stdin.isatty():
|
|
189
189
|
print("tripwire: grant must be run by a human in an interactive terminal", file=sys.stderr)
|
|
190
190
|
return 1
|
|
191
191
|
items = load_pending(root)
|
|
@@ -204,6 +204,13 @@ def cmd_grant(args, root: str) -> int:
|
|
|
204
204
|
if args.approver not in allowed:
|
|
205
205
|
print("tripwire: %s may not approve %s" % (args.approver, item["lock"]), file=sys.stderr)
|
|
206
206
|
return 1
|
|
207
|
+
# A pseudo-terminal alone proves nothing (agents can have one), so a person must type the name.
|
|
208
|
+
print("Approving: %s" % item["message"])
|
|
209
|
+
sys.stdout.write("Type the approver name '%s' to confirm: " % args.approver)
|
|
210
|
+
sys.stdout.flush()
|
|
211
|
+
if sys.stdin.readline().strip() != args.approver:
|
|
212
|
+
print("tripwire: confirmation did not match; nothing approved", file=sys.stderr)
|
|
213
|
+
return 1
|
|
207
214
|
path = os.path.join(root, APPROVALS_PATH)
|
|
208
215
|
existing: List[Approval] = []
|
|
209
216
|
if os.path.isfile(path):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tripwire-git
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Protect blocks of code in Git: changes need approval from an owner, an approver or a leader.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: git,code-owners,approval,pre-receive,ci
|
|
@@ -108,7 +108,9 @@ The agent acts as its own identity (`TRIPWIRE_AI_ACTOR`, default `ai-agent`),
|
|
|
108
108
|
which is not an owner unless you list it. When it tries to `Edit`, `MultiEdit`
|
|
109
109
|
or `Write` a protected block, the call is refused with a message naming who may
|
|
110
110
|
approve it, and the request is recorded. A human then approves that exact
|
|
111
|
-
change **from an interactive terminal
|
|
111
|
+
change **from an interactive terminal**; `grant` shows the request and makes
|
|
112
|
+
the person type the approver's name to confirm (a pseudo-terminal alone is not
|
|
113
|
+
trusted):
|
|
112
114
|
|
|
113
115
|
```bash
|
|
114
116
|
tripwire pending # what was blocked
|
|
@@ -125,8 +127,10 @@ Limits: shell commands that rewrite files (`sed -i`, redirects, scripts) are not
|
|
|
125
127
|
inspected, so keep the pre-commit hook and the server / CI check as backstops.
|
|
126
128
|
`grant` trusts whoever runs it in a terminal; a person with shell access can
|
|
127
129
|
also edit `.tripwire/approvals.json` by hand. Verified with simulated hook
|
|
128
|
-
payloads
|
|
129
|
-
|
|
130
|
+
payloads and end to end with a live Claude Code 2.1 session: the edit was
|
|
131
|
+
refused with the message shown to the agent, a human `grant` let the identical
|
|
132
|
+
retry through, and the agent's own attempt to run `tripwire grant` did not
|
|
133
|
+
approve anything.
|
|
130
134
|
|
|
131
135
|
## Platforms
|
|
132
136
|
|
|
@@ -1,136 +1,148 @@
|
|
|
1
|
-
import io
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import subprocess
|
|
5
|
-
import sys
|
|
6
|
-
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
from tripwire.cli import main
|
|
10
|
-
|
|
11
|
-
RULES = {
|
|
12
|
-
"leaders": ["lead"],
|
|
13
|
-
"locks": [{"id": "billing", "owners": ["olga"], "approvers": ["anna"]}],
|
|
14
|
-
}
|
|
15
|
-
CODE = "x = 1\n# @lock-begin billing\nrate = 5\n# @lock-end billing\ny = 2\n"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def git(cwd, *args):
|
|
19
|
-
env = {**os.environ, "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t", "GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t"}
|
|
20
|
-
return subprocess.run(["git", *args], cwd=cwd, env=env, capture_output=True, text=True, check=True).stdout.strip()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@pytest.fixture
|
|
24
|
-
def repo(tmp_path, monkeypatch):
|
|
25
|
-
git(tmp_path, "init", "-q", "-b", "main")
|
|
26
|
-
(tmp_path / ".tripwire").mkdir()
|
|
27
|
-
(tmp_path / ".tripwire" / "locks.json").write_text(json.dumps(RULES))
|
|
28
|
-
(tmp_path / "app.py").write_text(CODE)
|
|
29
|
-
git(tmp_path, "add", "-A")
|
|
30
|
-
git(tmp_path, "commit", "-q", "-m", "init")
|
|
31
|
-
monkeypatch.delenv("TRIPWIRE_AI_ACTOR", raising=False)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
assert hook(repo, monkeypatch, "
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def
|
|
81
|
-
monkeypatch
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
assert hook(repo, monkeypatch, "
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
assert
|
|
108
|
-
|
|
109
|
-
assert
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
monkeypatch
|
|
120
|
-
assert main(["--repo", str(repo), "grant", digest[:10], "--approver", "anna"]) == 1
|
|
121
|
-
assert not (repo / ".tripwire" / "approvals.json").exists()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
(repo
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from tripwire.cli import main
|
|
10
|
+
|
|
11
|
+
RULES = {
|
|
12
|
+
"leaders": ["lead"],
|
|
13
|
+
"locks": [{"id": "billing", "owners": ["olga"], "approvers": ["anna"]}],
|
|
14
|
+
}
|
|
15
|
+
CODE = "x = 1\n# @lock-begin billing\nrate = 5\n# @lock-end billing\ny = 2\n"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def git(cwd, *args):
|
|
19
|
+
env = {**os.environ, "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t", "GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t"}
|
|
20
|
+
return subprocess.run(["git", *args], cwd=cwd, env=env, capture_output=True, text=True, check=True).stdout.strip()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def repo(tmp_path, monkeypatch):
|
|
25
|
+
git(tmp_path, "init", "-q", "-b", "main")
|
|
26
|
+
(tmp_path / ".tripwire").mkdir()
|
|
27
|
+
(tmp_path / ".tripwire" / "locks.json").write_text(json.dumps(RULES))
|
|
28
|
+
(tmp_path / "app.py").write_text(CODE)
|
|
29
|
+
git(tmp_path, "add", "-A")
|
|
30
|
+
git(tmp_path, "commit", "-q", "-m", "init")
|
|
31
|
+
monkeypatch.delenv("TRIPWIRE_AI_ACTOR", raising=False)
|
|
32
|
+
return tmp_path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FakeTTY(io.StringIO):
|
|
36
|
+
def isatty(self):
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def tty(monkeypatch, text):
|
|
41
|
+
monkeypatch.setattr(sys, "stdin", FakeTTY(text))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def hook(repo, monkeypatch, tool, tool_input):
|
|
45
|
+
payload = {"tool_name": tool, "tool_input": tool_input, "cwd": str(repo)}
|
|
46
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
|
|
47
|
+
return main(["--repo", str(repo), "claude-hook"])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def edit(repo, old, new, **kw):
|
|
51
|
+
return {"file_path": str(repo / "app.py"), "old_string": old, "new_string": new, **kw}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_edit_of_protected_block_is_blocked_before_it_happens(repo, monkeypatch, capsys):
|
|
55
|
+
assert hook(repo, monkeypatch, "Edit", edit(repo, "rate = 5", "rate = 9")) == 2
|
|
56
|
+
err = capsys.readouterr().err
|
|
57
|
+
assert "billing" in err and "tripwire grant" in err
|
|
58
|
+
assert (repo / "app.py").read_text() == CODE # hook never touches the file
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_edit_of_unprotected_code_is_allowed(repo, monkeypatch):
|
|
62
|
+
assert hook(repo, monkeypatch, "Edit", edit(repo, "y = 2", "y = 3")) == 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_write_that_changes_protected_block_is_blocked(repo, monkeypatch):
|
|
66
|
+
new = CODE.replace("rate = 5", "rate = 9")
|
|
67
|
+
assert hook(repo, monkeypatch, "Write", {"file_path": str(repo / "app.py"), "content": new}) == 2
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_write_that_leaves_block_unchanged_is_allowed(repo, monkeypatch):
|
|
71
|
+
new = CODE.replace("y = 2", "y = 7")
|
|
72
|
+
assert hook(repo, monkeypatch, "Write", {"file_path": str(repo / "app.py"), "content": new}) == 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_multiedit_is_evaluated_as_a_whole(repo, monkeypatch):
|
|
76
|
+
edits = [{"old_string": "y = 2", "new_string": "y = 3"}, {"old_string": "rate = 5", "new_string": "rate = 6"}]
|
|
77
|
+
assert hook(repo, monkeypatch, "MultiEdit", {"file_path": str(repo / "app.py"), "edits": edits}) == 2
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_deleting_markers_is_blocked(repo, monkeypatch):
|
|
81
|
+
assert hook(repo, monkeypatch, "Edit", edit(repo, "# @lock-begin billing\n", "")) == 2
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_failing_edit_is_left_to_the_tool(repo, monkeypatch):
|
|
85
|
+
assert hook(repo, monkeypatch, "Edit", edit(repo, "no such text", "z")) == 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_ai_actor_can_be_made_owner(repo, monkeypatch):
|
|
89
|
+
monkeypatch.setenv("TRIPWIRE_AI_ACTOR", "olga")
|
|
90
|
+
assert hook(repo, monkeypatch, "Edit", edit(repo, "rate = 5", "rate = 9")) == 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_path_outside_repo_and_other_tools_are_ignored(repo, monkeypatch, tmp_path_factory):
|
|
94
|
+
other = tmp_path_factory.mktemp("elsewhere") / "f.py"
|
|
95
|
+
assert hook(repo, monkeypatch, "Write", {"file_path": str(other), "content": "x"}) == 0
|
|
96
|
+
assert hook(repo, monkeypatch, "Read", {"file_path": str(repo / "app.py")}) == 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_bash_cannot_touch_approvals_or_grant(repo, monkeypatch):
|
|
100
|
+
assert hook(repo, monkeypatch, "Bash", {"command": "tripwire grant abc --approver anna"}) == 2
|
|
101
|
+
assert hook(repo, monkeypatch, "Bash", {"command": "echo [] > .tripwire/approvals.json"}) == 2
|
|
102
|
+
assert hook(repo, monkeypatch, "Bash", {"command": "ls"}) == 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_agent_cannot_edit_approvals_or_rules_files(repo, monkeypatch):
|
|
106
|
+
f = {"file_path": str(repo / ".tripwire" / "approvals.json"), "content": "[]"}
|
|
107
|
+
assert hook(repo, monkeypatch, "Write", f) == 2
|
|
108
|
+
r = {"file_path": str(repo / ".tripwire" / "locks.json"), "content": json.dumps(dict(RULES, leaders=["ai-agent"]))}
|
|
109
|
+
assert hook(repo, monkeypatch, "Write", r) == 2
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_grant_then_same_edit_is_allowed_but_other_edit_is_not(repo, monkeypatch, capsys):
|
|
113
|
+
e = edit(repo, "rate = 5", "rate = 9")
|
|
114
|
+
assert hook(repo, monkeypatch, "Edit", e) == 2
|
|
115
|
+
assert main(["--repo", str(repo), "pending"]) == 0
|
|
116
|
+
digest = json.loads((repo / ".git" / "tripwire" / "pending.json").read_text())[0]["digest"]
|
|
117
|
+
tty(monkeypatch, "bob\n")
|
|
118
|
+
assert main(["--repo", str(repo), "grant", digest[:10], "--approver", "bob"]) == 1 # not an approver
|
|
119
|
+
tty(monkeypatch, "olga\n")
|
|
120
|
+
assert main(["--repo", str(repo), "grant", digest[:10], "--approver", "anna"]) == 1 # wrong confirmation
|
|
121
|
+
assert not (repo / ".tripwire" / "approvals.json").exists()
|
|
122
|
+
tty(monkeypatch, "anna\n")
|
|
123
|
+
assert main(["--repo", str(repo), "grant", digest[:10], "--approver", "anna"]) == 0
|
|
124
|
+
assert hook(repo, monkeypatch, "Edit", e) == 0
|
|
125
|
+
assert hook(repo, monkeypatch, "Edit", edit(repo, "rate = 5", "rate = 10")) == 2 # approval was for rate = 9
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_grant_refuses_non_interactive_use(repo, monkeypatch):
|
|
129
|
+
hook(repo, monkeypatch, "Edit", edit(repo, "rate = 5", "rate = 9"))
|
|
130
|
+
digest = json.loads((repo / ".git" / "tripwire" / "pending.json").read_text())[0]["digest"]
|
|
131
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO("anna\n")) # piped input is not a terminal
|
|
132
|
+
assert main(["--repo", str(repo), "grant", digest[:10], "--approver", "anna"]) == 1
|
|
133
|
+
assert not (repo / ".tripwire" / "approvals.json").exists()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_install_claude_hook_is_idempotent_and_keeps_other_settings(repo):
|
|
137
|
+
(repo / ".claude").mkdir()
|
|
138
|
+
(repo / ".claude" / "settings.json").write_text(json.dumps({"model": "x"}))
|
|
139
|
+
assert main(["--repo", str(repo), "install-claude-hook"]) == 0
|
|
140
|
+
assert main(["--repo", str(repo), "install-claude-hook"]) == 0
|
|
141
|
+
settings = json.loads((repo / ".claude" / "settings.json").read_text())
|
|
142
|
+
assert settings["model"] == "x"
|
|
143
|
+
assert len(settings["hooks"]["PreToolUse"]) == 1
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_not_a_git_repo_is_allowed(tmp_path, monkeypatch):
|
|
147
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps({"tool_name": "Write", "tool_input": {}})))
|
|
148
|
+
assert main(["--repo", str(tmp_path), "claude-hook"]) == 0
|
|
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
|