claude-dev-env 2.31.0 → 2.31.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.
- package/docs/CODE_RULES.md +2 -1
- package/hooks/blocking/code_rules_duplicate_body.py +272 -100
- package/hooks/blocking/code_rules_enforcer.py +32 -14
- package/hooks/blocking/code_rules_shared.py +64 -14
- package/hooks/blocking/pr_description_writer_gate.py +18 -25
- package/hooks/blocking/tdd_enforcer.py +8 -4
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +12 -1
- package/hooks/blocking/test_code_rules_enforcer_ephemeral_repository_root.py +118 -0
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +244 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +22 -0
- package/hooks/blocking/test_pr_description_writer_gate.py +21 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +9 -2
- package/hooks/blocking/test_state_description_blocker.py +99 -53
- package/hooks/blocking/test_tdd_enforcer.py +64 -0
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +34 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_content.py +2 -2
- package/hooks/hooks_constants/test_multi_edit_reconstruction.py +1 -0
- package/hooks/hooks_constants/test_subprocess_budget_completeness_content.py +33 -0
- package/hooks/observability/pr_description_writer_spawn_tracker.py +4 -12
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +6 -2
- package/hooks/validators/abbreviation_checks.py +3 -3
- package/hooks/validators/code_quality_checks.py +4 -4
- package/hooks/validators/config/baseline_identity_constants.py +12 -0
- package/hooks/validators/config/directory_exemption_constants.py +44 -1
- package/hooks/validators/config/validator_base_constants.py +9 -0
- package/hooks/validators/fast_save_validators.py +23 -80
- package/hooks/validators/pr_reference_checks.py +2 -2
- package/hooks/validators/python_antipattern_checks.py +3 -3
- package/hooks/validators/python_style_checks.py +3 -2
- package/hooks/validators/react_checks.py +2 -9
- package/hooks/validators/run_all_validators.py +190 -145
- package/hooks/validators/security_checks.py +3 -3
- package/hooks/validators/test_directory_exemption_constants.py +30 -24
- package/hooks/validators/test_fast_save_validators.py +16 -16
- package/hooks/validators/test_hook_timing_harness.py +10 -1
- package/hooks/validators/test_run_all_validators_pretooluse.py +161 -0
- package/hooks/validators/test_run_all_validators_repository_root.py +202 -0
- package/hooks/validators/test_validator_base.py +51 -2
- package/hooks/validators/todo_checks.py +2 -2
- package/hooks/validators/type_safety_checks.py +3 -3
- package/hooks/validators/useless_test_checks.py +3 -3
- package/hooks/validators/validator_base.py +73 -1
- package/package.json +1 -1
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +15 -0
- package/scripts/grok_headless_runner.py +15 -12
- package/scripts/shared_tree_paths.py +70 -0
- package/scripts/test_shared_tree_paths.py +143 -0
|
@@ -251,10 +251,50 @@ def _extract_fstring_literal_parts(
|
|
|
251
251
|
return "".join(display_segments), "".join(shape_segments)
|
|
252
252
|
|
|
253
253
|
|
|
254
|
-
def
|
|
254
|
+
def _normalized_absolute_comparison_path(raw_path: str) -> str:
|
|
255
|
+
"""Return raw_path as an absolute, lowercase, forward-slash path for root comparison.
|
|
256
|
+
|
|
257
|
+
Strips a leading Windows drive letter after normalizing, so a POSIX root
|
|
258
|
+
check and a Windows root check compare on the same shape.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
raw_path: The path to normalize.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
The absolute, lowercase, forward-slash path with any leading drive
|
|
265
|
+
letter removed.
|
|
266
|
+
"""
|
|
267
|
+
return LEADING_DRIVE_LETTER_PATTERN.sub("", os.path.abspath(raw_path).replace("\\", "/").lower())
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _is_at_or_under_root(normalized_candidate: str, normalized_root: str) -> bool:
|
|
271
|
+
"""Return whether normalized_candidate sits at or under normalized_root.
|
|
272
|
+
|
|
273
|
+
Requires a path-segment boundary (an exact match or a ``/`` before the
|
|
274
|
+
remainder), so ``/tmp/repo-sibling`` never counts as under ``/tmp/repo``.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
normalized_candidate: A path already run through
|
|
278
|
+
``_normalized_absolute_comparison_path``.
|
|
279
|
+
normalized_root: A root path already run through
|
|
280
|
+
``_normalized_absolute_comparison_path``.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
True when normalized_candidate equals normalized_root or names a
|
|
284
|
+
descendant of it.
|
|
285
|
+
"""
|
|
286
|
+
return normalized_candidate == normalized_root or normalized_candidate.startswith(normalized_root + "/")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def is_ephemeral_script_path(file_path: str, repository_root: str = "") -> bool:
|
|
255
290
|
"""Return True when the path is rooted at a throwaway scratch directory.
|
|
256
291
|
|
|
257
292
|
Checks these sources in order:
|
|
293
|
+
- ``repository_root`` — when given, a path inside it is never ephemeral,
|
|
294
|
+
whatever directory the repository itself is checked out under. This
|
|
295
|
+
keeps a repository placed under ``/tmp`` fully gated: the exemption
|
|
296
|
+
below is for scratch outside every repository, not for the repository's
|
|
297
|
+
own files.
|
|
258
298
|
- ``$CLAUDE_JOB_DIR/tmp`` — only when ``CLAUDE_JOB_DIR`` is set.
|
|
259
299
|
- Root-anchored ``/tmp`` and ``/temp`` (drive-letter tolerant).
|
|
260
300
|
|
|
@@ -262,20 +302,28 @@ def is_ephemeral_script_path(file_path: str) -> bool:
|
|
|
262
302
|
its sandbox fixtures there, so matching it would exempt the suite's own
|
|
263
303
|
targets. Returns False when ``CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT``
|
|
264
304
|
is truthy, when ``file_path`` is empty, and when no root matches. Path
|
|
265
|
-
classification is string-only;
|
|
305
|
+
classification is string-only; neither path need exist.
|
|
266
306
|
|
|
267
307
|
Args:
|
|
268
308
|
file_path: The candidate path to classify.
|
|
309
|
+
repository_root: The session's working directory — typically the
|
|
310
|
+
PreToolUse payload's ``cwd`` — or empty when no such directory is
|
|
311
|
+
known. A path outside this directory is unaffected by it.
|
|
269
312
|
|
|
270
313
|
Returns:
|
|
271
|
-
True when the path is rooted at a recognized ephemeral scratch
|
|
314
|
+
True when the path is rooted at a recognized ephemeral scratch
|
|
315
|
+
directory and does not sit inside ``repository_root``.
|
|
272
316
|
"""
|
|
273
317
|
if not file_path:
|
|
274
318
|
return False
|
|
275
319
|
disable_value = os.environ.get(EPHEMERAL_EXEMPT_DISABLE_ENVIRONMENT_VARIABLE_NAME, "").strip().lower()
|
|
276
320
|
if disable_value in ALL_EPHEMERAL_EXEMPT_DISABLE_TRUTHY_VALUES:
|
|
277
321
|
return False
|
|
278
|
-
normalized =
|
|
322
|
+
normalized = _normalized_absolute_comparison_path(file_path)
|
|
323
|
+
if repository_root and _is_at_or_under_root(
|
|
324
|
+
normalized, _normalized_absolute_comparison_path(repository_root)
|
|
325
|
+
):
|
|
326
|
+
return False
|
|
279
327
|
all_temp_roots: list[str] = []
|
|
280
328
|
job_dir = os.environ.get(CLAUDE_JOB_DIR_ENVIRONMENT_VARIABLE_NAME)
|
|
281
329
|
if job_dir:
|
|
@@ -286,7 +334,7 @@ def is_ephemeral_script_path(file_path: str) -> bool:
|
|
|
286
334
|
for each_root in ALL_ROOT_ANCHORED_EPHEMERAL_DIRECTORIES:
|
|
287
335
|
all_temp_roots.append(each_root)
|
|
288
336
|
for each_temp_root in all_temp_roots:
|
|
289
|
-
if normalized
|
|
337
|
+
if _is_at_or_under_root(normalized, each_temp_root):
|
|
290
338
|
return True
|
|
291
339
|
return False
|
|
292
340
|
|
|
@@ -426,23 +474,25 @@ def is_ephemeral_path(file_path: str, hook_payload: dict | None = None) -> bool:
|
|
|
426
474
|
Combines the path families a repo gate skips: the root-anchored ephemeral
|
|
427
475
|
scratch directories (``/tmp`` and ``$CLAUDE_JOB_DIR/tmp``), the harness
|
|
428
476
|
session scratchpad, and a coding agent's own home-directory tooling such as
|
|
429
|
-
``~/.grok/runs/``.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
477
|
+
``~/.grok/runs/``. A path inside the payload's own working directory
|
|
478
|
+
(``cwd``) is never treated as scratch, so a repository checked out under
|
|
479
|
+
``/tmp`` still receives full enforcement. The scratchpad match reads the
|
|
480
|
+
session id from the payload when supplied, else the harness environment
|
|
481
|
+
variable. The code-rules and TDD gates call the underlying predicates
|
|
482
|
+
``is_ephemeral_script_path``, ``is_agent_home_tooling``, and
|
|
435
483
|
``is_under_session_scratchpad`` directly.
|
|
436
484
|
|
|
437
485
|
Args:
|
|
438
486
|
file_path: The candidate path to classify.
|
|
439
|
-
hook_payload: The PreToolUse payload carrying the session id
|
|
440
|
-
read the session id from
|
|
487
|
+
hook_payload: The PreToolUse payload carrying the session id and the
|
|
488
|
+
working directory (``cwd``), or None to read the session id from
|
|
489
|
+
the environment alone.
|
|
441
490
|
|
|
442
491
|
Returns:
|
|
443
492
|
True when the path is scratch, agent tooling, or the session scratchpad.
|
|
444
493
|
"""
|
|
445
|
-
|
|
494
|
+
repository_root = str((hook_payload or {}).get("cwd") or "")
|
|
495
|
+
if is_ephemeral_script_path(file_path, repository_root):
|
|
446
496
|
return True
|
|
447
497
|
if is_agent_home_tooling(file_path):
|
|
448
498
|
return True
|
|
@@ -26,7 +26,6 @@ from __future__ import annotations
|
|
|
26
26
|
import json
|
|
27
27
|
import shlex
|
|
28
28
|
import sys
|
|
29
|
-
import tempfile
|
|
30
29
|
from pathlib import Path
|
|
31
30
|
|
|
32
31
|
_blocking_dir = str(Path(__file__).resolve().parent)
|
|
@@ -41,6 +40,9 @@ from hooks_constants.bash_pre_tool_use_dispatcher_constants import ( # noqa: E4
|
|
|
41
40
|
ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
42
41
|
)
|
|
43
42
|
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
43
|
+
from hooks_constants.pre_tool_use_stdin import ( # noqa: E402
|
|
44
|
+
read_hook_input_dictionary_from_stdin,
|
|
45
|
+
)
|
|
44
46
|
from hooks_constants.pr_description_writer_gate_constants import ( # noqa: E402
|
|
45
47
|
ALL_GH_EXECUTABLE_BASENAMES,
|
|
46
48
|
COMMAND_FIELD_NAME,
|
|
@@ -49,15 +51,14 @@ from hooks_constants.pr_description_writer_gate_constants import ( # noqa: E402
|
|
|
49
51
|
GH_PR_CREATE_MINIMUM_TOKEN_COUNT,
|
|
50
52
|
PR_SUBCOMMAND_TOKEN,
|
|
51
53
|
SPAWN_BYPASS_MARKER,
|
|
52
|
-
SPAWN_MARKER_FILE_PREFIX,
|
|
53
|
-
SPAWN_MARKER_FILE_SUFFIX,
|
|
54
54
|
TOOL_INPUT_FIELD_NAME,
|
|
55
55
|
TOOL_NAME_FIELD_NAME,
|
|
56
|
+
spawn_marker_path,
|
|
56
57
|
)
|
|
57
58
|
from hooks_constants.session_edit_stage_gate_constants import ( # noqa: E402
|
|
58
59
|
ALL_COMMAND_SEPARATOR_TOKENS,
|
|
59
|
-
SESSION_ID_UNSAFE_CHARACTERS_PATTERN,
|
|
60
60
|
)
|
|
61
|
+
from sensitive_file_protector import build_deny_response # noqa: E402
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
def _all_command_tokens(shell_command: str) -> list[str] | None:
|
|
@@ -149,7 +150,9 @@ def _all_pull_request_creation_segment_indexes(all_segments: list[list[str]]) ->
|
|
|
149
150
|
|
|
150
151
|
|
|
151
152
|
def _only_final_create_carries_bypass(
|
|
152
|
-
all_tokens: list[str],
|
|
153
|
+
all_tokens: list[str],
|
|
154
|
+
all_segments: list[list[str]],
|
|
155
|
+
all_creation_segment_indexes: list[int],
|
|
153
156
|
) -> bool:
|
|
154
157
|
"""Report whether one final gh pr create carries the final bypass comment.
|
|
155
158
|
|
|
@@ -159,6 +162,7 @@ def _only_final_create_carries_bypass(
|
|
|
159
162
|
|
|
160
163
|
Args:
|
|
161
164
|
all_tokens: The tokens of the whole command.
|
|
165
|
+
all_segments: The command's token segments, already split by the caller.
|
|
162
166
|
all_creation_segment_indexes: Indexes of segments that create pull requests.
|
|
163
167
|
|
|
164
168
|
Returns:
|
|
@@ -166,7 +170,7 @@ def _only_final_create_carries_bypass(
|
|
|
166
170
|
"""
|
|
167
171
|
if not _carries_trailing_bypass_marker(all_tokens):
|
|
168
172
|
return False
|
|
169
|
-
final_segment_index = len(
|
|
173
|
+
final_segment_index = len(all_segments) - 1
|
|
170
174
|
return all_creation_segment_indexes == [final_segment_index]
|
|
171
175
|
|
|
172
176
|
|
|
@@ -182,11 +186,8 @@ def _spawn_is_recorded(session_id: str) -> bool:
|
|
|
182
186
|
Returns:
|
|
183
187
|
True when this session's marker file is present.
|
|
184
188
|
"""
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return False
|
|
188
|
-
file_name = f"{SPAWN_MARKER_FILE_PREFIX}{sanitized_session_id}{SPAWN_MARKER_FILE_SUFFIX}"
|
|
189
|
-
return (Path(tempfile.gettempdir()) / file_name).exists()
|
|
189
|
+
marker_file = spawn_marker_path(session_id)
|
|
190
|
+
return marker_file is not None and marker_file.exists()
|
|
190
191
|
|
|
191
192
|
|
|
192
193
|
def _shell_command_or_none(all_hook_payload: dict[str, object]) -> str | None:
|
|
@@ -211,19 +212,12 @@ def _shell_command_or_none(all_hook_payload: dict[str, object]) -> str | None:
|
|
|
211
212
|
|
|
212
213
|
def _emit_denial() -> None:
|
|
213
214
|
"""Write the PreToolUse deny payload to stdout and log the block."""
|
|
214
|
-
deny_payload = {
|
|
215
|
-
"hookSpecificOutput": {
|
|
216
|
-
"hookEventName": "PreToolUse",
|
|
217
|
-
"permissionDecision": "deny",
|
|
218
|
-
"permissionDecisionReason": CORRECTIVE_MESSAGE,
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
215
|
log_hook_block(
|
|
222
216
|
calling_hook_name="pr_description_writer_gate.py",
|
|
223
217
|
hook_event="PreToolUse",
|
|
224
218
|
block_reason=CORRECTIVE_MESSAGE,
|
|
225
219
|
)
|
|
226
|
-
sys.stdout.write(json.dumps(
|
|
220
|
+
sys.stdout.write(json.dumps(build_deny_response(CORRECTIVE_MESSAGE)) + "\n")
|
|
227
221
|
sys.stdout.flush()
|
|
228
222
|
|
|
229
223
|
|
|
@@ -235,11 +229,8 @@ def main() -> None:
|
|
|
235
229
|
tokenize, a command opening no pull request, a trailing bypass marker, and
|
|
236
230
|
a recorded spawn each pass through untouched.
|
|
237
231
|
"""
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
except json.JSONDecodeError:
|
|
241
|
-
sys.exit(0)
|
|
242
|
-
if not isinstance(hook_payload, dict):
|
|
232
|
+
hook_payload = read_hook_input_dictionary_from_stdin()
|
|
233
|
+
if hook_payload is None:
|
|
243
234
|
sys.exit(0)
|
|
244
235
|
shell_command = _shell_command_or_none(hook_payload)
|
|
245
236
|
if shell_command is None:
|
|
@@ -251,7 +242,9 @@ def main() -> None:
|
|
|
251
242
|
all_creation_segment_indexes = _all_pull_request_creation_segment_indexes(all_segments)
|
|
252
243
|
if not all_creation_segment_indexes:
|
|
253
244
|
sys.exit(0)
|
|
254
|
-
if _only_final_create_carries_bypass(
|
|
245
|
+
if _only_final_create_carries_bypass(
|
|
246
|
+
all_tokens, all_segments, all_creation_segment_indexes
|
|
247
|
+
):
|
|
255
248
|
sys.exit(0)
|
|
256
249
|
if _spawn_is_recorded(str(hook_payload.get("session_id") or "")):
|
|
257
250
|
sys.exit(0)
|
|
@@ -80,8 +80,12 @@ def _resolve_payload(input_data: dict) -> tuple[str, dict, str]:
|
|
|
80
80
|
return input_data.get("tool_name", ""), tool_input, tool_input.get("file_path", "")
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
def _is_silently_skipped(
|
|
84
|
-
|
|
83
|
+
def _is_silently_skipped(
|
|
84
|
+
file_path: str, extension: str, path: Path, repository_root: str = ""
|
|
85
|
+
) -> bool:
|
|
86
|
+
if _is_inside_dotclaude_segment(file_path) or is_ephemeral_script_path(
|
|
87
|
+
file_path, repository_root
|
|
88
|
+
):
|
|
85
89
|
return True
|
|
86
90
|
if _is_agent_home_tooling_write(file_path):
|
|
87
91
|
return True
|
|
@@ -135,13 +139,13 @@ def _decide_for_target(
|
|
|
135
139
|
return None
|
|
136
140
|
path = Path(file_path)
|
|
137
141
|
extension = path.suffix.lower()
|
|
138
|
-
|
|
142
|
+
repository_root = str(input_data.get("cwd") or "")
|
|
143
|
+
if _is_silently_skipped(file_path, extension, path, repository_root):
|
|
139
144
|
return None
|
|
140
145
|
if _write_is_exempt(tool_name, tool_input, path, extension):
|
|
141
146
|
return True, ""
|
|
142
147
|
all_candidates = candidate_test_paths_for(path)
|
|
143
148
|
session_id = str(input_data.get("session_id") or "")
|
|
144
|
-
repository_root = str(input_data.get("cwd") or "")
|
|
145
149
|
is_recorded_or_fresh = has_recorded_or_fresh_test(
|
|
146
150
|
all_candidates, session_id, repository_root, _freshness_seconds()
|
|
147
151
|
)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
8
9
|
import tempfile
|
|
@@ -168,8 +169,18 @@ def test_should_return_true_for_root_anchored_tmp_and_temp(
|
|
|
168
169
|
raw_path: str,
|
|
169
170
|
monkeypatch: pytest.MonkeyPatch,
|
|
170
171
|
) -> None:
|
|
171
|
-
"""B4: classifier returns True for root-anchored /tmp and /temp paths.
|
|
172
|
+
"""B4: classifier returns True for root-anchored /tmp and /temp paths.
|
|
173
|
+
|
|
174
|
+
A Windows drive-letter literal is absolute only on Windows. On POSIX,
|
|
175
|
+
os.path.abspath() joins a non-absolute path with the process cwd, so a
|
|
176
|
+
drive-letter case's pass or fail state depends on whatever directory the
|
|
177
|
+
runner happens to sit in. Skip it on a host that does not treat the
|
|
178
|
+
literal as absolute, so the assertion checks its own precondition rather
|
|
179
|
+
than the runner's ambient cwd.
|
|
180
|
+
"""
|
|
172
181
|
monkeypatch.delenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", raising=False)
|
|
182
|
+
if not os.path.isabs(raw_path):
|
|
183
|
+
pytest.skip(f"{raw_path!r} is not absolute on this host")
|
|
173
184
|
assert is_ephemeral_script_path(raw_path) is True
|
|
174
185
|
|
|
175
186
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Repository-root exemption tests for code_rules_enforcer and the classifier.
|
|
2
|
+
|
|
3
|
+
A path inside the session's repository root is never ephemeral, whatever
|
|
4
|
+
directory the repository itself is checked out under. These tests cover the
|
|
5
|
+
classifier directly and the enforcer's real PreToolUse Write path, plus the
|
|
6
|
+
guard cases a too-broad fix could break: an absent cwd, a cwd that covers no
|
|
7
|
+
repository, and an unrelated sibling worktree that merely shares a name
|
|
8
|
+
prefix under /tmp.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
from blocking import _path_setup # noqa: F401 (pins this checkout ahead of sibling worktrees)
|
|
16
|
+
from blocking.code_rules_enforcer import main as enforcer_main
|
|
17
|
+
from blocking.code_rules_shared import is_ephemeral_script_path
|
|
18
|
+
from code_rules_enforcer_test_support import run_serialized_payload_entrypoint
|
|
19
|
+
|
|
20
|
+
_VIOLATING_PRODUCTION_SOURCE = "def process_data(payload: str) -> None:\n print(payload)\n"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _build_write_payload_with_cwd(file_path: str, content: str, cwd: str) -> str:
|
|
24
|
+
"""Build the JSON payload for a Write tool call that also carries a cwd."""
|
|
25
|
+
return json.dumps(
|
|
26
|
+
{
|
|
27
|
+
"tool_name": "Write",
|
|
28
|
+
"tool_input": {"file_path": file_path, "content": content},
|
|
29
|
+
"cwd": cwd,
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _run_main_with_write_payload_and_cwd(
|
|
35
|
+
file_path: str,
|
|
36
|
+
content: str,
|
|
37
|
+
cwd: str,
|
|
38
|
+
) -> tuple[str, int]:
|
|
39
|
+
"""Drive enforcer_main through its stdin entry point for a Write payload carrying cwd."""
|
|
40
|
+
captured_stdout, exit_code = run_serialized_payload_entrypoint(
|
|
41
|
+
enforcer_main, _build_write_payload_with_cwd(file_path, content, cwd)
|
|
42
|
+
)
|
|
43
|
+
return captured_stdout, int(exit_code or 0)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_should_return_false_for_file_inside_repository_root_under_tmp() -> None:
|
|
47
|
+
"""R1: a path inside repository_root is never ephemeral, even under /tmp."""
|
|
48
|
+
repository_root = "/tmp/pr-alpha"
|
|
49
|
+
file_path = "/tmp/pr-alpha/hooks/blocking/orders.py"
|
|
50
|
+
assert is_ephemeral_script_path(file_path, repository_root) is False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_should_return_true_for_sibling_directory_outside_repository_root() -> None:
|
|
54
|
+
"""R2: a /tmp sibling that merely shares repository_root's name prefix stays ephemeral."""
|
|
55
|
+
repository_root = "/tmp/pr-alpha"
|
|
56
|
+
sibling_file_path = "/tmp/pr-alpha-sibling/hooks/orders.py"
|
|
57
|
+
assert is_ephemeral_script_path(sibling_file_path, repository_root) is True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_should_return_true_when_repository_root_does_not_cover_target() -> None:
|
|
61
|
+
"""R3: an unrelated cwd must not blanket-exempt a /tmp target outside it."""
|
|
62
|
+
unrelated_repository_root = "/tmp/scratch-workspace"
|
|
63
|
+
scratch_target = "/tmp/scratch-elsewhere/notes.py"
|
|
64
|
+
assert is_ephemeral_script_path(scratch_target, unrelated_repository_root) is True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_should_deny_pretooluse_target_inside_repository_root_under_tmp() -> None:
|
|
68
|
+
"""B22: a production file inside a repository checked out under /tmp is gated for real."""
|
|
69
|
+
repository_root = "/tmp/pr-alpha"
|
|
70
|
+
file_path = f"{repository_root}/hooks/blocking/orders.py"
|
|
71
|
+
captured_stdout, exit_code = _run_main_with_write_payload_and_cwd(
|
|
72
|
+
file_path,
|
|
73
|
+
_VIOLATING_PRODUCTION_SOURCE,
|
|
74
|
+
repository_root,
|
|
75
|
+
)
|
|
76
|
+
assert "deny" in captured_stdout.lower(), (
|
|
77
|
+
f"enforcer must gate a repository file under /tmp, got exit_code={exit_code}, "
|
|
78
|
+
f"stdout={captured_stdout!r}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_should_stay_exempt_for_scratch_outside_repository_root() -> None:
|
|
83
|
+
"""B23: a genuine scratch file outside the repository stays exempt."""
|
|
84
|
+
repository_root = "/tmp/pr-alpha"
|
|
85
|
+
scratch_target = "/tmp/scratch/one_off.py"
|
|
86
|
+
captured_stdout, exit_code = _run_main_with_write_payload_and_cwd(
|
|
87
|
+
scratch_target,
|
|
88
|
+
_VIOLATING_PRODUCTION_SOURCE,
|
|
89
|
+
repository_root,
|
|
90
|
+
)
|
|
91
|
+
assert exit_code == 0
|
|
92
|
+
assert "deny" not in captured_stdout.lower()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_should_stay_exempt_when_working_directory_covers_no_repository() -> None:
|
|
96
|
+
"""B24: a cwd that covers no repository must not exempt an unrelated /tmp target."""
|
|
97
|
+
unrelated_working_directory = "/tmp/scratch-workspace"
|
|
98
|
+
scratch_target = "/tmp/scratch-elsewhere/notes.py"
|
|
99
|
+
captured_stdout, exit_code = _run_main_with_write_payload_and_cwd(
|
|
100
|
+
scratch_target,
|
|
101
|
+
_VIOLATING_PRODUCTION_SOURCE,
|
|
102
|
+
unrelated_working_directory,
|
|
103
|
+
)
|
|
104
|
+
assert exit_code == 0
|
|
105
|
+
assert "deny" not in captured_stdout.lower()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_should_stay_exempt_for_unrelated_worktree_sibling_under_tmp() -> None:
|
|
109
|
+
"""B25: a sibling worktree sharing repository_root's name prefix stays exempt."""
|
|
110
|
+
repository_root = "/tmp/pr-alpha"
|
|
111
|
+
unrelated_sibling_target = "/tmp/pr-alpha-sibling/hooks/orders.py"
|
|
112
|
+
captured_stdout, exit_code = _run_main_with_write_payload_and_cwd(
|
|
113
|
+
unrelated_sibling_target,
|
|
114
|
+
_VIOLATING_PRODUCTION_SOURCE,
|
|
115
|
+
repository_root,
|
|
116
|
+
)
|
|
117
|
+
assert exit_code == 0
|
|
118
|
+
assert "deny" not in captured_stdout.lower()
|