From 4924237a230eca8363541697ec0dc4084c07a4d0 Mon Sep 17 00:00:00 2001 From: pptx704 Date: Sun, 24 May 2026 05:02:08 +0000 Subject: [PATCH] v0.2.0 (#14) Co-authored-by: Tasnim Kabir Sadik Reviewed-on: https://git.omukk.dev/wrenn/python-sdk/pulls/14 Co-authored-by: pptx704 Co-committed-by: pptx704 --- README.md | 54 ++- api/openapi.yaml | 141 ++++++- pyproject.toml | 2 +- src/wrenn/exceptions.py | 13 + src/wrenn/models/__init__.py | 52 +-- src/wrenn/models/_generated.py | 704 +-------------------------------- uv.lock | 2 +- 7 files changed, 196 insertions(+), 772 deletions(-) diff --git a/README.md b/README.md index 2a48a06..e6facbe 100644 --- a/README.md +++ b/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") ``` diff --git a/api/openapi.yaml b/api/openapi.yaml index c8ad59f..0a4b218 100644 --- a/api/openapi.yaml +++ b/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: diff --git a/pyproject.toml b/pyproject.toml index 6ee4a11..b67c7a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/wrenn/exceptions.py b/src/wrenn/exceptions.py index 65ac7e8..2ead49d 100644 --- a/src/wrenn/exceptions.py +++ b/src/wrenn/exceptions.py @@ -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}") diff --git a/src/wrenn/models/__init__.py b/src/wrenn/models/__init__.py index 52bdc62..49c99b1 100644 --- a/src/wrenn/models/__init__.py +++ b/src/wrenn/models/__init__.py @@ -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", ] diff --git a/src/wrenn/models/_generated.py b/src/wrenn/models/_generated.py index e78331f..3104a87 100644 --- a/src/wrenn/models/_generated.py +++ b/src/wrenn/models/_generated.py @@ -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 diff --git a/uv.lock b/uv.lock index bc19b60..0dc0352 100644 --- a/uv.lock +++ b/uv.lock @@ -1166,7 +1166,7 @@ wheels = [ [[package]] name = "wrenn" -version = "0.1.5" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "certifi" },