Compare commits
4 Commits
27cfe42b3c
...
a2d9aac78b
| Author | SHA1 | Date | |
|---|---|---|---|
| a2d9aac78b | |||
| 4dcbc73003 | |||
| 7b04ba3949 | |||
| de72dfe9c8 |
54
README.md
54
README.md
@ -172,6 +172,8 @@ import sys
|
||||
# Stream a new command
|
||||
for event in capsule.commands.stream("python", args=["-u", "train.py"]):
|
||||
match event.type:
|
||||
case "start":
|
||||
print(f"PID: {event.pid}")
|
||||
case "stdout":
|
||||
print(event.data, end="")
|
||||
case "stderr":
|
||||
@ -181,8 +183,11 @@ for event in capsule.commands.stream("python", args=["-u", "train.py"]):
|
||||
|
||||
# Connect to a running background process
|
||||
for event in capsule.commands.connect(handle.pid):
|
||||
if event.type == "stdout":
|
||||
print(event.data, end="")
|
||||
match event.type:
|
||||
case "start":
|
||||
print(f"PID: {event.pid}")
|
||||
case "stdout":
|
||||
print(event.data, end="")
|
||||
```
|
||||
|
||||
#### Process Management
|
||||
@ -211,6 +216,7 @@ capsule.files.exists("/app/main.py") # True
|
||||
|
||||
# List directory
|
||||
entries = capsule.files.list("/home/user", depth=1)
|
||||
# FileEntry has: name, type (file/dir), size, modified_at
|
||||
for entry in entries:
|
||||
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")
|
||||
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`:
|
||||
|
||||
```python
|
||||
@ -308,7 +333,7 @@ except GitAuthError as e:
|
||||
```python
|
||||
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")
|
||||
for event in term:
|
||||
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 |
|
||||
| `error` | `ExecutionError \| None` | `.name`, `.value`, `.traceback` |
|
||||
| `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` |
|
||||
|
||||
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
|
||||
|
||||
@ -527,15 +553,15 @@ The SDK maps server error codes to typed exceptions:
|
||||
```python
|
||||
from wrenn import (
|
||||
WrennError,
|
||||
WrennValidationError, # 400
|
||||
WrennAuthenticationError, # 401
|
||||
WrennForbiddenError, # 403
|
||||
WrennNotFoundError, # 404
|
||||
WrennConflictError, # 409
|
||||
WrennHostHasCapsulesError, # 409 (host has running capsules)
|
||||
WrennAgentError, # 502
|
||||
WrennInternalError, # 500
|
||||
WrennHostUnavailableError, # 503
|
||||
WrennValidationError, # 400
|
||||
WrennAuthenticationError, # 401
|
||||
WrennForbiddenError, # 403
|
||||
WrennNotFoundError, # 404
|
||||
WrennConflictError, # 409
|
||||
WrennHostHasCapsulesError, # 409 (host has running capsules)
|
||||
WrennInternalError, # 500
|
||||
WrennAgentError, # 502
|
||||
WrennHostUnavailableError, # 503
|
||||
)
|
||||
|
||||
try:
|
||||
@ -603,7 +629,7 @@ with WrennClient(api_key="wrn_...") as client:
|
||||
|
||||
# Snapshots
|
||||
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")
|
||||
```
|
||||
|
||||
|
||||
141
api/openapi.yaml
141
api/openapi.yaml
@ -2716,14 +2716,39 @@ paths:
|
||||
tags: [admin]
|
||||
security:
|
||||
- sessionAuth: []
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
description: Page number for pagination.
|
||||
responses:
|
||||
"200":
|
||||
description: Teams list
|
||||
description: Paginated teams list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: {type: object}
|
||||
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:
|
||||
put:
|
||||
@ -2743,12 +2768,20 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [byoc]
|
||||
required: [enabled]
|
||||
properties:
|
||||
byoc: {type: boolean}
|
||||
enabled:
|
||||
type: boolean
|
||||
description: true to enable BYOC, false to disable.
|
||||
responses:
|
||||
"204":
|
||||
description: Updated
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
|
||||
/v1/admin/teams/{id}:
|
||||
delete:
|
||||
@ -2765,6 +2798,38 @@ paths:
|
||||
responses:
|
||||
"204":
|
||||
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:
|
||||
get:
|
||||
@ -3581,10 +3646,6 @@ components:
|
||||
type: integer
|
||||
memory_mb_reserved:
|
||||
type: integer
|
||||
sampled_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
peaks:
|
||||
type: object
|
||||
description: Maximum values over the last 30 days.
|
||||
@ -3633,10 +3694,6 @@ components:
|
||||
type: integer
|
||||
timeout_sec:
|
||||
type: integer
|
||||
guest_ip:
|
||||
type: string
|
||||
host_ip:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
@ -3661,7 +3718,11 @@ components:
|
||||
agent_version, envd_version) when running.
|
||||
disk_size_mb:
|
||||
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:
|
||||
type: object
|
||||
@ -4013,6 +4074,25 @@ components:
|
||||
updated_at:
|
||||
type: string
|
||||
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:
|
||||
type: object
|
||||
@ -4124,6 +4204,39 @@ components:
|
||||
items:
|
||||
$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:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "wrenn"
|
||||
version = "0.1.5"
|
||||
version = "0.2.0"
|
||||
description = "Python SDK for Wrenn"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@ -164,4 +164,17 @@ def __getattr__(name: str) -> type:
|
||||
stacklevel=2,
|
||||
)
|
||||
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}")
|
||||
|
||||
@ -1,63 +1,17 @@
|
||||
from wrenn.models._generated import (
|
||||
APIKeyResponse,
|
||||
Capsule,
|
||||
CreateAPIKeyRequest,
|
||||
CreateCapsuleRequest,
|
||||
CreateHostRequest,
|
||||
CreateHostResponse,
|
||||
CreateSnapshotRequest,
|
||||
Encoding,
|
||||
Error,
|
||||
Error1,
|
||||
ExecRequest,
|
||||
ExecResponse,
|
||||
FileEntry,
|
||||
Host,
|
||||
ListDirRequest,
|
||||
ListDirResponse,
|
||||
LoginRequest,
|
||||
MakeDirRequest,
|
||||
MakeDirResponse,
|
||||
ReadFileRequest,
|
||||
RegisterHostRequest,
|
||||
RegisterHostResponse,
|
||||
RemoveRequest,
|
||||
SignupRequest,
|
||||
Status,
|
||||
Status1,
|
||||
Template,
|
||||
Type,
|
||||
Type2,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"APIKeyResponse",
|
||||
"CreateAPIKeyRequest",
|
||||
"CreateHostRequest",
|
||||
"CreateHostResponse",
|
||||
"CreateCapsuleRequest",
|
||||
"CreateSnapshotRequest",
|
||||
"Encoding",
|
||||
"Error",
|
||||
"Error1",
|
||||
"ExecRequest",
|
||||
"ExecResponse",
|
||||
"FileEntry",
|
||||
"Host",
|
||||
"ListDirRequest",
|
||||
"ListDirResponse",
|
||||
"LoginRequest",
|
||||
"MakeDirRequest",
|
||||
"MakeDirResponse",
|
||||
"ReadFileRequest",
|
||||
"RegisterHostRequest",
|
||||
"RegisterHostResponse",
|
||||
"RemoveRequest",
|
||||
"Capsule",
|
||||
"SignupRequest",
|
||||
"FileEntry",
|
||||
"ListDirResponse",
|
||||
"MakeDirResponse",
|
||||
"Status",
|
||||
"Status1",
|
||||
"Template",
|
||||
"Type",
|
||||
"Type2",
|
||||
]
|
||||
|
||||
@ -1,147 +1,13 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: openapi.yaml
|
||||
# timestamp: 2026-05-22T19:20:45+00:00
|
||||
# timestamp: 2026-05-23T11:20:02+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
from pydantic import AwareDatetime, BaseModel, EmailStr, Field
|
||||
from typing import Annotated, Any
|
||||
from datetime import date as date_aliased
|
||||
from pydantic import AwareDatetime, BaseModel, Field
|
||||
from typing import Annotated
|
||||
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):
|
||||
pending = "pending"
|
||||
starting = "starting"
|
||||
@ -164,8 +30,6 @@ class Capsule(BaseModel):
|
||||
vcpus: int | None = None
|
||||
memory_mb: int | None = None
|
||||
timeout_sec: int | None = None
|
||||
guest_ip: str | None = None
|
||||
host_ip: str | None = None
|
||||
created_at: AwareDatetime | None = None
|
||||
started_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"
|
||||
),
|
||||
] = None
|
||||
disk_size_mb: int | None = None
|
||||
|
||||
|
||||
class CreateSnapshotRequest(BaseModel):
|
||||
sandbox_id: Annotated[
|
||||
str, Field(description="ID of the running capsule to snapshot.")
|
||||
]
|
||||
name: Annotated[
|
||||
str | None,
|
||||
Field(description="Name for the snapshot template. Auto-generated if omitted."),
|
||||
disk_size_mb: Annotated[
|
||||
int | None, Field(description="Maximum disk capacity in MiB.")
|
||||
] = None
|
||||
disk_used_mb: Annotated[
|
||||
int | None,
|
||||
Field(
|
||||
description="Current disk usage in MiB. Only populated on individual capsule GET; omitted in list responses."
|
||||
),
|
||||
] = None
|
||||
|
||||
|
||||
@ -216,121 +78,6 @@ class Template(BaseModel):
|
||||
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):
|
||||
file = "file"
|
||||
directory = "directory"
|
||||
@ -354,438 +101,9 @@ class FileEntry(BaseModel):
|
||||
symlink_target: str | None = None
|
||||
|
||||
|
||||
class MakeDirRequest(BaseModel):
|
||||
path: Annotated[
|
||||
str, Field(description="Directory path to create inside the capsule")
|
||||
]
|
||||
|
||||
|
||||
class MakeDirResponse(BaseModel):
|
||||
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):
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user