claude-dev-env 8.3.0 → 8.4.1

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 (27) hide show
  1. package/.agents/skills/team-advisor/SKILL.md +9 -4
  2. package/_shared/advisor/reference/spawn-walk-log.md +2 -1
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -0
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +169 -14
  5. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +20 -0
  6. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +99 -0
  7. package/bin/install.agents-home.test.mjs +10 -0
  8. package/bin/install.test.mjs +14 -0
  9. package/docs/references/advisor-tool.md +32 -0
  10. package/docs/references/team-advisor-skill.md +1 -1
  11. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
  12. package/hooks/validators/README.md +0 -1
  13. package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +1 -2
  14. package/package.json +1 -1
  15. package/scripts/policy_lint/config/approved_test_pairs.py +9 -0
  16. package/scripts/tests/test_policy_lint_rules_pairing.py +16 -0
  17. package/scripts/windows/pstack/pstack-command-shim.ps1 +41 -0
  18. package/scripts/windows/pstack/ship-pr.cmd +3 -0
  19. package/scripts/windows/pstack/watch-pr.cmd +3 -0
  20. package/hooks/blocking/pii_payload_scan.py +0 -348
  21. package/hooks/blocking/precommit_code_rules_gate.py +0 -50
  22. package/hooks/blocking/test_pii_payload_scan.py +0 -231
  23. package/hooks/blocking/test_precommit_code_rules_gate.py +0 -63
  24. package/hooks/validators/VALIDATION_REPORT.md +0 -287
  25. package/hooks/validators/validator_defaults.py +0 -5
  26. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +0 -12
  27. package/scripts/tests/test_code_review_constants.py +0 -80
