v0.2.0 (#14)
All checks were successful
ci/woodpecker/push/unit Pipeline was successful

Co-authored-by: Tasnim Kabir Sadik <tksadik92@gmail.com>
Reviewed-on: #14
Co-authored-by: pptx704 <rafeed@omukk.dev>
Co-committed-by: pptx704 <rafeed@omukk.dev>
This commit is contained in:
2026-05-24 05:02:08 +00:00
committed by Rafeed M. Bhuiyan
parent de72dfe9c8
commit 4924237a23
7 changed files with 196 additions and 772 deletions

View File

@ -172,6 +172,8 @@ import sys
# Stream a new command # Stream a new command
for event in capsule.commands.stream("python", args=["-u", "train.py"]): for event in capsule.commands.stream("python", args=["-u", "train.py"]):
match event.type: match event.type:
case "start":
print(f"PID: {event.pid}")
case "stdout": case "stdout":
print(event.data, end="") print(event.data, end="")
case "stderr": case "stderr":
@ -181,8 +183,11 @@ for event in capsule.commands.stream("python", args=["-u", "train.py"]):
# Connect to a running background process # Connect to a running background process
for event in capsule.commands.connect(handle.pid): for event in capsule.commands.connect(handle.pid):
if event.type == "stdout": match event.type:
print(event.data, end="") case "start":
print(f"PID: {event.pid}")
case "stdout":
print(event.data, end="")
``` ```
#### Process Management #### Process Management
@ -211,6 +216,7 @@ capsule.files.exists("/app/main.py") # True
# List directory # List directory
entries = capsule.files.list("/home/user", depth=1) entries = capsule.files.list("/home/user", depth=1)
# FileEntry has: name, type (file/dir), size, modified_at
for entry in entries: for entry in entries:
print(entry.name, entry.type, entry.size) print(entry.name, entry.type, entry.size)
@ -289,8 +295,27 @@ value = capsule.git.get_config("user.name", cwd="/app") # str | None
capsule.git.remote_add("upstream", "https://github.com/org/repo.git", cwd="/app") capsule.git.remote_add("upstream", "https://github.com/org/repo.git", cwd="/app")
url = capsule.git.remote_get("origin", cwd="/app") # str | None url = capsule.git.remote_get("origin", cwd="/app") # str | None
# Reset and restore
capsule.git.reset(mode="hard", ref="HEAD~1", cwd="/app")
capsule.git.restore(["file.txt"], staged=True, cwd="/app")
``` ```
#### Persistent Credential Store
For workflows that need repeated authenticated operations, you can persist credentials via the git credential store:
```python
capsule.git.dangerously_authenticate(
username="user",
password="ghp_token",
host="github.com",
protocol="https",
)
```
> **Warning:** Credentials are written in plaintext inside the capsule and are accessible to any process running there. Prefer per-operation `username`/`password` on `clone`, `push`, and `pull` instead.
Git errors raise `GitCommandError` (or `GitAuthError` for authentication failures), both inheriting from `GitError`: Git errors raise `GitCommandError` (or `GitAuthError` for authentication failures), both inheriting from `GitError`:
```python ```python
@ -308,7 +333,7 @@ except GitAuthError as e:
```python ```python
import sys import sys
with capsule.pty(cmd="/bin/bash", cols=120, rows=40, cwd="/home/user") as term: with capsule.pty(cmd="/bin/bash", cols=80, rows=24, cwd="/home/user") as term:
term.write(b"ls -la\n") term.write(b"ls -la\n")
for event in term: for event in term:
if event.type == "output": if event.type == "output":
@ -451,9 +476,10 @@ result = capsule.run_code("print('running on custom template')")
| `logs` | `Logs` | `.stdout: list[str]` and `.stderr: list[str]` chunks | | `logs` | `Logs` | `.stdout: list[str]` and `.stderr: list[str]` chunks |
| `error` | `ExecutionError \| None` | `.name`, `.value`, `.traceback` | | `error` | `ExecutionError \| None` | `.name`, `.value`, `.traceback` |
| `execution_count` | `int \| None` | Jupyter cell execution counter | | `execution_count` | `int \| None` | Jupyter cell execution counter |
| `timed_out` | `bool` | ``True`` when execution was cut short by the timeout |
| `text` | `str \| None` | (property) `text/plain` of the main `execute_result` | | `text` | `str \| None` | (property) `text/plain` of the main `execute_result` |
Each `Result` has typed MIME fields: `text`, `html`, `markdown`, `svg`, `png`, `jpeg`, `pdf`, `latex`, `json`, `javascript`, plus `extra` for unknown types. The `text` field is Jupyter's `text/plain` bundle verbatim — the Python `repr()` of the cell's last expression. So `run_code("'hi'").text` is `"'hi'"` (with quotes), and `run_code("42").text` is `"42"`. This preserves the distinction between the string `'2'` and the int `2`. Each `Result` has typed MIME fields: `text`, `html`, `markdown`, `svg`, `png`, `jpeg`, `gif`, `pdf`, `latex`, `json`, `javascript`, `plotly`, plus `extra` for unknown types. The `text` field is Jupyter's `text/plain` bundle verbatim — the Python `repr()` of the cell's last expression. So `run_code("'hi'").text` is `"'hi'"` (with quotes), and `run_code("42").text` is `"42"`. This preserves the distinction between the string `'2'` and the int `2`.
### Code Runner + Commands/Files ### Code Runner + Commands/Files
@ -527,15 +553,15 @@ The SDK maps server error codes to typed exceptions:
```python ```python
from wrenn import ( from wrenn import (
WrennError, WrennError,
WrennValidationError, # 400 WrennValidationError, # 400
WrennAuthenticationError, # 401 WrennAuthenticationError, # 401
WrennForbiddenError, # 403 WrennForbiddenError, # 403
WrennNotFoundError, # 404 WrennNotFoundError, # 404
WrennConflictError, # 409 WrennConflictError, # 409
WrennHostHasCapsulesError, # 409 (host has running capsules) WrennHostHasCapsulesError, # 409 (host has running capsules)
WrennAgentError, # 502 WrennInternalError, # 500
WrennInternalError, # 500 WrennAgentError, # 502
WrennHostUnavailableError, # 503 WrennHostUnavailableError, # 503
) )
try: try:
@ -603,7 +629,7 @@ with WrennClient(api_key="wrn_...") as client:
# Snapshots # Snapshots
template = client.snapshots.create(capsule_id="cl-abc", name="my-snap") template = client.snapshots.create(capsule_id="cl-abc", name="my-snap")
templates = client.snapshots.list() templates = client.snapshots.list(type="custom") # optional type filter
client.snapshots.delete("my-snap") client.snapshots.delete("my-snap")
``` ```

View File

@ -2716,14 +2716,39 @@ paths:
tags: [admin] tags: [admin]
security: security:
- sessionAuth: [] - sessionAuth: []
parameters:
- name: page
in: query
required: false
schema:
type: integer
minimum: 1
default: 1
description: Page number for pagination.
responses: responses:
"200": "200":
description: Teams list description: Paginated teams list
content: content:
application/json: application/json:
schema: schema:
type: array type: object
items: {type: object} properties:
teams:
type: array
items:
$ref: "#/components/schemas/AdminTeam"
total:
type: integer
page:
type: integer
per_page:
type: integer
total_pages:
type: integer
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
/v1/admin/teams/{id}/byoc: /v1/admin/teams/{id}/byoc:
put: put:
@ -2743,12 +2768,20 @@ paths:
application/json: application/json:
schema: schema:
type: object type: object
required: [byoc] required: [enabled]
properties: properties:
byoc: {type: boolean} enabled:
type: boolean
description: true to enable BYOC, false to disable.
responses: responses:
"204": "204":
description: Updated description: Updated
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
/v1/admin/teams/{id}: /v1/admin/teams/{id}:
delete: delete:
@ -2765,6 +2798,38 @@ paths:
responses: responses:
"204": "204":
description: Deleted description: Deleted
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
/v1/admin/hosts:
get:
summary: List all hosts (admin)
operationId: adminListHosts
tags: [admin]
security:
- sessionAuth: []
description: |
Returns all hosts across all teams with per-host resource consumption.
Includes team name for hosts associated with a team.
responses:
"200":
description: Hosts list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Host"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
/v1/admin/users: /v1/admin/users:
get: get:
@ -3581,10 +3646,6 @@ components:
type: integer type: integer
memory_mb_reserved: memory_mb_reserved:
type: integer type: integer
sampled_at:
type: string
format: date-time
nullable: true
peaks: peaks:
type: object type: object
description: Maximum values over the last 30 days. description: Maximum values over the last 30 days.
@ -3633,10 +3694,6 @@ components:
type: integer type: integer
timeout_sec: timeout_sec:
type: integer type: integer
guest_ip:
type: string
host_ip:
type: string
created_at: created_at:
type: string type: string
format: date-time format: date-time
@ -3661,7 +3718,11 @@ components:
agent_version, envd_version) when running. agent_version, envd_version) when running.
disk_size_mb: disk_size_mb:
type: integer type: integer
nullable: true description: Maximum disk capacity in MiB.
disk_used_mb:
type: integer
format: int64
description: Current disk usage in MiB. Only populated on individual capsule GET; omitted in list responses.
CreateSnapshotRequest: CreateSnapshotRequest:
type: object type: object
@ -4013,6 +4074,25 @@ components:
updated_at: updated_at:
type: string type: string
format: date-time format: date-time
team_name:
type: string
nullable: true
description: Team name (included when listing hosts as an admin).
running_vcpus:
type: integer
description: Total vCPUs allocated to running capsules on this host.
running_memory_mb:
type: integer
description: Total memory in MB allocated to running capsules on this host.
running_disk_mb:
type: integer
description: Total disk in MB allocated to running capsules on this host.
paused_memory_mb:
type: integer
description: Total memory in MB allocated to paused capsules on this host.
paused_disk_mb:
type: integer
description: Total disk in MB allocated to paused capsules on this host.
RefreshHostTokenRequest: RefreshHostTokenRequest:
type: object type: object
@ -4124,6 +4204,39 @@ components:
items: items:
$ref: "#/components/schemas/TeamMember" $ref: "#/components/schemas/TeamMember"
AdminTeam:
type: object
properties:
id:
type: string
name:
type: string
slug:
type: string
is_byoc:
type: boolean
created_at:
type: string
format: date-time
deleted_at:
type: string
format: date-time
nullable: true
member_count:
type: integer
owner_name:
type: string
owner_email:
type: string
active_sandbox_count:
type: integer
channel_count:
type: integer
running_vcpus:
type: integer
running_memory_mb:
type: integer
CapsuleMetrics: CapsuleMetrics:
type: object type: object
properties: properties:

