Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/agent-snapshot-fork.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"neevai": minor
---

Agents can be snapshotted and forked. `client.agents.create_snapshot(id, params=None)` and `list_snapshots(id, page=None, limit=None)` snapshot the sandbox backing an agent and list those snapshots, with the same params and `Snapshot` shape as the sandbox calls. `client.agents.fork(id, name)` forks an agent into a new agent with the source's template and configuration; it returns the new `Agent`, which is `Provisioning` until `wait_until_ready()`, and the source keeps running. The `Agent` and `AsyncAgent` handles gain `snapshot(params=None, *, wait_until_ready=False, ...)`, `snapshots()` and `fork(name)`.

`CreateSnapshotParams` takes `snapshot_type`: `"filesystem"` captures /workspace only, without process memory, so it can be restored onto any sandbox image; `"full"` captures both.

A new `TooEarlyError` is raised for HTTP 425, such as calling an agent that is still provisioning. It was a plain `APIError` before.

The audit trail now also records every program that runs in a sandbox, unless its `execution_trace` is turned off: a Process Activity event (`class_uid` 1007) for a program starting or ending, with `process` and `exit_code`, and a Network Activity event (4001) for a connection it opened. These events have no `api`, so check `e.api` before reading `e.api.operation`. Every event carries `device` (the sandbox) and `cloud.org`. `actor.user.type` is now `User`, `API Key`, `System` or `Unknown`; the `actor_type` filter still takes `"user"`, `"api_key"` and `"system"`.
2 changes: 1 addition & 1 deletion .changeset/audit-ocsf-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"neevai": minor
---

The audit trail returns events instead of records. `sandbox.audit()`, `agent.audit()` and the matching `sandboxes` / `agents` calls now return `AuditTrail.data: list[AuditEvent]`, each event saying which call ran (`api.operation`), who made it (`actor.user`, with the API key in `credential_uid`), how it ended (`status`), what it touched (`resources`) and when (`time`, in milliseconds since the Unix epoch). `AuditTrail.records`, `AuditTrail.sandbox_id` and the `AuditRecord` model are removed.
The audit trail returns events instead of records. `sandbox.audit()`, `agent.audit()` and the matching `sandboxes` / `agents` calls now return `AuditTrail.data: list[AuditEvent]`, each event saying which call ran (`api.operation`), who made it (`actor.user`, with the API key in `programmatic_credentials`), how it ended (`status`), what it touched (`resources`) and when (`time`, in milliseconds since the Unix epoch). `AuditTrail.records`, `AuditTrail.sandbox_id` and the `AuditRecord` model are removed.

The audit calls take new filters: `operation` (one name or a list), `status` (`"success"` or `"failure"`), `actor_type` (`"user"`, `"api_key"`, `"system"`, or a list), `actor_uid` and `credential_uid`.

Expand Down
2 changes: 1 addition & 1 deletion .changeset/sandbox-replicas-removed.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

`Sandbox.replicas` and `AsyncSandbox.replicas` are removed; read `phase` instead, which is `"Paused"` while a sandbox is paused. The field is deprecated in the API and is now optional on the generated `Sandbox` model, so a sandbox response without it parses.

`snapshot_type` adds `"filesystem"` for a files-only snapshot that cannot be restored; a snapshot of that type no longer fails response validation.
`snapshot_type` adds `"filesystem"` for a files-only snapshot that can be restored onto any sandbox image; a snapshot of that type no longer fails response validation.
36 changes: 33 additions & 3 deletions docs/api-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Everything in `neevai.__all__`:
| `AuditEvent` / `AuditActor` / `AuditActorUser` / `AuditAPI` / `AuditResource` / `AuditProcess` / `AuditFile` / `AuditDstEndpoint` / `AuditDevice` | model | generated |
| `SandboxEgressRule` | model | generated |
| `SandboxEgressRules` | model | generated |
| `NeevAIError`, `APIConnectionError`, `APITimeoutError`, `APIError`, `BadRequestError`, `AuthenticationError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `PreconditionFailedError`, `RateLimitError`, `DeadlineExceededError`, `InternalServerError`, `ServiceUnavailableError` | exceptions | `errors.py` |
| `NeevAIError`, `APIConnectionError`, `APITimeoutError`, `APIError`, `BadRequestError`, `AuthenticationError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `PreconditionFailedError`, `TooEarlyError`, `RateLimitError`, `DeadlineExceededError`, `InternalServerError`, `ServiceUnavailableError` | exceptions | `errors.py` |