@@ -0,0 +1,41 @@
1
+ $command = $args[0]
2
+ $commandArguments = @($args | Select-Object -Skip 1)
3
+
4
+ if (@("watch-pr", "ship-pr") -notcontains $command) {
5
+ Write-Error "The command must be watch-pr or ship-pr"
6
+ exit 1
7
+ }
8
+
9
+ $codexHome = if ([string]::IsNullOrWhiteSpace($env:CODEX_HOME)) {
10
+ Join-Path $env:USERPROFILE ".codex"
11
+ } else {
12
+ $env:CODEX_HOME
13
+ }
14
+ $cacheRoot = Join-Path $codexHome "plugins\cache\pstack-claude\pstack"
15
+ $pluginRoot = Get-ChildItem -LiteralPath $cacheRoot -Directory -ErrorAction SilentlyContinue |
16
+ Where-Object { $_.Name -match "^\d+\.\d+\.\d+$" } |
17
+ Sort-Object { [version]$_.Name } -Descending |
18
+ Select-Object -First 1
19
+
20
+ if ($null -eq $pluginRoot) {
21
+ Write-Error "No installed pstack-claude version was found under $cacheRoot"
22
+ exit 1
23
+ }
24
+
25
+ $entrypoint = Join-Path $pluginRoot.FullName "skills\poteto-mode\scripts\watch-pr\$command"
26
+ if (-not (Test-Path -LiteralPath $entrypoint -PathType Leaf)) {
27
+ Write-Error "The pstack entrypoint was not found: $entrypoint"
28
+ exit 1
29
+ }
30
+
31
+ $scriptsRoot = Split-Path -Parent (Split-Path -Parent $entrypoint)
32
+ $exitCode = 1
33
+ Push-Location $scriptsRoot
34
+ try {
35
+ & bun $entrypoint @commandArguments
36
+ $exitCode = $LASTEXITCODE
37
+ }
38
+ finally {
39
+ Pop-Location
40
+ }
41
+ exit $exitCode
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0pstack-command-shim.ps1" ship-pr %*
3
+ exit /b %ERRORLEVEL%
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0pstack-command-shim.ps1" watch-pr %*
3
+ exit /b %ERRORLEVEL%
@@ -1,348 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Scan Write/Edit payloads and durable post bodies for high-confidence PII.
3
-
4
- Reused by ``pii_prevention_blocker``: ``build_deny_reason`` composes the deny
5
- message, and ``evaluate_write_edit_payload`` / ``evaluate_post_body_texts`` judge
6
- new file content and durable GitHub post bodies against the pure scanners in
7
- ``pii_scanner``.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import sys
13
- from pathlib import Path
14
-
15
- try:
16
- _blocking_directory = str(Path(__file__).resolve().parent)
17
- _hooks_directory = str(Path(__file__).resolve().parent.parent)
18
- for each_bootstrap_directory in (_blocking_directory, _hooks_directory):
19
- if each_bootstrap_directory not in sys.path:
20
- sys.path.insert(0, each_bootstrap_directory)
21
- from code_rules_shared import is_ephemeral_path
22
- from codex_apply_patch import (
23
- CodexPatchError,
24
- parse_codex_apply_patch,
25
- payload_patch_command,
26
- )
27
- from pii_prevention_blocker_parts.repository_exemption import (
28
- repository_allowlisted_values,
29
- )
30
- from pii_scanner import (
31
- PiiFinding,
32
- is_path_exempt_from_pii_scan,
33
- scan_text_for_pii,
34
- )
35
- from precommit_code_rules_gate import resolve_repository_root
36
-
37
- from hooks_constants.local_identity import pii_allowlisted_values_by_repository
38
- from hooks_constants.multi_edit_reconstruction import edits_for_tool
39
- from hooks_constants.pii_prevention_constants import (
40
- ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
41
- CORRECTIVE_MESSAGE_FOOTER,
42
- CORRECTIVE_MESSAGE_HEADER,
43
- EDIT_TOOL_NAME,
44
- FINDING_LINE_TEMPLATE,
45
- MESSAGE_LINE_SEPARATOR,
46
- MULTI_EDIT_TOOL_NAME,
47
- WRITE_TOOL_NAME,
48
- )
49
- except ImportError as import_error:
50
- raise ImportError(
51
- "pii_payload_scan: cannot import its sibling modules; "
52
- "ensure the blocking and hooks directories are importable."
53
- ) from import_error
54
-
55
-
56
- def _apply_patch_post_contents(
57
- command: str, working_directory: str | None
58
- ) -> list[tuple[str, str]]:
59
- """Return (file_path, post_content) pairs for a Codex apply_patch command."""
60
- if not command:
61
- return []
62
- try:
63
- all_patch_files = parse_codex_apply_patch(command, working_directory)
64
- except CodexPatchError:
65
- return []
66
- return [
67
- (each_patch_file.file_path, each_patch_file.post_content)
68
- for each_patch_file in all_patch_files
69
- if each_patch_file.post_content
70
- ]
71
-
72
-
73
- def _apply_patch_target_deny_reason(
74
- each_file_path: str,
75
- each_post_content: str,
76
- hook_payload: dict | None,
77
- ) -> str | None:
78
- """Return the deny reason for one apply_patch target, or None when it is clean or exempt."""
79
- if is_path_exempt_from_pii_scan(each_file_path):
80
- return None
81
- if _target_is_ephemeral_outside_repository(each_file_path, hook_payload):
82
- return None
83
- return _write_deny_reason_for_texts([each_post_content], each_file_path, frozenset())
84
-
85
-
86
- def evaluate_apply_patch_payload(
87
- all_tool_input: dict[str, object],
88
- hook_payload: dict | None = None,
89
- ) -> str | None:
90
- """Return a deny reason when a Codex apply_patch payload's post content carries PII.
91
-
92
- Args:
93
- all_tool_input: The apply_patch payload's ``command`` and other input fields.
94
- hook_payload: The PreToolUse payload carrying ``cwd`` and the session id.
95
-
96
- Returns:
97
- Deny reason text for the first target carrying non-allowlisted PII, or
98
- None when every target is clean or out of scope.
99
- """
100
- command, working_directory = payload_patch_command(hook_payload, all_tool_input)
101
- for each_file_path, each_post_content in _apply_patch_post_contents(
102
- command, working_directory
103
- ):
104
- deny_reason = _apply_patch_target_deny_reason(
105
- each_file_path, each_post_content, hook_payload
106
- )
107
- if deny_reason is not None:
108
- return deny_reason
109
- return None
110
-
111
-
112
- def _target_is_ephemeral_outside_repository(
113
- file_path: str, hook_payload: dict | None
114
- ) -> bool:
115
- """Report whether a write target is a throwaway draft outside every repo.
116
-
117
- ::
118
-
119
- ephemeral path, no repo tree -> True (write scan skipped)
120
- ephemeral path inside a repo -> False (write scan kept)
121
- ordinary path -> False (write scan kept)
122
-
123
- A draft under the harness scratchpad or an ephemeral scratch root turns
124
- durable only through a surface that keeps its own scan, so scanning it at
125
- write time is a false positive. A draft that resolves inside a git tree
126
- still carries that repository's own allowlist, so the scan stays.
127
-
128
- Args:
129
- file_path: The write target path to classify.
130
- hook_payload: The PreToolUse payload carrying the session id, or None to
131
- read the session id from the environment alone.
132
-
133
- Returns:
134
- True only when the path is ephemeral and no enclosing git repository
135
- resolves.
136
- """
137
- if not file_path:
138
- return False
139
- if not is_ephemeral_path(file_path, hook_payload):
140
- return False
141
- resolution_directory = _existing_ancestor_directory(file_path)
142
- if resolution_directory is None:
143
- return True
144
- return resolve_repository_root(resolution_directory) is None
145
-
146
-
147
- def build_deny_reason(all_findings: list[PiiFinding], gate_surface: str) -> str:
148
- """Return the deny message listing each finding for *gate_surface*.
149
-
150
- Args:
151
- all_findings: Findings returned by ``scan_text_for_pii``.
152
- gate_surface: Human-readable surface (write, post body, staged commit).
153
-
154
- Returns:
155
- Multi-line deny reason for ``permissionDecisionReason``.
156
- """
157
- all_lines = [
158
- CORRECTIVE_MESSAGE_HEADER,
159
- f"Surface: {gate_surface}",
160
- ]
161
- for each_finding in all_findings:
162
- all_lines.append(
163
- FINDING_LINE_TEMPLATE.format(
164
- category=each_finding.category,
165
- preview=each_finding.preview,
166
- )
167
- )
168
- all_lines.append(CORRECTIVE_MESSAGE_FOOTER)
169
- message_line_separator = MESSAGE_LINE_SEPARATOR
170
- return message_line_separator.join(all_lines)
171
-
172
-
173
- def _new_strings_from_edits(
174
- tool_name: str, all_tool_input: dict[str, object]
175
- ) -> list[str]:
176
- """Return each non-empty ``new_string`` from an Edit/MultiEdit payload."""
177
- all_texts: list[str] = []
178
- for each_edit in edits_for_tool(tool_name, all_tool_input):
179
- if not isinstance(each_edit, dict):
180
- continue
181
- new_string = each_edit.get("new_string", "")
182
- if isinstance(new_string, str) and new_string:
183
- all_texts.append(new_string)
184
- return all_texts
185
-
186
-
187
- def _collect_write_edit_texts(
188
- tool_name: str, all_tool_input: dict[str, object]
189
- ) -> tuple[str, list[str]]:
190
- raw_file_path = all_tool_input.get("file_path", "")
191
- file_path = raw_file_path if isinstance(raw_file_path, str) else ""
192
- if is_path_exempt_from_pii_scan(file_path):
193
- return file_path, []
194
- if tool_name == WRITE_TOOL_NAME:
195
- write_content = all_tool_input.get("content", "")
196
- if isinstance(write_content, str) and write_content:
197
- return file_path, [write_content]
198
- return file_path, []
199
- if tool_name in (EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME):
200
- return file_path, _new_strings_from_edits(tool_name, all_tool_input)
201
- return file_path, []
202
-
203
-
204
- def _findings_after_allowlist(
205
- text: str, all_allowlisted_values: frozenset[str]
206
- ) -> list[PiiFinding]:
207
- """Return PII findings in *text* whose matched text is not allowlisted."""
208
- return [
209
- each_finding
210
- for each_finding in scan_text_for_pii(text)
211
- if each_finding.matched_text not in all_allowlisted_values
212
- ]
213
-
214
-
215
- def _first_findings_in_texts(
216
- all_texts: list[str], all_allowlisted_values: frozenset[str] = frozenset()
217
- ) -> list[PiiFinding]:
218
- for each_text in all_texts:
219
- all_findings = _findings_after_allowlist(each_text, all_allowlisted_values)
220
- if all_findings:
221
- return all_findings
222
- return []
223
-
224
-
225
- def _existing_ancestor_directory(file_path: str) -> str | None:
226
- """Return the nearest existing ancestor directory of *file_path*, or None.
227
-
228
- PreToolUse runs before Write creates nested parents, so the immediate parent
229
- may not exist yet; git resolution needs a directory that is already on disk.
230
- """
231
- starting_directory = Path(file_path).parent
232
- for each_ancestor in (starting_directory, *starting_directory.parents):
233
- if each_ancestor.exists():
234
- return str(each_ancestor)
235
- return None
236
-
237
-
238
- def _write_path_allowlisted_values(file_path: str) -> frozenset[str]:
239
- """Return the exact values allowlisted for the repository *file_path* sits in.
240
-
241
- ::
242
-
243
- no path, or no allowlist configured -> frozenset() (no git call)
244
- path inside an allowlisted repo tree -> that repo's frozenset of values
245
-
246
- Args:
247
- file_path: The write target path whose repository keys the allowlist.
248
-
249
- Returns:
250
- The exact values the repository allows past the scan, or an empty set.
251
- """
252
- if not file_path:
253
- return frozenset()
254
- if not pii_allowlisted_values_by_repository():
255
- return frozenset()
256
- resolution_directory = _existing_ancestor_directory(file_path)
257
- if resolution_directory is None:
258
- return frozenset()
259
- repository_root = resolve_repository_root(resolution_directory)
260
- if repository_root is None:
261
- return frozenset()
262
- return repository_allowlisted_values(repository_root)
263
-
264
-
265
- def _write_deny_reason_for_texts(
266
- all_texts: list[str],
267
- file_path: str,
268
- all_allowlisted_values: frozenset[str],
269
- ) -> str | None:
270
- """Return the deny reason for the first text carrying non-allowlisted PII.
271
-
272
- Repository allowlist resolution runs once, only after a raw PII hit, so a
273
- clean payload triggers no git call.
274
- """
275
- resolved_allowlisted_values: frozenset[str] | None = None
276
- for each_text in all_texts:
277
- all_raw_findings = scan_text_for_pii(each_text)
278
- if not all_raw_findings:
279
- continue
280
- if resolved_allowlisted_values is None:
281
- resolved_allowlisted_values = (
282
- all_allowlisted_values | _write_path_allowlisted_values(file_path)
283
- )
284
- all_findings = [
285
- each_finding
286
- for each_finding in all_raw_findings
287
- if each_finding.matched_text not in resolved_allowlisted_values
288
- ]
289
- if all_findings:
290
- gate_surface = f"file write ({file_path or 'unknown path'})"
291
- return build_deny_reason(all_findings, gate_surface)
292
- return None
293
-
294
-
295
- def evaluate_write_edit_payload(
296
- tool_name: str,
297
- all_tool_input: dict[str, object],
298
- all_allowlisted_values: frozenset[str] = frozenset(),
299
- hook_payload: dict | None = None,
300
- ) -> str | None:
301
- """Return a deny reason when Write/Edit/MultiEdit content carries PII.
302
-
303
- A value in the target repository's PII allowlist is dropped from the
304
- findings, so a write under that repository's tree may carry it. A draft
305
- under an ephemeral scratch root outside every repository is skipped, since
306
- it turns durable only through surfaces that keep their own scans.
307
-
308
- Args:
309
- tool_name: The intercepted tool name.
310
- all_tool_input: The tool input mapping.
311
- all_allowlisted_values: Extra exact values allowed past the scan,
312
- unioned with the target repository's own allowlist.
313
- hook_payload: The PreToolUse payload carrying the session id, threaded
314
- to the ephemeral-path predicate; None reads the session id from the
315
- environment alone.
316
-
317
- Returns:
318
- Deny reason text, or None when the write is clean or out of scope.
319
- """
320
- if tool_name not in ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES:
321
- return None
322
- file_path, all_texts = _collect_write_edit_texts(tool_name, all_tool_input)
323
- if not all_texts:
324
- return None
325
- if _target_is_ephemeral_outside_repository(file_path, hook_payload):
326
- return None
327
- return _write_deny_reason_for_texts(all_texts, file_path, all_allowlisted_values)
328
-
329
-
330
- def evaluate_post_body_texts(
331
- all_body_texts: list[str],
332
- all_allowlisted_values: frozenset[str] = frozenset(),
333
- ) -> str | None:
334
- """Return a deny reason when any durable post body carries PII.
335
-
336
- Args:
337
- all_body_texts: Body strings extracted from a gh or MCP post tool.
338
- all_allowlisted_values: Exact values the caller's repository allows;
339
- a finding matching one is not a finding. Callers that pass
340
- nothing keep every post fully scanned.
341
-
342
- Returns:
343
- Deny reason text, or None when every body is clean.
344
- """
345
- all_findings = _first_findings_in_texts(all_body_texts, all_allowlisted_values)
346
- if not all_findings:
347
- return None
348
- return build_deny_reason(all_findings, "durable GitHub post body")
@@ -1,50 +0,0 @@
1
- """Library helper that resolves a directory to its Git repository root.
2
-
3
- ``pii_payload_scan.py`` imports ``resolve_repository_root`` for staged scans.
4
- This module has no hook entry point. Native ``git-hooks/pre_commit.py`` shows
5
- the local verification advisory and does not enforce CODE_RULES.
6
- """
7
-
8
- import subprocess
9
- import sys
10
- from pathlib import Path
11
-
12
- _hooks_directory = str(Path(__file__).resolve().parent.parent)
13
- if _hooks_directory not in sys.path:
14
- sys.path.insert(0, _hooks_directory)
15
-
16
- from hooks_constants.precommit_code_rules_gate_constants import ( # noqa: E402
17
- ALL_GIT_REPOSITORY_ROOT_COMMAND,
18
- GIT_COMMAND_TIMEOUT_SECONDS,
19
- )
20
-
21
- from hooks_constants.subprocess_window import hidden_window_creation_flags
22
-
23
-
24
- def resolve_repository_root(working_directory: str | None) -> Path | None:
25
- """Resolve the Git repository root for a directory.
26
-
27
- Args:
28
- working_directory: Directory inside the repository.
29
-
30
- Returns:
31
- Repository root. Returns None when Git cannot resolve the directory.
32
- """
33
- try:
34
- completed_process = subprocess.run(
35
- list(ALL_GIT_REPOSITORY_ROOT_COMMAND),
36
- check=False,
37
- capture_output=True,
38
- text=True,
39
- timeout=GIT_COMMAND_TIMEOUT_SECONDS,
40
- cwd=working_directory,
41
- creationflags=hidden_window_creation_flags(),
42
- )
43
- except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
44
- return None
45
- if completed_process.returncode != 0:
46
- return None
47
- top_level_text = completed_process.stdout.strip()
48
- if not top_level_text:
49
- return None
50
- return Path(top_level_text)
@@ -1,231 +0,0 @@
1
- """Behavior tests for the Write/Edit payload PII evaluation."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import subprocess
7
- import sys
8
- from pathlib import Path
9
-
10
- import pytest
11
-
12
- _HOOK_DIR = Path(__file__).parent
13
- _HOOKS_DIR = _HOOK_DIR.parent
14
-
15
- try:
16
- from pii_payload_scan import (
17
- evaluate_apply_patch_payload,
18
- evaluate_post_body_texts,
19
- evaluate_write_edit_payload,
20
- )
21
- except ImportError:
22
- for each_bootstrap_directory in (str(_HOOK_DIR), str(_HOOKS_DIR)):
23
- if each_bootstrap_directory not in sys.path:
24
- sys.path.insert(0, each_bootstrap_directory)
25
- from pii_payload_scan import (
26
- evaluate_apply_patch_payload,
27
- evaluate_post_body_texts,
28
- evaluate_write_edit_payload,
29
- )
30
-
31
- pytestmark = pytest.mark.usefixtures("ephemeral_exempt_off")
32
-
33
- _ALLOW_SLUG = "AllowOwner/allow-repo"
34
- _OTHER_SLUG = "OtherOwner/other-repo"
35
-
36
-
37
- def test_post_body_with_pii_is_blocked_by_default() -> None:
38
- deny_reason = evaluate_post_body_texts([_assembled_fixture_email()])
39
- assert deny_reason is not None
40
- assert "post body" in deny_reason
41
-
42
-
43
- def test_post_body_allowlisted_values_plumb_through_to_the_scan() -> None:
44
- allowed_value = _assembled_fixture_email()
45
- deny_reason = evaluate_post_body_texts(
46
- [allowed_value], all_allowlisted_values=frozenset({allowed_value})
47
- )
48
- assert deny_reason is None
49
-
50
-
51
- def test_post_body_unlisted_value_still_blocked_beside_allowlist() -> None:
52
- deny_reason = evaluate_post_body_texts(
53
- [_assembled_unlisted_email()],
54
- all_allowlisted_values=frozenset({_assembled_fixture_email()}),
55
- )
56
- assert deny_reason is not None
57
- assert "post body" in deny_reason
58
-
59
-
60
- def _assembled_fixture_email() -> str:
61
- return "owner.fixture" + "@" + "acme-corp" + ".example" + ".io"
62
-
63
-
64
- def _assembled_unlisted_email() -> str:
65
- return "other.person" + "@" + "different-co" + ".example" + ".io"
66
-
67
-
68
- def _write_allowlist_identity(identity_path: Path, slug: str, value: str) -> None:
69
- identity_path.write_text(
70
- json.dumps({"pii_allowlisted_values": {slug: [value]}}),
71
- encoding="utf-8",
72
- )
73
-
74
-
75
- def _init_repo_with_github_origin(repository_root: Path, origin_slug: str) -> None:
76
- repository_root.mkdir(parents=True, exist_ok=True)
77
- subprocess.run(["git", "init", "-q"], cwd=repository_root, check=True)
78
- origin_url = "https://github.com/" + origin_slug + ".git"
79
- subprocess.run(
80
- ["git", "remote", "add", "origin", origin_url],
81
- cwd=repository_root,
82
- check=True,
83
- )
84
-
85
-
86
- def _write_deny_reason(target_path: Path, value: str) -> str | None:
87
- content = "owner email " + value + "\n"
88
- return evaluate_write_edit_payload(
89
- "Write", {"file_path": str(target_path), "content": content}
90
- )
91
-
92
-
93
- def test_write_payload_with_a_fixture_email_is_flagged(tmp_path: Path) -> None:
94
- deny_reason = _write_deny_reason(tmp_path / "notes.md", _assembled_fixture_email())
95
- assert deny_reason is not None
96
- assert "email" in deny_reason
97
-
98
-
99
- def test_allowlisted_value_under_the_allowlisted_repo_is_allowed(
100
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
101
- ) -> None:
102
- allowed_value = _assembled_fixture_email()
103
- identity_path = tmp_path / "local-identity.json"
104
- _write_allowlist_identity(identity_path, _ALLOW_SLUG, allowed_value)
105
- monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
106
- repository_root = tmp_path / "repo"
107
- _init_repo_with_github_origin(repository_root, _ALLOW_SLUG)
108
- assert _write_deny_reason(repository_root / "notes.md", allowed_value) is None
109
-
110
-
111
- def test_allowlisted_value_in_a_different_repo_is_blocked(
112
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
113
- ) -> None:
114
- allowed_value = _assembled_fixture_email()
115
- identity_path = tmp_path / "local-identity.json"
116
- _write_allowlist_identity(identity_path, _ALLOW_SLUG, allowed_value)
117
- monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
118
- repository_root = tmp_path / "repo"
119
- _init_repo_with_github_origin(repository_root, _OTHER_SLUG)
120
- deny_reason = _write_deny_reason(repository_root / "notes.md", allowed_value)
121
- assert deny_reason is not None
122
- assert "email" in deny_reason
123
-
124
-
125
- def test_unlisted_value_in_the_allowlisted_repo_is_blocked(
126
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
127
- ) -> None:
128
- identity_path = tmp_path / "local-identity.json"
129
- _write_allowlist_identity(identity_path, _ALLOW_SLUG, _assembled_fixture_email())
130
- monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
131
- repository_root = tmp_path / "repo"
132
- _init_repo_with_github_origin(repository_root, _ALLOW_SLUG)
133
- deny_reason = _write_deny_reason(
134
- repository_root / "notes.md", _assembled_unlisted_email()
135
- )
136
- assert deny_reason is not None
137
- assert "email" in deny_reason
138
-
139
-
140
- def test_clean_write_payload_skips_repository_resolution(
141
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
142
- ) -> None:
143
- identity_path = tmp_path / "local-identity.json"
144
- _write_allowlist_identity(identity_path, _ALLOW_SLUG, _assembled_fixture_email())
145
- monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
146
- repository_root = tmp_path / "repo"
147
- _init_repo_with_github_origin(repository_root, _ALLOW_SLUG)
148
-
149
- def _fail_if_called(_working_directory: str | None) -> None:
150
- raise AssertionError(
151
- "resolve_repository_root should not run for a payload with no PII"
152
- )
153
-
154
- monkeypatch.setattr(
155
- sys.modules["pii_payload_scan"], "resolve_repository_root", _fail_if_called
156
- )
157
- deny_reason = evaluate_write_edit_payload(
158
- "Write",
159
- {
160
- "file_path": str(repository_root / "notes.md"),
161
- "content": "nothing sensitive here\n",
162
- },
163
- )
164
- assert deny_reason is None
165
-
166
-
167
- def test_allowlisted_value_under_missing_nested_parent_is_allowed(
168
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
169
- ) -> None:
170
- allowed_value = _assembled_fixture_email()
171
- identity_path = tmp_path / "local-identity.json"
172
- _write_allowlist_identity(identity_path, _ALLOW_SLUG, allowed_value)
173
- monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
174
- repository_root = tmp_path / "repo"
175
- _init_repo_with_github_origin(repository_root, _ALLOW_SLUG)
176
- nested_target = repository_root / "new_dir" / "deeper" / "notes.md"
177
- assert not nested_target.parent.exists()
178
- assert _write_deny_reason(nested_target, allowed_value) is None
179
-
180
-
181
- def test_apply_patch_add_with_pii_is_denied(tmp_path: Path) -> None:
182
- repository_root = tmp_path / "repo"
183
- _init_repo_with_github_origin(repository_root, _ALLOW_SLUG)
184
- deny_reason = evaluate_apply_patch_payload(
185
- {
186
- "command": (
187
- "*** Begin Patch\n"
188
- "*** Add File: notes.md\n"
189
- f"+Reach me at {_assembled_fixture_email()}\n"
190
- "*** End Patch"
191
- )
192
- },
193
- hook_payload={"cwd": str(repository_root)},
194
- )
195
- assert deny_reason is not None
196
- assert "email" in deny_reason
197
-
198
-
199
- def test_apply_patch_add_with_clean_content_is_allowed(tmp_path: Path) -> None:
200
- repository_root = tmp_path / "repo"
201
- _init_repo_with_github_origin(repository_root, _ALLOW_SLUG)
202
- deny_reason = evaluate_apply_patch_payload(
203
- {
204
- "command": (
205
- "*** Begin Patch\n"
206
- "*** Add File: notes.md\n"
207
- "+nothing sensitive here\n"
208
- "*** End Patch"
209
- )
210
- },
211
- hook_payload={"cwd": str(repository_root)},
212
- )
213
- assert deny_reason is None
214
-
215
-
216
- def test_apply_patch_payload_accepts_the_tool_input_keyword(tmp_path: Path) -> None:
217
- """evaluate_apply_patch_payload's first parameter is named all_tool_input."""
218
- repository_root = tmp_path / "repo"
219
- _init_repo_with_github_origin(repository_root, _ALLOW_SLUG)
220
- deny_reason = evaluate_apply_patch_payload(
221
- all_tool_input={
222
- "command": (
223
- "*** Begin Patch\n"
224
- "*** Add File: notes.md\n"
225
- "+nothing sensitive here\n"
226
- "*** End Patch"
227
- )
228
- },
229
- hook_payload={"cwd": str(repository_root)},
230
- )
231
- assert deny_reason is None