claude-dev-env 8.48.0 → 8.48.2
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.
|
@@ -28,6 +28,8 @@ _ENFORCER_SCRIPT = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
|
28
28
|
|
|
29
29
|
_VIOLATING_PRODUCTION_SOURCE = "def process_data(payload: str) -> None:\n print(payload)\n"
|
|
30
30
|
|
|
31
|
+
_DIRECTORY_OUTSIDE_TEMP_ROOTS = Path("/home/user")
|
|
32
|
+
|
|
31
33
|
|
|
32
34
|
def _run_enforcer_cli(
|
|
33
35
|
all_cli_arguments: list[str],
|
|
@@ -74,6 +76,7 @@ def test_should_return_false_for_os_tempfile_gettempdir_root(
|
|
|
74
76
|
"""
|
|
75
77
|
monkeypatch.delenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", raising=False)
|
|
76
78
|
monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
|
|
79
|
+
monkeypatch.setattr(tempfile, "tempdir", str(_DIRECTORY_OUTSIDE_TEMP_ROOTS / "os_temp_root"))
|
|
77
80
|
system_temp_root = tempfile.gettempdir()
|
|
78
81
|
scratch_path = str(Path(system_temp_root) / "scratch_work.py")
|
|
79
82
|
assert is_ephemeral_script_path(scratch_path) is False
|
|
@@ -82,7 +85,6 @@ def test_should_return_false_for_os_tempfile_gettempdir_root(
|
|
|
82
85
|
@pytest.mark.parametrize("env_name", ["TMPDIR", "TEMP", "TMP"])
|
|
83
86
|
def test_should_return_false_for_tmpdir_temp_tmp_env_roots(
|
|
84
87
|
env_name: str,
|
|
85
|
-
tmp_path: Path,
|
|
86
88
|
monkeypatch: pytest.MonkeyPatch,
|
|
87
89
|
) -> None:
|
|
88
90
|
"""B3: $TMPDIR / $TEMP / $TMP are not ephemeral sources.
|
|
@@ -92,8 +94,9 @@ def test_should_return_false_for_tmpdir_temp_tmp_env_roots(
|
|
|
92
94
|
"""
|
|
93
95
|
monkeypatch.delenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", raising=False)
|
|
94
96
|
monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
env_root = _DIRECTORY_OUTSIDE_TEMP_ROOTS / "env_root"
|
|
98
|
+
monkeypatch.setenv(env_name, str(env_root))
|
|
99
|
+
scratch_path = str(env_root / "scratch.py")
|
|
97
100
|
assert is_ephemeral_script_path(scratch_path) is False
|
|
98
101
|
|
|
99
102
|
|
|
@@ -111,7 +114,8 @@ def test_should_return_false_for_pytest_tmp_path_when_job_dir_elsewhere(
|
|
|
111
114
|
"""
|
|
112
115
|
monkeypatch.delenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", raising=False)
|
|
113
116
|
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path / "elsewhere"))
|
|
114
|
-
|
|
117
|
+
monkeypatch.setattr(tempfile, "tempdir", str(_DIRECTORY_OUTSIDE_TEMP_ROOTS / "os_temp_root"))
|
|
118
|
+
pytest_sandbox_target = str(Path(tempfile.gettempdir()) / "pytest-of-user" / "candidate.py")
|
|
115
119
|
assert is_ephemeral_script_path(pytest_sandbox_target) is False
|
|
116
120
|
|
|
117
121
|
|
package/package.json
CHANGED
|
@@ -25,12 +25,14 @@ import codex_account_meters
|
|
|
25
25
|
from claude_account_profile import default_profile_home, validate_profile_name
|
|
26
26
|
from claude_chain_usage import WeeklyUtilizationProbeError, probe_account_meters
|
|
27
27
|
from dev_env_scripts_constants.account_broker_constants import (
|
|
28
|
+
ALL_BATCH_FILE_EXTENSIONS,
|
|
28
29
|
BROKER_STATE_DIRECTORY_NAME,
|
|
29
30
|
BROKER_STATE_FILE_NAME,
|
|
30
31
|
BROKER_STATE_LOCK_SUFFIX,
|
|
31
32
|
BROKER_STATE_TEMP_SUFFIX,
|
|
32
33
|
Account,
|
|
33
34
|
BrokerConfigurationError,
|
|
35
|
+
CMD_SHELL_METACHARACTERS,
|
|
34
36
|
Decision,
|
|
35
37
|
JobOutcome,
|
|
36
38
|
Meters,
|
|
@@ -434,7 +436,29 @@ def _resolve_command(all_argv: Sequence[str]) -> list[str]:
|
|
|
434
436
|
resolved_command = shutil.which(command_name)
|
|
435
437
|
if resolved_command is None and not os.path.dirname(command_name):
|
|
436
438
|
raise FileNotFoundError(errno.ENOENT, "command not found on PATH", command_name)
|
|
437
|
-
|
|
439
|
+
launched_command = resolved_command or command_name
|
|
440
|
+
_refuse_batch_file_shell_metacharacters(launched_command, all_arguments)
|
|
441
|
+
return [launched_command, *all_arguments]
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _refuse_batch_file_shell_metacharacters(launched_command: str, all_arguments: Sequence[str]) -> None:
|
|
445
|
+
if os.path.splitext(launched_command)[1].casefold() not in ALL_BATCH_FILE_EXTENSIONS:
|
|
446
|
+
return
|
|
447
|
+
shell_parsed_argument = next(
|
|
448
|
+
(
|
|
449
|
+
each_argument
|
|
450
|
+
for each_argument in all_arguments
|
|
451
|
+
if any(each_character in CMD_SHELL_METACHARACTERS for each_character in each_argument)
|
|
452
|
+
),
|
|
453
|
+
None,
|
|
454
|
+
)
|
|
455
|
+
if shell_parsed_argument is None:
|
|
456
|
+
return
|
|
457
|
+
raise OSError(
|
|
458
|
+
errno.EINVAL,
|
|
459
|
+
f"cmd.exe would parse the batch file argument {shell_parsed_argument!r}; send that text on stdin",
|
|
460
|
+
launched_command,
|
|
461
|
+
)
|
|
438
462
|
|
|
439
463
|
|
|
440
464
|
def _run_captured_subprocess(all_argv: Sequence[str], **options: object) -> subprocess.CompletedProcess[str]:
|
|
@@ -58,6 +58,8 @@ BROKER_STATE_FILE_NAME = "state.json"
|
|
|
58
58
|
BROKER_STATE_TEMP_SUFFIX = ".json"
|
|
59
59
|
BROKER_STATE_LOCK_SUFFIX = ".lock"
|
|
60
60
|
SECONDS_PER_HOUR = 3600
|
|
61
|
+
ALL_BATCH_FILE_EXTENSIONS = frozenset({".bat", ".cmd"})
|
|
62
|
+
CMD_SHELL_METACHARACTERS = "&|<>^%!\"\r\n"
|
|
61
63
|
ALL_PARENT_CLAUDE_SESSION_VARIABLES: frozenset[str] = frozenset({
|
|
62
64
|
"CLAUDE_CODE_SESSION_ID",
|
|
63
65
|
"CLAUDE_CODE_REMOTE",
|
|
@@ -190,3 +190,62 @@ def test_should_refuse_to_launch_a_bare_command_missing_from_path(monkeypatch: p
|
|
|
190
190
|
|
|
191
191
|
assert raised.value.errno == errno.ENOENT
|
|
192
192
|
assert raised.value.filename == "claude"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@pytest.mark.parametrize("metacharacter", ["&", "|", "<", ">", "^", "%", "!", '"', "\n", "\r"])
|
|
196
|
+
def test_should_refuse_a_batch_file_argument_that_cmd_would_parse(
|
|
197
|
+
monkeypatch: pytest.MonkeyPatch, metacharacter: str
|
|
198
|
+
) -> None:
|
|
199
|
+
resolved_command = r"C:\Users\someone\AppData\Roaming\npm\claude.cmd"
|
|
200
|
+
monkeypatch.setattr(shutil, "which", lambda name: resolved_command)
|
|
201
|
+
monkeypatch.setattr(support.subprocess, "run", lambda *arguments, **options: pytest.fail("a batch file ran"))
|
|
202
|
+
|
|
203
|
+
with pytest.raises(OSError) as raised:
|
|
204
|
+
support.subprocess_runner(["claude", "-p", f"fix{metacharacter}test"])
|
|
205
|
+
|
|
206
|
+
assert raised.value.errno == errno.EINVAL
|
|
207
|
+
assert raised.value.filename == resolved_command
|
|
208
|
+
assert "stdin" in raised.value.strerror
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_should_refuse_metacharacters_for_an_uppercase_bat_extension(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
212
|
+
resolved_command = r"C:\tools\CLAUDE.BAT"
|
|
213
|
+
monkeypatch.setattr(shutil, "which", lambda name: resolved_command)
|
|
214
|
+
monkeypatch.setattr(support.subprocess, "run", lambda *arguments, **options: pytest.fail("a batch file ran"))
|
|
215
|
+
|
|
216
|
+
with pytest.raises(OSError) as raised:
|
|
217
|
+
support.subprocess_runner(["claude", "a & b"])
|
|
218
|
+
|
|
219
|
+
assert raised.value.errno == errno.EINVAL
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_should_launch_a_batch_file_with_flags_only(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
223
|
+
resolved_command = r"C:\Users\someone\AppData\Roaming\npm\claude.cmd"
|
|
224
|
+
monkeypatch.setattr(shutil, "which", lambda name: resolved_command)
|
|
225
|
+
all_launched_argv: list[list[str]] = []
|
|
226
|
+
|
|
227
|
+
def fake_run(all_argv: list[str], **options: object) -> subprocess.CompletedProcess[bytes]:
|
|
228
|
+
all_launched_argv.append(list(all_argv))
|
|
229
|
+
return subprocess.CompletedProcess(all_argv, 0)
|
|
230
|
+
|
|
231
|
+
monkeypatch.setattr(support.subprocess, "run", fake_run)
|
|
232
|
+
|
|
233
|
+
support.subprocess_runner(["claude", "-p", "--output-format", "json", "--model", "opus"], input=b"a & b")
|
|
234
|
+
|
|
235
|
+
assert all_launched_argv == [[resolved_command, "-p", "--output-format", "json", "--model", "opus"]]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_should_pass_metacharacters_to_an_executable_unchanged(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
239
|
+
resolved_command = "/usr/local/bin/claude"
|
|
240
|
+
monkeypatch.setattr(shutil, "which", lambda name: resolved_command)
|
|
241
|
+
all_launched_argv: list[list[str]] = []
|
|
242
|
+
|
|
243
|
+
def fake_run(all_argv: list[str], **options: object) -> subprocess.CompletedProcess[bytes]:
|
|
244
|
+
all_launched_argv.append(list(all_argv))
|
|
245
|
+
return subprocess.CompletedProcess(all_argv, 0)
|
|
246
|
+
|
|
247
|
+
monkeypatch.setattr(support.subprocess, "run", fake_run)
|
|
248
|
+
|
|
249
|
+
support.subprocess_runner(["claude", "-p", 'say "a & b" | 100%!'])
|
|
250
|
+
|
|
251
|
+
assert all_launched_argv == [[resolved_command, "-p", 'say "a & b" | 100%!']]
|