Types exported from `neevai.types.__all__`:

Expand Down Expand Up @@ -674,6 +674,27 @@ Pause and resume return updated `Agent` handles. Delete returns `None` (HTTP 204
progress. `rollback` rolls the agent's backing sandbox back in place to a snapshot from
the same project. Both return the updated `Agent`.

### `client.agents.create_snapshot(id, params=None, ...)` / `client.agents.list_snapshots(id, page=None, limit=None, ...)`

Snapshot the sandbox backing an agent, and list those snapshots. Same params and
`Snapshot` shape as `client.sandboxes.create_snapshot` / `.list_snapshots`; the
snapshot starts `Pending`. Wait on it with `client.sandboxes.wait_for_snapshot`, or
use `agent.snapshot(wait_until_ready=True)`.

### `client.agents.fork(id, name, ...)`

Forks an agent into a new agent with the source's template and configuration and its
own gateway token. Returns the new `Agent`, which is `Provisioning` until it is ready;
call `wait_until_ready()` on it before use. The source agent keeps running.

```python
forked = client.agents.fork(agent.id, "web-fork")
forked.wait_until_ready()
```

A paused or still-provisioning agent raises `ConflictError` (409) or
`TooEarlyError` (425).

### Agent preview ports

`client.agents.expose_port(id, port, org_id=None, project_id=None, *, slug=None)`,
Expand Down Expand Up @@ -783,6 +804,11 @@ Resolves the backing sandbox as a `Sandbox` handle via `Agents.get_sandbox()`.
Handle wrappers for `client.agents.keepalive` / `.rollback` / `.audit`; the first two
update the handle in place and return `self`.

### `agent.snapshot(params=None, *, wait_until_ready=False, timeout_ms=..., poll_interval_ms=...)` / `agent.snapshots()` / `agent.fork(name)`

Same as `sandbox.snapshot()` / `.snapshots()`, for the sandbox backing this agent.
`fork` returns a new `Agent` handle for the fork; this handle is unchanged.

### `agent.expose_port(port, *, slug=None)` / `agent.list_ports()` / `agent.revoke_port(port)` / `agent.get_url(port, ..., *, slug=None)`

Same signatures as the sandbox handle's preview-port methods.
Expand Down Expand Up @@ -1929,7 +1955,7 @@ for `egress_add` / `egress_remove`.
| `api.operation` | `str` | API Activity only (e.g. `pty_command`, `ssh`, `exec`, `process.start`, `fs.read`) |
| `status` | `str` | yes (`Success` or `Failure`) |
| `status_code` | `str \| None` | no (`ok`, `permission_denied`, `internal`) |
| `actor.user.type` | `User` \| `API Key` \| `System` \| `Unknown` | yes (`Unknown` on program and connection events) |
| `actor.user.type` | `User` \| `API Key` \| `System` \| `Unknown` | yes (`Unknown` only for a program the sandbox ran on its own, such as a background process) |
| `actor.user.uid` | `str` | yes (the user's or API key's id) |
| `actor.user.name` | `str \| None` | no |
| `actor.user.programmatic_credentials` | `list \| None` | no (the API key used, as `[0].uid`; absent for a user) |
Expand All @@ -1938,7 +1964,7 @@ for `egress_add` / `egress_remove`.
| `exit_code` | `int \| None` | no (on a Terminate, unless a signal stopped the program) |
| `status_detail` | `str \| None` | no (`signal <n>` for a program a signal stopped; how a code-interpreter run ended) |
| `dst_endpoint` | `AuditDstEndpoint \| None` | Network Activity only (`ip` and `port`, or `name` as `unix:<path>`) |
| `device.uid` | `str \| None` | no (the sandbox id) |
| `device.uid` | `str` | yes (the sandbox id) |
| `actor.session.uid` | `str \| None` | no (the console session) |
| `resources` | `list[AuditResource] \| None` | API Activity only (the sandbox, then any file, process, program or terminal the call touched; `data.pid` on the process or terminal it started, which a program's pid or an ancestor's equals) |
| `duration` | `int \| None` | no (milliseconds) |
Expand Down Expand Up @@ -2110,6 +2136,7 @@ All SDK errors inherit from `NeevAIError`. Import from `neevai` or `neevai.error
| `NotFoundError` | HTTP 404 |
| `ConflictError` | HTTP 409 |
| `PreconditionFailedError` | HTTP 412 |
| `TooEarlyError` | HTTP 425 (not ready yet, such as an agent still provisioning) |
| `RateLimitError` | HTTP 429 |
| `DeadlineExceededError` | HTTP 504 |
| `InternalServerError` | HTTP 5xx |
Expand Down Expand Up @@ -2270,6 +2297,8 @@ Compact reviewer index. Each symbol should also appear in
| `Agents.delete` | method | `None` |
| `Agents.get_sandbox` | method | `Sandbox` |
| `Agents.keepalive` / `.rollback` | methods | `Agent` |
| `Agents.create_snapshot` / `.list_snapshots` | methods | `Snapshot` / `list[Snapshot]` |
| `Agents.fork` | method | `Agent` (the new agent) |
| `Agents.expose_port` / `.list_ports` / `.revoke_port` / `.get_port_url` | methods | `SandboxPort` / `list[SandboxPort]` / `None` / `str` |
| `Agents.audit` | method | `AuditTrail` |
| `AsyncAgents.*` | methods | Same as sync with `await` |
Expand All @@ -2293,6 +2322,7 @@ Compact reviewer index. Each symbol should also appear in
| `Agent.sandbox` | method | Backing `Sandbox` handle |
| `Agent.update` / `.pause` / `.resume` / `.delete` | methods | Scope-threaded |
| `Agent.keepalive` / `.rollback` / `.audit` | methods | Scope-threaded |
| `Agent.snapshot` / `.snapshots` / `.fork` | methods | `Snapshot` / `list[Snapshot]` / new `Agent` |
| `Agent.expose_port` / `.list_ports` / `.revoke_port` / `.get_url` | methods | Preview ports |
| `Agent.idle_timeout_seconds` | property | `int \| None` |
| `AsyncAgent.*` | mirror | Same surface with `await` |
Expand Down
15 changes: 12 additions & 3 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ A sandbox is an isolated machine for your code. `client.sandboxes` and `client.t
| `audit(id, *, from_=None, to=None, cursor=None, limit=None, ...)` | `AuditTrail` | One page of the audit trail, newest first: calls, programs and connections. See [Audit trail](#audit-trail). |
| `expose_port(id, port, ..., *, slug=None)` / `list_ports(id)` / `revoke_port(id, port)` | `SandboxPort` / `list[SandboxPort]` / `None` | Preview ports. A new `slug` on an exposed port rotates its URL. |
| `get_port_url(id, port, wait_until_ready=True, ..., *, slug=None)` | `str` | Exposes a port and returns its preview URL once routable. |
| `create_snapshot(id, params=None, ...)` | `Snapshot` | Creates a filesystem snapshot (returns immediately with status Pending). |
| `create_snapshot(id, params=None, ...)` | `Snapshot` | Creates a snapshot of the sandbox: process memory and filesystem by default, or `/workspace` only with `snapshot_type="filesystem"` (returns immediately with status Pending). |
| `list_snapshots(id, page=None, limit=None, ...)` | `list[Snapshot]` | Lists snapshots for a sandbox (unwraps pagination items). |
| `get_snapshot(snapshot_id, ...)` | `Snapshot` | Fetches snapshot metadata by project-scoped ID. |
| `delete_snapshot(snapshot_id, ...)` | `None` | Deletes a snapshot permanently. |
Expand Down Expand Up @@ -300,6 +300,8 @@ An agent is a catalogue template (such as `"claude-code"`) running on its own sa
| `get(id, org_id=None, project_id=None)` | `Agent` | Fetches the current record for an agent by ID or name. |
| `update(id, params, org_id=None, project_id=None, *, allow_internet=None, allow_egress=None)` | `Agent` | In-place update of egress, cpu/memory (`resources`; defaults & bounds in [Agent resources](./api-inventory.md#agent-resources)) and `idle_timeout_seconds`. `allow_internet` / `allow_egress` are the same egress convenience as `create`; `egress_add` / `egress_remove` edit the allow-list in place. Rejects `{}` locally, naming every field. |
| `keepalive(id, ...)` / `rollback(id, snapshot_id, ...)` | `Agent` | Resets the idle timer / rolls the backing sandbox back to a snapshot. |
| `create_snapshot(id, params=None, ...)` / `list_snapshots(id, page=None, limit=None, ...)` | `Snapshot` / `list[Snapshot]` | Snapshots the backing sandbox (starts `Pending`) / lists its snapshots. |
| `fork(id, name, ...)` | `Agent` | Forks into a new agent, `Provisioning` until ready; the source keeps running. |
| `expose_port` / `list_ports` / `revoke_port` / `get_port_url` | as on `client.sandboxes` | Agent preview ports, same signatures. |
| `audit(id, *, from_=None, to=None, cursor=None, limit=None, ...)` | `AuditTrail` | The agent's audit trail: the same events and filters as the [sandbox one](#audit-trail). An agent has no `execution_trace` setting. |
| `pause(id, org_id=None, project_id=None)` | `Agent` | Pauses the agent and its backing sandbox. |
Expand All @@ -324,6 +326,7 @@ Returned by `client.agents.create()`, `get()`, `list().items`, etc.
| `wait_until_ready(timeout_ms=120000, poll_interval_ms=2000, on_poll=None)` | method — polls until `Ready`; fails fast on `Failed` / `Paused` |
| `pause()` / `resume()` / `delete()` | methods |
| `keepalive()` / `rollback(snapshot_id)` / `audit(...)` | methods |
| `snapshot(params=None, *, wait_until_ready=False, ...)` / `snapshots()` / `fork(name)` | methods — `fork` returns a new `Agent` handle |
| `expose_port(port, *, slug=None)` / `list_ports()` / `revoke_port(port)` / `get_url(port, ..., *, slug=None)` | methods — preview ports |
| `sandbox()` | method — backing `Sandbox` handle for runtime `exec`, `files`, and `processes` |
| `to_json()` | method |
Expand Down Expand Up @@ -420,6 +423,7 @@ Every failure is a `NeevAIError`. API failures are `APIError` subclasses with `s
| `NotFoundError` | 404: no such resource |
| `ConflictError` | 409: conflicting state |
| `PreconditionFailedError` | 412: the sandbox is not in a state that allows the call |
| `TooEarlyError` | 425: not ready yet, such as an agent still provisioning; retry later |
| `RateLimitError` | 429: too many requests |
| `DeadlineExceededError` | 504: the call ran past its deadline |
| `InternalServerError` | 5xx (`ServiceUnavailableError` for 503) |
Expand Down Expand Up @@ -470,7 +474,10 @@ Minimal one-liners for each public API. Runnable examples link to repo paths.
| `client.agents.pause(id)` / `.resume(id)` | `agent = client.agents.pause(agent_id)` / `client.agents.resume(agent_id)` | `agent = await client.agents.pause(agent_id)` / `await client.agents.resume(agent_id)` | [create_agent.py](../examples/create_agent.py) (pause via handle) |
| `client.agents.delete(id)` | `client.agents.delete(agent_id)` | `await client.agents.delete(agent_id)` | [create_agent.py](../examples/create_agent.py) |
| `client.agents.keepalive(id)` | `client.agents.keepalive(agent_id)` | `await client.agents.keepalive(agent_id)` | [agent_ports_audit.py](../examples/agent_ports_audit.py) |
| `client.agents.rollback(id, snapshot_id)` | `client.agents.rollback(agent_id, snap_id)` | `await client.agents.rollback(agent_id, snap_id)` | — |
| `client.agents.rollback(id, snapshot_id)` | `client.agents.rollback(agent_id, snap_id)` | `await client.agents.rollback(agent_id, snap_id)` | [agent_snapshot_fork_rollback.py](../examples/agent_snapshot_fork_rollback.py) (via `agent.rollback`) |
| `client.agents.create_snapshot(id, ...)` | `snap = client.agents.create_snapshot(agent_id, {"name": "demo"})` | `snap = await client.agents.create_snapshot(agent_id, {"name": "demo"})` | [agent_snapshot_fork_rollback.py](../examples/agent_snapshot_fork_rollback.py) (via `agent.snapshot`) |
| `client.agents.list_snapshots(id)` | `snaps = client.agents.list_snapshots(agent_id)` | `snaps = await client.agents.list_snapshots(agent_id)` | [agent_snapshot_fork_rollback.py](../examples/agent_snapshot_fork_rollback.py) (via `agent.snapshots`) |
| `client.agents.fork(id, name)` | `fork = client.agents.fork(agent_id, "fork-name")` | `fork = await client.agents.fork(agent_id, "fork-name")` | [agent_snapshot_fork_rollback.py](../examples/agent_snapshot_fork_rollback.py) (via `agent.fork`) |
| `client.agents.expose_port(id, port, ..., *, slug)` | `p = client.agents.expose_port(agent_id, 3000)` | `p = await client.agents.expose_port(agent_id, 3000)` | [agent_ports_audit.py](../examples/agent_ports_audit.py) |
| `client.agents.audit(id, ...)` | `trail = client.agents.audit(agent_id, limit=50)` | `trail = await client.agents.audit(agent_id, limit=50)` | [agent_ports_audit.py](../examples/agent_ports_audit.py) |
| `client.agent_templates.list(...)` | `page = client.agent_templates.list()` | `page = await client.agent_templates.list()` | [create_agent.py](../examples/create_agent.py) |
Expand Down Expand Up @@ -509,7 +516,9 @@ Minimal one-liners for each public API. Runnable examples link to repo paths.
| `agent.sandbox()` | `sandbox = agent.sandbox()` — then `exec` / `files` / `processes` | `sandbox = await agent.sandbox()` | [create_agent.py](../examples/create_agent.py) |
| `agent.update(params)` | `agent.update({"resources": {...}})` | `await agent.update({...})` | [create_agent.py](../examples/create_agent.py) |
| `agent.pause()` / `.resume()` / `.delete()` | `agent.pause(); agent.resume(); agent.delete()` | `await agent.pause(); await agent.resume(); await agent.delete()` | [create_agent.py](../examples/create_agent.py) (pause/delete; resume via handle API) |
| `agent.keepalive()` / `.rollback(snapshot_id)` | `agent.keepalive()` | `await agent.keepalive()` | [agent_ports_audit.py](../examples/agent_ports_audit.py) |
| `agent.keepalive()` / `.rollback(snapshot_id)` | `agent.keepalive()` | `await agent.keepalive()` | [agent_ports_audit.py](../examples/agent_ports_audit.py), [agent_snapshot_fork_rollback.py](../examples/agent_snapshot_fork_rollback.py) (rollback) |
| `agent.snapshot(...)` / `.snapshots()` | `snap = agent.snapshot(wait_until_ready=True)` | `snap = await agent.snapshot(wait_until_ready=True)` | [agent_snapshot_fork_rollback.py](../examples/agent_snapshot_fork_rollback.py) |
| `agent.fork(name)` | `fork = agent.fork("fork-name"); fork.wait_until_ready()` | `fork = await agent.fork("fork-name")` | [agent_snapshot_fork_rollback.py](../examples/agent_snapshot_fork_rollback.py) |
| `agent.expose_port(port, *, slug)` / `.list_ports()` / `.revoke_port(port)` / `.get_url(port)` | `p = agent.expose_port(3000)` | `p = await agent.expose_port(3000)` | [agent_ports_audit.py](../examples/agent_ports_audit.py) |
| `agent.audit(...)` | `trail = agent.audit(limit=50)` | `trail = await agent.audit(limit=50)` | [agent_ports_audit.py](../examples/agent_ports_audit.py) |
| `agent.idle_timeout_seconds` | `print(agent.idle_timeout_seconds)` | same | — |
Expand Down
Loading
Loading