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:
Thales Maciel 2026-03-12 02:31:57 -03:00
parent 2de31306b6
commit 3f8293ad24
28 changed files with 3265 additions and 81 deletions

View file

@ -22,7 +22,7 @@ Networking: tun=yes ip_forward=yes
```bash
$ uvx --from pyro-mcp pyro env list
Catalog version: 2.4.0
Catalog version: 2.5.0
debian:12 [installed|not installed] Debian 12 environment with Git preinstalled for common agent workflows.
debian:12-base [installed|not installed] Minimal Debian 12 environment for shell and core Unix tooling.
debian:12-build [installed|not installed] Debian 12 environment with Git and common build tools preinstalled.
@ -72,6 +72,7 @@ deterministic structured result.
$ uvx --from pyro-mcp pyro demo
$ uvx --from pyro-mcp pyro workspace create debian:12 --seed-path ./repo
$ uvx --from pyro-mcp pyro workspace sync push WORKSPACE_ID ./changes
$ uvx --from pyro-mcp pyro workspace shell open WORKSPACE_ID
$ uvx --from pyro-mcp pyro mcp serve
```
@ -96,13 +97,24 @@ $ uvx --from pyro-mcp pyro workspace sync push WORKSPACE_ID ./changes --dest src
$ uvx --from pyro-mcp pyro workspace exec WORKSPACE_ID -- cat src/note.txt
hello from synced workspace
[workspace-exec] workspace_id=... sequence=1 cwd=/workspace execution_mode=guest_vsock exit_code=0 duration_ms=...
$ uvx --from pyro-mcp pyro workspace shell open WORKSPACE_ID
[workspace-shell-open] workspace_id=... shell_id=... state=running cwd=/workspace cols=120 rows=30 execution_mode=guest_vsock
$ uvx --from pyro-mcp pyro workspace shell write WORKSPACE_ID SHELL_ID --input 'pwd'
[workspace-shell-write] workspace_id=... shell_id=... state=running cwd=/workspace cols=120 rows=30 execution_mode=guest_vsock
$ uvx --from pyro-mcp pyro workspace shell read WORKSPACE_ID SHELL_ID
/workspace
[workspace-shell-read] workspace_id=... shell_id=... state=running cursor=0 next_cursor=... truncated=False execution_mode=guest_vsock
```
Use `--seed-path` when the workspace should start from a host directory or a local
`.tar` / `.tar.gz` / `.tgz` archive instead of an empty `/workspace`. Use
`pyro workspace sync push` when you need to import later host-side changes into a started
workspace. Sync is non-atomic in `2.4.0`; if it fails partway through, delete and recreate the
workspace.
workspace. Sync is non-atomic in `2.5.0`; if it fails partway through, delete and recreate the
workspace. Use `pyro workspace exec` for one-shot commands and `pyro workspace shell *` when you
need a persistent interactive PTY session in that same workspace.
Example output:

View file

@ -83,7 +83,7 @@ uvx --from pyro-mcp pyro env list
Expected output:
```bash
Catalog version: 2.4.0
Catalog version: 2.5.0
debian:12 [installed|not installed] Debian 12 environment with Git preinstalled for common agent workflows.
debian:12-base [installed|not installed] Minimal Debian 12 environment for shell and core Unix tooling.
debian:12-build [installed|not installed] Debian 12 environment with Git and common build tools preinstalled.
@ -176,6 +176,7 @@ After the CLI path works, you can move on to:
- persistent workspaces: `pyro workspace create debian:12 --seed-path ./repo`
- live workspace updates: `pyro workspace sync push WORKSPACE_ID ./changes`
- interactive shells: `pyro workspace shell open WORKSPACE_ID`
- MCP: `pyro mcp serve`
- Python SDK: `from pyro_mcp import Pyro`
- Demos: `pyro demo` or `pyro demo --network`
@ -188,6 +189,10 @@ Use `pyro workspace ...` when you need repeated commands in one sandbox instead
pyro workspace create debian:12 --seed-path ./repo
pyro workspace sync push WORKSPACE_ID ./changes --dest src
pyro workspace exec WORKSPACE_ID -- cat src/note.txt
pyro workspace shell open WORKSPACE_ID
pyro workspace shell write WORKSPACE_ID SHELL_ID --input 'pwd'
pyro workspace shell read WORKSPACE_ID SHELL_ID
pyro workspace shell close WORKSPACE_ID SHELL_ID
pyro workspace logs WORKSPACE_ID
pyro workspace delete WORKSPACE_ID
```
@ -196,8 +201,9 @@ Workspace commands default to the persistent `/workspace` directory inside the g
the identifier programmatically, use `--json` and read the `workspace_id` field. Use `--seed-path`
when the workspace should start from a host directory or a local `.tar` / `.tar.gz` / `.tgz`
archive. Use `pyro workspace sync push` for later host-side changes to a started workspace. Sync
is non-atomic in `2.4.0`; if it fails partway through, delete and recreate the workspace from its
seed.
is non-atomic in `2.5.0`; if it fails partway through, delete and recreate the workspace from its
seed. Use `pyro workspace exec` for one-shot commands and `pyro workspace shell *` when you need
an interactive PTY that survives across separate calls.
## Contributor Clone

View file

@ -31,6 +31,7 @@ Recommended surface:
- `vm_run`
- `workspace_create(seed_path=...)` + `workspace_sync_push` + `workspace_exec` when the agent needs persistent workspace state
- `open_shell` / `read_shell` / `write_shell` when the agent needs an interactive PTY inside that workspace
Canonical example:
@ -66,6 +67,7 @@ Recommended default:
- `Pyro.run_in_vm(...)`
- `Pyro.create_workspace(seed_path=...)` + `Pyro.push_workspace_sync(...)` + `Pyro.exec_workspace(...)` when repeated workspace commands are required
- `Pyro.open_shell(...)` + `Pyro.write_shell(...)` + `Pyro.read_shell(...)` when the agent needs an interactive PTY inside the workspace
Lifecycle note:
@ -76,12 +78,14 @@ Lifecycle note:
`/workspace` that starts from host content
- use `push_workspace_sync(...)` when later host-side changes need to be imported into that
running workspace without recreating it
- use `open_shell(...)` when the agent needs interactive shell state instead of one-shot execs
Examples:
- [examples/python_run.py](../examples/python_run.py)
- [examples/python_lifecycle.py](../examples/python_lifecycle.py)
- [examples/python_workspace.py](../examples/python_workspace.py)
- [examples/python_shell.py](../examples/python_shell.py)
## Agent Framework Wrappers

View file

@ -22,6 +22,11 @@ Top-level commands:
- `pyro workspace create`
- `pyro workspace sync push`
- `pyro workspace exec`
- `pyro workspace shell open`
- `pyro workspace shell read`
- `pyro workspace shell write`
- `pyro workspace shell signal`
- `pyro workspace shell close`
- `pyro workspace status`
- `pyro workspace logs`
- `pyro workspace delete`
@ -50,6 +55,7 @@ Behavioral guarantees:
- `pyro workspace create --seed-path PATH` seeds `/workspace` from a host directory or a local `.tar` / `.tar.gz` / `.tgz` archive before the workspace is returned.
- `pyro workspace sync push WORKSPACE_ID SOURCE_PATH [--dest WORKSPACE_PATH]` imports later host-side directory or archive content into a started workspace.
- `pyro workspace exec` runs in the persistent `/workspace` for that workspace and does not auto-clean.
- `pyro workspace shell *` manages persistent PTY sessions inside a started workspace.
- `pyro workspace logs` returns persisted command history for that workspace until `pyro workspace delete`.
- Workspace create/status results expose `workspace_seed` metadata describing how `/workspace` was initialized.
@ -70,6 +76,11 @@ Supported public entrypoints:
- `Pyro.create_vm(...)`
- `Pyro.create_workspace(...)`
- `Pyro.push_workspace_sync(workspace_id, source_path, *, dest="/workspace")`
- `Pyro.open_shell(workspace_id, *, cwd="/workspace", cols=120, rows=30)`
- `Pyro.read_shell(workspace_id, shell_id, *, cursor=0, max_chars=65536)`
- `Pyro.write_shell(workspace_id, shell_id, *, input, append_newline=True)`
- `Pyro.signal_shell(workspace_id, shell_id, *, signal_name="INT")`
- `Pyro.close_shell(workspace_id, shell_id)`
- `Pyro.start_vm(vm_id)`
- `Pyro.exec_vm(vm_id, *, command, timeout_seconds=30)`
- `Pyro.exec_workspace(workspace_id, *, command, timeout_seconds=30)`
@ -93,6 +104,11 @@ Stable public method names:
- `create_vm(...)`
- `create_workspace(...)`
- `push_workspace_sync(workspace_id, source_path, *, dest="/workspace")`
- `open_shell(workspace_id, *, cwd="/workspace", cols=120, rows=30)`
- `read_shell(workspace_id, shell_id, *, cursor=0, max_chars=65536)`
- `write_shell(workspace_id, shell_id, *, input, append_newline=True)`
- `signal_shell(workspace_id, shell_id, *, signal_name="INT")`
- `close_shell(workspace_id, shell_id)`
- `start_vm(vm_id)`
- `exec_vm(vm_id, *, command, timeout_seconds=30)`
- `exec_workspace(workspace_id, *, command, timeout_seconds=30)`
@ -116,6 +132,9 @@ Behavioral defaults:
- `Pyro.push_workspace_sync(...)` imports later host-side directory or archive content into a started workspace.
- `Pyro.exec_vm(...)` runs one command and auto-cleans that VM after the exec completes.
- `Pyro.exec_workspace(...)` runs one command in the persistent workspace and leaves it alive.
- `Pyro.open_shell(...)` opens a persistent PTY shell attached to one started workspace.
- `Pyro.read_shell(...)` reads merged text output from that shell by cursor.
- `Pyro.write_shell(...)`, `Pyro.signal_shell(...)`, and `Pyro.close_shell(...)` operate on that persistent shell session.
## MCP Contract
@ -140,6 +159,11 @@ Persistent workspace tools:
- `workspace_create`
- `workspace_sync_push`
- `workspace_exec`
- `shell_open`
- `shell_read`
- `shell_write`
- `shell_signal`
- `shell_close`
- `workspace_status`
- `workspace_logs`
- `workspace_delete`
@ -154,6 +178,7 @@ Behavioral defaults:
- `workspace_sync_push` imports later host-side directory or archive content into a started workspace, with an optional `dest` under `/workspace`.
- `vm_exec` runs one command and auto-cleans that VM after the exec completes.
- `workspace_exec` runs one command in a persistent `/workspace` and leaves the workspace alive.
- `shell_open`, `shell_read`, `shell_write`, `shell_signal`, and `shell_close` manage persistent PTY shells inside a started workspace.
## Versioning Rule

View file

@ -2,11 +2,12 @@
This roadmap turns the agent-workspace vision into release-sized milestones.
Current baseline is `2.4.0`:
Current baseline is `2.5.0`:
- workspace persistence exists and the public surface is now workspace-first
- host crossing currently covers create-time seeding and later sync push
- no shell, export, diff, service, snapshot, reset, or secrets contract exists yet
- persistent PTY shell sessions exist alongside one-shot `workspace exec`
- no export, diff, service, snapshot, reset, or secrets contract exists yet
Locked roadmap decisions:
@ -26,7 +27,7 @@ also expected to update:
## Milestones
1. [`2.4.0` Workspace Contract Pivot](task-workspace-ga/2.4.0-workspace-contract-pivot.md) - Done
2. [`2.5.0` PTY Shell Sessions](task-workspace-ga/2.5.0-pty-shell-sessions.md)
2. [`2.5.0` PTY Shell Sessions](task-workspace-ga/2.5.0-pty-shell-sessions.md) - Done
3. [`2.6.0` Structured Export And Baseline Diff](task-workspace-ga/2.6.0-structured-export-and-baseline-diff.md)
4. [`2.7.0` Service Lifecycle And Typed Readiness](task-workspace-ga/2.7.0-service-lifecycle-and-typed-readiness.md)
5. [`2.8.0` Named Snapshots And Reset](task-workspace-ga/2.8.0-named-snapshots-and-reset.md)

View file

@ -1,5 +1,7 @@
# `2.5.0` PTY Shell Sessions
Status: Done
## Goal
Add persistent interactive shells so an agent can inhabit a workspace instead