chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:39:17 +08:00
commit 4ed4e9ff99
1368 changed files with 334957 additions and 0 deletions
@@ -0,0 +1 @@
+632
View File
@@ -0,0 +1,632 @@
from __future__ import annotations
import io
import os
import tarfile
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
import pytest
from agents import function_tool
from agents.editor import ApplyPatchOperation
from agents.sandbox.capabilities import Capability
from agents.sandbox.entries import (
AzureBlobMount,
Dir,
File,
GCSMount,
GitRepo,
InContainerMountStrategy,
LocalDir,
LocalFile,
R2Mount,
RcloneMountPattern,
S3Mount,
)
from agents.sandbox.errors import (
ApplyPatchPathError,
InvalidManifestPathError,
WorkspaceReadNotFoundError,
)
from agents.sandbox.files import EntryKind
from agents.sandbox.manifest import Manifest
from agents.sandbox.session.base_sandbox_session import BaseSandboxSession
from agents.sandbox.workspace_paths import SandboxPathGrant
from agents.tool import Tool
BUILTIN_MANIFEST_ENTRY_TYPES = {
"azure_blob_mount",
"dir",
"file",
"gcs_mount",
"git_repo",
"local_dir",
"local_file",
"r2_mount",
"s3_mount",
}
DURABLE_WORKSPACE_TEXTS = {
"inline.txt": "inline file v1\n",
"delete_me.txt": "delete me v1\n",
"tree/nested.txt": "nested file v1\n",
"copied_file.txt": "local file source v1\n",
"copied_dir/child.txt": "local dir child v1\n",
"copied_dir/nested/grandchild.txt": "local dir grandchild v1\n",
"repo/README.md": "mock git repo readme v1\n",
"repo/pkg/module.py": "VALUE = 'mock git module v1'\n",
}
EPHEMERAL_WORKSPACE_TEXTS = {
"tree/ephemeral.txt": "ephemeral file v1\n",
}
MOUNT_WORKSPACE_TEXTS = {
"mounts/s3/.mock-rclone-mounted": "mock rclone mount\n",
"mounts/gcs/.mock-rclone-mounted": "mock rclone mount\n",
"mounts/r2/.mock-rclone-mounted": "mock rclone mount\n",
"mounts/azure/.mock-rclone-mounted": "mock rclone mount\n",
}
ARCHIVE_WORKSPACE_TEXTS = {
"archive_dir/hello.txt": "hello from tar archive\n",
}
RUNTIME_WORKSPACE_TEXTS = {
"runtime_note.txt": "runtime note v1\n",
}
PATCHED_WORKSPACE_TEXTS = {
"inline.txt": "inline file v2\n",
"created_by_patch.txt": "created by patch",
}
RESTORED_WORKSPACE_DIRS = {
"archive_dir",
"copied_dir",
"copied_dir/nested",
"mounts",
"mounts/azure",
"mounts/gcs",
"mounts/r2",
"mounts/s3",
"repo",
"repo/pkg",
"tree",
}
RESTORED_WORKSPACE_FILES = {
"archive_dir/hello.txt",
"bundle.tar",
"copied_dir/child.txt",
"copied_dir/nested/grandchild.txt",
"copied_file.txt",
"created_by_patch.txt",
"inline.txt",
"mounts/azure/.mock-rclone-mounted",
"mounts/gcs/.mock-rclone-mounted",
"mounts/r2/.mock-rclone-mounted",
"mounts/s3/.mock-rclone-mounted",
"repo/README.md",
"repo/pkg/module.py",
"runtime_note.txt",
"tree/ephemeral.txt",
"tree/nested.txt",
}
SANDBOX_INTERNAL_WORKSPACE_DIR_PREFIXES = (".sandbox-rclone-config",)
MOCK_TOOL_NAMES = (
"blobfuse2",
"cp",
"fusermount3",
"git",
"mount-s3",
"pkill",
"rclone",
"rm",
"umount",
)
@dataclass(frozen=True)
class MockExternalTools:
bin_dir: Path
log_path: Path
def calls(self) -> list[str]:
if not self.log_path.exists():
return []
return self.log_path.read_text(encoding="utf-8").splitlines()
def install_mock_external_tools(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> MockExternalTools:
bin_dir = tmp_path / "mock-bin"
bin_dir.mkdir()
log_path = tmp_path / "mock-tool-calls.tsv"
log_path.write_text("", encoding="utf-8")
for name in MOCK_TOOL_NAMES:
tool_path = bin_dir / name
tool_path.write_text(_mock_tool_script(), encoding="utf-8")
tool_path.chmod(0o755)
existing_path = os.environ.get("PATH", "")
monkeypatch.setenv("SANDBOX_INTEGRATION_TOOL_LOG", str(log_path))
monkeypatch.setenv("PATH", f"{bin_dir}{os.pathsep}{existing_path}")
return MockExternalTools(bin_dir=bin_dir, log_path=log_path)
def create_local_sources(tmp_path: Path) -> Path:
source_root = tmp_path / "manifest-sources"
local_dir = source_root / "local-dir"
nested_dir = local_dir / "nested"
nested_dir.mkdir(parents=True)
(source_root / "local-file.txt").write_text("local file source v1\n", encoding="utf-8")
(local_dir / "child.txt").write_text("local dir child v1\n", encoding="utf-8")
(nested_dir / "grandchild.txt").write_text("local dir grandchild v1\n", encoding="utf-8")
return source_root
def build_manifest_with_all_entry_types(*, workspace_root: Path, source_root: Path) -> Manifest:
return Manifest(
root=str(workspace_root),
extra_path_grants=(SandboxPathGrant(path=str(source_root)),),
entries={
"inline.txt": File(content=DURABLE_WORKSPACE_TEXTS["inline.txt"].encode("utf-8")),
"delete_me.txt": File(content=DURABLE_WORKSPACE_TEXTS["delete_me.txt"].encode("utf-8")),
"tree": Dir(
children={
"nested.txt": File(
content=DURABLE_WORKSPACE_TEXTS["tree/nested.txt"].encode("utf-8")
),
"ephemeral.txt": File(
content=EPHEMERAL_WORKSPACE_TEXTS["tree/ephemeral.txt"].encode("utf-8"),
ephemeral=True,
),
}
),
"copied_file.txt": LocalFile(
src=source_root / "local-file.txt",
),
"copied_dir": LocalDir(
src=source_root / "local-dir",
),
"repo": GitRepo(repo="openai/mock-sandbox-fixture", ref="main"),
"mounts/s3": S3Mount(
bucket="s3-bucket",
access_key_id="s3-access-key-id",
secret_access_key="s3-secret-access-key",
mount_strategy=InContainerMountStrategy(pattern=RcloneMountPattern()),
),
"mounts/gcs": GCSMount(
bucket="gcs-bucket",
access_id="gcs-access-id",
secret_access_key="gcs-secret-access-key",
mount_strategy=InContainerMountStrategy(pattern=RcloneMountPattern()),
),
"mounts/r2": R2Mount(
bucket="r2-bucket",
account_id="r2-account-id",
access_key_id="r2-access-key-id",
secret_access_key="r2-secret-access-key",
mount_strategy=InContainerMountStrategy(pattern=RcloneMountPattern()),
),
"mounts/azure": AzureBlobMount(
account="azure-account",
container="azure-container",
account_key="azure-account-key",
mount_strategy=InContainerMountStrategy(pattern=RcloneMountPattern()),
),
},
)
def manifest_entry_types(manifest: Manifest) -> set[str]:
return {entry.type for _path, entry in manifest.iter_entries()}
async def read_workspace_text(session: BaseSandboxSession, path: str | Path) -> str:
handle = await session.read(Path(path))
try:
payload = handle.read()
finally:
handle.close()
if isinstance(payload, str):
return payload
if isinstance(payload, bytes):
return payload.decode("utf-8")
raise TypeError(f"Unexpected workspace read payload type: {type(payload).__name__}")
async def write_workspace_text(session: BaseSandboxSession, path: str | Path, text: str) -> None:
await session.write(Path(path), io.BytesIO(text.encode("utf-8")))
async def assert_workspace_texts(
session: BaseSandboxSession,
expected: Mapping[str, str],
) -> None:
actual = {path: await read_workspace_text(session, path) for path in expected}
assert actual == dict(expected)
async def assert_manifest_materialized(session: BaseSandboxSession) -> None:
assert manifest_entry_types(session.state.manifest) == BUILTIN_MANIFEST_ENTRY_TYPES
await assert_workspace_texts(session, DURABLE_WORKSPACE_TEXTS)
await assert_workspace_texts(session, EPHEMERAL_WORKSPACE_TEXTS)
await assert_workspace_texts(session, MOUNT_WORKSPACE_TEXTS)
async def assert_lifecycle_patch_state(session: BaseSandboxSession) -> None:
await assert_workspace_texts(
session,
{
**{
path: text
for path, text in DURABLE_WORKSPACE_TEXTS.items()
if path != "delete_me.txt"
},
**RUNTIME_WORKSPACE_TEXTS,
**PATCHED_WORKSPACE_TEXTS,
},
)
await assert_workspace_missing(session, "delete_me.txt")
async def assert_restored_lifecycle_state(session: BaseSandboxSession) -> None:
assert manifest_entry_types(session.state.manifest) == BUILTIN_MANIFEST_ENTRY_TYPES
await assert_lifecycle_patch_state(session)
await assert_workspace_texts(session, ARCHIVE_WORKSPACE_TEXTS)
await assert_workspace_texts(session, EPHEMERAL_WORKSPACE_TEXTS)
await assert_workspace_texts(session, MOUNT_WORKSPACE_TEXTS)
await assert_restored_workspace_tree(session)
async def assert_workspace_missing(session: BaseSandboxSession, path: str) -> None:
try:
await read_workspace_text(session, path)
except WorkspaceReadNotFoundError:
return
raise AssertionError(f"Expected workspace path to be missing: {path}")
async def assert_workspace_escape_blocked(session: BaseSandboxSession) -> None:
for path in ("../outside.txt", "/tmp/sandbox-outside.txt"):
await _assert_read_blocked(session, path)
await _assert_write_blocked(session, path)
await _assert_patch_blocked(session, path)
await _assert_symlink_escape_blocked(session)
async def assert_restored_workspace_tree(session: BaseSandboxSession) -> None:
actual_dirs, actual_files = await _workspace_tree(session)
assert actual_dirs == RESTORED_WORKSPACE_DIRS, {
"actual_dirs": sorted(actual_dirs),
"expected_dirs": sorted(RESTORED_WORKSPACE_DIRS),
}
assert actual_files == RESTORED_WORKSPACE_FILES, {
"actual_files": sorted(actual_files),
"expected_files": sorted(RESTORED_WORKSPACE_FILES),
}
def lifecycle_patch_operations() -> list[ApplyPatchOperation | dict[str, object]]:
return [
ApplyPatchOperation(
type="update_file",
path="inline.txt",
diff="@@\n-inline file v1\n+inline file v2\n",
),
ApplyPatchOperation(
type="create_file",
path="created_by_patch.txt",
diff="+created by patch\n",
),
ApplyPatchOperation(
type="delete_file",
path="delete_me.txt",
),
]
class SandboxFileCapability(Capability):
type: str = "sandbox-file"
def __init__(self) -> None:
super().__init__(type="sandbox-file")
def tools(self) -> list[Tool]:
@function_tool(name_override="write_file", failure_error_function=None)
async def write_file(path: str, content: str) -> str:
if self.session is None:
raise AssertionError("SandboxFileCapability is not bound to a session.")
await write_workspace_text(self.session, path, content)
return f"wrote {path}"
@function_tool(name_override="read_file", failure_error_function=None)
async def read_file(path: str) -> str:
if self.session is None:
raise AssertionError("SandboxFileCapability is not bound to a session.")
return await read_workspace_text(self.session, path)
return [write_file, read_file]
class SandboxLifecycleProbeCapability(Capability):
type: str = "sandbox-lifecycle-probe"
pty_process_id: int | None = None
def __init__(self) -> None:
super().__init__(type="sandbox-lifecycle-probe")
def tools(self) -> list[Tool]:
@function_tool(name_override="assert_manifest_materialized", failure_error_function=None)
async def assert_manifest_materialized_tool() -> str:
session = self._require_session()
await assert_manifest_materialized(session)
return "manifest materialized"
@function_tool(name_override="apply_lifecycle_patch", failure_error_function=None)
async def apply_lifecycle_patch() -> str:
session = self._require_session()
result = await session.apply_patch(lifecycle_patch_operations())
assert result == "Done!"
await assert_lifecycle_patch_state(session)
return "lifecycle patch applied"
@function_tool(name_override="assert_workspace_escape_blocked", failure_error_function=None)
async def assert_workspace_escape_blocked_tool() -> str:
session = self._require_session()
await assert_workspace_escape_blocked(session)
return "workspace escape blocked"
@function_tool(name_override="extract_lifecycle_archive", failure_error_function=None)
async def extract_lifecycle_archive() -> str:
session = self._require_session()
await session.extract("bundle.tar", _tar_bytes(ARCHIVE_WORKSPACE_TEXTS))
await assert_workspace_texts(session, ARCHIVE_WORKSPACE_TEXTS)
return "archive extracted"
@function_tool(name_override="start_lifecycle_pty", failure_error_function=None)
async def start_lifecycle_pty() -> str:
session = self._require_session()
pty = await session.pty_exec_start(
"sh",
"-c",
"printf 'ready\\n'; while IFS= read -r line; do printf 'got:%s\\n' \"$line\"; done",
shell=False,
tty=True,
yield_time_s=0.25,
)
assert pty.process_id is not None
output = pty.output.decode("utf-8", errors="replace").replace("\r\n", "\n")
assert output == "ready\n"
self.pty_process_id = pty.process_id
update = await session.pty_write_stdin(
session_id=pty.process_id,
chars="hello pty\n",
yield_time_s=0.25,
)
write_output = update.output.decode("utf-8", errors="replace").replace("\r\n", "\n")
assert write_output == "hello pty\ngot:hello pty\n"
assert update.process_id == pty.process_id
assert update.exit_code is None
return "pty started and echoed stdin"
@function_tool(name_override="assert_restored_lifecycle_state", failure_error_function=None)
async def assert_restored_lifecycle_state_tool() -> str:
session = self._require_session()
await assert_restored_lifecycle_state(session)
return "restored lifecycle state verified"
return [
assert_manifest_materialized_tool,
apply_lifecycle_patch,
assert_workspace_escape_blocked_tool,
extract_lifecycle_archive,
start_lifecycle_pty,
assert_restored_lifecycle_state_tool,
]
def _require_session(self) -> BaseSandboxSession:
if self.session is None:
raise AssertionError("SandboxLifecycleProbeCapability is not bound to a session.")
return self.session
async def _assert_read_blocked(session: BaseSandboxSession, path: str) -> None:
try:
await read_workspace_text(session, path)
except InvalidManifestPathError:
return
raise AssertionError(f"Expected workspace read to be blocked: {path}")
async def _assert_write_blocked(session: BaseSandboxSession, path: str) -> None:
try:
await write_workspace_text(session, path, "outside write\n")
except InvalidManifestPathError:
return
raise AssertionError(f"Expected workspace write to be blocked: {path}")
async def _assert_patch_blocked(session: BaseSandboxSession, path: str) -> None:
try:
await session.apply_patch(
ApplyPatchOperation(
type="create_file",
path=path,
diff="+outside patch\n",
)
)
except (ApplyPatchPathError, InvalidManifestPathError):
return
raise AssertionError(f"Expected workspace patch to be blocked: {path}")
async def _assert_symlink_escape_blocked(session: BaseSandboxSession) -> None:
workspace_root = Path(session.state.manifest.root)
outside_path = workspace_root.parent / "symlink-outside.txt"
symlink_path = workspace_root / "symlink_escape.txt"
outside_path.write_text("outside symlink target\n", encoding="utf-8")
symlink_path.symlink_to(outside_path)
try:
await _assert_read_blocked(session, "symlink_escape.txt")
await _assert_write_blocked(session, "symlink_escape.txt")
await _assert_patch_blocked(session, "symlink_escape.txt")
finally:
symlink_path.unlink(missing_ok=True)
outside_path.unlink(missing_ok=True)
def _tar_bytes(members: Mapping[str, str]) -> io.BytesIO:
archive = io.BytesIO()
with tarfile.open(fileobj=archive, mode="w") as tar:
for name, text in members.items():
payload = text.encode("utf-8")
info = tarfile.TarInfo(name)
info.size = len(payload)
tar.addfile(info, io.BytesIO(payload))
archive.seek(0)
return archive
async def _workspace_tree(session: BaseSandboxSession) -> tuple[set[str], set[str]]:
root = Path(session.state.manifest.root).resolve(strict=False)
dirs: set[str] = set()
files: set[str] = set()
async def collect(path: Path) -> None:
for entry in await session.ls(path):
rel_path = _entry_workspace_rel_path(entry.path, root)
if entry.kind == EntryKind.DIRECTORY:
if _is_sandbox_internal_workspace_dir(rel_path):
continue
dirs.add(rel_path)
await collect(Path(rel_path))
elif entry.kind == EntryKind.FILE:
files.add(rel_path)
else:
raise AssertionError(
f"Unexpected workspace entry kind for {rel_path}: {entry.kind}"
)
await collect(Path("."))
return dirs, files
def _entry_workspace_rel_path(entry_path: str, root: Path) -> str:
path = Path(entry_path)
if path.is_absolute():
path = path.resolve(strict=False).relative_to(root)
return path.as_posix()
def _is_sandbox_internal_workspace_dir(path: str) -> bool:
return any(
path == prefix or path.startswith(f"{prefix}/")
for prefix in SANDBOX_INTERNAL_WORKSPACE_DIR_PREFIXES
)
def _mock_tool_script() -> str:
return """#!/bin/sh
set -eu
tool=$(basename "$0")
log_path="${SANDBOX_INTEGRATION_TOOL_LOG:-}"
if [ -n "$log_path" ]; then
{
printf "%s" "$tool"
for arg in "$@"; do
printf "\\t%s" "$arg"
done
printf "\\n"
} >> "$log_path"
fi
case "$tool" in
git)
exit 0
;;
cp)
dest=""
for arg in "$@"; do
dest="$arg"
done
mkdir -p "$dest/pkg"
printf "mock git repo readme v1\\n" > "$dest/README.md"
printf "VALUE = 'mock git module v1'\\n" > "$dest/pkg/module.py"
exit 0
;;
rclone)
if [ "${1:-}" = "mount" ] && [ -n "${3:-}" ]; then
mkdir -p "$3"
printf "mock rclone mount\\n" > "$3/.mock-rclone-mounted"
fi
exit 0
;;
blobfuse2)
if [ "${1:-}" = "mount" ]; then
dest=""
for arg in "$@"; do
dest="$arg"
done
mkdir -p "$dest"
printf "mock blobfuse mount\\n" > "$dest/.mock-blobfuse-mounted"
fi
exit 0
;;
mount-s3)
dest=""
for arg in "$@"; do
dest="$arg"
done
mkdir -p "$dest"
printf "mock mount-s3 mount\\n" > "$dest/.mock-mount-s3-mounted"
exit 0
;;
rm)
recursive=""
for arg in "$@"; do
case "$arg" in
-rf|-fr|-r|-f|--)
if [ "$arg" = "-rf" ] || [ "$arg" = "-fr" ] || [ "$arg" = "-r" ]; then
recursive="-r"
fi
;;
"$HOME"|"$HOME"/*)
if [ -n "$recursive" ]; then
/bin/rm -rf -- "$arg"
else
/bin/rm -f -- "$arg"
fi
;;
/*)
;;
*..*)
;;
*)
if [ -n "$recursive" ]; then
/bin/rm -rf -- "$arg"
else
/bin/rm -f -- "$arg"
fi
;;
esac
done
exit 0
;;
fusermount3|umount|pkill)
exit 0
;;
esac
exit 0
"""
@@ -0,0 +1,59 @@
from __future__ import annotations
import json
from collections.abc import Mapping, Sequence
from typing import Any
from agents.items import TResponseOutputItem
from tests.fake_model import FakeModel
from tests.test_responses import get_final_output_message, get_function_tool_call
__test__ = False
class TestModel(FakeModel):
"""Reusable queued model for sandbox integration tests."""
__test__ = False
def queue_turn(self, *items: TResponseOutputItem) -> None:
self.set_next_output(list(items))
def queue_function_call(
self,
name: str,
arguments: Mapping[str, Any] | str | None = None,
*,
call_id: str | None = None,
namespace: str | None = None,
) -> None:
self.queue_turn(
get_function_tool_call(
name,
_serialize_arguments(arguments),
call_id=call_id,
namespace=namespace,
)
)
def queue_function_calls(
self,
calls: Sequence[tuple[str, Mapping[str, Any] | str | None, str | None]],
) -> None:
self.queue_turn(
*[
get_function_tool_call(name, _serialize_arguments(arguments), call_id=call_id)
for name, arguments, call_id in calls
]
)
def queue_final_output(self, output: str) -> None:
self.queue_turn(get_final_output_message(output))
def _serialize_arguments(arguments: Mapping[str, Any] | str | None) -> str:
if arguments is None:
return "{}"
if isinstance(arguments, str):
return arguments
return json.dumps(arguments)
@@ -0,0 +1,183 @@
from __future__ import annotations
from collections.abc import Sequence
from pathlib import Path
import pytest
from agents import RunConfig, Runner, function_tool
from agents.items import RunItem, ToolCallOutputItem
from agents.run_state import RunState
from agents.sandbox import SandboxAgent, SandboxRunConfig
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
from agents.sandbox.session import CallbackSink, Instrumentation, SandboxSessionEvent
from tests.sandbox.integration_tests._helpers import (
SandboxFileCapability,
SandboxLifecycleProbeCapability,
build_manifest_with_all_entry_types,
create_local_sources,
install_mock_external_tools,
)
from tests.sandbox.integration_tests.test_model import TestModel
@pytest.mark.asyncio
async def test_runner_preserves_unix_local_lifecycle_state_across_pause_and_resume(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
install_mock_external_tools(monkeypatch, tmp_path)
source_root = create_local_sources(tmp_path)
manifest = build_manifest_with_all_entry_types(
workspace_root=Path("/workspace"),
source_root=source_root,
)
events: list[SandboxSessionEvent] = []
client = UnixLocalSandboxClient(
instrumentation=Instrumentation(
sinks=[CallbackSink(lambda event, _session: events.append(event), mode="sync")]
)
)
model = TestModel()
model.queue_function_call(
"assert_manifest_materialized",
{},
call_id="call_manifest_materialized",
)
model.queue_function_call(
"write_file",
{"path": "runtime_note.txt", "content": "runtime note v1\n"},
call_id="call_write_runtime_note",
)
model.queue_function_call(
"apply_lifecycle_patch",
{},
call_id="call_apply_lifecycle_patch",
)
model.queue_function_call(
"assert_workspace_escape_blocked",
{},
call_id="call_assert_workspace_escape_blocked",
)
model.queue_function_call(
"extract_lifecycle_archive",
{},
call_id="call_extract_lifecycle_archive",
)
model.queue_function_call(
"start_lifecycle_pty",
{},
call_id="call_start_lifecycle_pty",
)
model.queue_function_call("approval_tool", {}, call_id="call_approval")
@function_tool(name_override="approval_tool", needs_approval=True)
def approval_tool() -> str:
return "approved"
agent = SandboxAgent(
name="sandbox",
model=model,
instructions="Use the sandbox lifecycle tools.",
default_manifest=manifest,
tools=[approval_tool],
capabilities=[SandboxFileCapability(), SandboxLifecycleProbeCapability()],
)
first_run = await Runner.run(
agent,
"verify the UnixLocal sandbox lifecycle and wait for approval",
run_config=RunConfig(sandbox=SandboxRunConfig(client=client)),
)
assert _tool_outputs(first_run.new_items, agent=agent) == [
"manifest materialized",
"wrote runtime_note.txt",
"lifecycle patch applied",
"workspace escape blocked",
"archive extracted",
"pty started and echoed stdin",
]
assert len(first_run.interruptions) == 1
state = first_run.to_state()
assert state._sandbox is not None
assert state._sandbox["backend_id"] == "unix_local"
assert state._sandbox["current_agent_name"] == "sandbox"
session_state = state._sandbox["session_state"]
assert isinstance(session_state, dict)
snapshot = session_state["snapshot"]
assert isinstance(snapshot, dict)
assert snapshot["type"] == "local"
assert session_state["workspace_root_owned"] is True
assert session_state["workspace_root_ready"] is True
workspace_root = _session_state_manifest_root(session_state)
assert not workspace_root.exists()
assert _successful_event_count(events, op="stop") == 1
assert _successful_event_count(events, op="shutdown") == 1
resumed_model = TestModel()
resumed_model.queue_function_call(
"assert_restored_lifecycle_state",
{},
call_id="call_assert_restored_lifecycle_state",
)
resumed_model.queue_function_call(
"read_file",
{"path": "runtime_note.txt"},
call_id="call_read_runtime_note",
)
resumed_model.queue_final_output("done")
resumed_agent = SandboxAgent(
name="sandbox",
model=resumed_model,
instructions="Use the sandbox lifecycle tools.",
default_manifest=manifest,
tools=[approval_tool],
capabilities=[SandboxFileCapability(), SandboxLifecycleProbeCapability()],
)
restored_state = await RunState.from_json(resumed_agent, state.to_json())
restored_interruptions = restored_state.get_interruptions()
assert len(restored_interruptions) == 1
restored_state.approve(restored_interruptions[0])
resumed = await Runner.run(
resumed_agent,
restored_state,
run_config=RunConfig(sandbox=SandboxRunConfig(client=client)),
)
assert resumed.final_output == "done"
assert not workspace_root.exists()
assert _successful_event_count(events, op="stop") == 2
assert _successful_event_count(events, op="shutdown") == 2
assert _tool_outputs(resumed.new_items, agent=resumed_agent)[-3:] == [
"approved",
"restored lifecycle state verified",
"runtime note v1\n",
]
def _session_state_manifest_root(session_state: dict[str, object]) -> Path:
manifest = session_state["manifest"]
assert isinstance(manifest, dict)
root = manifest["root"]
assert isinstance(root, str)
return Path(root)
def _successful_event_count(events: list[SandboxSessionEvent], *, op: str) -> int:
return sum(
1
for event in events
if event.op == op and event.phase == "finish" and getattr(event, "ok", False) is True
)
def _tool_outputs(items: Sequence[RunItem], *, agent: SandboxAgent) -> list[str]:
outputs: list[str] = []
for item in items:
if isinstance(item, ToolCallOutputItem) and item.agent is agent:
assert isinstance(item.output, str)
outputs.append(item.output)
return outputs