claude-dev-env 1.93.1 → 1.95.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/_shared/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +35 -27
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -2
- package/_shared/advisor/scripts/model_tier_run_validator.py +23 -15
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +81 -17
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/audit-contract.md +1 -1
- package/_shared/pr-loop/gh-payloads.md +3 -3
- package/_shared/pr-loop/post-audit-thread-contract.md +51 -0
- package/_shared/pr-loop/scripts/README.md +2 -2
- package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -69
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
- package/_shared/pr-loop/state-schema.md +3 -22
- package/bin/CLAUDE.md +11 -2
- package/bin/ever-shipped-skills.mjs +70 -0
- package/bin/install.mjs +138 -13
- package/bin/install.prune.test.mjs +457 -0
- package/bin/install.test.mjs +10 -4
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_shared.py +82 -0
- package/hooks/blocking/code_rules_test_layout.py +9 -3
- package/hooks/blocking/pii_commit_command.py +398 -0
- package/hooks/blocking/pii_payload_scan.py +133 -0
- package/hooks/blocking/pii_prevention_blocker.py +151 -449
- package/hooks/blocking/plain_language_blocker.py +138 -4
- package/hooks/blocking/sensitive_file_protector.py +114 -48
- package/hooks/blocking/tdd_enforcer.py +9 -2
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/test_code_rules_shared.py +181 -0
- package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
- package/hooks/blocking/test_sensitive_file_protector.py +185 -0
- package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
- package/hooks/hooks_constants/CLAUDE.md +4 -2
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
- package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
- package/hooks/hooks_constants/local_identity.py +65 -15
- package/hooks/hooks_constants/pii_prevention_constants.py +16 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
- package/hooks/hooks_constants/test_local_identity.py +68 -0
- package/hooks/pyproject.toml +75 -4
- package/hooks/validators/CLAUDE.md +1 -1
- package/hooks/validators/README.md +2 -0
- package/hooks/validators/python_style_checks.py +114 -136
- package/hooks/validators/python_style_helpers.py +95 -0
- package/hooks/validators/test_python_style_checks.py +0 -164
- package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
- package/hooks/validators/test_python_style_fixes.py +251 -0
- package/hooks/validators/test_python_style_helpers.py +125 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/anti-corollary-tests.md +69 -0
- package/rules/bdd.md +1 -3
- package/rules/code-reviews.md +1 -1
- package/rules/gh-paginate.md +1 -1
- package/rules/plain-language.md +2 -0
- package/scripts/CLAUDE.md +10 -1
- package/scripts/Get-SessionAccount.ps1 +210 -0
- package/scripts/tests/CLAUDE.md +15 -1
- package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
- package/skills/CLAUDE.md +6 -27
- package/skills/anthropic-plan/SKILL.md +1 -13
- package/skills/autoconverge/CLAUDE.md +5 -2
- package/skills/autoconverge/SKILL.md +112 -387
- package/skills/autoconverge/reference/CLAUDE.md +4 -1
- package/skills/autoconverge/reference/closing-report.md +9 -2
- package/skills/autoconverge/reference/convergence.md +26 -22
- package/skills/autoconverge/reference/headless-safety.md +44 -0
- package/skills/autoconverge/reference/multi-pr.md +88 -0
- package/skills/autoconverge/reference/self-closing-loop.md +84 -0
- package/skills/autoconverge/reference/stop-conditions.md +21 -13
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
- package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +286 -96
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/CONSTRAINTS.md +3 -3
- package/skills/bugteam/PROMPTS.md +8 -19
- package/skills/bugteam/SKILL.md +13 -14
- package/skills/bugteam/reference/CLAUDE.md +1 -1
- package/skills/bugteam/reference/README.md +2 -3
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
- package/skills/bugteam/reference/github-pr-reviews.md +56 -38
- package/skills/bugteam/reference/team-setup.md +11 -11
- package/skills/bugteam/scripts/CLAUDE.md +6 -6
- package/skills/bugteam/scripts/README.md +20 -8
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
- package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
- package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
- package/skills/closeout/SKILL.md +153 -0
- package/skills/closeout/reference/handoff-prompt-template.md +72 -0
- package/skills/closeout/reference/issue-body-templates.md +108 -0
- package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
- package/skills/copilot-finding-triage/SKILL.md +80 -28
- package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
- package/skills/copilot-finding-triage/templates/notification.md +8 -3
- package/skills/everything-search/SKILL.md +1 -2
- package/skills/grokify/SKILL.md +58 -0
- package/skills/grokify/templates/handoff-template.md +92 -0
- package/skills/orchestrator/SKILL.md +35 -25
- package/skills/orchestrator-refresh/SKILL.md +15 -11
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +132 -233
- package/skills/pr-converge/reference/convergence-gates.md +102 -66
- package/skills/pr-converge/reference/examples.md +37 -25
- package/skills/pr-converge/reference/fix-protocol.md +7 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
- package/skills/pr-converge/reference/per-tick.md +64 -41
- package/skills/pr-converge/reference/state-schema.md +27 -2
- package/skills/pr-converge/scripts/CLAUDE.md +8 -0
- package/skills/pr-converge/scripts/README.md +4 -3
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
- package/skills/pr-converge/scripts/check_convergence.py +189 -645
- package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
- package/skills/pr-converge/scripts/conftest.py +8 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
- package/skills/pr-fix-protocol/SKILL.md +20 -20
- package/skills/privacy-hygiene/SKILL.md +4 -3
- package/skills/reviewer-gates/SKILL.md +26 -8
- package/skills/session-log/SKILL.md +11 -12
- package/skills/session-tidy/SKILL.md +40 -30
- package/skills/skill-builder/CLAUDE.md +13 -10
- package/skills/skill-builder/SKILL.md +31 -31
- package/skills/skill-builder/references/CLAUDE.md +10 -6
- package/skills/skill-builder/references/delegation-map.md +13 -4
- package/skills/skill-builder/references/description-field.md +113 -0
- package/skills/skill-builder/references/self-audit-checklist.md +21 -4
- package/skills/skill-builder/references/skill-modularity.md +113 -0
- package/skills/skill-builder/references/skill-types.md +4 -0
- package/skills/skill-builder/templates/CLAUDE.md +4 -4
- package/skills/skill-builder/templates/gap-analysis.md +25 -1
- package/skills/skill-builder/workflows/CLAUDE.md +6 -4
- package/skills/skill-builder/workflows/improve-skill.md +17 -16
- package/skills/skill-builder/workflows/new-skill.md +34 -10
- package/skills/skill-builder/workflows/polish-skill.md +59 -43
- package/skills/team-advisor/SKILL.md +15 -11
- package/skills/usage-pause/SKILL.md +10 -14
- package/system-prompts/software-engineer.xml +7 -6
- package/hooks/validators/test_verify_paths.py +0 -32
- package/hooks/validators/verify_paths.py +0 -57
- package/skills/bdd-protocol/CLAUDE.md +0 -26
- package/skills/bdd-protocol/SKILL.md +0 -30
- package/skills/bdd-protocol/references/CLAUDE.md +0 -10
- package/skills/bdd-protocol/references/anti-patterns.md +0 -26
- package/skills/bdd-protocol/references/example-mapping.md +0 -23
- package/skills/bg-agent/CLAUDE.md +0 -17
- package/skills/bg-agent/SKILL.md +0 -69
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
- package/skills/caveman/CLAUDE.md +0 -15
- package/skills/caveman/SKILL.md +0 -38
- package/skills/code/CLAUDE.md +0 -17
- package/skills/code/SKILL.md +0 -178
- package/skills/deep-research/CLAUDE.md +0 -17
- package/skills/deep-research/SKILL.md +0 -80
- package/skills/findbugs/CLAUDE.md +0 -20
- package/skills/findbugs/SKILL.md +0 -241
- package/skills/fixbugs/CLAUDE.md +0 -19
- package/skills/fixbugs/SKILL.md +0 -142
- package/skills/gh-paginate/CLAUDE.md +0 -18
- package/skills/gh-paginate/SKILL.md +0 -84
- package/skills/gotcha/CLAUDE.md +0 -33
- package/skills/gotcha/SKILL.md +0 -73
- package/skills/implement/CLAUDE.md +0 -27
- package/skills/implement/SKILL.md +0 -66
- package/skills/implement/scripts/CLAUDE.md +0 -22
- package/skills/implement/scripts/append_note.py +0 -133
- package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
- package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
- package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
- package/skills/implement/scripts/test_append_note.py +0 -191
- package/skills/log-audit/CLAUDE.md +0 -20
- package/skills/log-audit/SKILL.md +0 -66
- package/skills/log-audit/reference/CLAUDE.md +0 -9
- package/skills/log-audit/reference/charter.md +0 -52
- package/skills/log-audit/scripts/CLAUDE.md +0 -27
- package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
- package/skills/log-audit/scripts/collect_log_window.py +0 -199
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
- package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
- package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
- package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
- package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
- package/skills/logifix/CLAUDE.md +0 -36
- package/skills/logifix/SKILL.md +0 -69
- package/skills/logifix/scripts/CLAUDE.md +0 -16
- package/skills/logifix/scripts/logifix.ps1 +0 -205
- package/skills/monitor-open-prs/CLAUDE.md +0 -34
- package/skills/monitor-open-prs/SKILL.md +0 -88
- package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
- package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
- package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
- package/skills/monitor-open-prs/test_skill_contract.py +0 -27
- package/skills/post-audit-findings/SKILL.md +0 -80
- package/skills/pr-consistency-audit/CLAUDE.md +0 -34
- package/skills/pr-consistency-audit/SKILL.md +0 -116
- package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
- package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
- package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
- package/skills/pr-review-responder/CLAUDE.md +0 -35
- package/skills/pr-review-responder/EXAMPLES.md +0 -590
- package/skills/pr-review-responder/PRINCIPLES.md +0 -533
- package/skills/pr-review-responder/README.md +0 -168
- package/skills/pr-review-responder/SKILL.md +0 -203
- package/skills/pr-review-responder/TESTING.md +0 -350
- package/skills/pr-scope-resolve/SKILL.md +0 -45
- package/skills/pre-compact/CLAUDE.md +0 -24
- package/skills/pre-compact/SKILL.md +0 -134
- package/skills/qbug/CLAUDE.md +0 -40
- package/skills/qbug/SKILL.md +0 -387
- package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
- package/skills/refine/CLAUDE.md +0 -44
- package/skills/refine/SKILL.md +0 -262
- package/skills/refine/templates/CLAUDE.md +0 -17
- package/skills/refine/templates/implementation-notes-template.html +0 -56
- package/skills/refine/templates/plan-template.md +0 -60
- package/skills/research-mode/CLAUDE.md +0 -35
- package/skills/research-mode/SKILL.md +0 -53
- package/skills/structure-prompt/CLAUDE.md +0 -42
- package/skills/structure-prompt/SKILL.md +0 -44
- package/skills/structure-prompt/reference/CLAUDE.md +0 -28
- package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
- package/skills/structure-prompt/reference/block-classification.md +0 -27
- package/skills/structure-prompt/reference/canonical-case.md +0 -48
- package/skills/structure-prompt/reference/citation-depth.md +0 -70
- package/skills/structure-prompt/reference/cleanup.md +0 -33
- package/skills/structure-prompt/reference/constraints.md +0 -33
- package/skills/structure-prompt/reference/directives.md +0 -37
- package/skills/structure-prompt/reference/examples.md +0 -72
- package/skills/structure-prompt/reference/instantiation.md +0 -51
- package/skills/structure-prompt/reference/output-contract.md +0 -72
- package/skills/structure-prompt/reference/per-category.md +0 -23
- package/skills/structure-prompt/reference/persona.md +0 -38
- package/skills/structure-prompt/reference/research.md +0 -33
- package/skills/structure-prompt/reference/structure.md +0 -28
- package/skills/verified-build/CLAUDE.md +0 -33
- package/skills/verified-build/SKILL.md +0 -33
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Token-aware git-commit detection reused by ``pii_prevention_blocker``."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
12
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
13
|
+
if _blocking_directory not in sys.path:
|
|
14
|
+
sys.path.insert(0, _blocking_directory)
|
|
15
|
+
if _hooks_directory not in sys.path:
|
|
16
|
+
sys.path.insert(0, _hooks_directory)
|
|
17
|
+
|
|
18
|
+
from block_main_commit import extract_git_working_directory # noqa: E402
|
|
19
|
+
|
|
20
|
+
from hooks_constants.pii_prevention_constants import ( # noqa: E402
|
|
21
|
+
ALL_BASH_FAMILY_INTERPRETER_BASENAMES,
|
|
22
|
+
ALL_COMMAND_BOUNDARY_NEWLINE_CHARACTERS,
|
|
23
|
+
ALL_GIT_BINARY_BASENAMES,
|
|
24
|
+
ALL_LEADING_SKIPPABLE_COMMAND_TOKENS,
|
|
25
|
+
ALL_ONE_OPERAND_WRAPPER_TOKENS,
|
|
26
|
+
ALL_SHELL_COMMAND_SEPARATOR_TOKENS,
|
|
27
|
+
ALL_SHELL_INTERPRETER_BASENAMES,
|
|
28
|
+
ALL_SHELL_QUOTE_CHARACTERS,
|
|
29
|
+
ALL_VALUE_TAKING_GIT_OPTIONS,
|
|
30
|
+
DOUBLE_DASH_OPTION_PREFIX,
|
|
31
|
+
ENVIRONMENT_ASSIGNMENT_PATTERN,
|
|
32
|
+
GIT_COMMIT_SUBCOMMAND,
|
|
33
|
+
GIT_OPTION_WITH_VALUE_STEP,
|
|
34
|
+
GIT_WORKING_DIRECTORY_OPTION,
|
|
35
|
+
INLINE_COMMAND_FLAG_CLUSTER_CHARACTER,
|
|
36
|
+
INLINE_COMMAND_TOKEN_JOINER,
|
|
37
|
+
LINE_CONTINUATION_PATTERN,
|
|
38
|
+
POWERSHELL_INLINE_COMMAND_FLAG,
|
|
39
|
+
POWERSHELL_LINE_CONTINUATION_PATTERN,
|
|
40
|
+
SHELL_INLINE_COMMAND_FLAG,
|
|
41
|
+
SINGLE_DASH_OPTION_PREFIX,
|
|
42
|
+
SUBSHELL_GROUP_OPEN_TOKEN,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _strip_token_edge_quotes(token_text: str) -> str:
|
|
47
|
+
return token_text.strip("\"'")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _token_basename_lower(token_text: str) -> str:
|
|
51
|
+
stripped_token = _strip_token_edge_quotes(token_text)
|
|
52
|
+
return re.split(r"[\\/]", stripped_token)[-1].lower()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _token_is_git_binary(token_text: str) -> bool:
|
|
56
|
+
return _token_basename_lower(token_text) in ALL_GIT_BINARY_BASENAMES
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _token_is_shell_interpreter(token_text: str) -> bool:
|
|
60
|
+
return _token_basename_lower(token_text) in ALL_SHELL_INTERPRETER_BASENAMES
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _token_is_bash_family_interpreter(token_text: str) -> bool:
|
|
64
|
+
return _token_basename_lower(token_text) in ALL_BASH_FAMILY_INTERPRETER_BASENAMES
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _token_is_subshell_group_open(token_text: str) -> bool:
|
|
68
|
+
return token_text == SUBSHELL_GROUP_OPEN_TOKEN
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _token_is_skippable_prefix(token_text: str) -> bool:
|
|
72
|
+
if ENVIRONMENT_ASSIGNMENT_PATTERN.match(token_text):
|
|
73
|
+
return True
|
|
74
|
+
return _token_basename_lower(token_text) in ALL_LEADING_SKIPPABLE_COMMAND_TOKENS
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _following_tokens_invoke_commit(all_following_tokens: list[str]) -> bool:
|
|
78
|
+
token_index = 0
|
|
79
|
+
option_with_value_step = GIT_OPTION_WITH_VALUE_STEP
|
|
80
|
+
while token_index < len(all_following_tokens):
|
|
81
|
+
each_token = _strip_token_edge_quotes(all_following_tokens[token_index])
|
|
82
|
+
option_name = each_token
|
|
83
|
+
has_attached_value = False
|
|
84
|
+
if each_token.startswith(DOUBLE_DASH_OPTION_PREFIX) and "=" in each_token:
|
|
85
|
+
option_name, _, _attached_value = each_token.partition("=")
|
|
86
|
+
has_attached_value = True
|
|
87
|
+
if option_name in ALL_VALUE_TAKING_GIT_OPTIONS:
|
|
88
|
+
if has_attached_value:
|
|
89
|
+
token_index += 1
|
|
90
|
+
else:
|
|
91
|
+
token_index += option_with_value_step
|
|
92
|
+
continue
|
|
93
|
+
if each_token.startswith(SINGLE_DASH_OPTION_PREFIX):
|
|
94
|
+
token_index += 1
|
|
95
|
+
continue
|
|
96
|
+
return each_token.lower() == GIT_COMMIT_SUBCOMMAND
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _split_shell_command_segments(all_tokens: list[str]) -> list[list[str]]:
|
|
101
|
+
all_segments: list[list[str]] = [[]]
|
|
102
|
+
shell_separators = ALL_SHELL_COMMAND_SEPARATOR_TOKENS
|
|
103
|
+
for each_token in all_tokens:
|
|
104
|
+
if each_token in shell_separators:
|
|
105
|
+
all_segments.append([])
|
|
106
|
+
continue
|
|
107
|
+
all_segments[-1].append(each_token)
|
|
108
|
+
return all_segments
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _tokenize_shell_command(shell_command_piece: str) -> list[str] | None:
|
|
112
|
+
lexer = shlex.shlex(shell_command_piece, posix=True, punctuation_chars=True)
|
|
113
|
+
lexer.whitespace_split = True
|
|
114
|
+
lexer.escape = ""
|
|
115
|
+
lexer.commenters = ""
|
|
116
|
+
try:
|
|
117
|
+
return list(lexer)
|
|
118
|
+
except ValueError:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _fallback_segments_by_physical_line(shell_command_piece: str) -> list[list[str]]:
|
|
123
|
+
all_segments: list[list[str]] = []
|
|
124
|
+
for each_physical_line in shell_command_piece.splitlines():
|
|
125
|
+
line_tokens = each_physical_line.split()
|
|
126
|
+
all_segments.extend(_split_shell_command_segments(line_tokens))
|
|
127
|
+
return all_segments
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _segments_for_piece(shell_command_piece: str) -> list[list[str]]:
|
|
131
|
+
piece_tokens = _tokenize_shell_command(shell_command_piece)
|
|
132
|
+
if piece_tokens is None:
|
|
133
|
+
return _fallback_segments_by_physical_line(shell_command_piece)
|
|
134
|
+
return _split_shell_command_segments(piece_tokens)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _split_on_unquoted_newlines(shell_command: str) -> list[str]:
|
|
138
|
+
all_pieces: list[str] = []
|
|
139
|
+
current_characters: list[str] = []
|
|
140
|
+
active_quote_character = ""
|
|
141
|
+
for each_character in shell_command:
|
|
142
|
+
if active_quote_character:
|
|
143
|
+
if each_character == active_quote_character:
|
|
144
|
+
active_quote_character = ""
|
|
145
|
+
current_characters.append(each_character)
|
|
146
|
+
continue
|
|
147
|
+
if each_character in ALL_SHELL_QUOTE_CHARACTERS:
|
|
148
|
+
active_quote_character = each_character
|
|
149
|
+
current_characters.append(each_character)
|
|
150
|
+
continue
|
|
151
|
+
if each_character in ALL_COMMAND_BOUNDARY_NEWLINE_CHARACTERS:
|
|
152
|
+
all_pieces.append("".join(current_characters))
|
|
153
|
+
current_characters = []
|
|
154
|
+
continue
|
|
155
|
+
current_characters.append(each_character)
|
|
156
|
+
all_pieces.append("".join(current_characters))
|
|
157
|
+
return all_pieces
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _all_command_segments(shell_command: str) -> list[list[str]]:
|
|
161
|
+
collapsed_command = LINE_CONTINUATION_PATTERN.sub("", shell_command)
|
|
162
|
+
collapsed_command = POWERSHELL_LINE_CONTINUATION_PATTERN.sub("", collapsed_command)
|
|
163
|
+
all_segments: list[list[str]] = []
|
|
164
|
+
for each_piece in _split_on_unquoted_newlines(collapsed_command):
|
|
165
|
+
all_segments.extend(_segments_for_piece(each_piece))
|
|
166
|
+
return all_segments
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _token_is_leading_skip_target(token_text: str) -> bool:
|
|
170
|
+
return _token_is_git_binary(token_text) or _token_is_shell_interpreter(token_text)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _token_is_wrapper_option_flag(token_text: str) -> bool:
|
|
174
|
+
return token_text.startswith(SINGLE_DASH_OPTION_PREFIX)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _wrapper_leading_operand_count(token_text: str) -> int:
|
|
178
|
+
if _token_basename_lower(token_text) in ALL_ONE_OPERAND_WRAPPER_TOKENS:
|
|
179
|
+
return 1
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _flag_value_token_follows(all_segment_tokens: list[str], value_index: int) -> bool:
|
|
184
|
+
if value_index >= len(all_segment_tokens):
|
|
185
|
+
return False
|
|
186
|
+
if not _token_is_leading_skip_target(all_segment_tokens[value_index]):
|
|
187
|
+
return True
|
|
188
|
+
return _later_token_is_leading_skip_target(all_segment_tokens, value_index + 1)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _later_token_is_leading_skip_target(
|
|
192
|
+
all_segment_tokens: list[str], search_start_index: int
|
|
193
|
+
) -> bool:
|
|
194
|
+
for each_token in all_segment_tokens[search_start_index:]:
|
|
195
|
+
if _token_is_leading_skip_target(each_token):
|
|
196
|
+
return True
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _skip_leading_noop_tokens(all_segment_tokens: list[str]) -> int:
|
|
201
|
+
token_index = 0
|
|
202
|
+
has_skipped_wrapper_prefix = False
|
|
203
|
+
pending_operand_budget = 0
|
|
204
|
+
while token_index < len(all_segment_tokens):
|
|
205
|
+
each_token = all_segment_tokens[token_index]
|
|
206
|
+
if _token_is_subshell_group_open(each_token):
|
|
207
|
+
token_index += 1
|
|
208
|
+
continue
|
|
209
|
+
if _token_is_skippable_prefix(each_token):
|
|
210
|
+
has_skipped_wrapper_prefix = True
|
|
211
|
+
pending_operand_budget += _wrapper_leading_operand_count(each_token)
|
|
212
|
+
token_index += 1
|
|
213
|
+
continue
|
|
214
|
+
if not has_skipped_wrapper_prefix:
|
|
215
|
+
break
|
|
216
|
+
if _token_is_leading_skip_target(each_token):
|
|
217
|
+
break
|
|
218
|
+
if _token_is_wrapper_option_flag(each_token):
|
|
219
|
+
token_index += 1
|
|
220
|
+
if _flag_value_token_follows(all_segment_tokens, token_index):
|
|
221
|
+
token_index += 1
|
|
222
|
+
continue
|
|
223
|
+
if pending_operand_budget > 0:
|
|
224
|
+
pending_operand_budget -= 1
|
|
225
|
+
token_index += 1
|
|
226
|
+
continue
|
|
227
|
+
break
|
|
228
|
+
return token_index
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _token_is_powershell_command_flag_prefix(lowered_token: str) -> bool:
|
|
232
|
+
if lowered_token == SINGLE_DASH_OPTION_PREFIX:
|
|
233
|
+
return False
|
|
234
|
+
return POWERSHELL_INLINE_COMMAND_FLAG.startswith(lowered_token)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _token_is_interpreter_inline_command_flag(
|
|
238
|
+
token_text: str, interpreter_allows_short_flag_cluster: bool
|
|
239
|
+
) -> bool:
|
|
240
|
+
if not token_text.startswith(SINGLE_DASH_OPTION_PREFIX):
|
|
241
|
+
return False
|
|
242
|
+
if token_text.startswith(DOUBLE_DASH_OPTION_PREFIX):
|
|
243
|
+
return False
|
|
244
|
+
lowered_token = token_text.lower()
|
|
245
|
+
if lowered_token == SHELL_INLINE_COMMAND_FLAG:
|
|
246
|
+
return True
|
|
247
|
+
if _token_is_powershell_command_flag_prefix(lowered_token):
|
|
248
|
+
return True
|
|
249
|
+
if not interpreter_allows_short_flag_cluster:
|
|
250
|
+
return False
|
|
251
|
+
clustered_flag_characters = lowered_token[len(SINGLE_DASH_OPTION_PREFIX) :]
|
|
252
|
+
return INLINE_COMMAND_FLAG_CLUSTER_CHARACTER in clustered_flag_characters
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _interpreter_inline_command_invokes_commit(
|
|
256
|
+
interpreter_token: str, all_following_tokens: list[str]
|
|
257
|
+
) -> bool:
|
|
258
|
+
allows_short_flag_cluster = _token_is_bash_family_interpreter(interpreter_token)
|
|
259
|
+
token_index = 0
|
|
260
|
+
while token_index < len(all_following_tokens):
|
|
261
|
+
each_token = all_following_tokens[token_index]
|
|
262
|
+
if _token_is_interpreter_inline_command_flag(
|
|
263
|
+
each_token, allows_short_flag_cluster
|
|
264
|
+
):
|
|
265
|
+
argument_index = token_index + 1
|
|
266
|
+
if argument_index >= len(all_following_tokens):
|
|
267
|
+
return False
|
|
268
|
+
inline_command = INLINE_COMMAND_TOKEN_JOINER.join(
|
|
269
|
+
all_following_tokens[argument_index:]
|
|
270
|
+
)
|
|
271
|
+
return is_git_commit_shell_command(inline_command)
|
|
272
|
+
token_index += 1
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _segment_invokes_git_commit(all_segment_tokens: list[str]) -> bool:
|
|
277
|
+
command_index = _skip_leading_noop_tokens(all_segment_tokens)
|
|
278
|
+
if command_index >= len(all_segment_tokens):
|
|
279
|
+
return False
|
|
280
|
+
all_following_tokens = all_segment_tokens[command_index + 1 :]
|
|
281
|
+
command_token = all_segment_tokens[command_index]
|
|
282
|
+
if _token_is_shell_interpreter(command_token):
|
|
283
|
+
return _interpreter_inline_command_invokes_commit(
|
|
284
|
+
command_token, all_following_tokens
|
|
285
|
+
)
|
|
286
|
+
if not _token_is_git_binary(command_token):
|
|
287
|
+
return False
|
|
288
|
+
return _following_tokens_invoke_commit(all_following_tokens)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def is_git_commit_shell_command(shell_command: str) -> bool:
|
|
292
|
+
"""Report whether *shell_command* invokes git commit (token-aware).
|
|
293
|
+
|
|
294
|
+
Each segment is read past its leading noise to the real command word::
|
|
295
|
+
|
|
296
|
+
sudo git commit -m x -> skip the wrapper, then match commit
|
|
297
|
+
nice -n 10 git commit -> skip the wrapper and its flag value
|
|
298
|
+
then git commit -m x -> skip the keyword, then match commit
|
|
299
|
+
build & git commit -m x -> split on the background operator
|
|
300
|
+
bash -cx "git commit" -> unwrap the bash cluster
|
|
301
|
+
pwsh -ExecutionPolicy Bypass -Command ... -> skip past the pwsh flag
|
|
302
|
+
(git commit -m x) -> step over the group open
|
|
303
|
+
|
|
304
|
+
Skipped leading tokens: a subshell-group open ``(``, env-assignments, shell
|
|
305
|
+
keywords (then, do, else, elif), and wrapper commands (sudo, env, time,
|
|
306
|
+
nice, xargs, command, stdbuf) together with each wrapper's own option flags,
|
|
307
|
+
flag values, and the single leading operand that timeout and flock take
|
|
308
|
+
before their command. A non-wrapper command word between a wrapper and a
|
|
309
|
+
later git commit stops the scan, so the wrapper's own payload command is
|
|
310
|
+
read rather than the trailing git token. Segments split on unquoted control
|
|
311
|
+
separators (including
|
|
312
|
+
a lone ``&`` background operator) and newlines, and the git binary may be
|
|
313
|
+
path-prefixed and carry global flags (no-verify, config, and
|
|
314
|
+
working-directory) before its subcommand. A shell interpreter is unwrapped
|
|
315
|
+
at its inline-command flag by interpreter family: bash and sh take an
|
|
316
|
+
isolated ``-c`` or any short-flag cluster carrying ``c`` (``-lc``, ``-cx``),
|
|
317
|
+
and PowerShell (pwsh, powershell) takes only ``-Command`` or ``-c``, so a
|
|
318
|
+
leading pwsh flag whose name merely contains ``c`` (``-ExecutionPolicy``,
|
|
319
|
+
``-NonInteractive``) is stepped over until the real ``-Command`` is reached.
|
|
320
|
+
The inline command's remaining tokens rejoin into one string, so an
|
|
321
|
+
unquoted multi-token command is read whole.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
shell_command: Bash or PowerShell tool command string.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
True when a command segment invokes git with a commit subcommand.
|
|
328
|
+
"""
|
|
329
|
+
if not shell_command or not shell_command.strip():
|
|
330
|
+
return False
|
|
331
|
+
for each_segment in _all_command_segments(shell_command):
|
|
332
|
+
if _segment_invokes_git_commit(each_segment):
|
|
333
|
+
return True
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _following_tokens_working_directory(all_following_tokens: list[str]) -> str | None:
|
|
338
|
+
token_index = 0
|
|
339
|
+
option_with_value_step = GIT_OPTION_WITH_VALUE_STEP
|
|
340
|
+
while token_index < len(all_following_tokens):
|
|
341
|
+
each_token = _strip_token_edge_quotes(all_following_tokens[token_index])
|
|
342
|
+
if each_token == GIT_WORKING_DIRECTORY_OPTION:
|
|
343
|
+
value_index = token_index + 1
|
|
344
|
+
if value_index >= len(all_following_tokens):
|
|
345
|
+
return None
|
|
346
|
+
return _strip_token_edge_quotes(all_following_tokens[value_index])
|
|
347
|
+
option_name = each_token
|
|
348
|
+
has_attached_value = False
|
|
349
|
+
if each_token.startswith(DOUBLE_DASH_OPTION_PREFIX) and "=" in each_token:
|
|
350
|
+
option_name, _, _attached_value = each_token.partition("=")
|
|
351
|
+
has_attached_value = True
|
|
352
|
+
if option_name in ALL_VALUE_TAKING_GIT_OPTIONS:
|
|
353
|
+
token_index += 1 if has_attached_value else option_with_value_step
|
|
354
|
+
continue
|
|
355
|
+
if each_token.startswith(SINGLE_DASH_OPTION_PREFIX):
|
|
356
|
+
token_index += 1
|
|
357
|
+
continue
|
|
358
|
+
return None
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _segment_git_commit_working_directory(
|
|
363
|
+
all_segment_tokens: list[str],
|
|
364
|
+
) -> str | None:
|
|
365
|
+
command_index = _skip_leading_noop_tokens(all_segment_tokens)
|
|
366
|
+
if command_index >= len(all_segment_tokens):
|
|
367
|
+
return None
|
|
368
|
+
command_token = all_segment_tokens[command_index]
|
|
369
|
+
if not _token_is_git_binary(command_token):
|
|
370
|
+
return None
|
|
371
|
+
all_following_tokens = all_segment_tokens[command_index + 1 :]
|
|
372
|
+
return _following_tokens_working_directory(all_following_tokens)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def extract_git_commit_working_directory(shell_command: str) -> str | None:
|
|
376
|
+
"""Return the directory a git-commit command runs in, or None for the CWD.
|
|
377
|
+
|
|
378
|
+
Reads the ``-C`` value off the same commit-invoking segment the detector
|
|
379
|
+
matches, so every git-binary shape the detector recognizes resolves too::
|
|
380
|
+
|
|
381
|
+
git.exe -C /repoA commit -m x -> /repoA
|
|
382
|
+
git -C "C:/repo" commit -m x -> C:/repo
|
|
383
|
+
cd /repoB && git commit -m x -> /repoB (cd/pushd fallback)
|
|
384
|
+
git commit -m x -> None (runs in the CWD)
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
shell_command: Bash or PowerShell tool command string.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
The working directory the commit targets, or None when it uses the CWD.
|
|
391
|
+
"""
|
|
392
|
+
for each_segment in _all_command_segments(shell_command):
|
|
393
|
+
if not _segment_invokes_git_commit(each_segment):
|
|
394
|
+
continue
|
|
395
|
+
segment_directory = _segment_git_commit_working_directory(each_segment)
|
|
396
|
+
if segment_directory is not None:
|
|
397
|
+
return segment_directory
|
|
398
|
+
return extract_git_working_directory(shell_command)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Scan Write/Edit payloads and durable post bodies for high-confidence PII.
|
|
3
|
+
|
|
4
|
+
Reused by ``pii_prevention_blocker``: ``build_deny_reason`` composes the deny
|
|
5
|
+
message, and ``evaluate_write_edit_payload`` / ``evaluate_post_body_texts`` judge
|
|
6
|
+
new file content and durable GitHub post bodies against the pure scanners in
|
|
7
|
+
``pii_scanner``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
16
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
17
|
+
if _blocking_directory not in sys.path:
|
|
18
|
+
sys.path.insert(0, _blocking_directory)
|
|
19
|
+
if _hooks_directory not in sys.path:
|
|
20
|
+
sys.path.insert(0, _hooks_directory)
|
|
21
|
+
|
|
22
|
+
from pii_scanner import ( # noqa: E402
|
|
23
|
+
PiiFinding,
|
|
24
|
+
is_path_exempt_from_pii_scan,
|
|
25
|
+
scan_text_for_pii,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from hooks_constants.multi_edit_reconstruction import edits_for_tool # noqa: E402
|
|
29
|
+
from hooks_constants.pii_prevention_constants import ( # noqa: E402
|
|
30
|
+
ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
31
|
+
CORRECTIVE_MESSAGE_FOOTER,
|
|
32
|
+
CORRECTIVE_MESSAGE_HEADER,
|
|
33
|
+
EDIT_TOOL_NAME,
|
|
34
|
+
FINDING_LINE_TEMPLATE,
|
|
35
|
+
MESSAGE_LINE_SEPARATOR,
|
|
36
|
+
MULTI_EDIT_TOOL_NAME,
|
|
37
|
+
WRITE_TOOL_NAME,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_deny_reason(all_findings: list[PiiFinding], gate_surface: str) -> str:
|
|
42
|
+
"""Return the deny message listing each finding for *gate_surface*.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
all_findings: Findings returned by ``scan_text_for_pii``.
|
|
46
|
+
gate_surface: Human-readable surface (write, post body, staged commit).
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Multi-line deny reason for ``permissionDecisionReason``.
|
|
50
|
+
"""
|
|
51
|
+
all_lines = [
|
|
52
|
+
CORRECTIVE_MESSAGE_HEADER,
|
|
53
|
+
f"Surface: {gate_surface}",
|
|
54
|
+
]
|
|
55
|
+
for each_finding in all_findings:
|
|
56
|
+
all_lines.append(
|
|
57
|
+
FINDING_LINE_TEMPLATE.format(
|
|
58
|
+
category=each_finding.category,
|
|
59
|
+
preview=each_finding.preview,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
all_lines.append(CORRECTIVE_MESSAGE_FOOTER)
|
|
63
|
+
message_line_separator = MESSAGE_LINE_SEPARATOR
|
|
64
|
+
return message_line_separator.join(all_lines)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _collect_write_edit_texts(
|
|
68
|
+
tool_name: str, all_tool_input: dict[str, object]
|
|
69
|
+
) -> tuple[str, list[str]]:
|
|
70
|
+
raw_file_path = all_tool_input.get("file_path", "")
|
|
71
|
+
file_path = raw_file_path if isinstance(raw_file_path, str) else ""
|
|
72
|
+
if is_path_exempt_from_pii_scan(file_path):
|
|
73
|
+
return file_path, []
|
|
74
|
+
if tool_name == WRITE_TOOL_NAME:
|
|
75
|
+
write_content = all_tool_input.get("content", "")
|
|
76
|
+
if isinstance(write_content, str) and write_content:
|
|
77
|
+
return file_path, [write_content]
|
|
78
|
+
return file_path, []
|
|
79
|
+
if tool_name in (EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME):
|
|
80
|
+
all_texts: list[str] = []
|
|
81
|
+
for each_edit in edits_for_tool(tool_name, all_tool_input):
|
|
82
|
+
if not isinstance(each_edit, dict):
|
|
83
|
+
continue
|
|
84
|
+
new_string = each_edit.get("new_string", "")
|
|
85
|
+
if isinstance(new_string, str) and new_string:
|
|
86
|
+
all_texts.append(new_string)
|
|
87
|
+
return file_path, all_texts
|
|
88
|
+
return file_path, []
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _first_findings_in_texts(all_texts: list[str]) -> list[PiiFinding]:
|
|
92
|
+
for each_text in all_texts:
|
|
93
|
+
all_findings = scan_text_for_pii(each_text)
|
|
94
|
+
if all_findings:
|
|
95
|
+
return all_findings
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def evaluate_write_edit_payload(
|
|
100
|
+
tool_name: str, all_tool_input: dict[str, object]
|
|
101
|
+
) -> str | None:
|
|
102
|
+
"""Return a deny reason when Write/Edit/MultiEdit content carries PII.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
tool_name: The intercepted tool name.
|
|
106
|
+
all_tool_input: The tool input mapping.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Deny reason text, or None when the write is clean or out of scope.
|
|
110
|
+
"""
|
|
111
|
+
if tool_name not in ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES:
|
|
112
|
+
return None
|
|
113
|
+
file_path, all_texts = _collect_write_edit_texts(tool_name, all_tool_input)
|
|
114
|
+
all_findings = _first_findings_in_texts(all_texts)
|
|
115
|
+
if not all_findings:
|
|
116
|
+
return None
|
|
117
|
+
gate_surface = f"file write ({file_path or 'unknown path'})"
|
|
118
|
+
return build_deny_reason(all_findings, gate_surface)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def evaluate_post_body_texts(all_body_texts: list[str]) -> str | None:
|
|
122
|
+
"""Return a deny reason when any durable post body carries PII.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
all_body_texts: Body strings extracted from a gh or MCP post tool.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Deny reason text, or None when every body is clean.
|
|
129
|
+
"""
|
|
130
|
+
all_findings = _first_findings_in_texts(all_body_texts)
|
|
131
|
+
if not all_findings:
|
|
132
|
+
return None
|
|
133
|
+
return build_deny_reason(all_findings, "durable GitHub post body")
|