claude-dev-env 7.0.0 → 7.1.0
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/.agents/skills/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +189 -0
- package/.agents/skills/session-continuity/SKILL.md +114 -0
- package/.agents/skills/session-continuity/continuity.mjs +375 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +303 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +78 -0
- package/bin/install-session-continuity.test.mjs +42 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
|
@@ -286,7 +286,7 @@ test('installer fault after_file_staging restores prior managed installation', (
|
|
|
286
286
|
'utf8',
|
|
287
287
|
);
|
|
288
288
|
|
|
289
|
-
const failedRun = runInstaller(homeDirectory, ['--only', '
|
|
289
|
+
const failedRun = runInstaller(homeDirectory, ['--only', 'core'], {
|
|
290
290
|
faultPhase: FAULT_PHASES.AFTER_FILE_STAGING,
|
|
291
291
|
});
|
|
292
292
|
assert.notEqual(failedRun.status, 0, failedRun.stdout + failedRun.stderr);
|
|
@@ -333,7 +333,7 @@ test('installer fault after_manifest_write restores prior manifest', () => {
|
|
|
333
333
|
'utf8',
|
|
334
334
|
);
|
|
335
335
|
|
|
336
|
-
const failedRun = runInstaller(homeDirectory, ['--only', '
|
|
336
|
+
const failedRun = runInstaller(homeDirectory, ['--only', 'core'], {
|
|
337
337
|
faultPhase: FAULT_PHASES.AFTER_MANIFEST_WRITE,
|
|
338
338
|
});
|
|
339
339
|
assert.notEqual(failedRun.status, 0, failedRun.stdout + failedRun.stderr);
|
|
@@ -387,7 +387,7 @@ test('successful install after prior install leaves one manifest and no fault',
|
|
|
387
387
|
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-txn-e2e-ok-'));
|
|
388
388
|
try {
|
|
389
389
|
seedPriorInstall(homeDirectory);
|
|
390
|
-
const okRun = runInstaller(homeDirectory, ['--only', '
|
|
390
|
+
const okRun = runInstaller(homeDirectory, ['--only', 'core']);
|
|
391
391
|
assert.equal(okRun.status, 0, okRun.stdout + okRun.stderr);
|
|
392
392
|
const manifestPath = join(homeDirectory, '.claude', '.claude-dev-env-manifest.json');
|
|
393
393
|
assert.equal(existsSync(manifestPath), true);
|
package/docs/CODE_RULES.md
CHANGED
|
@@ -43,7 +43,7 @@ Before writing ANY constant: search `config/` for the exact value → semantic m
|
|
|
43
43
|
|
|
44
44
|
## 5. NO ABBREVIATIONS
|
|
45
45
|
|
|
46
|
-
Full words only (`context`, not `ctx`). Exceptions: `i`/`j`/`k` in loops, `e` for exception. Naming patterns: loop vars `each_*`; booleans `is_/has_/should_/can_/was_/did_`; collections `all_*`; maps `X_by_Y`; preposition params (`from_path=`, `to=`, `into=`). Banned names: `result`, `data`, `output`, `response`, `value`, `item`, `temp`. Banned prefixes: `handle`, `process`, `manage`, `do`. Name
|
|
46
|
+
Full words only (`context`, not `ctx`). Exceptions: `i`/`j`/`k` in loops, `e` for exception. Naming patterns: loop vars `each_*`; booleans `is_/has_/should_/can_/was_/did_`; collections `all_*`; maps `X_by_Y`; preposition params (`from_path=`, `to=`, `into=`). Banned names: `result`, `data`, `output`, `response`, `value`, `item`, `temp`. Banned prefixes: `handle`, `process`, `manage`, `do`. Name components for what they are: `Overlay`, `Validator`, `InvoicePreview`. The failure-blast-radius rule requires `ItemBlocked` or `RunFatal`. Names ending with either bypass the banned-noun word check.
|
|
47
47
|
|
|
48
48
|
### Public compatibility definitions
|
|
49
49
|
|
|
@@ -30,6 +30,7 @@ from code_rules_shared import ( # noqa: E402
|
|
|
30
30
|
from hooks_constants.banned_identifiers_constants import ( # noqa: E402
|
|
31
31
|
ALL_BANNED_IDENTIFIERS,
|
|
32
32
|
ALL_BANNED_NOUN_WORDS,
|
|
33
|
+
ALL_BLAST_RADIUS_EXCEPTION_SUFFIXES,
|
|
33
34
|
BANNED_IDENTIFIER_MESSAGE_SUFFIX,
|
|
34
35
|
BANNED_IDENTIFIER_SKIP_ADVISORY,
|
|
35
36
|
BANNED_NOUN_COMPATIBILITY_COMMENT,
|
|
@@ -228,6 +229,8 @@ def _find_banned_noun_word(identifier: str) -> str | None:
|
|
|
228
229
|
identifier (e.g., ``'result'`` for ``'HolidayPeakResult'``). Returns
|
|
229
230
|
``None`` when no banned noun word is present.
|
|
230
231
|
"""
|
|
232
|
+
if identifier.endswith(ALL_BLAST_RADIUS_EXCEPTION_SUFFIXES):
|
|
233
|
+
return None
|
|
231
234
|
for each_word in _identifier_word_parts(identifier):
|
|
232
235
|
if each_word in ALL_BANNED_NOUN_WORDS:
|
|
233
236
|
return each_word
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
"""Library helper
|
|
2
|
-
|
|
3
|
-
``
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
repository a commit's unstaged-edit check targets. This module carries no
|
|
7
|
-
hook entry point of its own; the native ``git-hooks/pre_commit.py`` runs
|
|
8
|
-
the real commit-time CODE_RULES enforcement.
|
|
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.
|
|
9
6
|
"""
|
|
10
7
|
|
|
11
8
|
import subprocess
|
|
@@ -1,61 +1,16 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Tests for the retired Bash and PowerShell blocking roster."""
|
|
2
2
|
|
|
3
|
-
Standalone registration starts one Python process per hosted hook. The
|
|
4
|
-
dispatcher replaces that chain with a single process that hosts every hook
|
|
5
|
-
in-process. This test pins the before/after interpreter-start counts so a
|
|
6
|
-
regression that re-splits the chain fails CI.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import subprocess
|
|
13
3
|
import sys
|
|
14
4
|
from pathlib import Path
|
|
15
5
|
|
|
16
|
-
|
|
17
|
-
if
|
|
18
|
-
sys.path.insert(0,
|
|
6
|
+
_HOOKS_ROOT = Path(__file__).resolve().parent.parent
|
|
7
|
+
if str(_HOOKS_ROOT) not in sys.path:
|
|
8
|
+
sys.path.insert(0, str(_HOOKS_ROOT))
|
|
19
9
|
|
|
20
|
-
from hooks_constants.bash_pre_tool_use_dispatcher_constants import (
|
|
10
|
+
from hooks_constants.bash_pre_tool_use_dispatcher_constants import (
|
|
21
11
|
ALL_BASH_HOSTED_HOOK_ENTRIES,
|
|
22
|
-
BASH_TOOL_NAME,
|
|
23
12
|
)
|
|
24
13
|
|
|
25
|
-
_BLOCKING_DIR = Path(__file__).resolve().parent
|
|
26
|
-
_HOOKS_ROOT = _BLOCKING_DIR.parent
|
|
27
|
-
_DISPATCHER_SCRIPT = str(_BLOCKING_DIR / "bash_pre_tool_use_dispatcher.py")
|
|
28
|
-
|
|
29
|
-
STANDALONE_INTERPRETER_STARTS_PER_BASH_CALL = len(ALL_BASH_HOSTED_HOOK_ENTRIES)
|
|
30
|
-
DISPATCHER_INTERPRETER_STARTS_PER_BASH_CALL = 1
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_roster_is_large_enough_that_consolidation_saves_starts() -> None:
|
|
34
|
-
"""Consolidation only pays off when the chain hosts more than one hook."""
|
|
35
|
-
assert STANDALONE_INTERPRETER_STARTS_PER_BASH_CALL >= 10
|
|
36
|
-
assert DISPATCHER_INTERPRETER_STARTS_PER_BASH_CALL == 1
|
|
37
|
-
assert STANDALONE_INTERPRETER_STARTS_PER_BASH_CALL > DISPATCHER_INTERPRETER_STARTS_PER_BASH_CALL
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def test_dispatcher_handles_a_bash_call_in_one_process() -> None:
|
|
41
|
-
"""One subprocess run of the dispatcher covers the full Bash hosted chain."""
|
|
42
|
-
payload_text = json.dumps(
|
|
43
|
-
{"tool_name": BASH_TOOL_NAME, "tool_input": {"command": "echo interpreter-start-probe"}}
|
|
44
|
-
)
|
|
45
|
-
completed = subprocess.run(
|
|
46
|
-
[sys.executable, _DISPATCHER_SCRIPT],
|
|
47
|
-
check=False,
|
|
48
|
-
input=payload_text,
|
|
49
|
-
capture_output=True,
|
|
50
|
-
text=True,
|
|
51
|
-
encoding="utf-8",
|
|
52
|
-
)
|
|
53
|
-
assert completed.returncode == 0
|
|
54
|
-
assert DISPATCHER_INTERPRETER_STARTS_PER_BASH_CALL == 1
|
|
55
|
-
|
|
56
14
|
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES:
|
|
60
|
-
assert (_HOOKS_ROOT / each_entry.script_relative_path).is_file()
|
|
61
|
-
assert len(ALL_BASH_HOSTED_HOOK_ENTRIES) == STANDALONE_INTERPRETER_STARTS_PER_BASH_CALL
|
|
15
|
+
def test_retired_bash_roster_has_no_effective_blocking_hooks() -> None:
|
|
16
|
+
assert ALL_BASH_HOSTED_HOOK_ENTRIES == ()
|
|
@@ -100,17 +100,6 @@ def test_clean_command_yields_no_decision_matching_the_standalone_chain() -> Non
|
|
|
100
100
|
assert dispatcher_decision == expected_decision
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
def test_destructive_command_asks_matching_the_standalone_chain() -> None:
|
|
104
|
-
"""A destructive command the chain asks on makes the dispatcher ask too."""
|
|
105
|
-
payload_text = _bash_payload("rm -rf /")
|
|
106
|
-
expected_decision = _expected_precedence(payload_text, BASH_TOOL_NAME)
|
|
107
|
-
dispatcher_decision, _reason = _decision_from_stdout(
|
|
108
|
-
_run_process(_DISPATCHER_SCRIPT, payload_text).stdout
|
|
109
|
-
)
|
|
110
|
-
assert dispatcher_decision == expected_decision
|
|
111
|
-
assert expected_decision == "ask"
|
|
112
|
-
|
|
113
|
-
|
|
114
103
|
def test_dispatcher_exits_zero_on_a_clean_command() -> None:
|
|
115
104
|
"""The dispatcher exits zero for a command no hook decides on."""
|
|
116
105
|
completed = _run_process(_DISPATCHER_SCRIPT, _bash_payload("echo hi"))
|
|
@@ -212,19 +201,6 @@ def test_additional_context_is_collected_from_deciding_hooks() -> None:
|
|
|
212
201
|
assert aggregated.all_additional_context == ["see docs/runbook.md"]
|
|
213
202
|
|
|
214
203
|
|
|
215
|
-
def test_powershell_selects_the_shared_hooks_in_registration_order() -> None:
|
|
216
|
-
"""Selecting for PowerShell yields the shared hooks in registration order."""
|
|
217
|
-
powershell_paths = [
|
|
218
|
-
each_entry.script_relative_path
|
|
219
|
-
for each_entry in select_applicable_entries(POWERSHELL_TOOL_NAME)
|
|
220
|
-
]
|
|
221
|
-
assert powershell_paths == [
|
|
222
|
-
"blocking/cursor_cli_python_misfire_blocker.py",
|
|
223
|
-
"blocking/unscoped_search_blocker.py",
|
|
224
|
-
"blocking/pii_prevention_blocker.py",
|
|
225
|
-
]
|
|
226
|
-
|
|
227
|
-
|
|
228
204
|
def test_dispatcher_imports_standalone_with_only_blocking_on_the_path() -> None:
|
|
229
205
|
"""The dispatcher's bootstrap resolves hooks_constants without hooks/ on PYTHONPATH."""
|
|
230
206
|
subprocess_environment = {**os.environ, "PYTHONPATH": str(_BLOCKING_DIR)}
|
|
@@ -259,48 +235,3 @@ def test_emit_decision_re_emits_silent_deny_fields(
|
|
|
259
235
|
assert hook_specific["additionalContext"] == "see docs/runbook.md"
|
|
260
236
|
assert emitted_payload["systemMessage"] == "[gh-gate] blocked redirected gh pr create"
|
|
261
237
|
assert emitted_payload["suppressOutput"] is True
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def test_dispatcher_denies_subshell_disguised_as_arithmetic_expansion() -> None:
|
|
265
|
-
"""The dispatcher denies a `$((cd X) && Y)` subshell, not only the bare hook.
|
|
266
|
-
|
|
267
|
-
`echo $((cd /tmp) && pwd)` opens with a `$((` two-character lookahead
|
|
268
|
-
match, so a function-level test alone does not prove the block reaches
|
|
269
|
-
the live Bash tool path.
|
|
270
|
-
"""
|
|
271
|
-
payload_text = _bash_payload("echo $((cd /tmp) && pwd)")
|
|
272
|
-
decision, _reason = _decision_from_stdout(_run_process(_DISPATCHER_SCRIPT, payload_text).stdout)
|
|
273
|
-
assert decision == "deny"
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def test_dispatch_emits_deny_immediately_and_skips_later_hooks(
|
|
277
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
278
|
-
capsys: pytest.CaptureFixture[str],
|
|
279
|
-
) -> None:
|
|
280
|
-
"""A mid-chain deny emits at once and does not invoke later hosted hooks."""
|
|
281
|
-
all_call_paths: list[str] = []
|
|
282
|
-
|
|
283
|
-
def _fake_run_hook(script_path: str, payload_text: str) -> HostedHookRun:
|
|
284
|
-
del payload_text
|
|
285
|
-
all_call_paths.append(script_path)
|
|
286
|
-
script_name = Path(script_path).name
|
|
287
|
-
if script_name == "destructive_command_blocker.py":
|
|
288
|
-
return HostedHookRun(
|
|
289
|
-
captured_stdout=_decision_json("deny", "blocked early"),
|
|
290
|
-
did_crash=False,
|
|
291
|
-
)
|
|
292
|
-
if script_name == "precommit_code_rules_gate.py":
|
|
293
|
-
raise AssertionError("later hooks must not run after an early deny")
|
|
294
|
-
return HostedHookRun(captured_stdout="", did_crash=False)
|
|
295
|
-
|
|
296
|
-
monkeypatch.setattr(
|
|
297
|
-
"bash_pre_tool_use_dispatcher.run_hook_capturing_output",
|
|
298
|
-
_fake_run_hook,
|
|
299
|
-
)
|
|
300
|
-
dispatch(_bash_payload("rm -rf /"), BASH_TOOL_NAME)
|
|
301
|
-
captured = capsys.readouterr()
|
|
302
|
-
decision, reason = _decision_from_stdout(captured.out)
|
|
303
|
-
assert decision == "deny"
|
|
304
|
-
assert "blocked early" in reason
|
|
305
|
-
assert any(path.endswith("destructive_command_blocker.py") for path in all_call_paths)
|
|
306
|
-
assert not any(path.endswith("precommit_code_rules_gate.py") for path in all_call_paths)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from code_rules_banned_identifiers import (
|
|
4
|
+
_find_banned_noun_word,
|
|
5
|
+
check_banned_noun_word_boundary,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
PRODUCTION_FILE_PATH = "packages/app/services/customer_pipeline.py"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_should_return_no_banned_word_for_item_blocked_exception_suffix() -> None:
|
|
12
|
+
assert _find_banned_noun_word("AssetSizingItemBlocked") is None
|
|
13
|
+
assert _find_banned_noun_word("NinePatchGuideFrameItemBlocked") is None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_should_return_no_banned_word_for_run_fatal_exception_suffix() -> None:
|
|
17
|
+
assert _find_banned_noun_word("AssetVariantRunFatal") is None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_should_return_banned_word_for_item_without_blast_radius_suffix() -> None:
|
|
21
|
+
assert _find_banned_noun_word("IconGridItem") == "item"
|
|
22
|
+
assert _find_banned_noun_word("item_key") == "item"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_should_allow_class_ending_with_item_blocked_suffix() -> None:
|
|
26
|
+
source = "class AssetSizingItemBlocked(Exception):\n pass\n"
|
|
27
|
+
issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
|
|
28
|
+
assert issues == []
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_should_allow_long_class_ending_with_item_blocked_suffix() -> None:
|
|
32
|
+
source = "class NinePatchGuideFrameItemBlocked(Exception):\n pass\n"
|
|
33
|
+
issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
|
|
34
|
+
assert issues == []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_should_allow_class_ending_with_run_fatal_suffix() -> None:
|
|
38
|
+
source = "class AssetVariantRunFatal(Exception):\n pass\n"
|
|
39
|
+
issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
|
|
40
|
+
assert issues == []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_should_flag_class_ending_with_item_without_blast_radius_suffix() -> None:
|
|
44
|
+
source = "class IconGridItem:\n pass\n"
|
|
45
|
+
issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
|
|
46
|
+
assert any("IconGridItem" in each_issue for each_issue in issues)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_should_flag_item_key_assignment() -> None:
|
|
50
|
+
source = "item_key = 'slot'\n"
|
|
51
|
+
issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
|
|
52
|
+
assert any("item_key" in each_issue for each_issue in issues)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_should_flag_output_path_assignment() -> None:
|
|
56
|
+
source = "output_path = 'slot'\n"
|
|
57
|
+
issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
|
|
58
|
+
assert any("output_path" in each_issue for each_issue in issues)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_should_flag_each_value_assignment() -> None:
|
|
62
|
+
source = "each_value = 1\n"
|
|
63
|
+
issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
|
|
64
|
+
assert any("each_value" in each_issue for each_issue in issues)
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
"""Golden differential and failure-mode tests for the PreToolUse dispatcher.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
dispatcher on the same payload and asserts equal decision and equal message
|
|
7
|
-
union.
|
|
8
|
-
|
|
9
|
-
The failure-mode tests cover one row each from spec/failure-modes.md:
|
|
10
|
-
early-exit-then-later-deny, multi-deny, context-survival, blocking-hook crash,
|
|
11
|
-
fail-open malformed input.
|
|
3
|
+
The live roster carries only nonblocking edit advisors. These tests keep the
|
|
4
|
+
aggregator contract and prove retired policy registrations fail open while
|
|
5
|
+
explicit standalone checks remain callable.
|
|
12
6
|
"""
|
|
13
7
|
|
|
14
8
|
from __future__ import annotations
|
|
@@ -31,7 +25,6 @@ if str(_HOOKS_ROOT) not in sys.path:
|
|
|
31
25
|
|
|
32
26
|
from hooks_constants.pre_tool_use_dispatcher_constants import ( # noqa: E402, I001
|
|
33
27
|
ALL_HOSTED_HOOK_ENTRIES,
|
|
34
|
-
ALL_IMMEDIATE_HARM_SCRIPT_PATHS,
|
|
35
28
|
APPLY_PATCH_TOOL_NAME,
|
|
36
29
|
BLOCKING_CRASH_DENY_REASON,
|
|
37
30
|
BLOCKING_CRASH_EXIT_CODE,
|
|
@@ -407,53 +400,6 @@ def test_write_existing_file_blocker_allows_multi_edit_to_an_existing_path(
|
|
|
407
400
|
_assert_dispatcher_matches_individual_hooks(payload_text, MULTI_EDIT_TOOL_NAME)
|
|
408
401
|
|
|
409
402
|
|
|
410
|
-
def test_context_survives_alongside_deny_reason(
|
|
411
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
412
|
-
) -> None:
|
|
413
|
-
"""A non-denying hook's additional context survives in the dispatcher output.
|
|
414
|
-
|
|
415
|
-
This tests that hooks whose output is additional-context (not a deny) still
|
|
416
|
-
have their output preserved when another hook denies.
|
|
417
|
-
"""
|
|
418
|
-
monkeypatch.setenv("HOME", str(tmp_path))
|
|
419
|
-
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
420
|
-
sensitive_path = str(Path.home() / ".env")
|
|
421
|
-
payload_text = _write_payload(sensitive_path, "SECRET=abc")
|
|
422
|
-
dispatcher_result = _run_dispatcher(payload_text)
|
|
423
|
-
is_deny, _reason = _parse_hook_decision(dispatcher_result)
|
|
424
|
-
assert is_deny, (
|
|
425
|
-
"sensitive_file_protector should deny a write to .env — "
|
|
426
|
-
"if it did not, check whether the path is on the sensitive list"
|
|
427
|
-
)
|
|
428
|
-
assert dispatcher_result.stdout.strip(), "Dispatcher must emit output when denying"
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
def test_all_deny_reasons_present_when_multiple_hooks_deny(
|
|
432
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path,
|
|
433
|
-
) -> None:
|
|
434
|
-
"""Preserve every deny reason from the retained file-boundary checks."""
|
|
435
|
-
monkeypatch.setenv("HOME", str(tmp_path))
|
|
436
|
-
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
437
|
-
existing_sensitive_path = tmp_path / ".env"
|
|
438
|
-
existing_sensitive_path.write_text("VALUE=fixture\n", encoding="utf-8")
|
|
439
|
-
payload_text = _write_payload(str(existing_sensitive_path), "VALUE=changed\n")
|
|
440
|
-
_assert_dispatcher_matches_individual_hooks(payload_text, WRITE_TOOL_NAME)
|
|
441
|
-
dispatcher_result = _run_dispatcher(payload_text)
|
|
442
|
-
dispatcher_is_deny, dispatcher_reason = _parse_hook_decision(dispatcher_result)
|
|
443
|
-
assert dispatcher_is_deny
|
|
444
|
-
all_expected_deny_reasons: list[str] = []
|
|
445
|
-
for each_entry in _applicable_entries_for_tool(WRITE_TOOL_NAME):
|
|
446
|
-
completed_process = _run_hook_subprocess(each_entry.script_relative_path, payload_text)
|
|
447
|
-
is_deny, reason_text = _parse_hook_decision(completed_process)
|
|
448
|
-
if is_deny and reason_text:
|
|
449
|
-
all_expected_deny_reasons.append(reason_text)
|
|
450
|
-
assert len(all_expected_deny_reasons) >= 2, (
|
|
451
|
-
"Existing-file and sensitive-file checks must both deny the fixture"
|
|
452
|
-
)
|
|
453
|
-
for each_reason in all_expected_deny_reasons:
|
|
454
|
-
assert each_reason in dispatcher_reason
|
|
455
|
-
|
|
456
|
-
|
|
457
403
|
def test_aggregate_exit_code_two_signals_deny() -> None:
|
|
458
404
|
"""A HostedHookResult with exit_code 2 and did_crash False signals deny.
|
|
459
405
|
|
|
@@ -672,81 +618,6 @@ def test_emit_allow_preserves_system_message_and_additional_context(
|
|
|
672
618
|
assert parsed["systemMessage"] == "system-a\nsystem-b"
|
|
673
619
|
|
|
674
620
|
|
|
675
|
-
def test_later_hook_deny_survives_early_hook_exit(tmp_path: Path) -> None:
|
|
676
|
-
"""A retained document-reference denial survives earlier successful checks."""
|
|
677
|
-
payload_text = _orphan_claude_md_payload(tmp_path)
|
|
678
|
-
_assert_dispatcher_matches_individual_hooks(payload_text, WRITE_TOOL_NAME)
|
|
679
|
-
is_deny, reason = _parse_hook_decision(_run_dispatcher(payload_text))
|
|
680
|
-
assert is_deny, reason
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
def test_dispatcher_write_applies_both_groups() -> None:
|
|
684
|
-
"""Write tool triggers both Group A and Group B hooks through the dispatcher.
|
|
685
|
-
|
|
686
|
-
Verifies that the set of applicable entries for Write includes entries from
|
|
687
|
-
both ALL_WRITE_AND_EDIT_TOOL_NAMES (Group A) and ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES
|
|
688
|
-
(Group B) in the constants.
|
|
689
|
-
"""
|
|
690
|
-
all_write_entries = _applicable_entries_for_tool(WRITE_TOOL_NAME)
|
|
691
|
-
all_write_script_paths = {each_entry.script_relative_path for each_entry in all_write_entries}
|
|
692
|
-
assert "blocking/write_existing_file_blocker.py" in all_write_script_paths, (
|
|
693
|
-
"write_existing_file_blocker (Group A) must be in Write applicable set"
|
|
694
|
-
)
|
|
695
|
-
assert len(all_write_entries) == 11, (
|
|
696
|
-
f"Write tool must apply to the 11 retained write hooks, got {len(all_write_entries)}"
|
|
697
|
-
)
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
def test_dispatcher_edit_applies_both_groups() -> None:
|
|
701
|
-
"""Edit triggers Group A, Group B, and the Edit-scoped entry through the
|
|
702
|
-
dispatcher.
|
|
703
|
-
"""
|
|
704
|
-
all_edit_entries = _applicable_entries_for_tool(EDIT_TOOL_NAME)
|
|
705
|
-
all_edit_script_paths = {each_entry.script_relative_path for each_entry in all_edit_entries}
|
|
706
|
-
assert "blocking/stale_comment_reference_blocker.py" not in all_edit_script_paths
|
|
707
|
-
assert "advisory/refactor_guard.py" in all_edit_script_paths, (
|
|
708
|
-
"refactor_guard is Edit-scoped and hosted, so it belongs in the Edit applicable set"
|
|
709
|
-
)
|
|
710
|
-
assert len(all_edit_entries) == 13, (
|
|
711
|
-
f"expected 13 retained Edit entries, got {len(all_edit_entries)}"
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
def test_dispatcher_multi_edit_reaches_the_sensitive_protector() -> None:
|
|
716
|
-
"""A MultiEdit onto a sensitive path meets the same gate a Write does.
|
|
717
|
-
|
|
718
|
-
test_edit_and_multi_edit_applicable_sets_are_equal in the constants suite
|
|
719
|
-
holds the Edit and MultiEdit rosters equal, so this pins the one hook whose
|
|
720
|
-
absence would let a secret through a MultiEdit.
|
|
721
|
-
"""
|
|
722
|
-
all_multi_edit_entries = _applicable_entries_for_tool(MULTI_EDIT_TOOL_NAME)
|
|
723
|
-
all_multi_edit_script_paths = {
|
|
724
|
-
each_entry.script_relative_path for each_entry in all_multi_edit_entries
|
|
725
|
-
}
|
|
726
|
-
assert "blocking/sensitive_file_protector.py" in all_multi_edit_script_paths, (
|
|
727
|
-
"sensitive_file_protector belongs in the MultiEdit applicable set"
|
|
728
|
-
)
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
def test_dispatcher_apply_patch_applies_immediate_harm_hooks() -> None:
|
|
732
|
-
"""apply_patch applies to exactly the immediate-harm roster.
|
|
733
|
-
|
|
734
|
-
apply_patch reaches only the retained immediate-harm roster the constants
|
|
735
|
-
module names, not the full Write/Edit/MultiEdit lint surface.
|
|
736
|
-
"""
|
|
737
|
-
all_apply_patch_entries = _applicable_entries_for_tool(APPLY_PATCH_TOOL_NAME)
|
|
738
|
-
all_apply_patch_script_paths = {
|
|
739
|
-
each_entry.script_relative_path for each_entry in all_apply_patch_entries
|
|
740
|
-
}
|
|
741
|
-
assert "blocking/sensitive_file_protector.py" in all_apply_patch_script_paths, (
|
|
742
|
-
"sensitive_file_protector belongs in the apply_patch applicable set"
|
|
743
|
-
)
|
|
744
|
-
assert all_apply_patch_script_paths == set(ALL_IMMEDIATE_HARM_SCRIPT_PATHS), (
|
|
745
|
-
"apply_patch must apply to exactly the immediate-harm roster, got: "
|
|
746
|
-
f"{sorted(all_apply_patch_script_paths)}"
|
|
747
|
-
)
|
|
748
|
-
|
|
749
|
-
|
|
750
621
|
def _assert_roster_names_no_run_all_validators_entry() -> None:
|
|
751
622
|
"""Assert no hosted-roster ``script_relative_path`` names run_all_validators."""
|
|
752
623
|
all_roster_script_paths = {
|
|
@@ -809,40 +680,6 @@ def _orphan_claude_md_payload(tmp_path: Path) -> str:
|
|
|
809
680
|
return _write_payload(claude_md_path, orphan_table)
|
|
810
681
|
|
|
811
682
|
|
|
812
|
-
def test_runpy_deny_preserves_additional_context_and_suppress_output(tmp_path: Path) -> None:
|
|
813
|
-
"""A runpy-hosted deny carries its additionalContext and suppressOutput through the dispatcher.
|
|
814
|
-
|
|
815
|
-
claude_md_orphan_file_blocker emits hookSpecificOutput.additionalContext and a
|
|
816
|
-
top-level suppressOutput flag on a deny. The dispatcher must preserve both so
|
|
817
|
-
the dispatched denial matches the standalone hook's deny shape.
|
|
818
|
-
|
|
819
|
-
Args:
|
|
820
|
-
tmp_path: Pytest temp directory hosting the throwaway CLAUDE.md.
|
|
821
|
-
"""
|
|
822
|
-
payload_text = _orphan_claude_md_payload(tmp_path)
|
|
823
|
-
|
|
824
|
-
standalone_result = _run_hook_subprocess(
|
|
825
|
-
"blocking/claude_md_orphan_file_blocker.py", payload_text
|
|
826
|
-
)
|
|
827
|
-
standalone_payload = json.loads(standalone_result.stdout.strip())
|
|
828
|
-
standalone_hook_specific = standalone_payload["hookSpecificOutput"]
|
|
829
|
-
expected_additional_context = standalone_hook_specific["additionalContext"]
|
|
830
|
-
|
|
831
|
-
dispatcher_result = _run_dispatcher(payload_text)
|
|
832
|
-
dispatcher_payload = json.loads(dispatcher_result.stdout.strip())
|
|
833
|
-
dispatcher_hook_specific = dispatcher_payload.get("hookSpecificOutput", {})
|
|
834
|
-
assert isinstance(dispatcher_hook_specific, dict)
|
|
835
|
-
assert dispatcher_hook_specific.get("additionalContext") == expected_additional_context, (
|
|
836
|
-
"Dispatcher must preserve the runpy hook's additionalContext.\n"
|
|
837
|
-
f"Expected: {expected_additional_context!r}\n"
|
|
838
|
-
f"Got: {dispatcher_hook_specific.get('additionalContext')!r}"
|
|
839
|
-
)
|
|
840
|
-
assert dispatcher_payload.get("suppressOutput") is True, (
|
|
841
|
-
"Dispatcher must preserve the runpy hook's suppressOutput flag.\n"
|
|
842
|
-
f"Got: {dispatcher_payload.get('suppressOutput')!r}"
|
|
843
|
-
)
|
|
844
|
-
|
|
845
|
-
|
|
846
683
|
def _parse_hook_allow(completed_process: subprocess.CompletedProcess[str]) -> bool:
|
|
847
684
|
"""Parse one hook's subprocess result for an explicit permissionDecision allow.
|
|
848
685
|
|
|
@@ -996,72 +833,6 @@ def _codex_add_patch(relative_file_path: str, file_body: str) -> str:
|
|
|
996
833
|
)
|
|
997
834
|
|
|
998
835
|
|
|
999
|
-
def test_dispatcher_denies_apply_patch_add_with_hardcoded_secret(
|
|
1000
|
-
tmp_path: Path,
|
|
1001
|
-
init_bare_git_repo: Callable[[Path], None],
|
|
1002
|
-
synthetic_github_token: str,
|
|
1003
|
-
) -> None:
|
|
1004
|
-
"""The dispatcher denies an apply_patch "add" that writes a hardcoded token.
|
|
1005
|
-
|
|
1006
|
-
Proves Done-when scenario 1 (hardcoded secret/PII denied) reaches apply_patch
|
|
1007
|
-
through the real dispatcher subprocess, not only the standalone
|
|
1008
|
-
pii_prevention_blocker test.
|
|
1009
|
-
"""
|
|
1010
|
-
repository_root = tmp_path / "repo"
|
|
1011
|
-
init_bare_git_repo(repository_root)
|
|
1012
|
-
patch_command = _codex_add_patch("leaked.py", f"token is {synthetic_github_token}\n")
|
|
1013
|
-
payload_text = _apply_patch_payload(str(repository_root), patch_command)
|
|
1014
|
-
|
|
1015
|
-
dispatcher_result = _run_dispatcher(payload_text)
|
|
1016
|
-
is_deny, reason_text = _parse_hook_decision(dispatcher_result)
|
|
1017
|
-
assert is_deny, (
|
|
1018
|
-
"dispatcher must deny an apply_patch add carrying a hardcoded secret, "
|
|
1019
|
-
f"got stdout {dispatcher_result.stdout.strip()!r}"
|
|
1020
|
-
)
|
|
1021
|
-
assert reason_text, "a deny must carry a non-empty reason"
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
def test_dispatcher_denies_apply_patch_targeting_a_sensitive_path(tmp_path: Path) -> None:
|
|
1025
|
-
"""The dispatcher denies an apply_patch "add" that targets a sensitive file.
|
|
1026
|
-
|
|
1027
|
-
Proves Done-when scenario 2 (sensitive path denied) reaches apply_patch
|
|
1028
|
-
through the real dispatcher subprocess.
|
|
1029
|
-
"""
|
|
1030
|
-
patch_command = _codex_add_patch(".env", "SECRET_KEY=irrelevant\n")
|
|
1031
|
-
payload_text = _apply_patch_payload(str(tmp_path), patch_command)
|
|
1032
|
-
|
|
1033
|
-
dispatcher_result = _run_dispatcher(payload_text)
|
|
1034
|
-
is_deny, reason_text = _parse_hook_decision(dispatcher_result)
|
|
1035
|
-
assert is_deny, (
|
|
1036
|
-
"dispatcher must deny an apply_patch add targeting a sensitive path, "
|
|
1037
|
-
f"got stdout {dispatcher_result.stdout.strip()!r}"
|
|
1038
|
-
)
|
|
1039
|
-
assert reason_text, "a deny must carry a non-empty reason"
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
def test_dispatcher_denies_apply_patch_add_onto_an_existing_path(tmp_path: Path) -> None:
|
|
1043
|
-
"""The dispatcher denies an apply_patch "add" that targets a path already on disk.
|
|
1044
|
-
|
|
1045
|
-
Proves Done-when scenario 3 (overwrite-without-reading denied) reaches
|
|
1046
|
-
apply_patch: a Codex "add" operation onto an existing path is caught by
|
|
1047
|
-
code_rules_enforcer's codex-patch reading, blocking/codex_apply_patch.py's
|
|
1048
|
-
_codex_read_patch_file, which raises CodexPatchError for an add already on
|
|
1049
|
-
disk, reported through the dispatcher's aggregate.
|
|
1050
|
-
"""
|
|
1051
|
-
existing_target = tmp_path / "already_here.py"
|
|
1052
|
-
existing_target.write_text("value = 1\n", encoding="utf-8")
|
|
1053
|
-
patch_command = _codex_add_patch("already_here.py", "value = 2\n")
|
|
1054
|
-
payload_text = _apply_patch_payload(str(tmp_path), patch_command)
|
|
1055
|
-
|
|
1056
|
-
dispatcher_result = _run_dispatcher(payload_text)
|
|
1057
|
-
is_deny, reason_text = _parse_hook_decision(dispatcher_result)
|
|
1058
|
-
assert is_deny, (
|
|
1059
|
-
"dispatcher must deny an apply_patch add onto an already-existing path, "
|
|
1060
|
-
f"got stdout {dispatcher_result.stdout.strip()!r}"
|
|
1061
|
-
)
|
|
1062
|
-
assert reason_text, "a deny must carry a non-empty reason"
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
836
|
def test_dispatcher_allows_clean_apply_patch_add(tmp_path: Path) -> None:
|
|
1066
837
|
"""A clean patch can proceed before the staged linter evaluates test pairing."""
|
|
1067
838
|
patch_command = _codex_add_patch(
|
|
@@ -1074,21 +845,11 @@ def test_dispatcher_allows_clean_apply_patch_add(tmp_path: Path) -> None:
|
|
|
1074
845
|
|
|
1075
846
|
|
|
1076
847
|
ALL_RETAINED_PATHS = {
|
|
1077
|
-
"blocking/write_existing_file_blocker.py",
|
|
1078
|
-
"blocking/sensitive_file_protector.py",
|
|
1079
|
-
"blocking/pii_prevention_blocker.py",
|
|
1080
|
-
"validation/hook_format_validator.py",
|
|
1081
|
-
"blocking/duplicate_rmtree_helper_blocker.py",
|
|
1082
|
-
"blocking/claude_md_orphan_file_blocker.py",
|
|
1083
|
-
"blocking/package_inventory_stale_blocker.py",
|
|
1084
|
-
"blocking/env_var_table_code_drift_blocker.py",
|
|
1085
|
-
"blocking/pytest_testpaths_orphan_blocker.py",
|
|
1086
|
-
"blocking/open_questions_in_plans_blocker.py",
|
|
1087
|
-
"blocking/docstring_rule_gate_count_blocker.py",
|
|
1088
848
|
"advisory/refactor_guard.py",
|
|
1089
849
|
"advisory/migration_safety_advisor.py",
|
|
1090
850
|
}
|
|
1091
851
|
ALL_RETIRED_PATHS = {
|
|
852
|
+
"validation/hook_format_validator.py",
|
|
1092
853
|
"blocking/code_rules_enforcer.py",
|
|
1093
854
|
"blocking/tdd_enforcer.py",
|
|
1094
855
|
"blocking/windows_rmtree_blocker.py",
|
|
@@ -1096,10 +857,15 @@ ALL_RETIRED_PATHS = {
|
|
|
1096
857
|
"blocking/subprocess_budget_completeness.py",
|
|
1097
858
|
"blocking/hook_prose_detector_consistency.py",
|
|
1098
859
|
"blocking/workflow_substitution_slot_blocker.py",
|
|
860
|
+
"blocking/open_questions_in_plans_blocker.py",
|
|
861
|
+
"blocking/docstring_rule_gate_count_blocker.py",
|
|
862
|
+
"blocking/duplicate_rmtree_helper_blocker.py",
|
|
863
|
+
"blocking/env_var_table_code_drift_blocker.py",
|
|
864
|
+
"blocking/pytest_testpaths_orphan_blocker.py",
|
|
1099
865
|
}
|
|
1100
866
|
|
|
1101
867
|
def test_hosted_hook_set_retains_boundaries_and_excludes_replaced_file_checks() -> None:
|
|
1102
|
-
"""Keep
|
|
868
|
+
"""Keep boundary controls and exclude every registry-backed policy hook."""
|
|
1103
869
|
all_hosted_script_paths = {
|
|
1104
870
|
each_entry.script_relative_path for each_entry in ALL_HOSTED_HOOK_ENTRIES
|
|
1105
871
|
}
|