claude-dev-env 1.92.1 → 1.93.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/CLAUDE.md +6 -2
- package/_shared/CLAUDE.md +2 -1
- package/_shared/advisor/CLAUDE.md +21 -0
- package/_shared/advisor/advisor-protocol.md +142 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
- package/_shared/advisor/scripts/pyproject.toml +3 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
- package/_shared/advisor/scripts/tier_model_ids.py +164 -0
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/precatch-rubric.md +65 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +260 -14
- package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +20 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
- package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
- package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
- package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +292 -2
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
- package/agents/CLAUDE.md +3 -2
- package/agents/clasp-deployment-orchestrator.md +2 -2
- package/agents/code-advisor.md +7 -5
- package/agents/code-verifier.md +6 -2
- package/agents/session-advisor.md +27 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
- package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
- package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
- package/bin/CLAUDE.md +1 -0
- package/bin/expand_home_directory_tokens.mjs +75 -0
- package/bin/install.mjs +21 -7
- package/bin/install.test.mjs +220 -10
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +4 -1
- package/hooks/blocking/_path_setup.py +13 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
- package/hooks/blocking/code_rules_enforcer.py +6 -0
- package/hooks/blocking/code_rules_imports_logging.py +34 -1
- package/hooks/blocking/code_rules_paths_syspath.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +272 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
- package/hooks/blocking/pii_prevention_blocker.py +781 -0
- package/hooks/blocking/pii_scanner.py +365 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/stop_dispatcher.py +84 -0
- package/hooks/blocking/test__path_setup.py +39 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
- package/hooks/blocking/test_destructive_command_blocker.py +29 -9
- package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
- package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
- package/hooks/blocking/test_pii_scanner.py +190 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
- package/hooks/blocking/test_stop_dispatcher.py +187 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
- package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
- package/hooks/diagnostic/migrations/README.md +25 -26
- package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
- package/hooks/hooks.json +10 -120
- package/hooks/hooks_constants/CLAUDE.md +12 -3
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
- package/hooks/hooks_constants/local_identity.py +182 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
- package/hooks/hooks_constants/pii_prevention_constants.py +295 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
- package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
- package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
- package/hooks/hooks_constants/test_layout_constants.py +28 -0
- package/hooks/hooks_constants/test_local_identity.py +88 -0
- package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
- package/hooks/validators/README.md +5 -6
- package/hooks/validators/__init__.py +2 -2
- package/hooks/validators/python_style_checks.py +298 -243
- package/hooks/validators/run_all_validators.py +6 -0
- package/hooks/validators/test_python_style_checks.py +278 -163
- package/package.json +2 -2
- package/rules/CLAUDE.md +2 -1
- package/rules/bdd.md +1 -1
- package/rules/nas-ssh-invocation.md +6 -4
- package/rules/no-justification-noise.md +61 -0
- package/scripts/test_setup_project_paths.py +1 -1
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +113 -35
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +44 -22
- package/skills/autoconverge/reference/gotchas.md +11 -0
- package/skills/autoconverge/reference/stop-conditions.md +23 -13
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
- package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +329 -68
- package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
- package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
- package/skills/bdd-protocol/SKILL.md +4 -5
- package/skills/bdd-protocol/references/anti-patterns.md +1 -1
- package/skills/bugteam/SKILL.md +8 -0
- package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
- package/skills/bugteam/reference/team-setup.md +7 -5
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/copilot-finding-triage/SKILL.md +124 -0
- package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
- package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
- package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
- package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
- package/skills/copilot-finding-triage/templates/notification.md +34 -0
- package/skills/copilot-review/SKILL.md +8 -6
- package/skills/findbugs/SKILL.md +4 -0
- package/skills/fixbugs/SKILL.md +8 -7
- package/skills/gotcha/CLAUDE.md +2 -2
- package/skills/gotcha/SKILL.md +4 -4
- package/skills/log-audit/SKILL.md +4 -6
- package/skills/monitor-open-prs/CLAUDE.md +1 -1
- package/skills/monitor-open-prs/SKILL.md +6 -1
- package/skills/orchestrator/SKILL.md +74 -78
- package/skills/orchestrator-refresh/SKILL.md +24 -12
- package/skills/post-audit-findings/SKILL.md +5 -9
- package/skills/pr-consistency-audit/SKILL.md +5 -1
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +86 -47
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +20 -17
- package/skills/pr-converge/reference/examples.md +63 -47
- package/skills/pr-converge/reference/fix-protocol.md +15 -15
- package/skills/pr-converge/reference/ground-rules.md +11 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
- package/skills/pr-converge/reference/per-tick.md +129 -107
- package/skills/pr-converge/reference/state-schema.md +15 -10
- package/skills/pr-converge/scripts/check_convergence.py +5 -4
- package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
- package/skills/pr-fix-protocol/SKILL.md +3 -8
- package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
- package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
- package/skills/pr-loop-lifecycle/SKILL.md +5 -9
- package/skills/pr-scope-resolve/SKILL.md +3 -6
- package/skills/privacy-hygiene/SKILL.md +114 -0
- package/skills/qbug/SKILL.md +8 -8
- package/skills/rebase/SKILL.md +5 -1
- package/skills/refine/SKILL.md +4 -5
- package/skills/reviewer-gates/SKILL.md +7 -11
- package/skills/session-log/SKILL.md +4 -1
- package/skills/skill-builder/SKILL.md +3 -6
- package/skills/structure-prompt/SKILL.md +4 -5
- package/skills/team-advisor/SKILL.md +56 -0
- package/skills/test_markdown_link_integrity.py +10 -6
- package/skills/update/SKILL.md +5 -1
- package/skills/usage-pause/SKILL.md +14 -5
- package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
- package/skills/verified-build/SKILL.md +4 -9
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Resolve the NAS host, ssh user, and ssh port for the NAS ssh enforcer hook.
|
|
2
|
+
|
|
3
|
+
The enforcer ships into ``~/.claude/`` and cannot read a repository file, so it
|
|
4
|
+
reads the real NAS values from the environment or a git-ignored file in the
|
|
5
|
+
Claude home directory, and it composes the two deny messages that quote those
|
|
6
|
+
values. The committed defaults are placeholders.
|
|
7
|
+
|
|
8
|
+
::
|
|
9
|
+
|
|
10
|
+
CLAUDE_NAS_HOST set to a host -> nas_host() returns that host
|
|
11
|
+
CLAUDE_NAS_SSH_PORT set to 2200 -> nas_ssh_port() returns 2200
|
|
12
|
+
(env unset, no file) -> nas_host() == "nas.example.local"
|
|
13
|
+
nas_ssh_port() == 22
|
|
14
|
+
|
|
15
|
+
Each value comes from its ``CLAUDE_NAS_*`` environment variable, then
|
|
16
|
+
``~/.claude/local-identity.json``, then the placeholder default.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
NAS_HOST_ENV_VAR = "CLAUDE_NAS_HOST"
|
|
26
|
+
NAS_SSH_USER_ENV_VAR = "CLAUDE_NAS_SSH_USER"
|
|
27
|
+
NAS_SSH_PORT_ENV_VAR = "CLAUDE_NAS_SSH_PORT"
|
|
28
|
+
NAS_JSON_KEY = "nas"
|
|
29
|
+
NAS_HOST_JSON_KEY = "host"
|
|
30
|
+
NAS_SSH_USER_JSON_KEY = "ssh_user"
|
|
31
|
+
NAS_SSH_PORT_JSON_KEY = "ssh_port"
|
|
32
|
+
CLAUDE_HOME_DIRECTORY_NAME = ".claude"
|
|
33
|
+
LOCAL_IDENTITY_FILE_NAME = "local-identity.json"
|
|
34
|
+
PLACEHOLDER_NAS_HOST = "nas.example.local"
|
|
35
|
+
PLACEHOLDER_NAS_SSH_USER = "operator"
|
|
36
|
+
PLACEHOLDER_NAS_SSH_PORT = 22
|
|
37
|
+
OPENSSH_INVOCATION_EXAMPLE_BINARY = '"/c/Windows/System32/OpenSSH/ssh.exe"'
|
|
38
|
+
NAS_SSH_RULE_REFERENCE = "~/.claude/rules/nas-ssh-invocation.md"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _local_identity_file_path() -> Path:
|
|
42
|
+
return Path.home() / CLAUDE_HOME_DIRECTORY_NAME / LOCAL_IDENTITY_FILE_NAME
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _nas_section_from_local_file() -> dict[str, object]:
|
|
46
|
+
identity_path = _local_identity_file_path()
|
|
47
|
+
if not identity_path.is_file():
|
|
48
|
+
return {}
|
|
49
|
+
try:
|
|
50
|
+
parsed_identity = json.loads(identity_path.read_text(encoding="utf-8"))
|
|
51
|
+
except (OSError, ValueError, UnicodeDecodeError):
|
|
52
|
+
return {}
|
|
53
|
+
if not isinstance(parsed_identity, dict):
|
|
54
|
+
return {}
|
|
55
|
+
nas_section = parsed_identity.get(NAS_JSON_KEY, {})
|
|
56
|
+
return nas_section if isinstance(nas_section, dict) else {}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def nas_host() -> str:
|
|
60
|
+
"""Return the NAS host the ssh enforcer guards.
|
|
61
|
+
|
|
62
|
+
::
|
|
63
|
+
|
|
64
|
+
CLAUDE_NAS_HOST set to a host -> that host
|
|
65
|
+
(env unset, no file) -> "nas.example.local"
|
|
66
|
+
|
|
67
|
+
The environment variable wins, then the git-ignored file, then the
|
|
68
|
+
placeholder default.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The NAS hostname or address.
|
|
72
|
+
"""
|
|
73
|
+
host_from_environment = os.environ.get(NAS_HOST_ENV_VAR)
|
|
74
|
+
if host_from_environment:
|
|
75
|
+
return host_from_environment
|
|
76
|
+
stored_host = _nas_section_from_local_file().get(NAS_HOST_JSON_KEY)
|
|
77
|
+
if isinstance(stored_host, str) and stored_host:
|
|
78
|
+
return stored_host
|
|
79
|
+
return PLACEHOLDER_NAS_HOST
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def nas_ssh_user() -> str:
|
|
83
|
+
"""Return the ssh user the enforcer expects for the NAS.
|
|
84
|
+
|
|
85
|
+
::
|
|
86
|
+
|
|
87
|
+
CLAUDE_NAS_SSH_USER set to a user -> that user
|
|
88
|
+
(env unset, no file) -> "operator"
|
|
89
|
+
|
|
90
|
+
The environment variable wins, then the git-ignored file, then the
|
|
91
|
+
placeholder default.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The ssh login name for the NAS.
|
|
95
|
+
"""
|
|
96
|
+
user_from_environment = os.environ.get(NAS_SSH_USER_ENV_VAR)
|
|
97
|
+
if user_from_environment:
|
|
98
|
+
return user_from_environment
|
|
99
|
+
stored_user = _nas_section_from_local_file().get(NAS_SSH_USER_JSON_KEY)
|
|
100
|
+
if isinstance(stored_user, str) and stored_user:
|
|
101
|
+
return stored_user
|
|
102
|
+
return PLACEHOLDER_NAS_SSH_USER
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def nas_ssh_port() -> int:
|
|
106
|
+
"""Return the ssh port the enforcer expects for the NAS.
|
|
107
|
+
|
|
108
|
+
::
|
|
109
|
+
|
|
110
|
+
CLAUDE_NAS_SSH_PORT set to 2200 -> 2200
|
|
111
|
+
(env unset, no file) -> 22
|
|
112
|
+
|
|
113
|
+
The environment variable wins, then the git-ignored file, then the
|
|
114
|
+
placeholder default.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The ssh port number for the NAS.
|
|
118
|
+
"""
|
|
119
|
+
port_from_environment = os.environ.get(NAS_SSH_PORT_ENV_VAR)
|
|
120
|
+
if port_from_environment and port_from_environment.isdigit():
|
|
121
|
+
return int(port_from_environment)
|
|
122
|
+
stored_port = _nas_section_from_local_file().get(NAS_SSH_PORT_JSON_KEY)
|
|
123
|
+
if isinstance(stored_port, int):
|
|
124
|
+
return stored_port
|
|
125
|
+
return PLACEHOLDER_NAS_SSH_PORT
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _openssh_invocation_example() -> str:
|
|
129
|
+
return (
|
|
130
|
+
f"{OPENSSH_INVOCATION_EXAMPLE_BINARY} -o BatchMode=yes -o ConnectTimeout=10 "
|
|
131
|
+
f'-p {nas_ssh_port()} {nas_ssh_user()}@{nas_host()} "<cmd>"'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def bare_ssh_binary_deny_message() -> str:
|
|
136
|
+
"""Return the deny message for a bare ssh-family word aimed at the NAS.
|
|
137
|
+
|
|
138
|
+
::
|
|
139
|
+
|
|
140
|
+
ssh -p 22 operator@nas.example.local "ls" -> this message text
|
|
141
|
+
(the text quotes the resolved host, user, and port)
|
|
142
|
+
|
|
143
|
+
Names the Git Bash MSYS-ssh hang and points at the Windows OpenSSH binary
|
|
144
|
+
with batch mode on.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
The full deny-message text for the bare ssh-family case.
|
|
148
|
+
"""
|
|
149
|
+
return (
|
|
150
|
+
f"BLOCKED [nas-ssh-binary]: Git Bash's MSYS ssh reads ~/.ssh/id_ed25519 as "
|
|
151
|
+
f"world-readable through its ACL mapping, rejects the key as bad permissions, "
|
|
152
|
+
f"and falls back to an interactive password prompt that hangs unattended "
|
|
153
|
+
f"sessions against the NAS at {nas_host()}.\n\n"
|
|
154
|
+
f"Use the Windows OpenSSH binary, which authenticates the key without prompting:\n"
|
|
155
|
+
f" {_openssh_invocation_example()}\n\n"
|
|
156
|
+
f"See {NAS_SSH_RULE_REFERENCE} for full guidance."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def missing_batch_mode_deny_message() -> str:
|
|
161
|
+
"""Return the deny message for the OpenSSH binary without batch mode.
|
|
162
|
+
|
|
163
|
+
::
|
|
164
|
+
|
|
165
|
+
"..ssh.exe" -p 22 operator@nas.example.local "ls" -> this message text
|
|
166
|
+
(the text quotes the resolved host, user, and port)
|
|
167
|
+
|
|
168
|
+
Names the interactive-prompt hang and asks for ``-o BatchMode=yes`` so a key
|
|
169
|
+
failure exits loudly.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The full deny-message text for the missing-batch-mode case.
|
|
173
|
+
"""
|
|
174
|
+
return (
|
|
175
|
+
f"BLOCKED [nas-ssh-binary]: this NAS ssh command uses the Windows OpenSSH binary "
|
|
176
|
+
f"but omits -o BatchMode=yes, so an authentication regression falls back to an "
|
|
177
|
+
f"interactive password prompt that hangs unattended sessions against the NAS at "
|
|
178
|
+
f"{nas_host()}.\n\n"
|
|
179
|
+
f"Add -o BatchMode=yes so a key failure exits loudly rather than prompting:\n"
|
|
180
|
+
f" {_openssh_invocation_example()}\n\n"
|
|
181
|
+
f"See {NAS_SSH_RULE_REFERENCE} for full guidance."
|
|
182
|
+
)
|
|
@@ -1,26 +1,15 @@
|
|
|
1
1
|
"""Constants for the NAS ssh binary enforcer hook.
|
|
2
2
|
|
|
3
|
-
Holds the Bash tool name, the
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Holds the Bash tool name, the ssh-family command basenames, the Windows OpenSSH
|
|
4
|
+
binary path suffixes, the launcher-wrapper set, the shell control-operator tokens
|
|
5
|
+
and their split pattern, the leading-assignment and launcher-duration patterns,
|
|
6
|
+
and the batch-mode pattern.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import re
|
|
10
10
|
|
|
11
11
|
BASH_TOOL_NAME = "Bash"
|
|
12
12
|
|
|
13
|
-
NAS_HOST_TOKEN_PATTERN = re.compile(r"(?:^|@)192\.168\.1\.100(?::|$)")
|
|
14
|
-
"""Match the NAS address only as a connection host.
|
|
15
|
-
|
|
16
|
-
Anchored to token start or an ``@`` and followed by ``:`` or token end, so the
|
|
17
|
-
address is matched as a bare host (``192.168.1.100``), a ``user@host``
|
|
18
|
-
(``jon@192.168.1.100``), or a ``host:path`` target (``192.168.1.100:/vol1/``,
|
|
19
|
-
``jon@192.168.1.100:/vol1/``). The address inside a remote-command argument
|
|
20
|
-
(``"ping 192.168.1.100"``) or a remote path component on another host
|
|
21
|
-
(``jon@other:/backup/192.168.1.100/``) does not count as a NAS connection.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
13
|
ALL_SSH_FAMILY_COMMAND_BASENAMES = frozenset(
|
|
25
14
|
{"ssh", "scp", "sftp", "ssh.exe", "scp.exe", "sftp.exe"}
|
|
26
15
|
)
|
|
@@ -35,25 +24,3 @@ CONTROL_OPERATOR_SPLIT_PATTERN = re.compile(r"(&&|\|\||;|\|&|\||(?<!>)&(?!>))")
|
|
|
35
24
|
LEADING_ASSIGNMENT_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
|
36
25
|
LAUNCHER_DURATION_PATTERN = re.compile(r"^\d+[a-z]*$", re.IGNORECASE)
|
|
37
26
|
BATCH_MODE_PATTERN = re.compile(r"batchmode\s*=\s*yes", re.IGNORECASE)
|
|
38
|
-
|
|
39
|
-
BARE_SSH_BINARY_MESSAGE = (
|
|
40
|
-
"BLOCKED [nas-ssh-binary]: Git Bash's MSYS ssh reads ~/.ssh/id_ed25519 as "
|
|
41
|
-
"world-readable through its ACL mapping, rejects the key as bad permissions, "
|
|
42
|
-
"and falls back to an interactive password prompt that hangs unattended "
|
|
43
|
-
"sessions against the NAS at 192.168.1.100.\n\n"
|
|
44
|
-
"Use the Windows OpenSSH binary, which authenticates the key without prompting:\n"
|
|
45
|
-
' "/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes -o ConnectTimeout=10 '
|
|
46
|
-
'-p 9222 jon@192.168.1.100 "<cmd>"\n\n'
|
|
47
|
-
"See ~/.claude/rules/nas-ssh-invocation.md for full guidance."
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
MISSING_BATCH_MODE_MESSAGE = (
|
|
51
|
-
"BLOCKED [nas-ssh-binary]: this NAS ssh command uses the Windows OpenSSH binary "
|
|
52
|
-
"but omits -o BatchMode=yes, so an authentication regression falls back to an "
|
|
53
|
-
"interactive password prompt that hangs unattended sessions against the NAS at "
|
|
54
|
-
"192.168.1.100.\n\n"
|
|
55
|
-
"Add -o BatchMode=yes so a key failure exits loudly rather than prompting:\n"
|
|
56
|
-
' "/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes -o ConnectTimeout=10 '
|
|
57
|
-
'-p 9222 jon@192.168.1.100 "<cmd>"\n\n'
|
|
58
|
-
"See ~/.claude/rules/nas-ssh-invocation.md for full guidance."
|
|
59
|
-
)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Named constants imported by `pii_prevention_blocker` and `pii_scanner`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from hooks_constants.hardcoded_user_path_constants import HARDCODED_USER_PATH_PATTERN
|
|
8
|
+
|
|
9
|
+
BASH_TOOL_NAME: str = "Bash"
|
|
10
|
+
POWERSHELL_TOOL_NAME: str = "PowerShell"
|
|
11
|
+
WRITE_TOOL_NAME: str = "Write"
|
|
12
|
+
EDIT_TOOL_NAME: str = "Edit"
|
|
13
|
+
MULTI_EDIT_TOOL_NAME: str = "MultiEdit"
|
|
14
|
+
|
|
15
|
+
ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES: frozenset[str] = frozenset(
|
|
16
|
+
{WRITE_TOOL_NAME, EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
ALL_SHELL_TOOL_NAMES: frozenset[str] = frozenset(
|
|
20
|
+
{BASH_TOOL_NAME, POWERSHELL_TOOL_NAME}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
ALL_GIT_BINARY_BASENAMES: frozenset[str] = frozenset({"git", "git.exe"})
|
|
24
|
+
GIT_COMMIT_SUBCOMMAND: str = "commit"
|
|
25
|
+
GIT_WORKING_DIRECTORY_OPTION: str = "-C"
|
|
26
|
+
ALL_VALUE_TAKING_GIT_OPTIONS: frozenset[str] = frozenset(
|
|
27
|
+
{GIT_WORKING_DIRECTORY_OPTION, "-c", "--git-dir", "--work-tree", "--namespace"}
|
|
28
|
+
)
|
|
29
|
+
GIT_OPTION_WITH_VALUE_STEP: int = 2
|
|
30
|
+
ALL_SHELL_COMMAND_SEPARATOR_TOKENS: frozenset[str] = frozenset(
|
|
31
|
+
{"&&", "||", ";", "|", "&"}
|
|
32
|
+
)
|
|
33
|
+
ALL_SHELL_KEYWORD_TOKENS: frozenset[str] = frozenset(
|
|
34
|
+
{"then", "do", "else", "elif"}
|
|
35
|
+
)
|
|
36
|
+
ALL_COMMAND_WRAPPER_TOKENS: frozenset[str] = frozenset(
|
|
37
|
+
{
|
|
38
|
+
"sudo",
|
|
39
|
+
"doas",
|
|
40
|
+
"env",
|
|
41
|
+
"time",
|
|
42
|
+
"timeout",
|
|
43
|
+
"nice",
|
|
44
|
+
"ionice",
|
|
45
|
+
"chrt",
|
|
46
|
+
"xargs",
|
|
47
|
+
"command",
|
|
48
|
+
"stdbuf",
|
|
49
|
+
"nohup",
|
|
50
|
+
"setsid",
|
|
51
|
+
"flock",
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
ALL_LEADING_SKIPPABLE_COMMAND_TOKENS: frozenset[str] = (
|
|
55
|
+
ALL_SHELL_KEYWORD_TOKENS | ALL_COMMAND_WRAPPER_TOKENS
|
|
56
|
+
)
|
|
57
|
+
ALL_ONE_OPERAND_WRAPPER_TOKENS: frozenset[str] = frozenset({"timeout", "flock"})
|
|
58
|
+
ALL_BASH_FAMILY_INTERPRETER_BASENAMES: frozenset[str] = frozenset(
|
|
59
|
+
{
|
|
60
|
+
"bash",
|
|
61
|
+
"sh",
|
|
62
|
+
"bash.exe",
|
|
63
|
+
"sh.exe",
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
ALL_POWERSHELL_INTERPRETER_BASENAMES: frozenset[str] = frozenset(
|
|
67
|
+
{
|
|
68
|
+
"pwsh",
|
|
69
|
+
"pwsh.exe",
|
|
70
|
+
"powershell",
|
|
71
|
+
"powershell.exe",
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
ALL_SHELL_INTERPRETER_BASENAMES: frozenset[str] = (
|
|
75
|
+
ALL_BASH_FAMILY_INTERPRETER_BASENAMES | ALL_POWERSHELL_INTERPRETER_BASENAMES
|
|
76
|
+
)
|
|
77
|
+
SUBSHELL_GROUP_OPEN_TOKEN: str = "("
|
|
78
|
+
SHELL_INLINE_COMMAND_FLAG: str = "-c"
|
|
79
|
+
POWERSHELL_INLINE_COMMAND_FLAG: str = "-command"
|
|
80
|
+
INLINE_COMMAND_FLAG_CLUSTER_CHARACTER: str = "c"
|
|
81
|
+
INLINE_COMMAND_TOKEN_JOINER: str = " "
|
|
82
|
+
SINGLE_DASH_OPTION_PREFIX: str = "-"
|
|
83
|
+
DOUBLE_DASH_OPTION_PREFIX: str = "--"
|
|
84
|
+
ALL_SHELL_QUOTE_CHARACTERS: frozenset[str] = frozenset({'"', "'"})
|
|
85
|
+
ALL_COMMAND_BOUNDARY_NEWLINE_CHARACTERS: frozenset[str] = frozenset({"\n", "\r"})
|
|
86
|
+
ENVIRONMENT_ASSIGNMENT_PATTERN: re.Pattern[str] = re.compile(
|
|
87
|
+
r"^[A-Za-z_][A-Za-z0-9_]*="
|
|
88
|
+
)
|
|
89
|
+
LINE_CONTINUATION_PATTERN: re.Pattern[str] = re.compile(r"\\\r?\n")
|
|
90
|
+
POWERSHELL_LINE_CONTINUATION_PATTERN: re.Pattern[str] = re.compile(r"`\r?\n")
|
|
91
|
+
|
|
92
|
+
MCP_GITHUB_TOOL_PREFIX: str = "mcp__plugin_github_github__"
|
|
93
|
+
|
|
94
|
+
HOOK_SCRIPT_BASENAME: str = "pii_prevention_blocker.py"
|
|
95
|
+
|
|
96
|
+
ALL_EXACT_LEGAL_NOTICE_BASENAMES: frozenset[str] = frozenset(
|
|
97
|
+
{
|
|
98
|
+
"license",
|
|
99
|
+
"license.md",
|
|
100
|
+
"license.txt",
|
|
101
|
+
"copying",
|
|
102
|
+
"copying.md",
|
|
103
|
+
"copying.txt",
|
|
104
|
+
"notice",
|
|
105
|
+
"notice.md",
|
|
106
|
+
"notice.txt",
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
ALL_SELF_MODULE_PATH_SUFFIXES: tuple[str, ...] = (
|
|
111
|
+
"/hooks/blocking/pii_prevention_blocker.py",
|
|
112
|
+
"/hooks/blocking/pii_scanner.py",
|
|
113
|
+
"/hooks/hooks_constants/pii_prevention_constants.py",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
PYTHON_SOURCE_FILE_SUFFIX: str = ".py"
|
|
117
|
+
CONFTEST_BASENAME: str = "conftest.py"
|
|
118
|
+
TEST_MODULE_BASENAME_PREFIX: str = "test_"
|
|
119
|
+
TEST_MODULE_BASENAME_SUFFIX: str = "_test.py"
|
|
120
|
+
TESTS_PATH_SEGMENT: str = "/tests/"
|
|
121
|
+
TESTS_PATH_PREFIX: str = "tests/"
|
|
122
|
+
SPEC_BASENAME_MARKER: str = ".spec."
|
|
123
|
+
TEST_BASENAME_MARKER: str = ".test."
|
|
124
|
+
ALL_SOURCE_TEST_FILE_SUFFIXES: tuple[str, ...] = (
|
|
125
|
+
".py",
|
|
126
|
+
".ts",
|
|
127
|
+
".tsx",
|
|
128
|
+
".js",
|
|
129
|
+
".jsx",
|
|
130
|
+
".mjs",
|
|
131
|
+
".cjs",
|
|
132
|
+
)
|
|
133
|
+
ALL_SAFE_EMAIL_DOMAINS: frozenset[str] = frozenset(
|
|
134
|
+
{
|
|
135
|
+
"example.com",
|
|
136
|
+
"example.org",
|
|
137
|
+
"example.net",
|
|
138
|
+
"example.edu",
|
|
139
|
+
"localhost",
|
|
140
|
+
"invalid",
|
|
141
|
+
"test",
|
|
142
|
+
"local",
|
|
143
|
+
"test.local",
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
ALL_PLACEHOLDER_HOME_USERNAMES: frozenset[str] = frozenset(
|
|
148
|
+
{
|
|
149
|
+
"example",
|
|
150
|
+
"user",
|
|
151
|
+
"username",
|
|
152
|
+
"your-user",
|
|
153
|
+
"your_user",
|
|
154
|
+
"you",
|
|
155
|
+
"name",
|
|
156
|
+
"alice",
|
|
157
|
+
"bob",
|
|
158
|
+
"carol",
|
|
159
|
+
"dave",
|
|
160
|
+
"placeholder",
|
|
161
|
+
"path",
|
|
162
|
+
"me",
|
|
163
|
+
"someone",
|
|
164
|
+
"default",
|
|
165
|
+
"admin",
|
|
166
|
+
"runner",
|
|
167
|
+
"container",
|
|
168
|
+
"ubuntu",
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
ALL_ALLOWLISTED_PRIVATE_IP_ADDRESSES: frozenset[str] = frozenset()
|
|
173
|
+
|
|
174
|
+
MAXIMUM_FINDINGS_PER_SCAN: int = 12
|
|
175
|
+
MAXIMUM_STAGED_FILE_BYTES: int = 1_000_000
|
|
176
|
+
MAXIMUM_OFFENDING_PREVIEW_LENGTH: int = 80
|
|
177
|
+
GIT_COMMAND_TIMEOUT_SECONDS: int = 10
|
|
178
|
+
|
|
179
|
+
ALL_STAGED_FILES_COMMAND: tuple[str, ...] = (
|
|
180
|
+
"git",
|
|
181
|
+
"diff",
|
|
182
|
+
"--cached",
|
|
183
|
+
"--name-only",
|
|
184
|
+
"--diff-filter=ACMR",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
ALL_STAGED_BLOB_SHOW_COMMAND_PREFIX: tuple[str, ...] = ("git", "show")
|
|
188
|
+
STAGED_BLOB_PREFIX: str = ":"
|
|
189
|
+
|
|
190
|
+
BODY_FILE_ENCODING: str = "utf-8"
|
|
191
|
+
NULL_BYTE_MARKER: bytes = b"\x00"
|
|
192
|
+
MESSAGE_LINE_SEPARATOR: str = "\n"
|
|
193
|
+
MINIMUM_ENV_STYLE_USERNAME_LENGTH: int = 3
|
|
194
|
+
IPV4_VERSION_NUMBER: int = 4
|
|
195
|
+
|
|
196
|
+
ALL_RFC1918_NETWORK_CIDRS: tuple[str, ...] = (
|
|
197
|
+
"10.0.0.0/8",
|
|
198
|
+
"172.16.0.0/12",
|
|
199
|
+
"192.168.0.0/16",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
CATEGORY_EMAIL: str = "email"
|
|
203
|
+
CATEGORY_HOME_PATH: str = "home-path"
|
|
204
|
+
CATEGORY_PRIVATE_IP: str = "private-ip"
|
|
205
|
+
CATEGORY_SECRET: str = "secret"
|
|
206
|
+
|
|
207
|
+
ALL_REDACTED_PREVIEW_CATEGORIES: frozenset[str] = frozenset(
|
|
208
|
+
{CATEGORY_EMAIL, CATEGORY_SECRET}
|
|
209
|
+
)
|
|
210
|
+
REDACTED_PREVIEW_PREFIX_LENGTH: int = 4
|
|
211
|
+
REDACTED_PREVIEW_SUFFIX_LENGTH: int = 4
|
|
212
|
+
REDACTED_PREVIEW_REVEALED_LENGTH: int = (
|
|
213
|
+
REDACTED_PREVIEW_PREFIX_LENGTH + REDACTED_PREVIEW_SUFFIX_LENGTH
|
|
214
|
+
)
|
|
215
|
+
REDACTED_PREVIEW_MINIMUM_HIDDEN_LENGTH: int = REDACTED_PREVIEW_REVEALED_LENGTH
|
|
216
|
+
REDACTED_PREVIEW_ELLIPSIS: str = "…"
|
|
217
|
+
REDACTED_SHORT_PREVIEW: str = "[redacted]"
|
|
218
|
+
MINIMUM_LENGTH_FOR_PARTIAL_REDACTION: int = (
|
|
219
|
+
REDACTED_PREVIEW_REVEALED_LENGTH + REDACTED_PREVIEW_MINIMUM_HIDDEN_LENGTH
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
EMAIL_PATTERN: re.Pattern[str] = re.compile(
|
|
223
|
+
r"(?i)\b([A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,})\b"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
IPV4_PATTERN: re.Pattern[str] = re.compile(
|
|
227
|
+
r"\b((?:25[0-5]|2[0-4]\d|1?\d?\d)\."
|
|
228
|
+
r"(?:25[0-5]|2[0-4]\d|1?\d?\d)\."
|
|
229
|
+
r"(?:25[0-5]|2[0-4]\d|1?\d?\d)\."
|
|
230
|
+
r"(?:25[0-5]|2[0-4]\d|1?\d?\d))\b"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
GITHUB_TOKEN_PATTERN: re.Pattern[str] = re.compile(
|
|
234
|
+
r"\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
GITHUB_FINE_GRAINED_TOKEN_PATTERN: re.Pattern[str] = re.compile(
|
|
238
|
+
r"\bgithub_pat_[A-Za-z0-9_]{20,}\b"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
AWS_ACCESS_KEY_PATTERN: re.Pattern[str] = re.compile(r"\bAKIA[0-9A-Z]{16}\b")
|
|
242
|
+
|
|
243
|
+
PEM_PRIVATE_KEY_HEADER_PATTERN: re.Pattern[str] = re.compile(
|
|
244
|
+
r"-----BEGIN (?:RSA |OPENSSH |EC |DSA |ENCRYPTED )?PRIVATE KEY-----"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
HOME_PATH_PATTERN: re.Pattern[str] = HARDCODED_USER_PATH_PATTERN
|
|
248
|
+
|
|
249
|
+
ALL_HOME_DIRECTORY_PATH_MARKERS: tuple[str, ...] = ("/users/", "/home/")
|
|
250
|
+
|
|
251
|
+
ALL_SECRET_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
252
|
+
GITHUB_TOKEN_PATTERN,
|
|
253
|
+
GITHUB_FINE_GRAINED_TOKEN_PATTERN,
|
|
254
|
+
AWS_ACCESS_KEY_PATTERN,
|
|
255
|
+
PEM_PRIVATE_KEY_HEADER_PATTERN,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
ANGLE_BRACKET_PLACEHOLDER_PATTERN: re.Pattern[str] = re.compile(r"^<[^>]+>$")
|
|
259
|
+
|
|
260
|
+
CORRECTIVE_MESSAGE_HEADER: str = (
|
|
261
|
+
"BLOCKED [pii_prevention_blocker]: high-confidence personal data or secret "
|
|
262
|
+
"material must not land in the repository or on a durable GitHub post."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
CORRECTIVE_MESSAGE_FOOTER: str = (
|
|
266
|
+
"Remediate: replace with placeholders (user@example.com, C:/Users/example/), "
|
|
267
|
+
"move secrets to an env or secret store, and run the privacy-hygiene skill "
|
|
268
|
+
"for a full sweep. Your own NAS host is allowlisted at scan time from "
|
|
269
|
+
"CLAUDE_NAS_HOST or ~/.claude/local-identity.json, so set it there rather "
|
|
270
|
+
"than committing the address."
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
FINDING_LINE_TEMPLATE: str = " [{category}] {preview}"
|
|
274
|
+
|
|
275
|
+
STAGED_LIST_FAILURE_REASON: str = (
|
|
276
|
+
"BLOCKED [pii_prevention_blocker]: could not list staged files for PII scan "
|
|
277
|
+
"(git diff --cached failed). Refuse commit until the index is readable."
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
REPOSITORY_ROOT_UNRESOLVED_REASON: str = (
|
|
281
|
+
"BLOCKED [pii_prevention_blocker]: could not resolve the git repository root "
|
|
282
|
+
"for a commit command (not a git work tree, git missing, or bad -C path). "
|
|
283
|
+
"Refuse commit until the repository root is resolvable."
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
STAGED_BLOB_UNSCANNABLE_REASON_TEMPLATE: str = (
|
|
287
|
+
"BLOCKED [pii_prevention_blocker]: staged file '{relative_path}' could not be "
|
|
288
|
+
"scanned for PII ({reason}). Refuse commit, shrink the blob, or keep binary "
|
|
289
|
+
"assets free of embedded secrets."
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
STAGED_BLOB_REASON_GIT_SHOW_FAILED: str = "git show of staged blob failed"
|
|
293
|
+
STAGED_BLOB_REASON_OVERSIZED: str = "blob exceeds MAXIMUM_STAGED_FILE_BYTES"
|
|
294
|
+
STAGED_BLOB_REASON_NULL_BYTES: str = "blob contains null bytes (binary/unscannable)"
|
|
295
|
+
STAGED_BLOB_REASON_DECODE_FAILED: str = "blob is not valid UTF-8 text"
|
|
@@ -80,6 +80,10 @@ ALL_HOSTED_HOOK_ENTRIES: tuple[HostedHookEntry, ...] = (
|
|
|
80
80
|
script_relative_path="blocking/sensitive_file_protector.py",
|
|
81
81
|
applicable_tool_names=ALL_WRITE_AND_EDIT_TOOL_NAMES,
|
|
82
82
|
),
|
|
83
|
+
HostedHookEntry(
|
|
84
|
+
script_relative_path="blocking/pii_prevention_blocker.py",
|
|
85
|
+
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
86
|
+
),
|
|
83
87
|
HostedHookEntry(
|
|
84
88
|
script_relative_path="validation/hook_format_validator.py",
|
|
85
89
|
applicable_tool_names=ALL_WRITE_AND_EDIT_TOOL_NAMES,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Constants for the python-style checks validator.
|
|
2
|
+
|
|
3
|
+
Two counts the checks compare against. One says how many blank lines belong
|
|
4
|
+
between one top-level function and the next. The other says how many
|
|
5
|
+
command-line arguments the runner needs before it has a file to check.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"EXPECTED_BLANK_LINES_BETWEEN_FUNCTIONS",
|
|
10
|
+
"MINIMUM_ARGUMENT_COUNT",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
EXPECTED_BLANK_LINES_BETWEEN_FUNCTIONS: int = 2
|
|
14
|
+
|
|
15
|
+
MINIMUM_ARGUMENT_COUNT: int = 2
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Constants for the Stop-hook dispatcher.
|
|
2
|
+
|
|
3
|
+
Holds the ordered hosted-hook roster for the Stop chain. The dispatcher imports
|
|
4
|
+
this roster to run every Stop hook in registration order inside one process.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ALL_STOP_HOSTED_HOOK_PATHS",
|
|
11
|
+
"BLOCK_DECISION",
|
|
12
|
+
"DECISION_KEY",
|
|
13
|
+
"REASON_KEY",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
BLOCK_DECISION = "block"
|
|
17
|
+
DECISION_KEY = "decision"
|
|
18
|
+
REASON_KEY = "reason"
|
|
19
|
+
|
|
20
|
+
ALL_STOP_HOSTED_HOOK_PATHS: tuple[str, ...] = (
|
|
21
|
+
"blocking/hedging_language_blocker.py",
|
|
22
|
+
"blocking/question_to_user_enforcer.py",
|
|
23
|
+
"blocking/intent_only_ending_blocker.py",
|
|
24
|
+
"blocking/session_handoff_blocker.py",
|
|
25
|
+
"diagnostic/hook_log_stop_wrapper.py",
|
|
26
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Tests for the Bash and PowerShell dispatcher hosted-hook roster.
|
|
2
|
+
|
|
3
|
+
The roster order and per-hook applicable-tool sets fix the firing sequence and
|
|
4
|
+
tool coverage the dispatcher must reproduce, so these tests pin both against the
|
|
5
|
+
registration order the standalone chain used.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
_HOOKS_DIR = str(Path(__file__).resolve().parent.parent)
|
|
14
|
+
if _HOOKS_DIR not in sys.path:
|
|
15
|
+
sys.path.insert(0, _HOOKS_DIR)
|
|
16
|
+
|
|
17
|
+
from hooks_constants.bash_pre_tool_use_dispatcher_constants import ( # noqa: E402
|
|
18
|
+
ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
19
|
+
ALL_BASH_HOSTED_HOOK_ENTRIES,
|
|
20
|
+
BASH_TOOL_NAME,
|
|
21
|
+
POWERSHELL_TOOL_NAME,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_EXPECTED_BASH_ORDER = (
|
|
25
|
+
"blocking/es_exe_path_rewriter.py",
|
|
26
|
+
"blocking/destructive_command_blocker.py",
|
|
27
|
+
"blocking/gh_body_arg_blocker.py",
|
|
28
|
+
"blocking/nas_ssh_binary_enforcer.py",
|
|
29
|
+
"blocking/volatile_path_in_post_blocker.py",
|
|
30
|
+
"blocking/pii_prevention_blocker.py",
|
|
31
|
+
"blocking/conventional_pr_title_gate.py",
|
|
32
|
+
"blocking/reviewer_spawn_gate.py",
|
|
33
|
+
"blocking/block_main_commit.py",
|
|
34
|
+
"blocking/precommit_code_rules_gate.py",
|
|
35
|
+
"blocking/session_edit_stage_gate.py",
|
|
36
|
+
"blocking/pr_description_enforcer.py",
|
|
37
|
+
"blocking/test_preflight_check.py",
|
|
38
|
+
"blocking/convergence_gate_blocker.py",
|
|
39
|
+
"blocking/windows_rmtree_blocker.py",
|
|
40
|
+
"blocking/gh_pr_author_enforcer.py",
|
|
41
|
+
"blocking/verified_commit_gate.py",
|
|
42
|
+
"blocking/verdict_directory_write_blocker.py",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
_POWERSHELL_APPLICABLE = (
|
|
46
|
+
"blocking/pii_prevention_blocker.py",
|
|
47
|
+
"blocking/verified_commit_gate.py",
|
|
48
|
+
"blocking/verdict_directory_write_blocker.py",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_roster_lists_all_bash_hooks_in_registration_order() -> None:
|
|
53
|
+
"""The roster reproduces the standalone Bash chain order exactly."""
|
|
54
|
+
actual_order = tuple(
|
|
55
|
+
each_entry.script_relative_path for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES
|
|
56
|
+
)
|
|
57
|
+
assert actual_order == _EXPECTED_BASH_ORDER
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_every_hook_applies_to_the_bash_tool() -> None:
|
|
61
|
+
"""Every hosted hook runs on a Bash tool call."""
|
|
62
|
+
for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES:
|
|
63
|
+
assert BASH_TOOL_NAME in each_entry.applicable_tool_names
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_powershell_applicable_hooks_include_pii_and_verified_commit_pair() -> None:
|
|
67
|
+
"""PowerShell runs the PII gate plus the verified-commit pair."""
|
|
68
|
+
powershell_hooks = tuple(
|
|
69
|
+
each_entry.script_relative_path
|
|
70
|
+
for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES
|
|
71
|
+
if POWERSHELL_TOOL_NAME in each_entry.applicable_tool_names
|
|
72
|
+
)
|
|
73
|
+
assert powershell_hooks == _POWERSHELL_APPLICABLE
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_powershell_hooks_carry_the_shared_tool_set() -> None:
|
|
77
|
+
"""The PowerShell-applicable hooks name both Bash and PowerShell."""
|
|
78
|
+
for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES:
|
|
79
|
+
if each_entry.script_relative_path in _POWERSHELL_APPLICABLE:
|
|
80
|
+
assert each_entry.applicable_tool_names == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_every_roster_path_points_at_an_existing_hook() -> None:
|
|
84
|
+
"""Each roster path names a hook script present on disk, catching a typo."""
|
|
85
|
+
hooks_root = Path(__file__).resolve().parent.parent
|
|
86
|
+
for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES:
|
|
87
|
+
assert (hooks_root / each_entry.script_relative_path).is_file()
|
|
@@ -13,9 +13,9 @@ from hooks_constants.hardcoded_user_path_constants import HARDCODED_USER_PATH_PA
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def test_pattern_matches_windows_user_home() -> None:
|
|
16
|
-
match = HARDCODED_USER_PATH_PATTERN.search("C:/Users/
|
|
16
|
+
match = HARDCODED_USER_PATH_PATTERN.search("C:/Users/example/notes")
|
|
17
17
|
assert match is not None
|
|
18
|
-
assert match.group(0) == "C:/Users/
|
|
18
|
+
assert match.group(0) == "C:/Users/example"
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def test_pattern_matches_macos_user_home() -> None:
|