Add persistent workspace shell sessions
Let agents inhabit a workspace across separate calls instead of only submitting one-shot execs. Add workspace shell open/read/write/signal/close across the CLI, Python SDK, and MCP server, with persisted shell records, a local PTY-backed mock implementation, and guest-agent support for real Firecracker workspaces. Mark the 2.5.0 roadmap milestone done, refresh docs/examples and the release metadata, and verify with uv lock, UV_CACHE_DIR=.uv-cache make check, and UV_CACHE_DIR=.uv-cache make dist-check.
This commit is contained in:
parent
2de31306b6
commit
3f8293ad24
28 changed files with 3265 additions and 81 deletions
|
|
@ -454,6 +454,73 @@ def test_workspace_sync_push_rejects_destination_outside_workspace(tmp_path: Pat
|
|||
manager.push_workspace_sync(workspace_id, source_path=source_dir, dest="../escape")
|
||||
|
||||
|
||||
def test_workspace_shell_lifecycle_and_rehydration(tmp_path: Path) -> None:
|
||||
manager = VmManager(
|
||||
backend_name="mock",
|
||||
base_dir=tmp_path / "vms",
|
||||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
created = manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
)
|
||||
workspace_id = str(created["workspace_id"])
|
||||
|
||||
opened = manager.open_shell(workspace_id)
|
||||
shell_id = str(opened["shell_id"])
|
||||
assert opened["state"] == "running"
|
||||
|
||||
manager.write_shell(workspace_id, shell_id, input_text="pwd")
|
||||
|
||||
output = ""
|
||||
deadline = time.time() + 5
|
||||
while time.time() < deadline:
|
||||
read = manager.read_shell(workspace_id, shell_id, cursor=0, max_chars=65536)
|
||||
output = str(read["output"])
|
||||
if "/workspace" in output:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
assert "/workspace" in output
|
||||
|
||||
manager_rehydrated = VmManager(
|
||||
backend_name="mock",
|
||||
base_dir=tmp_path / "vms",
|
||||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
second_opened = manager_rehydrated.open_shell(workspace_id)
|
||||
second_shell_id = str(second_opened["shell_id"])
|
||||
assert second_shell_id != shell_id
|
||||
|
||||
manager_rehydrated.write_shell(workspace_id, second_shell_id, input_text="printf 'ok\\n'")
|
||||
second_output = ""
|
||||
deadline = time.time() + 5
|
||||
while time.time() < deadline:
|
||||
read = manager_rehydrated.read_shell(
|
||||
workspace_id,
|
||||
second_shell_id,
|
||||
cursor=0,
|
||||
max_chars=65536,
|
||||
)
|
||||
second_output = str(read["output"])
|
||||
if "ok" in second_output:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
assert "ok" in second_output
|
||||
|
||||
logs = manager.logs_workspace(workspace_id)
|
||||
assert logs["count"] == 0
|
||||
|
||||
closed = manager.close_shell(workspace_id, shell_id)
|
||||
assert closed["closed"] is True
|
||||
with pytest.raises(ValueError, match="does not exist"):
|
||||
manager.read_shell(workspace_id, shell_id)
|
||||
|
||||
deleted = manager.delete_workspace(workspace_id)
|
||||
assert deleted["deleted"] is True
|
||||
with pytest.raises(ValueError, match="does not exist"):
|
||||
manager_rehydrated.read_shell(workspace_id, second_shell_id)
|
||||
|
||||
|
||||
def test_workspace_create_rejects_unsafe_seed_archive(tmp_path: Path) -> None:
|
||||
archive_path = tmp_path / "bad.tgz"
|
||||
with tarfile.open(archive_path, "w:gz") as archive:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue