claude-dev-env 8.54.5 → 8.54.7
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/bin/install.test.mjs +4 -0
- package/hooks/blocking/session_title_stop_gate.py +15 -2
- package/hooks/blocking/test_session_title_stop_gate.py +37 -0
- package/hooks/hooks_constants/session_title_constants.py +1 -0
- package/package.json +1 -1
- package/scripts/ci/install-playtest.test.mjs +2 -0
- package/scripts/dev_env_scripts_constants/test_account_broker_constants.py +8 -1
package/bin/install.test.mjs
CHANGED
|
@@ -2986,6 +2986,10 @@ test('the help output states that the installer reads only flags', () => {
|
|
|
2986
2986
|
assert.equal(helpRun.status, 0, helpRun.stderr);
|
|
2987
2987
|
assert.match(helpRun.stdout, /reads only flags/i);
|
|
2988
2988
|
assert.match(helpRun.stdout, /bare path argument carries no meaning/i);
|
|
2989
|
+
assert.match(
|
|
2990
|
+
helpRun.stdout,
|
|
2991
|
+
/--update\s+Full install: copy the package over the prior install, then prune files it no longer ships/,
|
|
2992
|
+
);
|
|
2989
2993
|
});
|
|
2990
2994
|
|
|
2991
2995
|
|
|
@@ -11,7 +11,8 @@ The hook stays silent when:
|
|
|
11
11
|
- the stop is already a retry after a block (stop_hook_active is true);
|
|
12
12
|
- the session shows no title tool: no title tool named anywhere in the
|
|
13
13
|
transcript, and either no remote session id in the environment or a
|
|
14
|
-
deferred tool listing that leaves the title tool out
|
|
14
|
+
deferred tool listing that leaves the title tool out, or an earlier
|
|
15
|
+
Stop block the session never met;
|
|
15
16
|
- the transcript cannot be read.
|
|
16
17
|
"""
|
|
17
18
|
|
|
@@ -32,6 +33,7 @@ from hooks_constants.session_title_constants import (
|
|
|
32
33
|
ALLOW_EXIT_CODE,
|
|
33
34
|
BLOCK_DECISION,
|
|
34
35
|
DEFERRED_TOOL_ATTACHMENT_TYPES,
|
|
36
|
+
HOOK_BLOCKING_ERROR_ATTACHMENT_TYPE,
|
|
35
37
|
REMOTE_SESSION_ENVIRONMENT_VARIABLE,
|
|
36
38
|
REMOTE_TITLE_TOOL_NAME,
|
|
37
39
|
STOP_BLOCK_REASON,
|
|
@@ -85,6 +87,15 @@ def _lists_deferred_tools(all_entry_fields: dict[str, object]) -> bool:
|
|
|
85
87
|
return isinstance(attachment, dict) and attachment.get("type") in DEFERRED_TOOL_ATTACHMENT_TYPES
|
|
86
88
|
|
|
87
89
|
|
|
90
|
+
def _is_stop_block(all_entry_fields: dict[str, object]) -> bool:
|
|
91
|
+
attachment = all_entry_fields.get("attachment")
|
|
92
|
+
return (
|
|
93
|
+
isinstance(attachment, dict)
|
|
94
|
+
and attachment.get("type") == HOOK_BLOCKING_ERROR_ATTACHMENT_TYPE
|
|
95
|
+
and attachment.get("hookEvent") == STOP_GATE_EVENT_NAME
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
88
99
|
def _is_title_call(entry_type: object, all_block_fields: dict[str, object]) -> bool:
|
|
89
100
|
return (
|
|
90
101
|
entry_type == "assistant"
|
|
@@ -132,6 +143,7 @@ def stop_block_reason(all_entries: list[dict[str, object]], is_remote_session: b
|
|
|
132
143
|
prompt -> work, no title call => block reason
|
|
133
144
|
no title tool anywhere, not remote => None
|
|
134
145
|
remote, tool list lacks the title tool => None
|
|
146
|
+
remote, earlier Stop block never met => None
|
|
135
147
|
|
|
136
148
|
Args:
|
|
137
149
|
all_entries: The transcript entries, oldest first.
|
|
@@ -144,7 +156,8 @@ def stop_block_reason(all_entries: list[dict[str, object]], is_remote_session: b
|
|
|
144
156
|
each_name for each_entry in all_entries for each_name in _title_tool_names_in(each_entry)
|
|
145
157
|
]
|
|
146
158
|
if not all_seen_tool_names and (
|
|
147
|
-
not is_remote_session
|
|
159
|
+
not is_remote_session
|
|
160
|
+
or any(_lists_deferred_tools(each) or _is_stop_block(each) for each in all_entries)
|
|
148
161
|
):
|
|
149
162
|
return None
|
|
150
163
|
if _title_set_since(all_entries[_turn_start_index(all_entries) :]):
|
|
@@ -199,6 +199,43 @@ def test_should_block_a_remote_session_whose_tool_list_names_the_title_tool(
|
|
|
199
199
|
assert json.loads(stdout_text)["decision"] == "block"
|
|
200
200
|
|
|
201
201
|
|
|
202
|
+
def earlier_stop_block() -> dict[str, object]:
|
|
203
|
+
return {
|
|
204
|
+
"type": "attachment",
|
|
205
|
+
"attachment": {"type": "hook_blocking_error", "hookEvent": "Stop"},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_should_stay_silent_in_a_remote_session_after_an_unmet_earlier_block(
|
|
210
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
211
|
+
) -> None:
|
|
212
|
+
monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
|
|
213
|
+
all_entries = [
|
|
214
|
+
prompt("Fix the runner"),
|
|
215
|
+
earlier_stop_block(),
|
|
216
|
+
prompt("Next task"),
|
|
217
|
+
tool_call("t1", "Bash"),
|
|
218
|
+
tool_result("t1"),
|
|
219
|
+
]
|
|
220
|
+
assert run_gate(monkeypatch, capsys, tmp_path, all_entries) == (0, "")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_should_block_after_an_earlier_block_once_the_title_tool_is_listed(
|
|
224
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
225
|
+
) -> None:
|
|
226
|
+
monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
|
|
227
|
+
all_entries = [
|
|
228
|
+
deferred_tools(REMOTE_TITLE_TOOL),
|
|
229
|
+
prompt("Fix the runner"),
|
|
230
|
+
earlier_stop_block(),
|
|
231
|
+
prompt("Next task"),
|
|
232
|
+
tool_call("t1", "Bash"),
|
|
233
|
+
tool_result("t1"),
|
|
234
|
+
]
|
|
235
|
+
_, stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries)
|
|
236
|
+
assert json.loads(stdout_text)["decision"] == "block"
|
|
237
|
+
|
|
238
|
+
|
|
202
239
|
def test_should_stay_silent_on_the_retry_after_a_block(
|
|
203
240
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|
|
204
241
|
) -> None:
|
|
@@ -15,6 +15,7 @@ ALLOW_EXIT_CODE = 0
|
|
|
15
15
|
BLOCK_EXIT_CODE = 2
|
|
16
16
|
BLOCK_DECISION = "block"
|
|
17
17
|
DEFERRED_TOOL_ATTACHMENT_TYPES = frozenset({"deferred_tools_delta", "deferred_tools_record"})
|
|
18
|
+
HOOK_BLOCKING_ERROR_ATTACHMENT_TYPE = "hook_blocking_error"
|
|
18
19
|
|
|
19
20
|
STATUS_PREFIX_PATTERN = re.compile("^(\U0001f6a9|\u2705|\u23f3)\ufe0f? (?=\\S)")
|
|
20
21
|
MAXIMUM_NAME_LENGTH = 25
|
package/package.json
CHANGED
|
@@ -30,6 +30,8 @@ test('the driver reads an envelope from every stage of a fresh install, and repo
|
|
|
30
30
|
assert.match(greenRun.stdout, /\[PASS\] playtest install .* sha256=[0-9a-f]{64}/);
|
|
31
31
|
assert.match(greenRun.stdout, /\[PASS\] playtest session_start_hook .* sha256=[0-9a-f]{64}/);
|
|
32
32
|
assert.match(greenRun.stdout, /\[PASS\] playtest blocking_hook .* sha256=[0-9a-f]{64}/);
|
|
33
|
+
const blockingEnvelope = JSON.parse(readFileSync(join(evidenceDirectory, 'blocking_hook.json'), 'utf8'));
|
|
34
|
+
assert.equal(Object.hasOwn(blockingEnvelope, 'permissionDecision'), false);
|
|
33
35
|
assert.ok(
|
|
34
36
|
readFileSync(join(scratchHome, SANDBOX_GIT_CONFIG_NAME), 'utf8').startsWith(SANDBOX_GIT_CONFIG_BODY),
|
|
35
37
|
);
|
|
@@ -16,9 +16,11 @@ import pytest
|
|
|
16
16
|
|
|
17
17
|
from dev_env_scripts_constants import account_broker_constants as broker_constants
|
|
18
18
|
from dev_env_scripts_constants.account_broker_constants import (
|
|
19
|
+
ALL_BATCH_FILE_EXTENSIONS,
|
|
19
20
|
ALL_CLAUDE_FLOORS,
|
|
20
|
-
ALL_PARENT_CLAUDE_SESSION_VARIABLES,
|
|
21
21
|
ALL_CODEX_FLOORS,
|
|
22
|
+
ALL_PARENT_CLAUDE_SESSION_VARIABLES,
|
|
23
|
+
CMD_SHELL_METACHARACTERS,
|
|
22
24
|
JobOutcome,
|
|
23
25
|
)
|
|
24
26
|
from dev_env_scripts_constants.claude_account_constants import (
|
|
@@ -53,6 +55,11 @@ def test_should_keep_codex_floors_tied_to_the_tier_limits() -> None:
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
|
|
58
|
+
def test_batch_command_boundaries_include_shell_metacharacters() -> None:
|
|
59
|
+
assert ALL_BATCH_FILE_EXTENSIONS == frozenset({".bat", ".cmd"})
|
|
60
|
+
assert set(CMD_SHELL_METACHARACTERS) == set('&|<>^%!"\r\n')
|
|
61
|
+
|
|
62
|
+
|
|
56
63
|
def test_should_freeze_job_outcome() -> None:
|
|
57
64
|
outcome = JobOutcome(0, "", "", None, (), "served", None, None)
|
|
58
65
|
with pytest.raises(dataclasses.FrozenInstanceError):
|