282a6809ac
e4d4319 deprecated the allowedWorktrees whitelist (git worktree registry +
same common-dir checks replace it) but left 4 tests asserting the old
contract:
- test_ack_skill: template must NOT contain allowedWorktrees: anymore,
only the deprecation comment
- test_ack_worker_profiles: legacy allowlist field is tolerated (no
'requires at least one path'); receipt no longer validated against it
- test_ack_launch_worker: capture_worktree_identity dropped the
allowlist arg; assert registered-worktree pass, symlink impostor and
unregistered-rejection under the new signature
1551 lines
54 KiB
Python
1551 lines
54 KiB
Python
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
import shlex
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
ACK_SCRIPTS = REPO_ROOT / "skills" / "ack" / "scripts"
|
|
sys.path.insert(0, str(ACK_SCRIPTS))
|
|
|
|
import launch_worker # noqa: E402
|
|
import validate_worker_command # noqa: E402
|
|
import worker_profiles # noqa: E402
|
|
|
|
|
|
def orchestration() -> dict:
|
|
return {
|
|
"profileVersion": 1,
|
|
"mode": "orca",
|
|
"allowedWorktrees": ["/repo/worktree"],
|
|
"modelAllowlist": {
|
|
"codex": {
|
|
"developer": {
|
|
"standard": ["gpt-5.6-terra"],
|
|
},
|
|
"test": {
|
|
"standard": ["gpt-5.6-luna"],
|
|
},
|
|
}
|
|
},
|
|
"profiles": {
|
|
"codex-dev-standard": {
|
|
"role": "developer",
|
|
"cli": "codex",
|
|
"tier": "standard",
|
|
"model": "gpt-5.6-terra",
|
|
"reasoningEffort": "medium",
|
|
"permissionMode": "workspace-write",
|
|
},
|
|
"codex-test-standard": {
|
|
"role": "test",
|
|
"cli": "codex",
|
|
"tier": "standard",
|
|
"model": "gpt-5.6-luna",
|
|
"reasoningEffort": "low",
|
|
"permissionMode": "workspace-write",
|
|
},
|
|
},
|
|
"defaults": {
|
|
"developer": "codex-dev-standard",
|
|
"test": "codex-test-standard",
|
|
},
|
|
}
|
|
|
|
|
|
def board(project_root: Path) -> dict:
|
|
return {
|
|
"version": 1,
|
|
"project": {
|
|
"name": "test-project",
|
|
"repoPath": str(project_root),
|
|
"orchestration": orchestration(),
|
|
},
|
|
"workerReceipts": [],
|
|
"tasks": [
|
|
{
|
|
"id": "TASK-001",
|
|
"title": "test task",
|
|
"status": "open",
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
def worktree_identity(path: Path) -> dict:
|
|
metadata = path.stat()
|
|
git_path = path / ".git"
|
|
git_metadata = git_path.stat() if git_path.exists() else metadata
|
|
return {
|
|
"path": str(path),
|
|
"device": metadata.st_dev,
|
|
"inode": metadata.st_ino,
|
|
"gitCommonDir": str(git_path if git_path.exists() else path),
|
|
"gitCommonDevice": git_metadata.st_dev,
|
|
"gitCommonInode": git_metadata.st_ino,
|
|
}
|
|
|
|
|
|
def plan_for(path: Path, executable: Path) -> dict:
|
|
profile = orchestration()["profiles"]["codex-dev-standard"]
|
|
task_board = board(path)
|
|
board_hash = worker_profiles.canonical_sha256(task_board)
|
|
identity = worktree_identity(path)
|
|
executable_metadata = executable.stat()
|
|
argv = worker_profiles.render_worker_argv(
|
|
profile,
|
|
str(executable),
|
|
str(path),
|
|
)
|
|
requested = {
|
|
"cli": "codex",
|
|
"tier": "standard",
|
|
"model": "gpt-5.6-terra",
|
|
"reasoningEffort": "medium",
|
|
"permissionMode": "workspace-write",
|
|
"executable": str(executable),
|
|
"executableDevice": executable_metadata.st_dev,
|
|
"executableInode": executable_metadata.st_ino,
|
|
"cliVersion": "codex-cli 1.0",
|
|
"argv": argv,
|
|
"argvHash": worker_profiles.canonical_sha256(argv),
|
|
"environmentPolicy": "per-cli-allowlist-v1",
|
|
}
|
|
created_for = {
|
|
"taskId": "TASK-001",
|
|
"attemptId": "TASK-001-A1",
|
|
"role": "developer",
|
|
}
|
|
profile_digest = worker_profiles.profile_hash(profile)
|
|
fingerprint = worker_profiles.canonical_sha256(
|
|
{
|
|
"protocolVersion": 1,
|
|
"backend": "orca",
|
|
"projectRoot": str(path),
|
|
"boardHash": board_hash,
|
|
"profileId": "codex-dev-standard",
|
|
"profileHash": profile_digest,
|
|
"createdFor": created_for,
|
|
"worktree": identity,
|
|
"requested": requested,
|
|
"slot": 1,
|
|
}
|
|
)
|
|
digest_short = fingerprint.split(":", 1)[1][:10]
|
|
return {
|
|
"protocolVersion": 1,
|
|
"backend": "orca",
|
|
"projectRoot": str(path),
|
|
"boardHash": board_hash,
|
|
"taskId": "TASK-001",
|
|
"attemptId": "TASK-001-A1",
|
|
"role": "developer",
|
|
"profileId": "codex-dev-standard",
|
|
"profileHash": profile_digest,
|
|
"launchFingerprint": fingerprint,
|
|
"worktree": identity,
|
|
"requested": requested,
|
|
"slot": 1,
|
|
"title": f"ACK-DEV-CODEX-STANDARD-{digest_short}-1",
|
|
}
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def post_handle_failure_context(
|
|
*,
|
|
update_side_effect: object,
|
|
close_side_effect: object,
|
|
):
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
executable = Path(temporary) / "codex"
|
|
executable.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
executable.chmod(0o700)
|
|
plan = plan_for(REPO_ROOT, executable)
|
|
close = mock.Mock()
|
|
if isinstance(close_side_effect, BaseException):
|
|
close.side_effect = close_side_effect
|
|
else:
|
|
close.return_value = close_side_effect
|
|
|
|
with (
|
|
mock.patch.object(
|
|
launch_worker.secrets,
|
|
"token_hex",
|
|
side_effect=["a" * 64, "b" * 64],
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"create_record",
|
|
return_value=Path("/tmp/ack-record.json"),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"resolve_executable",
|
|
return_value=Path("/trusted/orca"),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_json",
|
|
side_effect=[
|
|
{"ok": True, "_meta": {"runtimeId": "runtime-1"}},
|
|
launch_worker.LaunchError("show failed"),
|
|
],
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_orca_create",
|
|
return_value=(
|
|
{
|
|
"ok": True,
|
|
"result": {"terminal": {"handle": "term-1"}},
|
|
"_meta": {"runtimeId": "runtime-1"},
|
|
},
|
|
"term-1",
|
|
),
|
|
),
|
|
mock.patch.object(launch_worker, "assert_identity_current"),
|
|
mock.patch.object(launch_worker, "run_orca_close", new=close),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"update_record",
|
|
side_effect=update_side_effect,
|
|
) as update,
|
|
):
|
|
yield plan, close, update
|
|
|
|
|
|
class ArgumentBoundaryTests(unittest.TestCase):
|
|
def test_rejects_duplicate_separator_and_free_command_arguments(self) -> None:
|
|
cases = [
|
|
[
|
|
"profile-hash",
|
|
"--project-root",
|
|
"/repo",
|
|
"--project-root",
|
|
"/other",
|
|
"--profile-id",
|
|
"codex-dev-standard",
|
|
],
|
|
[
|
|
"profile-hash",
|
|
"--project-root",
|
|
"/repo",
|
|
"--profile-id",
|
|
"codex-dev-standard",
|
|
"--",
|
|
"forged",
|
|
],
|
|
[
|
|
"profile-hash",
|
|
"--project-root",
|
|
"/repo",
|
|
"--profile-id",
|
|
"codex-dev-standard",
|
|
"--command",
|
|
"codex; touch forged",
|
|
],
|
|
]
|
|
for arguments in cases:
|
|
with self.subTest(arguments=arguments), contextlib.redirect_stderr(
|
|
io.StringIO()
|
|
):
|
|
self.assertEqual(launch_worker.main(arguments), 2)
|
|
|
|
def test_legacy_free_command_path_always_fails_closed(self) -> None:
|
|
with (
|
|
contextlib.redirect_stderr(io.StringIO()),
|
|
contextlib.redirect_stdout(io.StringIO()),
|
|
):
|
|
self.assertEqual(
|
|
validate_worker_command.main(
|
|
[
|
|
"--role",
|
|
"developer",
|
|
"--command",
|
|
"codex; touch /tmp/forged",
|
|
]
|
|
),
|
|
2,
|
|
)
|
|
self.assertEqual(validate_worker_command.main(["--self-test"]), 0)
|
|
|
|
def test_launch_requires_reviewed_fingerprint_and_refuses_drift(self) -> None:
|
|
fake_plan = {"launchFingerprint": "sha256:" + "a" * 64}
|
|
arguments = [
|
|
"launch",
|
|
"--project-root",
|
|
"/repo",
|
|
"--task-id",
|
|
"TASK-001",
|
|
"--attempt-id",
|
|
"TASK-001-A1",
|
|
"--role",
|
|
"developer",
|
|
"--profile-id",
|
|
"codex-dev-standard",
|
|
"--worktree",
|
|
"/repo/worktree",
|
|
"--expected-launch-fingerprint",
|
|
"sha256:" + "b" * 64,
|
|
]
|
|
with (
|
|
mock.patch.object(launch_worker, "build_plan", return_value=fake_plan),
|
|
mock.patch.object(launch_worker, "launch_with_orca") as launch,
|
|
contextlib.redirect_stderr(io.StringIO()),
|
|
):
|
|
self.assertEqual(launch_worker.main(arguments), 1)
|
|
launch.assert_not_called()
|
|
|
|
|
|
class EnvironmentAndExecutableTests(unittest.TestCase):
|
|
def test_environment_splits_control_and_per_cli_credentials(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
hostile_path = str(Path(temporary))
|
|
with mock.patch.dict(
|
|
os.environ,
|
|
{
|
|
"PATH": hostile_path,
|
|
"LD_AUDIT": "/tmp/evil.so",
|
|
"LD_PRELOAD": "/tmp/evil.so",
|
|
"NODE_OPTIONS": "--require=/tmp/evil.js",
|
|
"PYTHONPATH": "/tmp/evil",
|
|
"CODEX_HOME": "/tmp/evil-codex",
|
|
"GIT_SSH_COMMAND": "touch /tmp/evil",
|
|
"OPENAI_API_KEY": "test-token",
|
|
"AZURE_OPENAI_API_KEY": "azure-token",
|
|
"CURSOR_API_KEY": "cursor-token",
|
|
"XAI_API_KEY": "xai-token",
|
|
"GROK_HOME": "/tmp/evil-grok",
|
|
"GROK_SANDBOX": "off",
|
|
"ANTHROPIC_API_KEY": "anthropic-token",
|
|
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/tmp/dbus",
|
|
"DISPLAY": ":99",
|
|
"WAYLAND_DISPLAY": "wayland-99",
|
|
"XDG_RUNTIME_DIR": "/tmp/runtime",
|
|
"LANG": "C.UTF-8",
|
|
},
|
|
clear=True,
|
|
):
|
|
control = launch_worker.control_environment()
|
|
codex = launch_worker.worker_environment("codex")
|
|
cursor = launch_worker.worker_environment("cursor-agent")
|
|
grok = launch_worker.worker_environment("grok")
|
|
|
|
for environment in (control, codex, cursor, grok):
|
|
self.assertNotIn(hostile_path, environment["PATH"].split(os.pathsep))
|
|
self.assertEqual(environment["LANG"], "C.UTF-8")
|
|
self.assertNotIn("OPENAI_API_KEY", control)
|
|
self.assertNotIn("CURSOR_API_KEY", control)
|
|
self.assertNotIn("XAI_API_KEY", control)
|
|
self.assertEqual(codex["OPENAI_API_KEY"], "test-token")
|
|
self.assertEqual(codex["AZURE_OPENAI_API_KEY"], "azure-token")
|
|
self.assertNotIn("CURSOR_API_KEY", codex)
|
|
self.assertNotIn("XAI_API_KEY", codex)
|
|
self.assertEqual(cursor["CURSOR_API_KEY"], "cursor-token")
|
|
self.assertNotIn("OPENAI_API_KEY", cursor)
|
|
self.assertNotIn("XAI_API_KEY", cursor)
|
|
self.assertEqual(grok["XAI_API_KEY"], "xai-token")
|
|
self.assertNotIn("OPENAI_API_KEY", grok)
|
|
self.assertNotIn("CURSOR_API_KEY", grok)
|
|
for forbidden in (
|
|
"ANTHROPIC_API_KEY",
|
|
"DBUS_SESSION_BUS_ADDRESS",
|
|
"DISPLAY",
|
|
"LD_AUDIT",
|
|
"LD_PRELOAD",
|
|
"NODE_OPTIONS",
|
|
"PYTHONPATH",
|
|
"CODEX_HOME",
|
|
"GROK_HOME",
|
|
"GROK_SANDBOX",
|
|
"GIT_SSH_COMMAND",
|
|
"WAYLAND_DISPLAY",
|
|
"XDG_RUNTIME_DIR",
|
|
):
|
|
for environment in (control, codex, cursor, grok):
|
|
self.assertNotIn(forbidden, environment)
|
|
|
|
def test_executable_resolution_ignores_hostile_path(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
fake = Path(temporary) / "git"
|
|
fake.write_text("#!/bin/sh\nexit 99\n", encoding="utf-8")
|
|
fake.chmod(0o755)
|
|
with mock.patch.dict(os.environ, {"PATH": temporary}):
|
|
resolved = launch_worker.resolve_executable("git")
|
|
|
|
self.assertEqual(resolved, Path("/usr/bin/git"))
|
|
|
|
def test_grok_vendor_layout_is_trusted_and_outside_artifacts_are_not(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
home = Path(temporary) / "home"
|
|
downloads = home / ".grok" / "downloads"
|
|
downloads.mkdir(parents=True)
|
|
artifact = downloads / "grok-linux-x86_64"
|
|
artifact.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
artifact.chmod(0o775)
|
|
bindir = home / ".local" / "bin"
|
|
bindir.mkdir(parents=True)
|
|
(bindir / "grok").symlink_to(artifact)
|
|
|
|
hostile_dir = Path(temporary) / "tmp"
|
|
hostile_dir.mkdir()
|
|
hostile = hostile_dir / "grok-linux-x86_64"
|
|
hostile.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
hostile.chmod(0o775)
|
|
hostile_bin = Path(temporary) / "hostile-bin"
|
|
hostile_bin.mkdir()
|
|
(hostile_bin / "grok").symlink_to(hostile)
|
|
|
|
with mock.patch.object(
|
|
launch_worker,
|
|
"account_identity",
|
|
return_value=(home.resolve(strict=True), "ace"),
|
|
), mock.patch.object(
|
|
launch_worker,
|
|
"trusted_path_entries",
|
|
return_value=[bindir.resolve(strict=True)],
|
|
):
|
|
resolved = launch_worker.resolve_executable("grok")
|
|
self.assertEqual(resolved, artifact.resolve(strict=True))
|
|
|
|
with mock.patch.object(
|
|
launch_worker,
|
|
"account_identity",
|
|
return_value=(home.resolve(strict=True), "ace"),
|
|
), mock.patch.object(
|
|
launch_worker,
|
|
"trusted_path_entries",
|
|
return_value=[hostile_bin.resolve(strict=True)],
|
|
):
|
|
with self.assertRaises(launch_worker.LaunchError):
|
|
launch_worker.resolve_executable("grok")
|
|
|
|
|
|
class PlanTests(unittest.TestCase):
|
|
def test_authoritative_board_is_derived_from_project_root_without_repo_path(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
project = Path(temporary).resolve()
|
|
ack_dir = project / "docs" / "ack"
|
|
ack_dir.mkdir(parents=True)
|
|
task_board = board(project)
|
|
del task_board["project"]["repoPath"]
|
|
(ack_dir / "tasks.yaml").write_text(
|
|
json.dumps(task_board),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
loaded_root, loaded_board = launch_worker.load_authoritative_board(
|
|
str(project)
|
|
)
|
|
|
|
self.assertEqual(loaded_root, project)
|
|
self.assertEqual(loaded_board, task_board)
|
|
|
|
def test_authoritative_board_ignores_legacy_repo_path(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
project = Path(temporary).resolve()
|
|
ack_dir = project / "docs" / "ack"
|
|
ack_dir.mkdir(parents=True)
|
|
task_board = board(project)
|
|
task_board["project"]["repoPath"] = "/legacy/other-worktree"
|
|
(ack_dir / "tasks.yaml").write_text(
|
|
json.dumps(task_board),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
loaded_root, loaded_board = launch_worker.load_authoritative_board(
|
|
str(project)
|
|
)
|
|
|
|
self.assertEqual(loaded_root, project)
|
|
self.assertEqual(loaded_board, task_board)
|
|
|
|
def test_plan_uses_exact_renderer_and_binds_created_for(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
project = Path(temporary).resolve()
|
|
executable = project / "codex"
|
|
executable.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
executable.chmod(0o700)
|
|
identity = worktree_identity(project)
|
|
routing = orchestration()
|
|
routing["allowedWorktrees"] = [str(project)]
|
|
task_board = board(project)
|
|
task_board["project"]["orchestration"] = routing
|
|
|
|
with (
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"load_authoritative_board",
|
|
return_value=(project, task_board),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"capture_worktree_identity",
|
|
return_value=identity,
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"resolve_executable",
|
|
return_value=executable,
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_text",
|
|
return_value="codex-cli 1.0",
|
|
),
|
|
):
|
|
plan = launch_worker.build_plan(
|
|
project_root_value=str(project),
|
|
task_id="TASK-001",
|
|
attempt_id="TASK-001-A1",
|
|
role="developer",
|
|
profile_id="codex-dev-standard",
|
|
worktree_value=str(project),
|
|
slot=1,
|
|
)
|
|
|
|
expected_fingerprint = worker_profiles.canonical_sha256(
|
|
{
|
|
"protocolVersion": 1,
|
|
"backend": "orca",
|
|
"projectRoot": str(project),
|
|
"boardHash": worker_profiles.canonical_sha256(task_board),
|
|
"profileId": plan["profileId"],
|
|
"profileHash": plan["profileHash"],
|
|
"createdFor": {
|
|
"taskId": "TASK-001",
|
|
"attemptId": "TASK-001-A1",
|
|
"role": "developer",
|
|
},
|
|
"worktree": plan["worktree"],
|
|
"requested": plan["requested"],
|
|
"slot": 1,
|
|
}
|
|
)
|
|
self.assertEqual(plan["launchFingerprint"], expected_fingerprint)
|
|
self.assertNotIn("--dangerously-bypass-approvals-and-sandbox", plan["requested"]["argv"])
|
|
self.assertNotIn("--yolo", plan["requested"]["argv"])
|
|
|
|
def test_plan_fingerprint_binds_slot(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
project = Path(temporary).resolve()
|
|
executable = project / "codex"
|
|
executable.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
executable.chmod(0o700)
|
|
identity = worktree_identity(project)
|
|
routing = orchestration()
|
|
routing["allowedWorktrees"] = [str(project)]
|
|
task_board = board(project)
|
|
task_board["project"]["orchestration"] = routing
|
|
|
|
with (
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"load_authoritative_board",
|
|
return_value=(project, task_board),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"capture_worktree_identity",
|
|
return_value=identity,
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"resolve_executable",
|
|
return_value=executable,
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_text",
|
|
return_value="codex-cli 1.0",
|
|
),
|
|
):
|
|
first = launch_worker.build_plan(
|
|
project_root_value=str(project),
|
|
task_id="TASK-001",
|
|
attempt_id="TASK-001-A1",
|
|
role="developer",
|
|
profile_id="codex-dev-standard",
|
|
worktree_value=str(project),
|
|
slot=1,
|
|
)
|
|
second = launch_worker.build_plan(
|
|
project_root_value=str(project),
|
|
task_id="TASK-001",
|
|
attempt_id="TASK-001-A1",
|
|
role="developer",
|
|
profile_id="codex-dev-standard",
|
|
worktree_value=str(project),
|
|
slot=2,
|
|
)
|
|
|
|
self.assertNotEqual(first["launchFingerprint"], second["launchFingerprint"])
|
|
self.assertNotEqual(first["title"], second["title"])
|
|
|
|
|
|
class GitIdentityTests(unittest.TestCase):
|
|
def test_registered_worktree_passes_and_git_symlink_impostor_fails(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
base = Path(temporary).resolve()
|
|
project = base / "project"
|
|
project.mkdir()
|
|
subprocess.run(
|
|
["/usr/bin/git", "init", "-q", str(project)],
|
|
check=True,
|
|
)
|
|
|
|
def registered(git: Path, root: Path) -> set[Path]:
|
|
return {project}
|
|
|
|
# v0.19 起 capture_worktree_identity 不再接收 allowedWorktrees 白名单;
|
|
# worktree 合法性由 git worktree 注册表 + 同 common-dir 约束保证。
|
|
with mock.patch.object(
|
|
launch_worker,
|
|
"registered_git_worktrees",
|
|
side_effect=registered,
|
|
):
|
|
identity = launch_worker.capture_worktree_identity(
|
|
project,
|
|
str(project),
|
|
)
|
|
self.assertEqual(identity["path"], str(project))
|
|
|
|
impostor = base / "impostor"
|
|
impostor.mkdir()
|
|
(impostor / ".git").symlink_to(project / ".git")
|
|
with self.assertRaisesRegex(launch_worker.LaunchError, "symlink"):
|
|
launch_worker.capture_worktree_identity(
|
|
project,
|
|
str(impostor),
|
|
)
|
|
|
|
def test_unregistered_worktree_is_rejected(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
base = Path(temporary).resolve()
|
|
project = base / "project"
|
|
subprocess.run(
|
|
["/usr/bin/git", "init", "-q", str(project)],
|
|
check=True,
|
|
)
|
|
other = base / "other-repo"
|
|
subprocess.run(
|
|
["/usr/bin/git", "init", "-q", str(other)],
|
|
check=True,
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
launch_worker.LaunchError,
|
|
"未出现在 git worktree list",
|
|
):
|
|
launch_worker.capture_worktree_identity(
|
|
project,
|
|
str(other),
|
|
)
|
|
|
|
|
|
class BootstrapProtocolTests(unittest.TestCase):
|
|
def test_launcher_uses_shared_protocol_version(self) -> None:
|
|
self.assertEqual(
|
|
launch_worker.PROTOCOL_VERSION,
|
|
worker_profiles.LAUNCH_PROTOCOL_VERSION,
|
|
)
|
|
|
|
def test_orca_command_contains_only_fixed_bootstrap_and_opaque_id(self) -> None:
|
|
first_id = "a" * 64
|
|
second_id = "b" * 64
|
|
first = shlex.split(launch_worker.build_bootstrap_command(first_id))
|
|
second = shlex.split(launch_worker.build_bootstrap_command(second_id))
|
|
|
|
self.assertEqual(first[0], "exec")
|
|
self.assertEqual(first[2], "-I")
|
|
self.assertEqual(first[4:6], ["_bootstrap", "--launch-id"])
|
|
self.assertEqual(first[-1], first_id)
|
|
self.assertEqual(first[:-1], second[:-1])
|
|
self.assertEqual(second[-1], second_id)
|
|
|
|
def test_bootstrap_proof_is_nonce_and_plan_bound(self) -> None:
|
|
launch_id = "a" * 64
|
|
fingerprint = "sha256:" + "b" * 64
|
|
nonce = "c" * 64
|
|
proof = launch_worker.bootstrap_ready_proof(
|
|
launch_id,
|
|
fingerprint,
|
|
nonce,
|
|
)
|
|
|
|
self.assertRegex(proof, r"^sha256:[0-9a-f]{64}$")
|
|
self.assertNotEqual(
|
|
proof,
|
|
launch_worker.bootstrap_ready_proof(
|
|
launch_id,
|
|
fingerprint,
|
|
"d" * 64,
|
|
),
|
|
)
|
|
self.assertNotEqual(
|
|
proof,
|
|
launch_worker.bootstrap_ready_proof(
|
|
launch_id,
|
|
"sha256:" + "e" * 64,
|
|
nonce,
|
|
),
|
|
)
|
|
|
|
def test_spoofed_ready_state_without_proof_is_rejected(self) -> None:
|
|
with (
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"read_record",
|
|
return_value={"state": "bootstrap-ready", "bootstrapProof": "forged"},
|
|
),
|
|
self.assertRaisesRegex(launch_worker.LaunchError, "proof"),
|
|
):
|
|
launch_worker.wait_for_bootstrap(
|
|
"a" * 64,
|
|
"sha256:" + "b" * 64,
|
|
)
|
|
|
|
def test_bootstrap_executes_exact_argv_without_shell_after_authorization(self) -> None:
|
|
launch_id = "a" * 64
|
|
nonce = "b" * 64
|
|
project = REPO_ROOT
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
executable = Path(temporary.name) / "codex"
|
|
executable.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
executable.chmod(0o700)
|
|
plan = plan_for(project, executable)
|
|
record = {
|
|
"state": "prepared",
|
|
"expiresAt": launch_worker.format_timestamp(
|
|
launch_worker.utc_now()
|
|
+ launch_worker.timedelta(seconds=30)
|
|
),
|
|
"plan": plan,
|
|
"authorizationHash": launch_worker.bootstrap_authorization_hash(
|
|
launch_id,
|
|
plan["launchFingerprint"],
|
|
nonce,
|
|
),
|
|
}
|
|
|
|
@contextlib.contextmanager
|
|
def fake_locked_record(_launch_id: str):
|
|
yield record
|
|
|
|
process = mock.Mock(pid=1234)
|
|
process.wait.return_value = 0
|
|
fake_stdin = io.StringIO(nonce + "\n")
|
|
with (
|
|
mock.patch.object(launch_worker, "locked_record", fake_locked_record),
|
|
mock.patch.object(launch_worker, "build_plan", return_value=plan),
|
|
mock.patch.object(launch_worker, "assert_identity_current"),
|
|
mock.patch.object(
|
|
launch_worker.select,
|
|
"select",
|
|
return_value=([fake_stdin], [], []),
|
|
),
|
|
mock.patch.object(launch_worker.sys, "stdin", fake_stdin),
|
|
mock.patch.object(launch_worker.subprocess, "Popen", return_value=process) as popen,
|
|
):
|
|
self.assertEqual(launch_worker.bootstrap_worker(launch_id), 0)
|
|
|
|
popen.assert_called_once_with(
|
|
plan["requested"]["argv"],
|
|
shell=False,
|
|
cwd=plan["worktree"]["path"],
|
|
env=mock.ANY,
|
|
)
|
|
self.assertEqual(
|
|
record["bootstrapProof"],
|
|
launch_worker.bootstrap_ready_proof(
|
|
launch_id,
|
|
plan["launchFingerprint"],
|
|
nonce,
|
|
),
|
|
)
|
|
self.assertEqual(record["state"], "closed")
|
|
|
|
def test_bootstrap_cleanup_wins_before_agent_spawn(self) -> None:
|
|
launch_id = "a" * 64
|
|
nonce = "b" * 64
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
executable = Path(temporary.name) / "codex"
|
|
executable.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
executable.chmod(0o700)
|
|
plan = plan_for(REPO_ROOT, executable)
|
|
record = {
|
|
"state": "prepared",
|
|
"expiresAt": launch_worker.format_timestamp(
|
|
launch_worker.utc_now()
|
|
+ launch_worker.timedelta(seconds=30)
|
|
),
|
|
"plan": plan,
|
|
"authorizationHash": launch_worker.bootstrap_authorization_hash(
|
|
launch_id,
|
|
plan["launchFingerprint"],
|
|
nonce,
|
|
),
|
|
}
|
|
|
|
@contextlib.contextmanager
|
|
def fake_locked_record(_launch_id: str):
|
|
yield record
|
|
|
|
fake_stdin = io.StringIO(nonce + "\n")
|
|
|
|
def parent_marks_cleanup(*_args, **_kwargs):
|
|
record.update(
|
|
state="indeterminate",
|
|
error="parent requested cleanup",
|
|
cleanup={
|
|
"confirmed": False,
|
|
"reconcileRequired": True,
|
|
},
|
|
)
|
|
return ([fake_stdin], [], [])
|
|
|
|
with (
|
|
mock.patch.object(launch_worker, "locked_record", fake_locked_record),
|
|
mock.patch.object(launch_worker, "build_plan", return_value=plan),
|
|
mock.patch.object(launch_worker, "assert_identity_current"),
|
|
mock.patch.object(
|
|
launch_worker.select,
|
|
"select",
|
|
side_effect=parent_marks_cleanup,
|
|
),
|
|
mock.patch.object(launch_worker.sys, "stdin", fake_stdin),
|
|
mock.patch.object(launch_worker.subprocess, "Popen") as popen,
|
|
contextlib.redirect_stderr(io.StringIO()),
|
|
):
|
|
self.assertEqual(launch_worker.bootstrap_worker(launch_id), 1)
|
|
|
|
popen.assert_not_called()
|
|
self.assertEqual(record["state"], "indeterminate")
|
|
self.assertTrue(record["cleanup"]["reconcileRequired"])
|
|
|
|
|
|
class OrcaCreateTests(unittest.TestCase):
|
|
def test_orca_control_json_requires_explicit_ok_true(self) -> None:
|
|
valid = json.dumps({"ok": True, "result": {}})
|
|
with mock.patch.object(launch_worker, "run_text", return_value=valid):
|
|
self.assertTrue(
|
|
launch_worker.run_json(["orca", "status", "--json"], "orca status")[
|
|
"ok"
|
|
]
|
|
)
|
|
|
|
unsafe = [
|
|
json.dumps({"result": {}}),
|
|
json.dumps({"ok": None, "result": {}}),
|
|
json.dumps({"ok": 1, "result": {}}),
|
|
]
|
|
for output in unsafe:
|
|
with (
|
|
self.subTest(output=output),
|
|
mock.patch.object(launch_worker, "run_text", return_value=output),
|
|
self.assertRaisesRegex(launch_worker.LaunchError, "ok=true"),
|
|
):
|
|
launch_worker.run_json(
|
|
["orca", "status", "--json"],
|
|
"orca status",
|
|
)
|
|
|
|
def test_create_response_must_have_certain_handle(self) -> None:
|
|
valid = subprocess.CompletedProcess(
|
|
["orca"],
|
|
0,
|
|
stdout=json.dumps(
|
|
{
|
|
"ok": True,
|
|
"result": {"terminal": {"handle": "term-1"}},
|
|
"_meta": {"runtimeId": "runtime-1"},
|
|
}
|
|
),
|
|
stderr="",
|
|
)
|
|
with mock.patch.object(launch_worker, "run_process", return_value=valid):
|
|
response, handle = launch_worker.run_orca_create(["orca"])
|
|
self.assertEqual(handle, "term-1")
|
|
self.assertTrue(response["ok"])
|
|
|
|
unsafe_results = [
|
|
subprocess.CompletedProcess(["orca"], 1, stdout="", stderr="failed"),
|
|
subprocess.CompletedProcess(["orca"], 0, stdout="{", stderr=""),
|
|
subprocess.CompletedProcess(
|
|
["orca"],
|
|
0,
|
|
stdout=json.dumps({"ok": True, "result": {}}),
|
|
stderr="",
|
|
),
|
|
subprocess.CompletedProcess(
|
|
["orca"],
|
|
0,
|
|
stdout=json.dumps(
|
|
{"result": {"terminal": {"handle": "term-1"}}}
|
|
),
|
|
stderr="",
|
|
),
|
|
]
|
|
for completed in unsafe_results:
|
|
with (
|
|
self.subTest(completed=completed),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_process",
|
|
return_value=completed,
|
|
),
|
|
self.assertRaises(launch_worker.IndeterminateLaunch),
|
|
):
|
|
launch_worker.run_orca_create(["orca"])
|
|
|
|
def test_create_transport_and_decode_failures_are_indeterminate(self) -> None:
|
|
failures = (
|
|
UnicodeDecodeError("utf-8", b"\xff", 0, 1, "invalid start byte"),
|
|
subprocess.SubprocessError("transport failed"),
|
|
KeyboardInterrupt(),
|
|
RecursionError("response nesting too deep"),
|
|
)
|
|
for failure in failures:
|
|
with (
|
|
self.subTest(failure=type(failure).__name__),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_process",
|
|
side_effect=failure,
|
|
),
|
|
self.assertRaisesRegex(
|
|
launch_worker.IndeterminateLaunch,
|
|
"可能已创建终端",
|
|
),
|
|
):
|
|
launch_worker.run_orca_create(["orca"])
|
|
|
|
def test_indeterminate_create_records_identity_and_does_not_retry(self) -> None:
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
executable = Path(temporary.name) / "codex"
|
|
executable.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
executable.chmod(0o700)
|
|
plan = plan_for(REPO_ROOT, executable)
|
|
updates: list[tuple[str, dict]] = []
|
|
|
|
def capture_update(launch_id: str, **changes):
|
|
updates.append((launch_id, changes))
|
|
return changes
|
|
|
|
with (
|
|
mock.patch.object(
|
|
launch_worker.secrets,
|
|
"token_hex",
|
|
side_effect=["a" * 64, "b" * 64],
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"create_record",
|
|
return_value=Path("/tmp/ack-record.json"),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"resolve_executable",
|
|
return_value=Path("/trusted/orca"),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_json",
|
|
return_value={"ok": True, "_meta": {"runtimeId": "runtime-1"}},
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_orca_create",
|
|
side_effect=launch_worker.IndeterminateLaunch("lost response"),
|
|
) as create,
|
|
mock.patch.object(launch_worker, "assert_identity_current"),
|
|
mock.patch.object(launch_worker, "update_record", side_effect=capture_update),
|
|
):
|
|
with self.assertRaisesRegex(
|
|
launch_worker.IndeterminateLaunch,
|
|
"launchId=" + "a" * 64,
|
|
):
|
|
launch_worker.launch_with_orca(plan)
|
|
|
|
self.assertEqual(create.call_count, 1)
|
|
self.assertTrue(
|
|
any(change.get("state") == "indeterminate" for _, change in updates)
|
|
)
|
|
|
|
def test_indeterminate_create_record_failure_does_not_mask_reconcile(self) -> None:
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
executable = Path(temporary.name) / "codex"
|
|
executable.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
executable.chmod(0o700)
|
|
plan = plan_for(REPO_ROOT, executable)
|
|
|
|
with (
|
|
mock.patch.object(
|
|
launch_worker.secrets,
|
|
"token_hex",
|
|
side_effect=["a" * 64, "b" * 64],
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"create_record",
|
|
return_value=Path("/tmp/ack-record.json"),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"resolve_executable",
|
|
return_value=Path("/trusted/orca"),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_json",
|
|
return_value={"ok": True, "_meta": {"runtimeId": "runtime-1"}},
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_orca_create",
|
|
side_effect=launch_worker.IndeterminateLaunch("lost response"),
|
|
),
|
|
mock.patch.object(launch_worker, "assert_identity_current"),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"update_record",
|
|
side_effect=PermissionError("record denied"),
|
|
),
|
|
self.assertRaisesRegex(
|
|
launch_worker.IndeterminateLaunch,
|
|
"reconcile required",
|
|
) as raised,
|
|
):
|
|
launch_worker.launch_with_orca(plan)
|
|
|
|
detail = str(raised.exception)
|
|
self.assertIn("launchId=" + "a" * 64, detail)
|
|
self.assertIn("record=/tmp/ack-record.json", detail)
|
|
self.assertIn("record denied", detail)
|
|
|
|
def test_close_requires_durable_structured_confirmation(self) -> None:
|
|
valid = subprocess.CompletedProcess(
|
|
["orca"],
|
|
0,
|
|
stdout=json.dumps(
|
|
{
|
|
"ok": True,
|
|
"result": {
|
|
"close": {
|
|
"handle": "term-1",
|
|
"tabId": "tab-1",
|
|
"closeMode": "tab",
|
|
}
|
|
},
|
|
"_meta": {"runtimeId": "runtime-1"},
|
|
}
|
|
),
|
|
stderr="",
|
|
)
|
|
with mock.patch.object(
|
|
launch_worker,
|
|
"run_process",
|
|
return_value=valid,
|
|
) as run_process:
|
|
confirmation = launch_worker.run_orca_close(
|
|
Path("/trusted/orca"),
|
|
"term-1",
|
|
"runtime-1",
|
|
)
|
|
|
|
self.assertEqual(
|
|
confirmation,
|
|
{
|
|
"runtimeId": "runtime-1",
|
|
"handle": "term-1",
|
|
"tabId": "tab-1",
|
|
"closeMode": "tab",
|
|
},
|
|
)
|
|
close_argv = run_process.call_args.args[0]
|
|
self.assertEqual(
|
|
close_argv,
|
|
[
|
|
"/trusted/orca",
|
|
"terminal",
|
|
"close",
|
|
"--terminal",
|
|
"term-1",
|
|
"--tab",
|
|
"--json",
|
|
],
|
|
)
|
|
|
|
unsafe_results = [
|
|
subprocess.CompletedProcess(
|
|
["orca"],
|
|
1,
|
|
stdout="",
|
|
stderr="close rejected",
|
|
),
|
|
subprocess.CompletedProcess(
|
|
["orca"],
|
|
0,
|
|
stdout="{",
|
|
stderr="",
|
|
),
|
|
subprocess.CompletedProcess(
|
|
["orca"],
|
|
0,
|
|
stdout=json.dumps(
|
|
{
|
|
"ok": True,
|
|
"result": {"close": {"handle": "term-1"}},
|
|
"_meta": {"runtimeId": "runtime-1"},
|
|
}
|
|
),
|
|
stderr="",
|
|
),
|
|
subprocess.CompletedProcess(
|
|
["orca"],
|
|
0,
|
|
stdout=json.dumps(
|
|
{
|
|
"ok": True,
|
|
"result": {
|
|
"close": {
|
|
"handle": "term-other",
|
|
"tabId": "tab-1",
|
|
"closeMode": "tab",
|
|
}
|
|
},
|
|
"_meta": {"runtimeId": "runtime-1"},
|
|
}
|
|
),
|
|
stderr="",
|
|
),
|
|
subprocess.CompletedProcess(
|
|
["orca"],
|
|
0,
|
|
stdout=json.dumps(
|
|
{
|
|
"ok": True,
|
|
"result": {
|
|
"close": {
|
|
"handle": "term-1",
|
|
"tabId": "tab-1",
|
|
"closeMode": "tab",
|
|
}
|
|
},
|
|
"_meta": {"runtimeId": "runtime-other"},
|
|
}
|
|
),
|
|
stderr="",
|
|
),
|
|
]
|
|
for completed in unsafe_results:
|
|
with (
|
|
self.subTest(completed=completed),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_process",
|
|
return_value=completed,
|
|
),
|
|
self.assertRaises(launch_worker.IndeterminateLaunch),
|
|
):
|
|
launch_worker.run_orca_close(
|
|
Path("/trusted/orca"),
|
|
"term-1",
|
|
"runtime-1",
|
|
)
|
|
|
|
def test_post_handle_failure_keeps_original_error_when_close_is_confirmed(
|
|
self,
|
|
) -> None:
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
executable = Path(temporary.name) / "codex"
|
|
executable.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
executable.chmod(0o700)
|
|
plan = plan_for(REPO_ROOT, executable)
|
|
updates: list[tuple[str, dict]] = []
|
|
|
|
def capture_update(launch_id: str, **changes):
|
|
updates.append((launch_id, changes))
|
|
return changes
|
|
|
|
with (
|
|
mock.patch.object(
|
|
launch_worker.secrets,
|
|
"token_hex",
|
|
side_effect=["a" * 64, "b" * 64],
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"create_record",
|
|
return_value=Path("/tmp/ack-record.json"),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"resolve_executable",
|
|
return_value=Path("/trusted/orca"),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_json",
|
|
side_effect=[
|
|
{"ok": True, "_meta": {"runtimeId": "runtime-1"}},
|
|
launch_worker.LaunchError("show failed"),
|
|
],
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_orca_create",
|
|
return_value=(
|
|
{
|
|
"ok": True,
|
|
"result": {"terminal": {"handle": "term-1"}},
|
|
"_meta": {"runtimeId": "runtime-1"},
|
|
},
|
|
"term-1",
|
|
),
|
|
),
|
|
mock.patch.object(launch_worker, "assert_identity_current"),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_orca_close",
|
|
return_value={
|
|
"runtimeId": "runtime-1",
|
|
"handle": "term-1",
|
|
"tabId": "tab-1",
|
|
"closeMode": "tab",
|
|
},
|
|
) as close,
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"update_record",
|
|
side_effect=capture_update,
|
|
),
|
|
self.assertRaisesRegex(launch_worker.LaunchError, "show failed"),
|
|
):
|
|
launch_worker.launch_with_orca(plan)
|
|
|
|
close.assert_called_once_with(
|
|
Path("/trusted/orca"),
|
|
"term-1",
|
|
"runtime-1",
|
|
)
|
|
self.assertEqual(updates[-1][1]["state"], "failed")
|
|
self.assertEqual(updates[-1][1]["error"], "show failed")
|
|
self.assertTrue(updates[-1][1]["cleanup"]["confirmed"])
|
|
|
|
def test_post_handle_close_failure_is_indeterminate_and_requires_reconcile(
|
|
self,
|
|
) -> None:
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
executable = Path(temporary.name) / "codex"
|
|
executable.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
executable.chmod(0o700)
|
|
plan = plan_for(REPO_ROOT, executable)
|
|
updates: list[tuple[str, dict]] = []
|
|
|
|
def capture_update(launch_id: str, **changes):
|
|
updates.append((launch_id, changes))
|
|
return changes
|
|
|
|
with (
|
|
mock.patch.object(
|
|
launch_worker.secrets,
|
|
"token_hex",
|
|
side_effect=["a" * 64, "b" * 64],
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"create_record",
|
|
return_value=Path("/tmp/ack-record.json"),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"resolve_executable",
|
|
return_value=Path("/trusted/orca"),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_json",
|
|
side_effect=[
|
|
{"ok": True, "_meta": {"runtimeId": "runtime-1"}},
|
|
launch_worker.LaunchError("show failed"),
|
|
],
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_orca_create",
|
|
return_value=(
|
|
{
|
|
"ok": True,
|
|
"result": {"terminal": {"handle": "term-1"}},
|
|
"_meta": {"runtimeId": "runtime-1"},
|
|
},
|
|
"term-1",
|
|
),
|
|
),
|
|
mock.patch.object(launch_worker, "assert_identity_current"),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_orca_close",
|
|
side_effect=launch_worker.IndeterminateLaunch("close timeout"),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"update_record",
|
|
side_effect=capture_update,
|
|
),
|
|
self.assertRaisesRegex(
|
|
launch_worker.IndeterminateLaunch,
|
|
"reconcile required",
|
|
),
|
|
):
|
|
launch_worker.launch_with_orca(plan)
|
|
|
|
self.assertEqual(updates[-1][1]["state"], "indeterminate")
|
|
self.assertTrue(updates[-1][1]["cleanup"]["reconcileRequired"])
|
|
self.assertIn("show failed", updates[-1][1]["error"])
|
|
self.assertIn("close timeout", updates[-1][1]["error"])
|
|
|
|
def test_pending_record_failure_does_not_skip_post_handle_close(self) -> None:
|
|
updates: list[dict] = []
|
|
|
|
def fail_pending(_launch_id: str, **changes):
|
|
updates.append(changes)
|
|
if len(updates) == 1:
|
|
raise PermissionError("pending denied")
|
|
return changes
|
|
|
|
close_confirmation = {
|
|
"runtimeId": "runtime-1",
|
|
"handle": "term-1",
|
|
"tabId": "tab-1",
|
|
"closeMode": "tab",
|
|
}
|
|
with post_handle_failure_context(
|
|
update_side_effect=fail_pending,
|
|
close_side_effect=close_confirmation,
|
|
) as (plan, close, _update):
|
|
with self.assertRaisesRegex(launch_worker.LaunchError, "show failed"):
|
|
launch_worker.launch_with_orca(plan)
|
|
|
|
close.assert_called_once_with(
|
|
Path("/trusted/orca"),
|
|
"term-1",
|
|
"runtime-1",
|
|
)
|
|
self.assertEqual(len(updates), 2)
|
|
self.assertEqual(updates[-1]["state"], "failed")
|
|
|
|
def test_close_and_reconcile_record_failures_stay_indeterminate(self) -> None:
|
|
updates: list[dict] = []
|
|
|
|
def fail_reconcile(_launch_id: str, **changes):
|
|
updates.append(changes)
|
|
if len(updates) == 2:
|
|
raise OSError("reconcile write failed")
|
|
return changes
|
|
|
|
with post_handle_failure_context(
|
|
update_side_effect=fail_reconcile,
|
|
close_side_effect=launch_worker.IndeterminateLaunch("close timeout"),
|
|
) as (plan, close, _update):
|
|
with self.assertRaisesRegex(
|
|
launch_worker.IndeterminateLaunch,
|
|
"reconcile required",
|
|
) as raised:
|
|
launch_worker.launch_with_orca(plan)
|
|
|
|
close.assert_called_once()
|
|
detail = str(raised.exception)
|
|
self.assertIn("close timeout", detail)
|
|
self.assertIn("reconcile write failed", detail)
|
|
self.assertIn("launchId=" + "a" * 64, detail)
|
|
self.assertIn("record=/tmp/ack-record.json", detail)
|
|
|
|
def test_confirmed_close_with_failed_state_write_is_indeterminate(self) -> None:
|
|
updates: list[dict] = []
|
|
|
|
def fail_final(_launch_id: str, **changes):
|
|
updates.append(changes)
|
|
if len(updates) == 2:
|
|
raise PermissionError("final write denied")
|
|
return changes
|
|
|
|
close_confirmation = {
|
|
"runtimeId": "runtime-1",
|
|
"handle": "term-1",
|
|
"tabId": "tab-1",
|
|
"closeMode": "tab",
|
|
}
|
|
with post_handle_failure_context(
|
|
update_side_effect=fail_final,
|
|
close_side_effect=close_confirmation,
|
|
) as (plan, close, _update):
|
|
with self.assertRaisesRegex(
|
|
launch_worker.IndeterminateLaunch,
|
|
"reconcile required",
|
|
) as raised:
|
|
launch_worker.launch_with_orca(plan)
|
|
|
|
close.assert_called_once()
|
|
detail = str(raised.exception)
|
|
self.assertIn("close confirmed", detail)
|
|
self.assertIn("final write denied", detail)
|
|
self.assertIn("launchId=" + "a" * 64, detail)
|
|
self.assertIn("record=/tmp/ack-record.json", detail)
|
|
|
|
def test_successful_launch_binds_terminal_before_nonce_and_receipt(self) -> None:
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
executable = Path(temporary.name) / "codex"
|
|
executable.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
executable.chmod(0o700)
|
|
plan = plan_for(REPO_ROOT, executable)
|
|
launch_id = "a" * 64
|
|
nonce = "b" * 64
|
|
metadata = {
|
|
"handle": "term-1",
|
|
"incarnationId": "incarnation-1",
|
|
"worktreePath": str(REPO_ROOT),
|
|
"connected": True,
|
|
"writable": True,
|
|
}
|
|
|
|
def fake_run_json(argv: list[str], _label: str) -> dict:
|
|
if argv[1] == "status":
|
|
return {"ok": True, "_meta": {"runtimeId": "runtime-1"}}
|
|
if argv[1:3] == ["terminal", "show"]:
|
|
return {
|
|
"ok": True,
|
|
"result": {"terminal": metadata},
|
|
"_meta": {"runtimeId": "runtime-1"},
|
|
}
|
|
if argv[1:3] == ["terminal", "send"]:
|
|
self.assertEqual(argv[argv.index("--text") + 1], nonce)
|
|
return {"ok": True, "_meta": {"runtimeId": "runtime-1"}}
|
|
self.fail(f"unexpected Orca call: {argv}")
|
|
|
|
with (
|
|
mock.patch.object(
|
|
launch_worker.secrets,
|
|
"token_hex",
|
|
side_effect=[launch_id, nonce],
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"create_record",
|
|
return_value=Path("/tmp/ack-record.json"),
|
|
),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"resolve_executable",
|
|
return_value=Path("/trusted/orca"),
|
|
),
|
|
mock.patch.object(launch_worker, "run_json", side_effect=fake_run_json),
|
|
mock.patch.object(
|
|
launch_worker,
|
|
"run_orca_create",
|
|
return_value=(
|
|
{
|
|
"ok": True,
|
|
"result": {"terminal": {"handle": "term-1"}},
|
|
"_meta": {"runtimeId": "runtime-1"},
|
|
},
|
|
"term-1",
|
|
),
|
|
) as create,
|
|
mock.patch.object(launch_worker, "assert_identity_current"),
|
|
mock.patch.object(launch_worker, "update_record"),
|
|
mock.patch.object(launch_worker, "wait_for_bootstrap") as wait,
|
|
):
|
|
receipt = launch_worker.launch_with_orca(plan)
|
|
|
|
expected_proof = launch_worker.bootstrap_ready_proof(
|
|
launch_id,
|
|
plan["launchFingerprint"],
|
|
nonce,
|
|
)
|
|
wait.assert_called_once_with(launch_id, expected_proof)
|
|
create_argv = create.call_args.args[0]
|
|
command = create_argv[create_argv.index("--command") + 1]
|
|
command_tokens = shlex.split(command)
|
|
self.assertEqual(command_tokens[-1], launch_id)
|
|
self.assertNotIn(plan["requested"]["model"], command_tokens)
|
|
self.assertNotIn(plan["taskId"], command_tokens)
|
|
self.assertEqual(receipt["binding"]["handle"], "term-1")
|
|
self.assertEqual(
|
|
receipt["receiptHash"],
|
|
worker_profiles.receipt_hash(receipt),
|
|
)
|
|
|
|
|
|
class RecordSecurityTests(unittest.TestCase):
|
|
def test_bootstrap_update_cannot_overwrite_cleanup_reconciliation(self) -> None:
|
|
record = {
|
|
"state": "indeterminate",
|
|
"error": "reconcile required",
|
|
"cleanup": {
|
|
"confirmed": False,
|
|
"reconcileRequired": True,
|
|
},
|
|
}
|
|
|
|
@contextlib.contextmanager
|
|
def fake_locked_record(_launch_id: str):
|
|
yield record
|
|
|
|
with mock.patch.object(
|
|
launch_worker,
|
|
"locked_record",
|
|
fake_locked_record,
|
|
):
|
|
result = launch_worker.update_record(
|
|
"a" * 64,
|
|
preserve_cleanup_state=True,
|
|
state="closed",
|
|
error=None,
|
|
)
|
|
|
|
self.assertEqual(result["state"], "indeterminate")
|
|
self.assertEqual(result["error"], "reconcile required")
|
|
self.assertTrue(result["cleanup"]["reconcileRequired"])
|
|
|
|
def test_record_requires_exact_private_mode_and_single_link(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
launch_id = "a" * 64
|
|
record = root / f"{launch_id}.json"
|
|
record.write_text(
|
|
json.dumps({"launchId": launch_id}),
|
|
encoding="utf-8",
|
|
)
|
|
record.chmod(0o644)
|
|
with self.assertRaisesRegex(launch_worker.LaunchError, "0600"):
|
|
launch_worker._read_record_file(record)
|
|
|
|
record.chmod(0o600)
|
|
hardlink = root / "hardlink.json"
|
|
os.link(record, hardlink)
|
|
with self.assertRaisesRegex(launch_worker.LaunchError, "hard link"):
|
|
launch_worker._read_record_file(record)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|