claude-dev-env 2.3.0 → 2.4.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/CLAUDE.md +3 -2
- package/agents/CLAUDE.md +1 -0
- package/agents/code-verifier.md +3 -3
- package/agents/skill-writer-agent.md +84 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/docs/CLAUDE.md +1 -0
- package/docs/references/CLAUDE.md +1 -0
- package/docs/references/code-review-enforcement.md +97 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +8 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +194 -0
- package/hooks/blocking/code_review_push_gate.py +140 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
- package/hooks/blocking/config/verified_commit_constants.py +13 -9
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
- package/hooks/blocking/test_code_review_push_gate.py +189 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
- package/hooks/blocking/test_verification_verdict_store.py +920 -903
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verification_verdict_store.py +27 -5
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +103 -0
- package/hooks/hooks.json +16 -1
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +211 -23
- package/hooks/validators/run_all_validators.py +21 -14
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_ruff_integration.py +68 -1
- package/hooks/validators/test_run_all_validators.py +25 -0
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +20 -43
- package/rules/durable-post-artifacts.md +7 -0
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +8 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +44 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +8 -1
- package/scripts/test_invoke_code_review.py +298 -4
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
- package/skills/autoconverge/workflow/converge.mjs +24 -8
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/team-advisor/SKILL.md +2 -2
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Prove both sandbox safety hooks block their probe payloads before trusting them.
|
|
3
|
+
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
python probe_sandbox_safety.py --settings sandbox-settings.json
|
|
7
|
+
exit 0 -> pii_prevention_blocker and destructive_command_blocker both blocked
|
|
8
|
+
exit 3 -> one hook allowed its probe payload or errored
|
|
9
|
+
|
|
10
|
+
The probe reads each safety hook command from the settings, feeds it a
|
|
11
|
+
PreToolUse payload the hook's own tests prove it blocks, and confirms the
|
|
12
|
+
hook returns a hard-deny decision. It runs each hook under the settings'
|
|
13
|
+
``env`` block, so the destructive gate runs in deny mode and its block holds
|
|
14
|
+
the way it does for the launched session. It runs the hook scripts directly,
|
|
15
|
+
so it confirms each script is present, imports its constants package, and
|
|
16
|
+
blocks its probe payload — it does not exercise Claude Code's matcher dispatch.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
import shlex
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from build_sandbox_settings import read_settings_document
|
|
31
|
+
from prototype_scripts_constants.config.build_sandbox_settings_constants import (
|
|
32
|
+
ALL_SAFETY_HOOK_SCRIPT_BASENAMES,
|
|
33
|
+
COMMAND_KEY,
|
|
34
|
+
ENV_KEY,
|
|
35
|
+
HOOKS_KEY,
|
|
36
|
+
MATCHER_KEY,
|
|
37
|
+
PRE_TOOL_USE_KEY,
|
|
38
|
+
)
|
|
39
|
+
from prototype_scripts_constants.config.probe_sandbox_safety_constants import (
|
|
40
|
+
ALL_DESTRUCTIVE_PROBE_COMMAND_TOKENS,
|
|
41
|
+
COMMAND_TOKEN_JOIN_SEPARATOR,
|
|
42
|
+
DESTRUCTIVE_PROBE_TOOL_NAME,
|
|
43
|
+
ENVELOPE_HOOK_EVENT_NAME_KEY,
|
|
44
|
+
ENVELOPE_PRE_TOOL_USE_EVENT_NAME,
|
|
45
|
+
ENVELOPE_PROBE_SESSION_ID,
|
|
46
|
+
ENVELOPE_SESSION_ID_KEY,
|
|
47
|
+
ENVELOPE_TOOL_INPUT_KEY,
|
|
48
|
+
ENVELOPE_TOOL_NAME_KEY,
|
|
49
|
+
HARD_DENY_DECISION,
|
|
50
|
+
HOOK_SPECIFIC_REPLY_KEY,
|
|
51
|
+
MATCHER_JOIN_SEPARATOR,
|
|
52
|
+
PERMISSION_DECISION_KEY,
|
|
53
|
+
PII_PROBE_CONTENT_TEMPLATE,
|
|
54
|
+
PII_PROBE_FILE_PATH,
|
|
55
|
+
PII_PROBE_TOKEN_BODY_CHARACTER,
|
|
56
|
+
PII_PROBE_TOKEN_BODY_LENGTH,
|
|
57
|
+
PII_PROBE_TOKEN_PREFIX,
|
|
58
|
+
PII_PROBE_TOOL_NAME,
|
|
59
|
+
PROBE_FAILURE_EXIT_CODE,
|
|
60
|
+
PROBE_HOOK_TIMEOUT_SECONDS,
|
|
61
|
+
PROBE_SUCCESS_EXIT_CODE,
|
|
62
|
+
TOOL_INPUT_COMMAND_KEY,
|
|
63
|
+
TOOL_INPUT_CONTENT_KEY,
|
|
64
|
+
TOOL_INPUT_FILE_PATH_KEY,
|
|
65
|
+
)
|
|
66
|
+
from prototype_scripts_constants.config.prototype_common_constants import LOGGING_FORMAT
|
|
67
|
+
|
|
68
|
+
logger = logging.getLogger("probe_sandbox_safety")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _build_pii_probe_payload() -> dict:
|
|
72
|
+
synthetic_secret = (
|
|
73
|
+
PII_PROBE_TOKEN_PREFIX
|
|
74
|
+
+ PII_PROBE_TOKEN_BODY_CHARACTER * PII_PROBE_TOKEN_BODY_LENGTH
|
|
75
|
+
)
|
|
76
|
+
probe_content = PII_PROBE_CONTENT_TEMPLATE.format(secret=synthetic_secret)
|
|
77
|
+
return {
|
|
78
|
+
ENVELOPE_SESSION_ID_KEY: ENVELOPE_PROBE_SESSION_ID,
|
|
79
|
+
ENVELOPE_HOOK_EVENT_NAME_KEY: ENVELOPE_PRE_TOOL_USE_EVENT_NAME,
|
|
80
|
+
ENVELOPE_TOOL_NAME_KEY: PII_PROBE_TOOL_NAME,
|
|
81
|
+
ENVELOPE_TOOL_INPUT_KEY: {
|
|
82
|
+
TOOL_INPUT_FILE_PATH_KEY: PII_PROBE_FILE_PATH,
|
|
83
|
+
TOOL_INPUT_CONTENT_KEY: probe_content,
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _build_destructive_probe_payload() -> dict:
|
|
89
|
+
destructive_command = COMMAND_TOKEN_JOIN_SEPARATOR.join(
|
|
90
|
+
ALL_DESTRUCTIVE_PROBE_COMMAND_TOKENS
|
|
91
|
+
)
|
|
92
|
+
return {
|
|
93
|
+
ENVELOPE_SESSION_ID_KEY: ENVELOPE_PROBE_SESSION_ID,
|
|
94
|
+
ENVELOPE_HOOK_EVENT_NAME_KEY: ENVELOPE_PRE_TOOL_USE_EVENT_NAME,
|
|
95
|
+
ENVELOPE_TOOL_NAME_KEY: DESTRUCTIVE_PROBE_TOOL_NAME,
|
|
96
|
+
ENVELOPE_TOOL_INPUT_KEY: {TOOL_INPUT_COMMAND_KEY: destructive_command},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def build_probe_payload_for_basename(basename: str) -> dict:
|
|
101
|
+
"""Build the PreToolUse probe payload a safety hook is proven to block.
|
|
102
|
+
|
|
103
|
+
::
|
|
104
|
+
|
|
105
|
+
"pii_prevention_blocker.py" -> Write payload carrying a secret
|
|
106
|
+
"destructive_command_blocker.py" -> Bash payload carrying rm -rf
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
basename: the safety hook script basename to build a payload for.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
The PreToolUse payload the named hook blocks.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ValueError: when the basename names no known safety hook.
|
|
116
|
+
"""
|
|
117
|
+
if basename == ALL_SAFETY_HOOK_SCRIPT_BASENAMES[0]:
|
|
118
|
+
return _build_pii_probe_payload()
|
|
119
|
+
if basename == ALL_SAFETY_HOOK_SCRIPT_BASENAMES[1]:
|
|
120
|
+
return _build_destructive_probe_payload()
|
|
121
|
+
raise ValueError(f"no probe payload for hook basename: {basename}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _command_in_block_for_basename(hook_block: dict, basename: str) -> str | None:
|
|
125
|
+
for each_subhook in hook_block.get(HOOKS_KEY, []):
|
|
126
|
+
command = each_subhook.get(COMMAND_KEY, "")
|
|
127
|
+
if basename in command:
|
|
128
|
+
return command
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def find_hook_command_for_basename(
|
|
133
|
+
settings_document: dict, basename: str
|
|
134
|
+
) -> str | None:
|
|
135
|
+
"""Find the first hook command in the settings that runs the named script.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
settings_document: the minimal sandbox settings document.
|
|
139
|
+
basename: the safety hook script basename to look for.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The command string that runs the named script, or None when no
|
|
143
|
+
hook command names it.
|
|
144
|
+
"""
|
|
145
|
+
pre_tool_use_blocks = settings_document.get(HOOKS_KEY, {}).get(PRE_TOOL_USE_KEY, [])
|
|
146
|
+
for each_block in pre_tool_use_blocks:
|
|
147
|
+
command = _command_in_block_for_basename(each_block, basename)
|
|
148
|
+
if command is not None:
|
|
149
|
+
return command
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def settings_environment_overrides(settings_document: dict) -> dict[str, str]:
|
|
154
|
+
"""Read the env-block overrides the settings apply to each hook subprocess.
|
|
155
|
+
|
|
156
|
+
::
|
|
157
|
+
|
|
158
|
+
{"env": {deny-mode override}} -> that mapping
|
|
159
|
+
settings with no env block -> {}
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
settings_document: the minimal sandbox settings document.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
The env-block mapping keyed by variable name, or an empty mapping when
|
|
166
|
+
the settings carry no env block.
|
|
167
|
+
"""
|
|
168
|
+
return settings_document.get(ENV_KEY, {})
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _find_matchers_for_basename(settings_document: dict, basename: str) -> list[str]:
|
|
172
|
+
pre_tool_use_blocks = settings_document.get(HOOKS_KEY, {}).get(PRE_TOOL_USE_KEY, [])
|
|
173
|
+
all_matchers = []
|
|
174
|
+
for each_block in pre_tool_use_blocks:
|
|
175
|
+
names_basename = any(
|
|
176
|
+
basename in each_subhook.get(COMMAND_KEY, "")
|
|
177
|
+
for each_subhook in each_block.get(HOOKS_KEY, [])
|
|
178
|
+
)
|
|
179
|
+
if names_basename:
|
|
180
|
+
all_matchers.append(str(each_block.get(MATCHER_KEY)))
|
|
181
|
+
return all_matchers
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def parse_command_argv(command: str) -> list[str]:
|
|
185
|
+
"""Split a hook command string into an argument vector, quotes removed.
|
|
186
|
+
|
|
187
|
+
::
|
|
188
|
+
|
|
189
|
+
'"py" "a b.py"' -> ["py", "a b.py"]
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
command: the hook command string from the settings.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The argument vector with any surrounding quotes stripped.
|
|
196
|
+
"""
|
|
197
|
+
all_tokens = shlex.split(command, posix=False)
|
|
198
|
+
return [each_token.strip('"').strip("'") for each_token in all_tokens]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _run_hook_and_parse_reply(
|
|
202
|
+
all_command_tokens: list[str],
|
|
203
|
+
probe_payload: dict,
|
|
204
|
+
override_by_name: dict[str, str],
|
|
205
|
+
) -> dict | None:
|
|
206
|
+
child_environment = {**os.environ, **override_by_name}
|
|
207
|
+
try:
|
|
208
|
+
completed_process = subprocess.run(
|
|
209
|
+
all_command_tokens,
|
|
210
|
+
input=json.dumps(probe_payload),
|
|
211
|
+
text=True,
|
|
212
|
+
capture_output=True,
|
|
213
|
+
timeout=PROBE_HOOK_TIMEOUT_SECONDS,
|
|
214
|
+
check=False,
|
|
215
|
+
env=child_environment,
|
|
216
|
+
)
|
|
217
|
+
except (OSError, subprocess.SubprocessError):
|
|
218
|
+
return None
|
|
219
|
+
if not completed_process.stdout.strip():
|
|
220
|
+
return None
|
|
221
|
+
try:
|
|
222
|
+
return json.loads(completed_process.stdout)
|
|
223
|
+
except json.JSONDecodeError:
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def hook_blocks_probe(
|
|
228
|
+
all_command_tokens: list[str],
|
|
229
|
+
probe_payload: dict,
|
|
230
|
+
override_by_name: dict[str, str],
|
|
231
|
+
) -> bool:
|
|
232
|
+
"""Run a hook against its probe payload and report whether it blocks.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
all_command_tokens: the hook argument vector to run.
|
|
236
|
+
probe_payload: the PreToolUse payload fed to the hook on stdin.
|
|
237
|
+
override_by_name: env values keyed by variable name that the settings
|
|
238
|
+
apply to the hook, overlaid on the current environment first.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
True when the hook emits a hard-deny decision, False when it asks,
|
|
242
|
+
allows, emits no decision, or errors.
|
|
243
|
+
"""
|
|
244
|
+
parsed_hook_reply = _run_hook_and_parse_reply(
|
|
245
|
+
all_command_tokens, probe_payload, override_by_name
|
|
246
|
+
)
|
|
247
|
+
if parsed_hook_reply is None:
|
|
248
|
+
return False
|
|
249
|
+
decision = parsed_hook_reply.get(HOOK_SPECIFIC_REPLY_KEY, {}).get(
|
|
250
|
+
PERMISSION_DECISION_KEY
|
|
251
|
+
)
|
|
252
|
+
return decision == HARD_DENY_DECISION
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def probe_safety_hook(settings_document: dict, basename: str) -> bool:
|
|
256
|
+
"""Probe one safety hook from the settings and report whether it blocks.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
settings_document: the minimal sandbox settings document.
|
|
260
|
+
basename: the safety hook script basename to probe.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
True when the hook blocks its probe payload, False otherwise.
|
|
264
|
+
"""
|
|
265
|
+
command = find_hook_command_for_basename(settings_document, basename)
|
|
266
|
+
if command is None:
|
|
267
|
+
logger.error("no hook command in settings runs %s", basename)
|
|
268
|
+
return False
|
|
269
|
+
all_matchers = _find_matchers_for_basename(settings_document, basename)
|
|
270
|
+
logger.info("%s matchers: %s", basename, MATCHER_JOIN_SEPARATOR.join(all_matchers))
|
|
271
|
+
all_command_tokens = parse_command_argv(command)
|
|
272
|
+
probe_payload = build_probe_payload_for_basename(basename)
|
|
273
|
+
override_by_name = settings_environment_overrides(settings_document)
|
|
274
|
+
return hook_blocks_probe(all_command_tokens, probe_payload, override_by_name)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _parse_arguments(all_arguments: list[str] | None) -> argparse.Namespace:
|
|
278
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
279
|
+
parser.add_argument("--settings", required=True, help="the sandbox settings file")
|
|
280
|
+
return parser.parse_args(all_arguments)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def main(all_arguments: list[str] | None = None) -> int:
|
|
284
|
+
"""Probe both safety hooks and report whether the sandbox is contained.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
all_arguments: the command-line arguments, or None to read sys.argv.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
0 when both safety hooks block their probe payloads, 3 when either
|
|
291
|
+
allows, errors, or the settings cannot be read.
|
|
292
|
+
"""
|
|
293
|
+
logging.basicConfig(level=logging.INFO, format=LOGGING_FORMAT)
|
|
294
|
+
arguments = _parse_arguments(all_arguments)
|
|
295
|
+
settings_path = Path(arguments.settings).expanduser()
|
|
296
|
+
try:
|
|
297
|
+
settings_document = read_settings_document(settings_path)
|
|
298
|
+
except (OSError, json.JSONDecodeError) as read_error:
|
|
299
|
+
logger.error("cannot read settings %s: %s", settings_path, read_error)
|
|
300
|
+
return PROBE_FAILURE_EXIT_CODE
|
|
301
|
+
all_hook_block_outcomes = [
|
|
302
|
+
probe_safety_hook(settings_document, each_basename)
|
|
303
|
+
for each_basename in ALL_SAFETY_HOOK_SCRIPT_BASENAMES
|
|
304
|
+
]
|
|
305
|
+
if all(all_hook_block_outcomes):
|
|
306
|
+
return PROBE_SUCCESS_EXIT_CODE
|
|
307
|
+
return PROBE_FAILURE_EXIT_CODE
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
if __name__ == "__main__":
|
|
311
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Named constants for the prototype skill scripts."""
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Constants for the sandbox safety-settings builder.
|
|
2
|
+
|
|
3
|
+
Groups: the two safety hook script basenames the sandbox keeps, the matchers
|
|
4
|
+
each safety hook must cover, the mapping from basename to those matchers, the
|
|
5
|
+
settings JSON key names, the env-block key with the deny-mode env variable and
|
|
6
|
+
value, the default live settings source, the pretty-print indent, the join
|
|
7
|
+
separator for the missing-basename message, and the exit codes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
ALL_SAFETY_HOOK_SCRIPT_BASENAMES = (
|
|
13
|
+
"pii_prevention_blocker.py",
|
|
14
|
+
"destructive_command_blocker.py",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
ALL_PII_REQUIRED_MATCHERS = ("Write", "Edit", "MultiEdit", "Bash")
|
|
18
|
+
ALL_DESTRUCTIVE_REQUIRED_MATCHERS = ("Bash",)
|
|
19
|
+
|
|
20
|
+
ALL_REQUIRED_MATCHERS_BY_SAFETY_BASENAME = {
|
|
21
|
+
ALL_SAFETY_HOOK_SCRIPT_BASENAMES[0]: ALL_PII_REQUIRED_MATCHERS,
|
|
22
|
+
ALL_SAFETY_HOOK_SCRIPT_BASENAMES[1]: ALL_DESTRUCTIVE_REQUIRED_MATCHERS,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
HOOKS_KEY = "hooks"
|
|
26
|
+
PRE_TOOL_USE_KEY = "PreToolUse"
|
|
27
|
+
MATCHER_KEY = "matcher"
|
|
28
|
+
COMMAND_KEY = "command"
|
|
29
|
+
ENV_KEY = "env"
|
|
30
|
+
|
|
31
|
+
DESTRUCTIVE_DENY_MODE_ENV_VAR = "CLAUDE_DESTRUCTIVE_DENY_MODE"
|
|
32
|
+
DESTRUCTIVE_DENY_MODE_ENV_VALUE = "1"
|
|
33
|
+
|
|
34
|
+
DEFAULT_SETTINGS_SOURCE = "~/.claude/settings.json"
|
|
35
|
+
JSON_INDENT_SPACES = 2
|
|
36
|
+
|
|
37
|
+
MISSING_BASENAMES_JOIN_SEPARATOR = ", "
|
|
38
|
+
|
|
39
|
+
BUILD_SUCCESS_EXIT_CODE = 0
|
|
40
|
+
SETTINGS_MISSING_SAFETY_HOOK_EXIT_CODE = 2
|
|
41
|
+
SETTINGS_SOURCE_UNREADABLE_EXIT_CODE = 2
|
package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Constants for the hookless sandbox launcher.
|
|
2
|
+
|
|
3
|
+
Groups: the headless claude executable name and its flag tokens, the default
|
|
4
|
+
session timeout, the JSON summary key names, the missing-path exit code, and
|
|
5
|
+
the wall-clock timeout exit code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
CLAUDE_EXECUTABLE_NAME = "claude"
|
|
11
|
+
PROMPT_FLAG = "-p"
|
|
12
|
+
BARE_FLAG = "--bare"
|
|
13
|
+
SKIP_PERMISSIONS_FLAG = "--dangerously-skip-permissions"
|
|
14
|
+
SETTINGS_FLAG = "--settings"
|
|
15
|
+
|
|
16
|
+
DEFAULT_TIMEOUT_SECONDS = 3600
|
|
17
|
+
|
|
18
|
+
SUMMARY_KEY_WORKTREE = "worktree"
|
|
19
|
+
SUMMARY_KEY_SETTINGS = "settings"
|
|
20
|
+
SUMMARY_KEY_EXIT_CODE = "exit_code"
|
|
21
|
+
|
|
22
|
+
LAUNCH_MISSING_PATH_EXIT_CODE = 2
|
|
23
|
+
LAUNCH_TIMEOUT_EXIT_CODE = 124
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Constants for the sandbox safety-hook probe.
|
|
2
|
+
|
|
3
|
+
Groups: the PreToolUse envelope keys and their probe payloads, the tool-input
|
|
4
|
+
keys, the hook decision keys and the block decisions, the join separators, the
|
|
5
|
+
destructive and personal-data probe payloads, the probe hook timeout, and the
|
|
6
|
+
exit codes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
ENVELOPE_SESSION_ID_KEY = "session_id"
|
|
12
|
+
ENVELOPE_PROBE_SESSION_ID = "probe"
|
|
13
|
+
ENVELOPE_HOOK_EVENT_NAME_KEY = "hook_event_name"
|
|
14
|
+
ENVELOPE_PRE_TOOL_USE_EVENT_NAME = "PreToolUse"
|
|
15
|
+
ENVELOPE_TOOL_NAME_KEY = "tool_name"
|
|
16
|
+
ENVELOPE_TOOL_INPUT_KEY = "tool_input"
|
|
17
|
+
|
|
18
|
+
TOOL_INPUT_COMMAND_KEY = "command"
|
|
19
|
+
TOOL_INPUT_FILE_PATH_KEY = "file_path"
|
|
20
|
+
TOOL_INPUT_CONTENT_KEY = "content"
|
|
21
|
+
|
|
22
|
+
HOOK_SPECIFIC_REPLY_KEY = "hookSpecificOutput"
|
|
23
|
+
PERMISSION_DECISION_KEY = "permissionDecision"
|
|
24
|
+
HARD_DENY_DECISION = "deny"
|
|
25
|
+
|
|
26
|
+
MATCHER_JOIN_SEPARATOR = ", "
|
|
27
|
+
COMMAND_TOKEN_JOIN_SEPARATOR = " "
|
|
28
|
+
|
|
29
|
+
DESTRUCTIVE_PROBE_TOOL_NAME = "Bash"
|
|
30
|
+
ALL_DESTRUCTIVE_PROBE_COMMAND_TOKENS = ("rm", "-rf", "/var/log/myapp")
|
|
31
|
+
DESTRUCTIVE_PROBE_COMMAND = COMMAND_TOKEN_JOIN_SEPARATOR.join(
|
|
32
|
+
ALL_DESTRUCTIVE_PROBE_COMMAND_TOKENS
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
PII_PROBE_TOOL_NAME = "Write"
|
|
36
|
+
PII_PROBE_FILE_PATH = "src/config.env.example.md"
|
|
37
|
+
PII_PROBE_TOKEN_PREFIX = "ghp" + "_"
|
|
38
|
+
PII_PROBE_TOKEN_BODY_CHARACTER = "C"
|
|
39
|
+
PII_PROBE_TOKEN_BODY_LENGTH = 36
|
|
40
|
+
PII_PROBE_CONTENT_TEMPLATE = "TOKEN={secret}\n"
|
|
41
|
+
|
|
42
|
+
PROBE_HOOK_TIMEOUT_SECONDS = 30
|
|
43
|
+
|
|
44
|
+
PROBE_SUCCESS_EXIT_CODE = 0
|
|
45
|
+
PROBE_FAILURE_EXIT_CODE = 3
|
package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Constants shared by every prototype skill script.
|
|
2
|
+
|
|
3
|
+
Groups: the log line format the scripts configure, and the text encoding
|
|
4
|
+
for reading the settings source and the sandbox task file.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
LOGGING_FORMAT = "%(levelname)s %(name)s: %(message)s"
|
|
10
|
+
TEXT_ENCODING_UTF8 = "utf-8"
|