Skip to content
Merged
28 changes: 17 additions & 11 deletions sdk/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,20 @@ if the bootstrap has been sitting for a while.

## Status

Phase 1 of CORE-58 — the hello-world closed loop: `Sandbox` /
`AsyncSandbox` create/connect/list, `kill`/`pause`/`info` (`pause` and
the paused-sandbox reconnect path are wire-complete but reject with an
unimplemented error until the daemon's CORE-21 lands), `commands.run`
(foreground result + background handle with streamed output,
`wait_for_exit`, `kill`), and whole-file `files` read/write. Deferred:
PTY, `ports`, `wait_for_port`/`wait_for_log`, stdin, filesystem path
verbs (stat/list/mkdir/...), `Template` statics, `events()`,
`set_lifecycle`, the capabilities handshake, and the SDK-side default
idle-reaping policy (design decision 4 — applied once the daemon
enforces the lifecycle knobs).
Phase 2a of CORE-58, on both the sync and async surfaces. Shipped:

| Surface | Notes |
| --- | --- |
| `Sandbox` create/connect/list, `kill`/`pause`/`info` | pause/resume are live daemon-side (CORE-21); data-plane calls auto-resume a paused sandbox |
| `commands.run` | foreground result + background handle; `stdin=str\|bytes` (write-then-close) or `stdin=True` (keep open); `pty=PtySize(cols, rows)` |
| `CommandHandle` | streamed `output` with transparent offset-resume across stream death (bounded retries → `ConnectionLostError`), `wait_for_exit`, `kill`, `write_stdin`/`close_stdin`/`stdin_status` (offset-idempotent), `resize` |
| `commands.get(id)` | re-attach a handle by execution id (stdin cursor seeded from the daemon) |
| `sandbox.events()` | typed lifecycle events, keepalives filtered; sync reads genuinely block; a mid-stream drop is `ConnectionLostError` |
| `sandbox.set_lifecycle()` | tri-state: omitted (`UNCHANGED`) = unchanged, `None` = restore default (as on `create`), value = replace |
| `arcbox.capabilities()` | daemon handshake (version/protocol/features/nested virt), cached per client |
| `files` | whole-file read/write |

Deferred to phase 2b (the daemon answers Unimplemented today):
`commands.list()` (ListExecutions), `ports` + `wait_for_port`,
filesystem path verbs (stat/list/mkdir/...), `Template` statics, and the
SDK-side default idle-reaping policy (design decision 4).
1 change: 1 addition & 0 deletions sdk/python/scripts/gen_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"AsyncCommands": "Commands",
"AsyncCommandHandle": "CommandHandle",
"AsyncOutputStream": "OutputStream",
"AsyncEventStream": "EventStream",
"AsyncFiles": "Files",
"AsyncConnectClient": "ConnectClient",
"AsyncServerStream": "ServerStream",
Expand Down
22 changes: 20 additions & 2 deletions sdk/python/src/arcbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,30 @@
from arcbox._async._client import AsyncConnectClient
from arcbox._async.commands import AsyncCommandHandle, AsyncCommands, AsyncOutputStream
from arcbox._async.files import AsyncFiles
from arcbox._async.sandbox import AsyncArcBox, AsyncSandbox
from arcbox._async.sandbox import AsyncArcBox, AsyncEventStream, AsyncSandbox
from arcbox._connection import Connection
from arcbox._sync._client import ConnectClient
from arcbox._sync.commands import CommandHandle, Commands, OutputStream
from arcbox._sync.files import Files
from arcbox._sync.sandbox import ArcBox, Sandbox
from arcbox._sync.sandbox import ArcBox, EventStream, Sandbox
from arcbox._types import (
MAX_FILE_BYTES,
UNCHANGED,
Capabilities,
CommandResult,
IdlePolicy,
NestedVirtCapability,
OutputChannel,
OutputChunk,
PtySize,
SandboxEvent,
SandboxEventKind,
SandboxInfo,
SandboxState,
SandboxSummary,
SignalName,
StdinStatus,
Unchanged,
)

try:
Expand All @@ -39,27 +47,37 @@

__all__ = [
"MAX_FILE_BYTES",
"UNCHANGED",
"ArcBox",
"AsyncArcBox",
"AsyncCommandHandle",
"AsyncCommands",
"AsyncConnectClient",
"AsyncEventStream",
"AsyncFiles",
"AsyncOutputStream",
"AsyncSandbox",
"Capabilities",
"CommandHandle",
"CommandResult",
"Commands",
"ConnectClient",
"Connection",
"EventStream",
"Files",
"IdlePolicy",
"NestedVirtCapability",
"OutputChannel",
"OutputChunk",
"OutputStream",
"PtySize",
"Sandbox",
"SandboxEvent",
"SandboxEventKind",
"SandboxInfo",
"SandboxState",
"SandboxSummary",
"SignalName",
"StdinStatus",
"Unchanged",
]
11 changes: 7 additions & 4 deletions sdk/python/src/arcbox/_async/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
end_stream_error,
unary_error,
)
from arcbox.errors import ArcBoxError, InvalidArgumentError
from arcbox.errors import ArcBoxError, ConnectionLostError, InvalidArgumentError

if TYPE_CHECKING:
from collections.abc import AsyncIterator, Iterable, Mapping
Expand Down Expand Up @@ -171,8 +171,9 @@ async def client_stream(
if not ended:
# Without the terminal frame a truncated body is
# indistinguishable from success — never report one as
# completed (WriteFile rides this path).
raise ArcBoxError("the stream ended without an EndStreamResponse")
# completed (WriteFile rides this path). Truncation means the
# connection died mid-body: a stream-death error.
raise ConnectionLostError("the stream ended without an EndStreamResponse")
if message is None:
raise ArcBoxError("the stream ended without a response message")
return message
Expand Down Expand Up @@ -252,4 +253,6 @@ async def _messages(self) -> AsyncIterator[M]:
message = self._response_type()
message.ParseFromString(payload)
yield message
raise ArcBoxError("the stream ended without an EndStreamResponse")
# Truncation means the connection died mid-body: a stream-death
# error, which resumable consumers treat as retryable.
raise ConnectionLostError("the stream ended without an EndStreamResponse")
Loading
Loading