View File

@ -1,6 +1,6 @@
[project] [project]
name = "wrenn" name = "wrenn"
version = "0.1.5" version = "0.2.0"
description = "Python SDK for Wrenn" description = "Python SDK for Wrenn"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"

View File

@ -164,4 +164,17 @@ def __getattr__(name: str) -> type:
stacklevel=2, stacklevel=2,
) )
return WrennHostHasCapsulesError return WrennHostHasCapsulesError
if name in ("GitError", "GitCommandError", "GitAuthError"):
from wrenn._git.exceptions import (
GitAuthError as _GitAuthError,
GitCommandError as _GitCommandError,
GitError as _GitError,
)
_m: dict[str, type] = {
"GitError": _GitError,
"GitCommandError": _GitCommandError,
"GitAuthError": _GitAuthError,
}
return _m[name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

View File

@ -1,63 +1,17 @@
from wrenn.models._generated import ( from wrenn.models._generated import (
APIKeyResponse,
Capsule, Capsule,
CreateAPIKeyRequest,
CreateCapsuleRequest,
CreateHostRequest,
CreateHostResponse,
CreateSnapshotRequest,
Encoding,
Error,
Error1,
ExecRequest,
ExecResponse,
FileEntry, FileEntry,
Host,
ListDirRequest,
ListDirResponse, ListDirResponse,
LoginRequest,
MakeDirRequest,
MakeDirResponse, MakeDirResponse,
ReadFileRequest,
RegisterHostRequest,
RegisterHostResponse,
RemoveRequest,
SignupRequest,
Status, Status,
Status1,
Template, Template,
Type,
Type2,
) )
__all__ = [ __all__ = [
"APIKeyResponse",
"CreateAPIKeyRequest",
"CreateHostRequest",
"CreateHostResponse",
"CreateCapsuleRequest",
"CreateSnapshotRequest",
"Encoding",
"Error",
"Error1",
"ExecRequest",
"ExecResponse",
"FileEntry",
"Host",
"ListDirRequest",
"ListDirResponse",
"LoginRequest",
"MakeDirRequest",
"MakeDirResponse",
"ReadFileRequest",
"RegisterHostRequest",
"RegisterHostResponse",
"RemoveRequest",
"Capsule", "Capsule",
"SignupRequest", "FileEntry",
"ListDirResponse",
"MakeDirResponse",
"Status", "Status",
"Status1",
"Template", "Template",
"Type",
"Type2",
] ]

View File

@ -1,147 +1,13 @@
# generated by datamodel-codegen: # generated by datamodel-codegen:
# filename: openapi.yaml # filename: openapi.yaml
# timestamp: 2026-05-22T19:20:45+00:00 # timestamp: 2026-05-23T11:20:02+00:00
from __future__ import annotations from __future__ import annotations
from pydantic import AwareDatetime, BaseModel, EmailStr, Field from pydantic import AwareDatetime, BaseModel, Field
from typing import Annotated, Any from typing import Annotated
from datetime import date as date_aliased
from enum import StrEnum from enum import StrEnum
class SignupRequest(BaseModel):
email: EmailStr
password: Annotated[str, Field(min_length=8)]
name: Annotated[str, Field(max_length=100)]
class LoginRequest(BaseModel):
email: EmailStr
password: str
class SignupResponse(BaseModel):
message: Annotated[
str | None,
Field(description="Confirmation message instructing user to check email"),
] = None
class SessionResponse(BaseModel):
"""
Returned by login, activate, and switch-team. The actual auth credential
is the wrenn_sid cookie set on the response. The body carries identity
data the SPA needs to bootstrap.
"""
user_id: str | None = None
team_id: str | None = None
email: str | None = None
name: str | None = None
role: str | None = None
is_admin: bool | None = None
class CreateAPIKeyRequest(BaseModel):
name: str | None = "Unnamed API Key"
class APIKeyResponse(BaseModel):
id: str | None = None
team_id: str | None = None
name: str | None = None
key_prefix: Annotated[
str | None, Field(description='Display prefix (e.g. "wrn_ab12cd34...")')
] = None
created_at: AwareDatetime | None = None
last_used: AwareDatetime | None = None
key: Annotated[
str | None,
Field(
description="Full plaintext key. Only returned on creation, never again."
),
] = None
class CreateCapsuleRequest(BaseModel):
template: str | None = "minimal-ubuntu"
vcpus: int | None = 1
memory_mb: int | None = 512
disk_size_mb: Annotated[
int | None,
Field(
description="Maximum size of the per-capsule copy-on-write disk in MB. Capped at 5 GB by default; the actual size is max(disk_size_mb, origin rootfs size).\n"
),
] = 5120
timeout_sec: Annotated[
int | None,
Field(
description="Auto-pause TTL in seconds. The capsule is automatically paused after this duration of inactivity (no exec or ping). 0 means no auto-pause. Positive values below 60 are silently clamped to 60 (the agent's startup envelope).\n",
ge=0,
),
] = 0
class Point(BaseModel):
date: date_aliased | None = None
cpu_minutes: float | None = None
ram_mb_minutes: float | None = None
class UsageResponse(BaseModel):
from_: Annotated[date_aliased | None, Field(alias="from")] = None
to: date_aliased | None = None
points: list[Point] | None = None
class Range(StrEnum):
field_5m = "5m"
field_1h = "1h"
field_6h = "6h"
field_24h = "24h"
field_30d = "30d"
class Current(BaseModel):
running_count: int | None = None
vcpus_reserved: int | None = None
memory_mb_reserved: int | None = None
sampled_at: AwareDatetime | None = None
class Peaks(BaseModel):
"""
Maximum values over the last 30 days.
"""
running_count: int | None = None
vcpus: int | None = None
memory_mb: int | None = None
class Series(BaseModel):
"""
Parallel arrays for chart rendering.
"""
labels: list[AwareDatetime] | None = None
running: list[int] | None = None
vcpus: list[int] | None = None
memory_mb: list[int] | None = None
class CapsuleStats(BaseModel):
range: Range | None = None
current: Current | None = None
peaks: Annotated[
Peaks | None, Field(description="Maximum values over the last 30 days.")
] = None
series: Annotated[
Series | None, Field(description="Parallel arrays for chart rendering.")
] = None
class Status(StrEnum): class Status(StrEnum):
pending = "pending" pending = "pending"
starting = "starting" starting = "starting"
@ -164,8 +30,6 @@ class Capsule(BaseModel):
vcpus: int | None = None vcpus: int | None = None
memory_mb: int | None = None memory_mb: int | None = None
timeout_sec: int | None = None timeout_sec: int | None = None
guest_ip: str | None = None
host_ip: str | None = None
created_at: AwareDatetime | None = None created_at: AwareDatetime | None = None
started_at: AwareDatetime | None = None started_at: AwareDatetime | None = None
last_active_at: AwareDatetime | None = None last_active_at: AwareDatetime | None = None
@ -176,16 +40,14 @@ class Capsule(BaseModel):
description="Free-form key/value labels attached at create-time. Also carries\nagent-side version info (kernel_version, vmm_version,\nagent_version, envd_version) when running.\n" description="Free-form key/value labels attached at create-time. Also carries\nagent-side version info (kernel_version, vmm_version,\nagent_version, envd_version) when running.\n"
), ),
] = None ] = None
disk_size_mb: int | None = None disk_size_mb: Annotated[
int | None, Field(description="Maximum disk capacity in MiB.")
] = None
class CreateSnapshotRequest(BaseModel): disk_used_mb: Annotated[
sandbox_id: Annotated[ int | None,
str, Field(description="ID of the running capsule to snapshot.") Field(
] description="Current disk usage in MiB. Only populated on individual capsule GET; omitted in list responses."
name: Annotated[ ),
str | None,
Field(description="Name for the snapshot template. Auto-generated if omitted."),
] = None ] = None
@ -216,121 +78,6 @@ class Template(BaseModel):
metadata: dict[str, str] | None = None metadata: dict[str, str] | None = None
class AdminTemplate(BaseModel):
"""
Template as returned by the admin templates list. Unlike `Template`
(the team-facing snapshot shape), this includes the owning `team_id`
and omits `platform`/`metadata`.
"""
name: str | None = None
type: Type | None = None
vcpus: int | None = None
memory_mb: int | None = None
size_bytes: int | None = None
team_id: Annotated[
str | None,
Field(
description="Owning team ID (formatted, e.g. `team-…`). Platform team for global templates."
),
] = None
created_at: AwareDatetime | None = None
protected: Annotated[
bool | None,
Field(
description="True for built-in system base templates (minimal-ubuntu,\nminimal-alpine, minimal-arch, minimal-fedora). Protected templates\ncannot be deleted.\n"
),
] = None
class ExecRequest(BaseModel):
cmd: str
args: list[str] | None = None
timeout_sec: Annotated[
int | None,
Field(description="Timeout in seconds (foreground exec only, default 30)"),
] = 30
background: Annotated[
bool | None,
Field(
description="If true, starts the process in the background and returns immediately with a PID and tag (HTTP 202)"
),
] = False
tag: Annotated[
str | None,
Field(
description="Optional user-chosen tag for the background process. Auto-generated if omitted. Only used when background is true."
),
] = None
envs: Annotated[
dict[str, str] | None,
Field(
description="Environment variables for the process (background exec only)"
),
] = None
cwd: Annotated[
str | None,
Field(description="Working directory for the process (background exec only)"),
] = None
class BackgroundExecResponse(BaseModel):
sandbox_id: str | None = None
cmd: str | None = None
pid: int | None = None
tag: str | None = None
class ProcessEntry(BaseModel):
pid: int | None = None
tag: str | None = None
cmd: str | None = None
args: list[str] | None = None
class ProcessListResponse(BaseModel):
processes: list[ProcessEntry] | None = None
class Encoding(StrEnum):
"""
Output encoding. "base64" when stdout/stderr contain binary data.
"""
utf_8 = "utf-8"
base64 = "base64"
class ExecResponse(BaseModel):
sandbox_id: str | None = None
cmd: str | None = None
stdout: str | None = None
stderr: str | None = None
exit_code: int | None = None
duration_ms: int | None = None
encoding: Annotated[
Encoding | None,
Field(
description='Output encoding. "base64" when stdout/stderr contain binary data.'
),
] = None
class ReadFileRequest(BaseModel):
path: Annotated[str, Field(description="Absolute file path inside the capsule")]
class ListDirRequest(BaseModel):
path: Annotated[str, Field(description="Directory path inside the capsule")]
depth: Annotated[
int | None,
Field(
description="Recursion depth (0 = non-recursive, 1 = immediate children)"
),
] = 1
class Type2(StrEnum): class Type2(StrEnum):
file = "file" file = "file"
directory = "directory" directory = "directory"
@ -354,438 +101,9 @@ class FileEntry(BaseModel):
symlink_target: str | None = None symlink_target: str | None = None
class MakeDirRequest(BaseModel):
path: Annotated[
str, Field(description="Directory path to create inside the capsule")
]
class MakeDirResponse(BaseModel): class MakeDirResponse(BaseModel):
entry: FileEntry | None = None entry: FileEntry | None = None
class RemoveRequest(BaseModel):
path: Annotated[str, Field(description="Path to remove inside the capsule")]
class Type3(StrEnum):
"""
Host type. Regular hosts are shared; BYOC hosts belong to a team.
"""
regular = "regular"
byoc = "byoc"
class CreateHostRequest(BaseModel):
type: Annotated[
Type3,
Field(
description="Host type. Regular hosts are shared; BYOC hosts belong to a team."
),
]
team_id: Annotated[str | None, Field(description="Required for BYOC hosts.")] = None
provider: Annotated[
str | None,
Field(description="Cloud provider (e.g. aws, gcp, hetzner, bare-metal)."),
] = None
availability_zone: Annotated[
str | None, Field(description="Availability zone (e.g. us-east, eu-west).")
] = None
class RegisterHostRequest(BaseModel):
token: Annotated[
str, Field(description="One-time registration token from POST /v1/hosts.")
]
arch: Annotated[
str | None, Field(description="CPU architecture (e.g. x86_64, aarch64).")
] = None
cpu_cores: int | None = None
memory_mb: int | None = None
disk_gb: int | None = None
address: Annotated[str, Field(description="Host agent address (ip:port).")]
class Type4(StrEnum):
regular = "regular"
byoc = "byoc"
class Status1(StrEnum):
pending = "pending"
online = "online"
offline = "offline"
draining = "draining"
unreachable = "unreachable"
class Host(BaseModel):
id: str | None = None
type: Type4 | None = None
team_id: str | None = None
provider: str | None = None
availability_zone: str | None = None
arch: str | None = None
cpu_cores: int | None = None
memory_mb: int | None = None
disk_gb: int | None = None
address: str | None = None
status: Status1 | None = None
last_heartbeat_at: AwareDatetime | None = None
created_by: str | None = None
created_at: AwareDatetime | None = None
updated_at: AwareDatetime | None = None
class RefreshHostTokenRequest(BaseModel):
refresh_token: Annotated[
str,
Field(
description="Refresh token obtained from registration or a previous refresh."
),
]
class RefreshHostTokenResponse(BaseModel):
host: Host | None = None
token: Annotated[
str | None, Field(description="New host JWT. Valid for 7 days.")
] = None
refresh_token: Annotated[
str | None,
Field(
description="New refresh token. Valid for 60 days; old token is revoked."
),
] = None
class HostDeletePreview(BaseModel):
host: Host | None = None
sandbox_ids: Annotated[
list[str] | None,
Field(description="IDs of capsules that would be destroyed on force-delete."),
] = None
class Error(BaseModel):
code: Annotated[str | None, Field(examples=["host_has_sandboxes"])] = None
message: str | None = None
sandbox_ids: Annotated[
list[str] | None, Field(description="IDs of active capsules blocking deletion.")
] = None
class HostHasCapsulesError(BaseModel):
error: Error | None = None
class AddTagRequest(BaseModel):
tag: str
class UserSearchResult(BaseModel):
user_id: str | None = None
email: str | None = None
class Team(BaseModel):
id: str | None = None
name: str | None = None
slug: Annotated[
str | None, Field(description="Immutable 12-char hex slug (e.g. a1b2c3-d1e2f3)")
] = None
created_at: AwareDatetime | None = None
class Role(StrEnum):
owner = "owner"
admin = "admin"
member = "member"
class TeamWithRole(Team):
role: Role | None = None
class TeamMember(BaseModel):
user_id: str | None = None
email: str | None = None
role: Role | None = None
joined_at: AwareDatetime | None = None
class TeamDetail(BaseModel):
team: Team | None = None
members: list[TeamMember] | None = None
class Range1(StrEnum):
field_5m = "5m"
field_10m = "10m"
field_1h = "1h"
field_2h = "2h"
field_6h = "6h"
field_12h = "12h"
field_24h = "24h"
class MetricPoint(BaseModel):
timestamp_unix: int | None = None
cpu_pct: Annotated[
float | None,
Field(
description="CPU utilization percentage (0-100), normalized to vCPU count"
),
] = None
mem_bytes: Annotated[
int | None,
Field(
description="Resident memory in bytes (VmRSS of Cloud Hypervisor process)"
),
] = None
disk_bytes: Annotated[
int | None, Field(description="Allocated disk bytes for the CoW sparse file")
] = None
class Provider(StrEnum):
discord = "discord"
slack = "slack"
teams = "teams"
googlechat = "googlechat"
telegram = "telegram"
matrix = "matrix"
webhook = "webhook"
class Event(StrEnum):
capsule_create = "capsule.create"
capsule_pause = "capsule.pause"
capsule_resume = "capsule.resume"
capsule_destroy = "capsule.destroy"
template_snapshot_create = "template.snapshot.create"
template_snapshot_delete = "template.snapshot.delete"
host_up = "host.up"
host_down = "host.down"
class CreateChannelRequest(BaseModel):
name: Annotated[str, Field(description="Unique channel name within the team.")]
provider: Provider
config: Annotated[
dict[str, str],
Field(
description='Provider-specific configuration fields. Discord/Slack/Teams/Google Chat: {"webhook_url": "..."}. Telegram: {"bot_token": "...", "chat_id": "..."}. Matrix: {"homeserver_url": "...", "access_token": "...", "room_id": "..."}. Webhook: {"url": "...", "secret": "..."} (secret is auto-generated if omitted).\n'
),
]
events: list[Event]
class TestChannelRequest(BaseModel):
provider: Provider
config: Annotated[
dict[str, str],
Field(
description="Provider-specific configuration fields (same as CreateChannelRequest.config)."
),
]
class RotateConfigRequest(BaseModel):
config: Annotated[
dict[str, str],
Field(
description="New provider configuration fields. Must include all required fields for the channel's provider. Replaces the existing config entirely.\n"
),
]
class UpdateChannelRequest(BaseModel):
name: str
events: list[Event]
class ChannelResponse(BaseModel):
id: str | None = None
team_id: str | None = None
name: str | None = None
provider: Provider | None = None
events: list[str] | None = None
created_at: AwareDatetime | None = None
updated_at: AwareDatetime | None = None
secret: Annotated[
str | None,
Field(description="Webhook secret. Only returned on creation, never again."),
] = None
class MeResponse(BaseModel):
name: str | None = None
email: EmailStr | None = None
has_password: Annotated[
bool | None,
Field(
description="Whether the user has a password set (false for OAuth-only accounts)"
),
] = None
providers: Annotated[
list[str] | None,
Field(description='List of linked OAuth provider names (e.g. ["github"])'),
] = None
class ChangePasswordRequest(BaseModel):
current_password: Annotated[
str | None, Field(description="Required when changing an existing password")
] = None
new_password: Annotated[str, Field(min_length=8)]
confirm_password: Annotated[
str | None,
Field(
description="Required when adding a password to an OAuth-only account (must match new_password)"
),
] = None
class Error2(BaseModel):
code: str | None = None
message: str | None = None
class Error1(BaseModel):
error: Error2 | None = None
class ActorType(StrEnum):
user = "user"
api_key = "api_key"
host = "host"
system = "system"
class Status2(StrEnum):
success = "success"
failure = "failure"
class AuditLogEntry(BaseModel):
id: str | None = None
actor_type: ActorType | None = None
actor_id: str | None = None
actor_name: str | None = None
resource_type: str | None = None
resource_id: str | None = None
action: str | None = None
scope: str | None = None
status: Status2 | None = None
metadata: dict[str, Any] | None = None
created_at: AwareDatetime | None = None
class Event2(StrEnum):
connected = "connected"
capsule_create = "capsule.create"
capsule_pause = "capsule.pause"
capsule_resume = "capsule.resume"
capsule_destroy = "capsule.destroy"
capsule_state_changed = "capsule.state.changed"
template_snapshot_create = "template.snapshot.create"
template_snapshot_delete = "template.snapshot.delete"
host_up = "host.up"
host_down = "host.down"
class Outcome(StrEnum):
"""
Present for action events (capsule.* except state.changed,
template.snapshot.*). Absent for host.up/down, capsule.state.changed,
and the connected sentinel.
"""
success = "success"
error = "error"
class Resource(BaseModel):
id: str | None = None
type: str | None = None
class Type5(StrEnum):
user = "user"
api_key = "api_key"
system = "system"
class Actor(BaseModel):
type: Type5 | None = None
id: str | None = None
name: str | None = None
class SSEEvent(BaseModel):
"""
Wire format of one SSE message body. The event name (`event:` line) is
the `kind` and the JSON below is the `data:` line.
"""
event: Event2 | None = None
outcome: Annotated[
Outcome | None,
Field(
description="Present for action events (capsule.* except state.changed,\ntemplate.snapshot.*). Absent for host.up/down, capsule.state.changed,\nand the connected sentinel.\n"
),
] = None
resource: Resource | None = None
actor: Actor | None = None
metadata: Annotated[
dict[str, str] | None,
Field(
description="Event-specific context. Examples: `reason` (ttl_expired,\nhost_failure, cleanup_after_create_error, orphaned),\n`host_ip`, `from`/`to` (for capsule.state.changed).\n"
),
] = None
error: Annotated[
str | None, Field(description="Failure reason; only set when outcome=error.")
] = None
sandbox: Annotated[
Capsule | None,
Field(description="Populated for capsule.* events; null if DB lookup failed."),
] = None
timestamp: AwareDatetime | None = None
class ListDirResponse(BaseModel): class ListDirResponse(BaseModel):
entries: list[FileEntry] | None = None entries: list[FileEntry] | None = None
class CreateHostResponse(BaseModel):
host: Host | None = None
registration_token: Annotated[
str | None,
Field(
description="One-time registration token for the host agent. Expires in 1 hour."
),
] = None
class RegisterHostResponse(BaseModel):
host: Host | None = None
token: Annotated[
str | None,
Field(description="Host JWT for X-Host-Token header. Valid for 7 days."),
] = None
refresh_token: Annotated[
str | None,
Field(
description="Refresh token for obtaining new JWTs. Valid for 60 days; rotated on each use."
),
] = None
class CapsuleMetrics(BaseModel):
sandbox_id: str | None = None
range: Range1 | None = None
points: list[MetricPoint] | None = None

2
uv.lock generated
View File

@ -1166,7 +1166,7 @@ wheels = [
[[package]] [[package]]
name = "wrenn" name = "wrenn"
version = "0.1.5" version = "0.2.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },