claude-dev-env 7.0.0 → 7.2.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/.agents/skills/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +213 -0
- package/.agents/skills/session-continuity/SKILL.md +125 -0
- package/.agents/skills/session-continuity/continuity.mjs +447 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +373 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +88 -0
- package/bin/install-session-continuity.test.mjs +55 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
"""Behavior tests for the Codex Luna fast-mode spawn gate."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import importlib.util
|
|
6
|
-
import io
|
|
7
|
-
import json
|
|
8
|
-
import pathlib
|
|
9
|
-
import subprocess
|
|
10
|
-
import sys
|
|
11
|
-
from typing import Any
|
|
12
|
-
from unittest import mock
|
|
13
|
-
|
|
14
|
-
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
15
|
-
_HOOKS_TREE = _HOOK_DIR.parent
|
|
16
|
-
for each_path in (str(_HOOK_DIR), str(_HOOKS_TREE)):
|
|
17
|
-
if each_path not in sys.path:
|
|
18
|
-
sys.path.insert(0, each_path)
|
|
19
|
-
|
|
20
|
-
hook_spec = importlib.util.spec_from_file_location(
|
|
21
|
-
"luna_fast_mode_gate",
|
|
22
|
-
_HOOK_DIR / "luna_fast_mode_gate.py",
|
|
23
|
-
)
|
|
24
|
-
assert hook_spec is not None
|
|
25
|
-
assert hook_spec.loader is not None
|
|
26
|
-
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
27
|
-
hook_spec.loader.exec_module(hook_module)
|
|
28
|
-
|
|
29
|
-
from hooks_constants.luna_fast_mode_gate_constants import ( # noqa: E402
|
|
30
|
-
AGENT_TOOL_NAME,
|
|
31
|
-
CALLING_HOOK_NAME,
|
|
32
|
-
CODEX_AGENT_TOOL_NAME,
|
|
33
|
-
FAST_SERVICE_TIER,
|
|
34
|
-
TASK_TOOL_NAME,
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
_MODEL_ID = "gpt-5.6-luna"
|
|
38
|
-
_NON_LUNA_MODEL_ID = "gpt-5.6-sol"
|
|
39
|
-
_UNRELATED_TOOL_NAME = "Write"
|
|
40
|
-
_SERVICE_TIER_FIELD_NAME = "service_tier"
|
|
41
|
-
_MODEL_FIELD_NAME = "model"
|
|
42
|
-
_DENY_DECISION = "deny"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _spawn_payload(
|
|
46
|
-
*,
|
|
47
|
-
tool_name: str = AGENT_TOOL_NAME,
|
|
48
|
-
model: object = _MODEL_ID,
|
|
49
|
-
service_tier: object = None,
|
|
50
|
-
include_service_tier: bool = True,
|
|
51
|
-
) -> dict[str, Any]:
|
|
52
|
-
"""Build one PreToolUse payload for a spawn surface."""
|
|
53
|
-
tool_input: dict[str, object] = {
|
|
54
|
-
_MODEL_FIELD_NAME: model,
|
|
55
|
-
}
|
|
56
|
-
if include_service_tier:
|
|
57
|
-
tool_input[_SERVICE_TIER_FIELD_NAME] = service_tier
|
|
58
|
-
return {"tool_name": tool_name, "tool_input": tool_input}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _run_main(payload: object) -> str:
|
|
62
|
-
"""Run the production hook and return its stdout."""
|
|
63
|
-
with mock.patch("sys.stdin", io.StringIO(json.dumps(payload))):
|
|
64
|
-
with mock.patch("sys.stdout", new_callable=io.StringIO) as captured_stdout:
|
|
65
|
-
try:
|
|
66
|
-
hook_module.main()
|
|
67
|
-
except SystemExit:
|
|
68
|
-
pass
|
|
69
|
-
return captured_stdout.getvalue()
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _decision(payload: object) -> str:
|
|
73
|
-
"""Return the hook decision for a payload."""
|
|
74
|
-
return json.loads(_run_main(payload))["hookSpecificOutput"]["permissionDecision"]
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def test_blocks_luna_without_fast_for_headless_and_in_session_surfaces() -> None:
|
|
78
|
-
all_tool_names = (AGENT_TOOL_NAME, TASK_TOOL_NAME, CODEX_AGENT_TOOL_NAME)
|
|
79
|
-
for each_tool_name in all_tool_names:
|
|
80
|
-
payload = _spawn_payload(tool_name=each_tool_name, service_tier="flex")
|
|
81
|
-
assert _decision(payload) == _DENY_DECISION
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def test_allows_luna_with_exact_fast_service_tier() -> None:
|
|
85
|
-
for each_tool_name in (AGENT_TOOL_NAME, TASK_TOOL_NAME, CODEX_AGENT_TOOL_NAME):
|
|
86
|
-
payload = _spawn_payload(
|
|
87
|
-
tool_name=each_tool_name,
|
|
88
|
-
service_tier=FAST_SERVICE_TIER,
|
|
89
|
-
)
|
|
90
|
-
assert _run_main(payload) == ""
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def test_allows_luna_with_native_priority_service_tier() -> None:
|
|
94
|
-
payload = _spawn_payload(
|
|
95
|
-
tool_name=CODEX_AGENT_TOOL_NAME,
|
|
96
|
-
service_tier="priority",
|
|
97
|
-
)
|
|
98
|
-
assert _run_main(payload) == ""
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def test_blocks_luna_with_priority_for_agent_and_task_surfaces() -> None:
|
|
102
|
-
for each_tool_name in (AGENT_TOOL_NAME, TASK_TOOL_NAME):
|
|
103
|
-
payload = _spawn_payload(
|
|
104
|
-
tool_name=each_tool_name,
|
|
105
|
-
service_tier="priority",
|
|
106
|
-
)
|
|
107
|
-
assert _decision(payload) == _DENY_DECISION
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def test_blocks_missing_and_non_string_service_tier() -> None:
|
|
111
|
-
assert _decision(_spawn_payload(include_service_tier=False)) == _DENY_DECISION
|
|
112
|
-
assert _decision(_spawn_payload(service_tier=None)) == _DENY_DECISION
|
|
113
|
-
assert _decision(_spawn_payload(service_tier=True)) == _DENY_DECISION
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def test_blocks_default_and_wrong_case_fast_service_tiers() -> None:
|
|
117
|
-
assert _decision(_spawn_payload(service_tier="default")) == _DENY_DECISION
|
|
118
|
-
assert _decision(_spawn_payload(service_tier="FAST")) == _DENY_DECISION
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def test_allows_non_luna_and_unrelated_tool_payloads() -> None:
|
|
122
|
-
assert _run_main(_spawn_payload(model=_NON_LUNA_MODEL_ID)) == ""
|
|
123
|
-
assert _run_main(
|
|
124
|
-
_spawn_payload(tool_name=_UNRELATED_TOOL_NAME, service_tier="flex")
|
|
125
|
-
) == ""
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def test_recognizes_case_insensitive_luna_model_segments() -> None:
|
|
129
|
-
assert _decision(_spawn_payload(model="GPT-5.6-LUNA", service_tier="flex")) == (
|
|
130
|
-
_DENY_DECISION
|
|
131
|
-
)
|
|
132
|
-
assert _run_main(_spawn_payload(model="gpt-5.6-lunacy", service_tier="flex")) == ""
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def test_malformed_outer_payload_allows() -> None:
|
|
136
|
-
assert _run_main({"tool_name": AGENT_TOOL_NAME}) == ""
|
|
137
|
-
assert _run_main({"tool_name": AGENT_TOOL_NAME, "tool_input": []}) == ""
|
|
138
|
-
assert _run_main("not an object") == ""
|
|
139
|
-
assert _run_main({"tool_name": AGENT_TOOL_NAME, "tool_input": {_MODEL_FIELD_NAME: 7}}) == ""
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def test_deny_payload_and_log_do_not_include_a_prompt() -> None:
|
|
143
|
-
payload = _spawn_payload(service_tier="default")
|
|
144
|
-
with mock.patch.object(hook_module, "log_hook_block") as recorded_block:
|
|
145
|
-
output = _run_main(payload)
|
|
146
|
-
parsed_output = json.loads(output)
|
|
147
|
-
assert parsed_output["hookSpecificOutput"]["permissionDecision"] == _DENY_DECISION
|
|
148
|
-
assert recorded_block.call_args.kwargs["calling_hook_name"] == CALLING_HOOK_NAME
|
|
149
|
-
assert "prompt" not in recorded_block.call_args.kwargs["offending_input_preview"]
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def test_hooks_json_registers_all_spawn_surfaces() -> None:
|
|
153
|
-
hooks_configuration = json.loads(
|
|
154
|
-
(_HOOKS_TREE / "hooks.json").read_text(encoding="utf-8")
|
|
155
|
-
)
|
|
156
|
-
matching_groups = [
|
|
157
|
-
each_group
|
|
158
|
-
for each_group in hooks_configuration["hooks"]["PreToolUse"]
|
|
159
|
-
if CALLING_HOOK_NAME
|
|
160
|
-
in " ".join(each_hook["command"] for each_hook in each_group["hooks"])
|
|
161
|
-
]
|
|
162
|
-
assert len(matching_groups) == 1
|
|
163
|
-
assert matching_groups[0]["matcher"] == (
|
|
164
|
-
f"{AGENT_TOOL_NAME}|{TASK_TOOL_NAME}|{CODEX_AGENT_TOOL_NAME}"
|
|
165
|
-
)
|
|
166
|
-
registered_command = matching_groups[0]["hooks"][0]["command"]
|
|
167
|
-
assert registered_command.endswith(
|
|
168
|
-
f"/hooks/blocking/{CALLING_HOOK_NAME}"
|
|
169
|
-
)
|
|
170
|
-
assert (_HOOKS_TREE / "blocking" / CALLING_HOOK_NAME).is_file()
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def test_registered_command_runs_under_windows_cmd_for_allow_and_deny() -> None:
|
|
174
|
-
hooks_configuration = json.loads(
|
|
175
|
-
(_HOOKS_TREE / "hooks.json").read_text(encoding="utf-8")
|
|
176
|
-
)
|
|
177
|
-
matching_group = next(
|
|
178
|
-
each_group
|
|
179
|
-
for each_group in hooks_configuration["hooks"]["PreToolUse"]
|
|
180
|
-
if CALLING_HOOK_NAME
|
|
181
|
-
in " ".join(each_hook["command"] for each_hook in each_group["hooks"])
|
|
182
|
-
)
|
|
183
|
-
registered_command = matching_group["hooks"][0]["command"]
|
|
184
|
-
assert registered_command.startswith("python ")
|
|
185
|
-
command_line = registered_command.replace(
|
|
186
|
-
"${CLAUDE_PLUGIN_ROOT}",
|
|
187
|
-
str(_HOOKS_TREE.parent).replace("\\", "/"),
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
for each_service_tier, expected_output in (
|
|
191
|
-
(FAST_SERVICE_TIER, ""),
|
|
192
|
-
("flex", _DENY_DECISION),
|
|
193
|
-
):
|
|
194
|
-
completed_process = subprocess.run(
|
|
195
|
-
command_line,
|
|
196
|
-
input=json.dumps(_spawn_payload(service_tier=each_service_tier)),
|
|
197
|
-
capture_output=True,
|
|
198
|
-
shell=True,
|
|
199
|
-
text=True,
|
|
200
|
-
check=False,
|
|
201
|
-
)
|
|
202
|
-
assert completed_process.returncode == 0, completed_process.stderr
|
|
203
|
-
if expected_output == "":
|
|
204
|
-
assert completed_process.stdout == ""
|
|
205
|
-
else:
|
|
206
|
-
assert (
|
|
207
|
-
json.loads(completed_process.stdout)["hookSpecificOutput"][
|
|
208
|
-
"permissionDecision"
|
|
209
|
-
]
|
|
210
|
-
== expected_output
|
|
211
|
-
)
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
"""Unit tests for nas-ssh-binary-enforcer PreToolUse hook.
|
|
2
|
-
|
|
3
|
-
The real NAS host, ssh user, and port are private, so these tests set them via
|
|
4
|
-
the ``CLAUDE_NAS_*`` environment variables and drive the real hook, asserting
|
|
5
|
-
the runtime-built host pattern and deny messages.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import importlib.util
|
|
9
|
-
import os
|
|
10
|
-
import pathlib
|
|
11
|
-
import sys
|
|
12
|
-
from collections.abc import Iterator
|
|
13
|
-
from unittest.mock import patch
|
|
14
|
-
|
|
15
|
-
import pytest
|
|
16
|
-
|
|
17
|
-
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
18
|
-
if str(_HOOK_DIR) not in sys.path:
|
|
19
|
-
sys.path.insert(0, str(_HOOK_DIR))
|
|
20
|
-
if str(_HOOK_DIR.parent) not in sys.path:
|
|
21
|
-
sys.path.insert(0, str(_HOOK_DIR.parent))
|
|
22
|
-
|
|
23
|
-
hook_spec = importlib.util.spec_from_file_location(
|
|
24
|
-
"nas_ssh_binary_enforcer",
|
|
25
|
-
_HOOK_DIR / "nas_ssh_binary_enforcer.py",
|
|
26
|
-
)
|
|
27
|
-
assert hook_spec is not None
|
|
28
|
-
assert hook_spec.loader is not None
|
|
29
|
-
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
30
|
-
hook_spec.loader.exec_module(hook_module)
|
|
31
|
-
_find_nas_ssh_violation = hook_module._find_nas_ssh_violation
|
|
32
|
-
|
|
33
|
-
from hooks_constants.local_identity import ( # noqa: E402
|
|
34
|
-
bare_ssh_binary_deny_message,
|
|
35
|
-
missing_batch_mode_deny_message,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
NAS_HOST = "test-nas.example.net"
|
|
39
|
-
NAS_SSH_USER = "tester"
|
|
40
|
-
NAS_SSH_PORT = "2222"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@pytest.fixture(autouse=True)
|
|
44
|
-
def _set_nas_identity_env() -> Iterator[None]:
|
|
45
|
-
with patch.dict(
|
|
46
|
-
os.environ,
|
|
47
|
-
{
|
|
48
|
-
"CLAUDE_NAS_HOST": NAS_HOST,
|
|
49
|
-
"CLAUDE_NAS_SSH_USER": NAS_SSH_USER,
|
|
50
|
-
"CLAUDE_NAS_SSH_PORT": NAS_SSH_PORT,
|
|
51
|
-
},
|
|
52
|
-
clear=False,
|
|
53
|
-
):
|
|
54
|
-
yield
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_bare_ssh_to_nas_is_denied_with_binary_message() -> None:
|
|
58
|
-
assert (
|
|
59
|
-
_find_nas_ssh_violation(
|
|
60
|
-
f'ssh -p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "ls /volume1"'
|
|
61
|
-
)
|
|
62
|
-
== bare_ssh_binary_deny_message()
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def test_bare_scp_to_nas_is_denied() -> None:
|
|
67
|
-
assert (
|
|
68
|
-
_find_nas_ssh_violation(
|
|
69
|
-
f"scp -P {NAS_SSH_PORT} file.txt {NAS_SSH_USER}@{NAS_HOST}:/volume1/"
|
|
70
|
-
)
|
|
71
|
-
== bare_ssh_binary_deny_message()
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def test_bare_sftp_to_nas_is_denied() -> None:
|
|
76
|
-
assert (
|
|
77
|
-
_find_nas_ssh_violation(f"sftp -P {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST}")
|
|
78
|
-
== bare_ssh_binary_deny_message()
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def test_bare_ssh_to_nas_behind_launcher_wrapper_is_denied() -> None:
|
|
83
|
-
assert (
|
|
84
|
-
_find_nas_ssh_violation(
|
|
85
|
-
f'timeout 10 ssh -p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "uptime"'
|
|
86
|
-
)
|
|
87
|
-
== bare_ssh_binary_deny_message()
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def test_bare_ssh_to_nas_after_shell_operator_is_denied() -> None:
|
|
92
|
-
assert (
|
|
93
|
-
_find_nas_ssh_violation(
|
|
94
|
-
f'cd /tmp && ssh -p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "uptime"'
|
|
95
|
-
)
|
|
96
|
-
== bare_ssh_binary_deny_message()
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def test_bare_ssh_to_nas_after_glued_operator_is_denied() -> None:
|
|
101
|
-
assert (
|
|
102
|
-
_find_nas_ssh_violation(
|
|
103
|
-
f'true;ssh -p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "uptime"'
|
|
104
|
-
)
|
|
105
|
-
== bare_ssh_binary_deny_message()
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def test_bare_ssh_to_nas_after_pipe_ampersand_operator_is_denied() -> None:
|
|
110
|
-
assert (
|
|
111
|
-
_find_nas_ssh_violation(
|
|
112
|
-
f'echo start |& ssh -p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "echo hi"'
|
|
113
|
-
)
|
|
114
|
-
== bare_ssh_binary_deny_message()
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def test_full_openssh_binary_to_nas_without_batchmode_is_denied() -> None:
|
|
119
|
-
assert (
|
|
120
|
-
_find_nas_ssh_violation(
|
|
121
|
-
'"/c/Windows/System32/OpenSSH/ssh.exe" -o ConnectTimeout=10 '
|
|
122
|
-
f'-p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "uptime"'
|
|
123
|
-
)
|
|
124
|
-
== missing_batch_mode_deny_message()
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def test_full_openssh_scp_to_nas_without_batchmode_is_denied() -> None:
|
|
129
|
-
assert (
|
|
130
|
-
_find_nas_ssh_violation(
|
|
131
|
-
'"/c/Windows/System32/OpenSSH/scp.exe" '
|
|
132
|
-
f"-P {NAS_SSH_PORT} file.txt {NAS_SSH_USER}@{NAS_HOST}:/volume1/"
|
|
133
|
-
)
|
|
134
|
-
== missing_batch_mode_deny_message()
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def test_full_openssh_binary_to_nas_with_batchmode_is_allowed() -> None:
|
|
139
|
-
assert (
|
|
140
|
-
_find_nas_ssh_violation(
|
|
141
|
-
'"/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes '
|
|
142
|
-
f'-o ConnectTimeout=10 -p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "uptime"'
|
|
143
|
-
)
|
|
144
|
-
is None
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def test_full_openssh_binary_glued_batchmode_flag_is_allowed() -> None:
|
|
149
|
-
assert (
|
|
150
|
-
_find_nas_ssh_violation(
|
|
151
|
-
'"/c/Windows/System32/OpenSSH/ssh.exe" -oBatchMode=yes '
|
|
152
|
-
f'-p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "uptime"'
|
|
153
|
-
)
|
|
154
|
-
is None
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def test_bare_ssh_to_other_host_is_allowed() -> None:
|
|
159
|
-
assert _find_nas_ssh_violation(f'ssh -p 22 {NAS_SSH_USER}@example.com "uptime"') is None
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def test_full_openssh_binary_to_other_host_without_batchmode_is_allowed() -> None:
|
|
163
|
-
assert (
|
|
164
|
-
_find_nas_ssh_violation(
|
|
165
|
-
f'"/c/Windows/System32/OpenSSH/ssh.exe" -p 22 {NAS_SSH_USER}@example.com "uptime"'
|
|
166
|
-
)
|
|
167
|
-
is None
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def test_nas_host_as_echoed_data_is_allowed() -> None:
|
|
172
|
-
assert _find_nas_ssh_violation(f'echo "connect via ssh to {NAS_HOST}"') is None
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def test_nas_host_mentioned_without_ssh_command_word_is_allowed() -> None:
|
|
176
|
-
assert _find_nas_ssh_violation(f"ping -c 1 {NAS_HOST}") is None
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def test_ssh_family_word_only_in_remote_command_argument_is_allowed() -> None:
|
|
180
|
-
assert (
|
|
181
|
-
_find_nas_ssh_violation(
|
|
182
|
-
'"/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes '
|
|
183
|
-
f'-p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "grep ssh /etc/config"'
|
|
184
|
-
)
|
|
185
|
-
is None
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def test_nas_host_in_different_segment_than_ssh_is_allowed() -> None:
|
|
190
|
-
assert (
|
|
191
|
-
_find_nas_ssh_violation(
|
|
192
|
-
f'echo {NAS_HOST} && ssh -p 22 {NAS_SSH_USER}@example.com "id"'
|
|
193
|
-
)
|
|
194
|
-
is None
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def test_ssh_to_other_host_with_nas_host_in_remote_command_is_allowed() -> None:
|
|
199
|
-
assert (
|
|
200
|
-
_find_nas_ssh_violation(
|
|
201
|
-
f'ssh -p 22 {NAS_SSH_USER}@example.com "ping -c1 {NAS_HOST}"'
|
|
202
|
-
)
|
|
203
|
-
is None
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def test_scp_to_other_host_with_nas_host_in_remote_path_is_allowed() -> None:
|
|
208
|
-
assert (
|
|
209
|
-
_find_nas_ssh_violation(
|
|
210
|
-
f"scp -P 22 f.txt {NAS_SSH_USER}@example.com:/backup/{NAS_HOST}/"
|
|
211
|
-
)
|
|
212
|
-
is None
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def test_similar_host_suffix_is_not_matched() -> None:
|
|
217
|
-
assert _find_nas_ssh_violation(f'ssh -p 22 {NAS_SSH_USER}@{NAS_HOST}x "id"') is None
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def test_empty_command_is_allowed() -> None:
|
|
221
|
-
assert _find_nas_ssh_violation("") is None
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def test_unbalanced_quotes_command_is_allowed() -> None:
|
|
225
|
-
assert _find_nas_ssh_violation(f"ssh 'unterminated {NAS_HOST}") is None
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
"""Tests for orchestrator_refresh_reschedule_gate PreToolUse hook."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import importlib.util
|
|
6
|
-
import io
|
|
7
|
-
import json
|
|
8
|
-
import sys
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from types import ModuleType
|
|
11
|
-
|
|
12
|
-
import pytest
|
|
13
|
-
|
|
14
|
-
BLOCKING_DIRECTORY = Path(__file__).resolve().parent
|
|
15
|
-
HOOKS_DIRECTORY = BLOCKING_DIRECTORY.parent
|
|
16
|
-
if str(HOOKS_DIRECTORY) not in sys.path:
|
|
17
|
-
sys.path.insert(0, str(HOOKS_DIRECTORY))
|
|
18
|
-
|
|
19
|
-
SKILL_SCRIPTS = HOOKS_DIRECTORY.parent / ".agents" / "skills" / "orchestrator" / "scripts"
|
|
20
|
-
if str(SKILL_SCRIPTS) not in sys.path:
|
|
21
|
-
sys.path.insert(0, str(SKILL_SCRIPTS))
|
|
22
|
-
|
|
23
|
-
from status_gate_constants.config.constants import RUN_STATUS_ACTIVE, RUN_STATUS_DONE # noqa: E402
|
|
24
|
-
from status_gate import write_status_file # noqa: E402
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def load_gate_module() -> ModuleType:
|
|
28
|
-
module_path = BLOCKING_DIRECTORY / "orchestrator_refresh_reschedule_gate.py"
|
|
29
|
-
spec = importlib.util.spec_from_file_location(
|
|
30
|
-
"orchestrator_refresh_reschedule_gate",
|
|
31
|
-
module_path,
|
|
32
|
-
)
|
|
33
|
-
assert spec is not None
|
|
34
|
-
assert spec.loader is not None
|
|
35
|
-
gate_module = importlib.util.module_from_spec(spec)
|
|
36
|
-
sys.modules[spec.name] = gate_module
|
|
37
|
-
spec.loader.exec_module(gate_module)
|
|
38
|
-
return gate_module
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def run_main_with_payload(
|
|
42
|
-
gate_module: ModuleType,
|
|
43
|
-
payload: dict[str, object],
|
|
44
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
45
|
-
) -> tuple[int, str]:
|
|
46
|
-
stdin_buffer = io.StringIO(json.dumps(payload))
|
|
47
|
-
stdout_buffer = io.StringIO()
|
|
48
|
-
monkeypatch.setattr(sys, "stdin", stdin_buffer)
|
|
49
|
-
monkeypatch.setattr(sys, "stdout", stdout_buffer)
|
|
50
|
-
exit_code = 0
|
|
51
|
-
try:
|
|
52
|
-
gate_module.main()
|
|
53
|
-
except SystemExit as system_exit:
|
|
54
|
-
exit_code = int(system_exit.code or 0)
|
|
55
|
-
return exit_code, stdout_buffer.getvalue()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class TestOrchestratorRefreshRescheduleGate:
|
|
59
|
-
def should_allow_when_status_is_active(
|
|
60
|
-
self,
|
|
61
|
-
temporary_directory: Path,
|
|
62
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
63
|
-
) -> None:
|
|
64
|
-
status_file_path = temporary_directory / "status.json"
|
|
65
|
-
write_status_file(status_file_path, RUN_STATUS_ACTIVE, "", is_rearm_pending=False)
|
|
66
|
-
monkeypatch.setenv("ORCHESTRATOR_RUN_STATUS_FILE", str(status_file_path))
|
|
67
|
-
gate_module = load_gate_module()
|
|
68
|
-
payload = {
|
|
69
|
-
"tool_name": "ScheduleWakeup",
|
|
70
|
-
"tool_input": {"prompt": "/orchestrator-refresh", "delaySeconds": 1200},
|
|
71
|
-
"cwd": str(temporary_directory),
|
|
72
|
-
}
|
|
73
|
-
exit_code, stdout_text = run_main_with_payload(
|
|
74
|
-
gate_module, payload, monkeypatch
|
|
75
|
-
)
|
|
76
|
-
assert exit_code == 0
|
|
77
|
-
assert stdout_text == ""
|
|
78
|
-
|
|
79
|
-
def should_deny_when_status_is_done(
|
|
80
|
-
self,
|
|
81
|
-
temporary_directory: Path,
|
|
82
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
83
|
-
) -> None:
|
|
84
|
-
status_file_path = temporary_directory / "status.json"
|
|
85
|
-
write_status_file(status_file_path, RUN_STATUS_DONE, "", is_rearm_pending=False)
|
|
86
|
-
monkeypatch.setenv("ORCHESTRATOR_RUN_STATUS_FILE", str(status_file_path))
|
|
87
|
-
gate_module = load_gate_module()
|
|
88
|
-
payload = {
|
|
89
|
-
"tool_name": "ScheduleWakeup",
|
|
90
|
-
"tool_input": {"prompt": "/orchestrator-refresh re-arm"},
|
|
91
|
-
"cwd": str(temporary_directory),
|
|
92
|
-
}
|
|
93
|
-
exit_code, stdout_text = run_main_with_payload(
|
|
94
|
-
gate_module, payload, monkeypatch
|
|
95
|
-
)
|
|
96
|
-
assert exit_code == 0
|
|
97
|
-
denial = json.loads(stdout_text)
|
|
98
|
-
assert denial["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
99
|
-
|
|
100
|
-
def should_deny_when_status_file_missing(
|
|
101
|
-
self,
|
|
102
|
-
temporary_directory: Path,
|
|
103
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
104
|
-
) -> None:
|
|
105
|
-
missing_path = temporary_directory / "nope.json"
|
|
106
|
-
monkeypatch.setenv("ORCHESTRATOR_RUN_STATUS_FILE", str(missing_path))
|
|
107
|
-
gate_module = load_gate_module()
|
|
108
|
-
payload = {
|
|
109
|
-
"tool_name": "ScheduleWakeup",
|
|
110
|
-
"tool_input": {"prompt": "/orchestrator-refresh now"},
|
|
111
|
-
"cwd": str(temporary_directory),
|
|
112
|
-
}
|
|
113
|
-
_exit_code, stdout_text = run_main_with_payload(
|
|
114
|
-
gate_module, payload, monkeypatch
|
|
115
|
-
)
|
|
116
|
-
denial = json.loads(stdout_text)
|
|
117
|
-
assert denial["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
118
|
-
|
|
119
|
-
def should_deny_when_rearm_slot_already_pending(
|
|
120
|
-
self,
|
|
121
|
-
temporary_directory: Path,
|
|
122
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
123
|
-
) -> None:
|
|
124
|
-
status_file_path = temporary_directory / "status.json"
|
|
125
|
-
write_status_file(
|
|
126
|
-
status_file_path, RUN_STATUS_ACTIVE, "", is_rearm_pending=True
|
|
127
|
-
)
|
|
128
|
-
monkeypatch.setenv("ORCHESTRATOR_RUN_STATUS_FILE", str(status_file_path))
|
|
129
|
-
gate_module = load_gate_module()
|
|
130
|
-
payload = {
|
|
131
|
-
"tool_name": "ScheduleWakeup",
|
|
132
|
-
"tool_input": {"prompt": "/orchestrator-refresh", "delaySeconds": 1200},
|
|
133
|
-
"cwd": str(temporary_directory),
|
|
134
|
-
}
|
|
135
|
-
_exit_code, stdout_text = run_main_with_payload(
|
|
136
|
-
gate_module, payload, monkeypatch
|
|
137
|
-
)
|
|
138
|
-
denial = json.loads(stdout_text)
|
|
139
|
-
assert denial["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
140
|
-
assert "rearm_already_pending" in denial["hookSpecificOutput"][
|
|
141
|
-
"permissionDecisionReason"
|
|
142
|
-
]
|
|
143
|
-
|
|
144
|
-
def should_always_deny_cron_create_for_orchestrator_refresh(
|
|
145
|
-
self,
|
|
146
|
-
temporary_directory: Path,
|
|
147
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
148
|
-
) -> None:
|
|
149
|
-
status_file_path = temporary_directory / "status.json"
|
|
150
|
-
write_status_file(status_file_path, RUN_STATUS_ACTIVE, "", is_rearm_pending=False)
|
|
151
|
-
monkeypatch.setenv("ORCHESTRATOR_RUN_STATUS_FILE", str(status_file_path))
|
|
152
|
-
gate_module = load_gate_module()
|
|
153
|
-
payload = {
|
|
154
|
-
"tool_name": "CronCreate",
|
|
155
|
-
"tool_input": {"prompt": "/orchestrator-refresh"},
|
|
156
|
-
"cwd": str(temporary_directory),
|
|
157
|
-
}
|
|
158
|
-
_exit_code, stdout_text = run_main_with_payload(
|
|
159
|
-
gate_module, payload, monkeypatch
|
|
160
|
-
)
|
|
161
|
-
denial = json.loads(stdout_text)
|
|
162
|
-
assert denial["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
163
|
-
assert "cron_create_forbidden" in denial["hookSpecificOutput"][
|
|
164
|
-
"permissionDecisionReason"
|
|
165
|
-
]
|
|
166
|
-
|
|
167
|
-
def should_ignore_unrelated_schedule_prompts(
|
|
168
|
-
self,
|
|
169
|
-
temporary_directory: Path,
|
|
170
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
171
|
-
) -> None:
|
|
172
|
-
gate_module = load_gate_module()
|
|
173
|
-
payload = {
|
|
174
|
-
"tool_name": "ScheduleWakeup",
|
|
175
|
-
"tool_input": {"prompt": "/pr-converge"},
|
|
176
|
-
"cwd": str(temporary_directory),
|
|
177
|
-
}
|
|
178
|
-
exit_code, stdout_text = run_main_with_payload(
|
|
179
|
-
gate_module, payload, monkeypatch
|
|
180
|
-
)
|
|
181
|
-
assert exit_code == 0
|
|
182
|
-
assert stdout_text == ""
|
|
183
|
-
|
|
184
|
-
def should_ignore_prose_mention_of_orchestrator_refresh(
|
|
185
|
-
self,
|
|
186
|
-
temporary_directory: Path,
|
|
187
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
188
|
-
) -> None:
|
|
189
|
-
gate_module = load_gate_module()
|
|
190
|
-
payload = {
|
|
191
|
-
"tool_name": "ScheduleWakeup",
|
|
192
|
-
"tool_input": {
|
|
193
|
-
"prompt": "See docs about /orchestrator-refresh for later"
|
|
194
|
-
},
|
|
195
|
-
"cwd": str(temporary_directory),
|
|
196
|
-
}
|
|
197
|
-
exit_code, stdout_text = run_main_with_payload(
|
|
198
|
-
gate_module, payload, monkeypatch
|
|
199
|
-
)
|
|
200
|
-
assert exit_code == 0
|
|
201
|
-
assert stdout_text == ""
|
|
202
|
-
|
|
203
|
-
def should_allow_when_prompt_carries_run_slug_for_scoped_status(
|
|
204
|
-
self,
|
|
205
|
-
temporary_directory: Path,
|
|
206
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
207
|
-
) -> None:
|
|
208
|
-
run_slug = "demo-run"
|
|
209
|
-
status_file_path = temporary_directory.joinpath(
|
|
210
|
-
"docs", "plans", run_slug, ".orchestrator-run-status.json"
|
|
211
|
-
)
|
|
212
|
-
write_status_file(status_file_path, RUN_STATUS_ACTIVE, run_slug, is_rearm_pending=False)
|
|
213
|
-
monkeypatch.delenv("ORCHESTRATOR_RUN_STATUS_FILE", raising=False)
|
|
214
|
-
gate_module = load_gate_module()
|
|
215
|
-
payload = {
|
|
216
|
-
"tool_name": "ScheduleWakeup",
|
|
217
|
-
"tool_input": {
|
|
218
|
-
"prompt": f"/orchestrator-refresh --run-slug {run_slug}"
|
|
219
|
-
},
|
|
220
|
-
"cwd": str(temporary_directory),
|
|
221
|
-
}
|
|
222
|
-
exit_code, stdout_text = run_main_with_payload(
|
|
223
|
-
gate_module, payload, monkeypatch
|
|
224
|
-
)
|
|
225
|
-
assert exit_code == 0
|
|
226
|
-
assert stdout_text == ""
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
@pytest.fixture
|
|
230
|
-
def temporary_directory(tmp_path: Path) -> Path:
|
|
231
|
-
return tmp_path
|