claude-dev-env 8.46.5 → 8.46.6
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.
|
@@ -13,6 +13,8 @@ if hooks_directory not in sys.path:
|
|
|
13
13
|
sys.path.insert(0, hooks_directory)
|
|
14
14
|
|
|
15
15
|
from hooks_constants.pr_lifecycle_skill_gate_constants import (
|
|
16
|
+
AGENT_ID_FIELD,
|
|
17
|
+
AGENT_ID_PATTERN,
|
|
16
18
|
ALL_API_ACTION_NAMES,
|
|
17
19
|
ALL_COMMAND_PREFIX_WORDS,
|
|
18
20
|
ALL_GITHUB_MCP_TOOL_SUFFIXES,
|
|
@@ -40,8 +42,11 @@ from hooks_constants.pr_lifecycle_skill_gate_constants import (
|
|
|
40
42
|
PERMISSION_DECISION_REASON_KEY,
|
|
41
43
|
PULL_REQUEST_SCRIPT_NAME,
|
|
42
44
|
PYTHON_OPTIONS_WITH_VALUE,
|
|
45
|
+
SESSION_TRANSCRIPT_PATH_FIELD,
|
|
43
46
|
SHELL_TOOL_NAMES,
|
|
44
47
|
SKILL_NAME,
|
|
48
|
+
SUBAGENT_TRANSCRIPT_DIRECTORY_NAME,
|
|
49
|
+
SUBAGENT_TRANSCRIPT_FILE_TEMPLATE,
|
|
45
50
|
)
|
|
46
51
|
from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
|
|
47
52
|
from hooks_constants.setup_project_paths_constants import DECODE_ERRORS_POLICY, UTF8_ENCODING
|
|
@@ -174,6 +179,21 @@ def _transcript_load_status(path: str) -> bool | None:
|
|
|
174
179
|
return None
|
|
175
180
|
|
|
176
181
|
|
|
182
|
+
def _subagent_transcript_path(all_payload_fields: dict[str, object]) -> str | None:
|
|
183
|
+
session_path = all_payload_fields.get(SESSION_TRANSCRIPT_PATH_FIELD)
|
|
184
|
+
agent_id = all_payload_fields.get(AGENT_ID_FIELD)
|
|
185
|
+
if not isinstance(session_path, str) or not session_path:
|
|
186
|
+
return None
|
|
187
|
+
if not isinstance(agent_id, str) or not re.fullmatch(AGENT_ID_PATTERN, agent_id):
|
|
188
|
+
return None
|
|
189
|
+
subagent_path = (
|
|
190
|
+
Path(session_path).with_suffix("")
|
|
191
|
+
/ SUBAGENT_TRANSCRIPT_DIRECTORY_NAME
|
|
192
|
+
/ SUBAGENT_TRANSCRIPT_FILE_TEMPLATE.format(agent_id=agent_id)
|
|
193
|
+
)
|
|
194
|
+
return str(subagent_path) if subagent_path.is_file() else None
|
|
195
|
+
|
|
196
|
+
|
|
177
197
|
def decision_for(all_payload_fields: dict[str, object]) -> dict[str, object] | None:
|
|
178
198
|
"""Return a deny for a governed action with readable, unloaded transcripts.
|
|
179
199
|
|
|
@@ -183,7 +203,8 @@ def decision_for(all_payload_fields: dict[str, object]) -> dict[str, object] | N
|
|
|
183
203
|
if not _is_governed_action(all_payload_fields):
|
|
184
204
|
return None
|
|
185
205
|
paths = [all_payload_fields.get(field) for field in ALL_TRANSCRIPT_PATH_FIELDS]
|
|
186
|
-
|
|
206
|
+
paths.append(_subagent_transcript_path(all_payload_fields))
|
|
207
|
+
readable_paths = list(dict.fromkeys(path for path in paths if isinstance(path, str) and path))
|
|
187
208
|
if not readable_paths:
|
|
188
209
|
return None
|
|
189
210
|
statuses = [_transcript_load_status(path) for path in readable_paths]
|
|
@@ -82,6 +82,49 @@ def test_agent_transcript_can_supply_invocation(tmp_path: Path) -> None:
|
|
|
82
82
|
assert gate.decision_for(payload) is None
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def _subagent_transcript(tmp_path: Path, agent_id: str, skill_name: str | None) -> Path:
|
|
86
|
+
subagent_directory = tmp_path / "session" / "subagents"
|
|
87
|
+
subagent_directory.mkdir(parents=True)
|
|
88
|
+
subagent_path = subagent_directory / f"agent-{agent_id}.jsonl"
|
|
89
|
+
lines = [json.dumps({"type": "user", "message": {"content": "Task."}})]
|
|
90
|
+
if skill_name:
|
|
91
|
+
lines.append(json.dumps({"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "Skill", "input": {"skill": skill_name}}]}}))
|
|
92
|
+
subagent_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
93
|
+
return subagent_path
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_subagent_transcript_located_by_agent_id_can_supply_invocation(tmp_path: Path) -> None:
|
|
97
|
+
session_path = _transcript(tmp_path)
|
|
98
|
+
_subagent_transcript(tmp_path, "a8865a3f3c71595ce", "pr-lifecycle")
|
|
99
|
+
payload = _payload("git commit", session_path)
|
|
100
|
+
payload["agent_id"] = "a8865a3f3c71595ce"
|
|
101
|
+
assert _run_main(payload) == (0, "")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_subagent_without_invocation_is_denied(tmp_path: Path) -> None:
|
|
105
|
+
session_path = _transcript(tmp_path)
|
|
106
|
+
_subagent_transcript(tmp_path, "a8865a3f3c71595ce", None)
|
|
107
|
+
payload = _payload("git push", session_path)
|
|
108
|
+
payload["agent_id"] = "a8865a3f3c71595ce"
|
|
109
|
+
_assert_denied(payload)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_missing_subagent_transcript_keeps_session_deny(tmp_path: Path) -> None:
|
|
113
|
+
payload = _payload("git push", _transcript(tmp_path))
|
|
114
|
+
payload["agent_id"] = "a8865a3f3c71595ce"
|
|
115
|
+
_assert_denied(payload)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_agent_id_with_path_separator_is_not_followed(tmp_path: Path) -> None:
|
|
119
|
+
session_path = _transcript(tmp_path)
|
|
120
|
+
escaped_path = tmp_path / "session" / "agent-x.jsonl"
|
|
121
|
+
escaped_path.parent.mkdir(parents=True)
|
|
122
|
+
escaped_path.write_text(json.dumps({"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "Skill", "input": {"skill": "pr-lifecycle"}}]}}) + "\n", encoding="utf-8")
|
|
123
|
+
payload = _payload("git push", session_path)
|
|
124
|
+
payload["agent_id"] = "../agent-x"
|
|
125
|
+
_assert_denied(payload)
|
|
126
|
+
|
|
127
|
+
|
|
85
128
|
def test_unreadable_or_missing_transcript_allows_silently(tmp_path: Path) -> None:
|
|
86
129
|
payload = _payload("gh pr create", tmp_path / "missing.jsonl")
|
|
87
130
|
assert _run_main(payload) == (0, "")
|
|
@@ -18,6 +18,11 @@ ALL_GITHUB_MCP_TOOL_SUFFIXES = (
|
|
|
18
18
|
"__update_pull_request",
|
|
19
19
|
)
|
|
20
20
|
ALL_TRANSCRIPT_PATH_FIELDS = ("transcript_path", "agent_transcript_path")
|
|
21
|
+
SESSION_TRANSCRIPT_PATH_FIELD = "transcript_path"
|
|
22
|
+
AGENT_ID_FIELD = "agent_id"
|
|
23
|
+
AGENT_ID_PATTERN = r"[A-Za-z0-9_-]+"
|
|
24
|
+
SUBAGENT_TRANSCRIPT_DIRECTORY_NAME = "subagents"
|
|
25
|
+
SUBAGENT_TRANSCRIPT_FILE_TEMPLATE = "agent-{agent_id}.jsonl"
|
|
21
26
|
COMMAND_SEPARATORS = ";|&\n()`"
|
|
22
27
|
COMMAND_WHITESPACE = " \t\r"
|
|
23
28
|
OPTION_AND_VALUE_WORD_COUNT = 2
|