diff --git a/.changeset/agent-tools-files-processes.md b/.changeset/agent-tools-files-processes.md deleted file mode 100644 index 2302641..0000000 --- a/.changeset/agent-tools-files-processes.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"neevai": minor ---- - -`neevai.agent_tools` gains `stat_path`, `move_path` and `delete_path`, on by default, and `list_processes`, enabled with `allow_processes`. `stat_path` returns `{"exists": False}` for a missing path, otherwise its `type`, `size` and `modified_time`. `delete_path` deletes a non-empty directory only with `recursive: true`. `list_processes` returns each process's `process_id`, `command`, `state` and, once exited, `exit_code`. A process started with `start_process` is listed by the command the model gave, without its `sh -lc` wrapper. `names()` lists the file tools after `read_file`, and `list_processes` after `stop_process`. - -`read_file` takes optional `offset` and `length` (1 to 10000, default 10000) to page through a large file. A paged read returns whole UTF-8 characters plus `next_offset` and `eof`, and `next_offset` always advances until `eof`, even when `length` is shorter than one character. Without them it reads the whole file and cuts it in the middle as before. Its description now says so. A new read-only `TITLES` mapping gives every tool a short display title. `call()` now checks boolean parameters: anything other than `true`, `false` or `null` is `recursive must be boolean`. - -`sandbox.files.read` and `read_text` (sync and async) take optional `offset` and `length` to read a byte range. The response is streamed and closed once the range has arrived. A negative `offset`, a `length` below 1, or either one not an `int` raises `ValueError`. diff --git a/.changeset/agent-tools-integrations.md b/.changeset/agent-tools-integrations.md deleted file mode 100644 index 1661500..0000000 --- a/.changeset/agent-tools-integrations.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"neevai": minor ---- - -`SandboxTools.specs()` and `AsyncSandboxTools.specs()` return a frozen `ToolSpec` per enabled tool, in `names()` order. Each carries `name`, `title`, `description`, the read-only `parameters` schema and a bound `call(args)`. `FatalGuard` stops one agent run at the first exception a tool raises. `call` (sync) and `acall` (async) return the read-only `FATAL_RESULT` (`{"error": "The sandbox call failed and the run is stopping."}`) instead, so the exception's text never reaches the model, and keep the first exception for `raise_if_failed()`. - -New optional adapters build framework tools from these. `neevai.agent_tools.claude.to_claude(tools)` gives `ClaudeAgentOptions` keyword arguments for an in-process MCP server with Claude Code's own tools off, plus a `watch()` that ends the run and raises on a sandbox failure. `neevai.agent_tools.openai_agents.to_openai_agents(tools)` returns OpenAI Agents SDK `FunctionTool`s, and `neevai.agent_tools.langchain.to_langchain(tools)` returns LangChain `StructuredTool`s. Install a framework with the `claude`, `openai-agents` or `langchain` extra. diff --git a/.changeset/agent-tools.md b/.changeset/agent-tools.md deleted file mode 100644 index 84a092e..0000000 --- a/.changeset/agent-tools.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"neevai": minor ---- - -New `neevai.agent_tools` module: the sandbox tool contract for building your own agent, with no framework dependency. `SandboxTools(sandbox, *, allow_preview=False, allow_rollback=False, allow_processes=False)` exposes `run_command`, `write_file`, `read_file`, `expose_port`, `checkpoint`, `rollback`, `start_process`, `read_process_output` and `stop_process`, each returning a JSON-serializable dict typed as a `TypedDict` (`RunCommandToolResult`, `ReadProcessOutputToolResult`, ... or `ToolErrorResult`). `names()` lists the enabled tools, `DESCRIPTIONS` and `PARAMETERS` hold each tool's model-facing description and JSON Schema, and `call(name, args)` runs a tool call by name. `AsyncSandboxTools` wraps an `AsyncSandbox` with the same methods as coroutines. `allow_preview` or `allow_rollback` on a handle without client context raises `ValueError`. - -The model sees the same text as from the JS SDK. Errors it can fix return `{"error": ...}` while auth, rate-limit, server and network errors raise. An API error reads `HTTP : `, leaving out the code or message when absent and never including the request method, URL or request id; the message is the first non-empty string among the body's `message`, `error` and `details`, and a body that is not a JSON object is used as raw text. A file that is not UTF-8 returns `not UTF-8 text`, while command output keeps its text with U+FFFD in place of bad bytes. A command silent long enough for the network edge to answer 524 returns an error result pointing at `start_process`. Every error text keeps its first line, at most 500 characters, and output is capped at 10 KB per stream. - -`call()` never raises for model input: an unknown or disabled tool, arguments that are not an object, an unknown key (checked first), a missing required key, or a property of the wrong type each return an error result, with key names quoted as JSON (`missing a required argument: "path"`). `timeout_s` and `wait_s` accept numbers or plain ASCII decimal strings: through `call()` anything else is an error result, while a direct method call falls back to the default; `timeout_s` rounds to milliseconds half up. `read_process_output` takes `wait_s` (0 to 60) to wait for a running process to exit before reading. - -`DESCRIPTIONS` and `PARAMETERS` are read-only at every level and JSON-serializable, with real lists inside the schemas; `copy.copy`, `copy.deepcopy` and `pickle` give plain dicts and lists for frameworks that rewrite a copied schema. - -Error parsing is shared by the runtime and lifecycle transports. A runtime error's `APIError.body` is now the parsed response body (before it held only `code` and `message`), with `code` taken from sandboxd's `reason_code` when `code` is absent or empty, so `details` and `str(error)` now include sandboxd's `error` and `details` fields. Lifecycle errors fall back to `reason_code` for `code` the same way. An error body that is not a JSON object, such as `[1,2]` or `null`, is kept as raw `details` text instead of crashing the runtime transport with `AttributeError`, and one leading byte order mark is dropped from any error body. diff --git a/.changeset/audit-ocsf-events.md b/.changeset/audit-ocsf-events.md deleted file mode 100644 index c96bb2d..0000000 --- a/.changeset/audit-ocsf-events.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"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 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`. - -Snapshots carry an optional `trigger`: `manual`, or `memory_pressure` for one taken automatically as the sandbox neared its memory limit. diff --git a/.changeset/code-interpreter.md b/.changeset/code-interpreter.md deleted file mode 100644 index aff66fe..0000000 --- a/.changeset/code-interpreter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"neevai": minor ---- - -Code interpreter, on both the sync and async clients: `sandbox.code.run(code, ...)` runs Python in a persistent kernel in sandboxes created from the interpreter template. It returns an `Execution` with `stdout`, `stderr`, `logs`, display `results` with typed properties (`text`, `html`, `markdown`, `svg`, `png`, `jpeg`, `pdf`, `latex`, `json`, `formats()`), the last expression's `text`, any raised `error`, `execution_count`, and how the run ended (`end_reason`). Options: `context` (a context or its id), `language`, `envs` for this run only, `timeout_ms`, `request_timeout_ms`, and `on_stdout` / `on_stderr` (each given an `OutputMessage`), `on_result` and `on_error`. `sandbox.code.create_context(language=, cwd=)`, `list_contexts`, `restart_context` and `delete_context` manage separate kernels, each in its own working directory. `APIError.reason` refines `code` where one code covers several cases, such as `context_busy`. diff --git a/.changeset/exec-read-timeout.md b/.changeset/exec-read-timeout.md deleted file mode 100644 index dfb725e..0000000 --- a/.changeset/exec-read-timeout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"neevai": patch ---- - -`exec`, `exec_stream` and `files.watch` no longer time out at the client's read timeout (60 s by default) when given a longer `timeout_ms`: the runtime request now waits up to `timeout_ms` plus 30 s for the server to answer. `processes.get(id, wait=True)` likewise waits up to the server's 5-minute wait ceiling plus 30 s. A shorter `timeout_ms` never lowers the client's read timeout. The async client matches. The network edge in front of the runtime still cuts off a response that stays silent for about 2 minutes, so long quiet work belongs in `sandbox.processes`. diff --git a/.changeset/preview-wait-app-404.md b/.changeset/preview-wait-app-404.md deleted file mode 100644 index e3061d0..0000000 --- a/.changeset/preview-wait-app-404.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"neevai": patch ---- - -`get_url` / `get_port_url` no longer wait out the whole timeout when the app itself answers 403 or 404 at its root, as an API-only or sign-in-gated server does. Those answers now count as routed; only a 403/404 from before the URL is routed keeps the wait going. diff --git a/.changeset/retire-base-templates.md b/.changeset/retire-base-templates.md deleted file mode 100644 index 3482bee..0000000 --- a/.changeset/retire-base-templates.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"neevai": patch ---- - -Examples, docs and tests use the `sb-ubuntu-26-04-dev` template, which ships `bash`, `python3` and `node`, in place of the minimal templates that are no longer offered. A new `examples/docker.py` runs a container from the `sb-ubuntu-26-04-docker` template. The template docs cover the `interpreter` and `min_memory_gb` fields and recommend `templates.list()` over hard-coded template ids. diff --git a/.changeset/sandbox-labels.md b/.changeset/sandbox-labels.md deleted file mode 100644 index 862af63..0000000 --- a/.changeset/sandbox-labels.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"neevai": minor ---- - -Sandboxes take key-value labels. `client.sandboxes.create(params, labels={"team": "payments"})` tags a sandbox, `client.sandboxes.list(labels={"team": "payments"})` returns only the sandboxes carrying every given label (sent as one repeated `label=key=value` query parameter per label), and `sandbox.labels` reads them back as a `dict`, `{}` when there are none. The async client takes the same arguments. More than 16 labels or a value over 63 characters raises `ResponseValidationError` before the request; a key or value in the wrong format is rejected by the server with `BadRequestError`. diff --git a/.changeset/sandbox-replicas-removed.md b/.changeset/sandbox-replicas-removed.md deleted file mode 100644 index 772b18f..0000000 --- a/.changeset/sandbox-replicas-removed.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"neevai": minor ---- - -`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. diff --git a/.changeset/snapshot-wait.md b/.changeset/snapshot-wait.md deleted file mode 100644 index f266c55..0000000 --- a/.changeset/snapshot-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"neevai": minor ---- - -Wait for a snapshot to finish. `client.sandboxes.wait_for_snapshot(snapshot_id, timeout_ms=300000, poll_interval_ms=2000)` polls until the snapshot is Ready and returns it, and raises `NeevAIError` if it fails (with its reason) or is not Ready in time. `sandbox.snapshot(params, wait_until_ready=True)` takes a snapshot and waits the same way. Both work on the sync and async clients, as the JS SDK's `waitForSnapshot` and `snapshot({ waitUntilReady: true })` do. diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 17f3ad7..c79c83e 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -6,6 +6,35 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.9.0] - 2026-10-17 + +### Breaking changes + +- The audit trail returns OCSF API Activity events. `AuditTrail.records` is now `data: list[AuditEvent]`, and `AuditRecord` and `AuditTrail.sandbox_id` are removed. Each event has `time` (ms), `api.operation`, `actor`, `status` and `resources`. +- `Sandbox.replicas` and `AsyncSandbox.replicas` are removed. Read `phase` instead, which is `"Paused"` while a sandbox is paused. +- `APIError.body` on runtime errors is now the full parsed response body, not just `code` and `message`. + +### Added + +- Agent tools: `SandboxTools` and `AsyncSandboxTools` in `neevai.agent_tools` give a model sandbox tools to run commands, read, write, stat, move and delete files, expose ports, checkpoint and roll back, and run background processes. Descriptions, schemas and error text match the JS SDK. +- `specs()` and `FatalGuard` plug the tools into any framework, with ready adapters for the Claude Agent SDK, OpenAI Agents and LangChain (`neevai.agent_tools.claude`, `.openai_agents`, `.langchain`; install the `claude`, `openai-agents` or `langchain` extra). +- Code interpreter: `sandbox.code.run(code)` runs Python in a persistent kernel on the interpreter template, and `sandbox.code.create_context()`, `list_contexts`, `restart_context` and `delete_context` manage separate kernels. Sync and async. +- Sandbox labels: `labels=` on create and list, and `sandbox.labels`. +- `client.sandboxes.wait_for_snapshot(id)` and `sandbox.snapshot(params, wait_until_ready=True)` wait for a snapshot to be Ready. +- Audit filters: `operation`, `status`, `actor_type`, `actor_uid` and `credential_uid`. +- `files.read` and `files.read_text` take `offset` and `length` to read a byte range. +- Snapshots have an optional `trigger` (`manual` or `memory_pressure`), and `snapshot_type` adds `"filesystem"`. + +### Changed + +- API errors are parsed the same way for every call. `code` falls back to the sandbox's `reason_code`, and a body that isn't a JSON object is kept as raw `details` instead of raising `AttributeError`. +- Examples and docs use the `sb-ubuntu-26-04-dev` template, and `examples/docker.py` is new. + +### Fixed + +- `exec`, `exec_stream`, `files.watch` and `processes.get(id, wait=True)` no longer time out at the client's 60 s read timeout when given a longer `timeout_ms`. +- `get_url` / `get_port_url` no longer wait out their timeout when the app itself answers 403 or 404. + ## [0.8.2] - 2026-10-05 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 132b24f..be21b67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "neevai" -version = "0.8.2" +version = "0.9.0" description = "Official NeevCloud SDK for the Neev platform — Python. First up: agent sandboxes." readme = "README.md" requires-python = ">=3.10" diff --git a/uv.lock b/uv.lock index e32948c..dec477b 100644 --- a/uv.lock +++ b/uv.lock @@ -1279,7 +1279,7 @@ wheels = [ [[package]] name = "neevai" -version = "0.8.2" +version = "0.9.0" source = { editable = "." } dependencies = [ { name = "httpx" },