openapi: "3.1.0" info: title: Wrenn API description: AI agent execution platform API. version: "0.2.0" servers: - url: http://localhost:8080 description: Local development security: [] paths: /v1/auth/signup: post: summary: Create a new account operationId: signup tags: [auth] description: | Creates an inactive user account and sends an activation email. The user must activate their account within 30 minutes. Does not return a JWT — the user must activate first, then sign in. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SignupRequest" responses: "201": description: Account created, activation email sent content: application/json: schema: $ref: "#/components/schemas/SignupResponse" "400": description: Invalid request (bad email, short password) content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Email already registered or signup cooldown active content: application/json: schema: $ref: "#/components/schemas/Error" /v1/auth/activate: post: summary: Activate account via email token operationId: activate tags: [auth] description: | Consumes the activation token sent via email and activates the user account. Creates a default team and sets a session cookie to log the user in. requestBody: required: true content: application/json: schema: type: object required: [token] properties: token: type: string responses: "200": description: Account activated, session cookie set content: application/json: schema: $ref: "#/components/schemas/SessionResponse" "400": description: Invalid or expired token content: application/json: schema: $ref: "#/components/schemas/Error" /v1/auth/logout: post: summary: Revoke the current session operationId: logout tags: [auth] security: - sessionAuth: [] responses: "204": description: Session revoked; cookies cleared "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /v1/auth/logout-all: post: summary: Revoke every session for the current user operationId: logoutAll tags: [auth] description: | Revokes every active session for the calling user across all devices, including the caller's own. Returns 204 and clears cookies on the response. Triggered automatically by password change, password add, and password reset. security: - sessionAuth: [] responses: "204": description: All sessions revoked "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /v1/me/sessions: get: summary: List the caller's active sessions operationId: listSessions tags: [me] security: - sessionAuth: [] responses: "200": description: Sessions list content: application/json: schema: type: object properties: sessions: type: array items: type: object properties: id: type: string user_agent: type: string ip_address: type: string created_at: type: string format: date-time last_seen_at: type: string format: date-time expires_at: type: string format: date-time current: type: boolean "401": $ref: "#/components/responses/Unauthorized" /v1/me/sessions/{id}: delete: summary: Revoke a single session operationId: revokeSession tags: [me] security: - sessionAuth: [] parameters: - name: id in: path required: true schema: type: string responses: "204": description: Session revoked "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /v1/auth/switch-team: post: summary: Switch active team operationId: switchTeam tags: [auth] security: - sessionAuth: [] description: | Rotates the session SID and updates its team scope. The user must be a member of the target team (verified from DB). The new wrenn_sid and wrenn_csrf cookies are set on the response. requestBody: required: true content: application/json: schema: type: object required: [team_id] properties: team_id: type: string responses: "200": description: New session issued for the target team; cookies refreshed content: application/json: schema: $ref: "#/components/schemas/SessionResponse" "403": description: Not a member of this team content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Team not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/auth/login: post: summary: Log in with email and password operationId: login tags: [auth] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/LoginRequest" responses: "200": description: Login successful content: application/json: schema: $ref: "#/components/schemas/SessionResponse" "401": description: Invalid credentials content: application/json: schema: $ref: "#/components/schemas/Error" /auth/oauth/{provider}: parameters: - name: provider in: path required: true schema: type: string enum: [github] description: OAuth provider name get: summary: Start OAuth login flow operationId: oauthRedirect tags: [auth] description: | Redirects the user to the OAuth provider's authorization page. Sets a short-lived CSRF state cookie for validation on callback. responses: "302": description: Redirect to provider authorization URL "404": description: Provider not found or not configured content: application/json: schema: $ref: "#/components/schemas/Error" /auth/oauth/{provider}/callback: parameters: - name: provider in: path required: true schema: type: string enum: [github] description: OAuth provider name get: summary: OAuth callback operationId: oauthCallback tags: [auth] description: | Handles the OAuth provider's callback after user authorization. Exchanges the authorization code for a user profile, creates or logs in the user, sets the wrenn_sid + wrenn_csrf cookies, and redirects to the SPA callback page. **On success:** redirects to `{OAUTH_REDIRECT_URL}/auth/{provider}/callback` (no tokens in URL). **On error:** redirects to `{OAUTH_REDIRECT_URL}/auth/{provider}/callback?error=...` Possible error codes: `access_denied`, `invalid_state`, `missing_code`, `exchange_failed`, `email_taken`, `internal_error`. parameters: - name: code in: query schema: type: string description: Authorization code from the OAuth provider - name: state in: query schema: type: string description: CSRF state parameter (must match the cookie) responses: "302": description: Redirect to frontend with token or error /v1/me: get: summary: Get current user profile operationId: getMe tags: [account] security: - sessionAuth: [] responses: "200": description: User profile content: application/json: schema: $ref: "#/components/schemas/MeResponse" patch: summary: Update display name operationId: updateName tags: [account] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: type: string minLength: 1 maxLength: 100 responses: "204": description: Name updated; session caches refreshed "400": description: Invalid name content: application/json: schema: $ref: "#/components/schemas/Error" delete: summary: Delete current account operationId: deleteAccount tags: [account] security: - sessionAuth: [] description: | Soft-deletes the account (sets status=deleted, deleted_at=now). The account is permanently removed after 15 days. Blocked if the user owns any team that has other members. requestBody: required: true content: application/json: schema: type: object required: [confirmation] properties: confirmation: type: string description: Must match the user's email address (case-insensitive) responses: "204": description: Account scheduled for deletion "400": description: Confirmation does not match email content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: User owns teams with other members content: application/json: schema: $ref: "#/components/schemas/Error" /v1/me/password: post: summary: Change or add password operationId: changePassword tags: [account] security: - sessionAuth: [] description: | For users with an existing password: requires `current_password` and `new_password`. For OAuth-only users adding a password: requires `new_password` and `confirm_password`. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ChangePasswordRequest" responses: "204": description: Password updated "400": description: Invalid request (short password, mismatch, etc.) content: application/json: schema: $ref: "#/components/schemas/Error" "401": description: Current password is incorrect content: application/json: schema: $ref: "#/components/schemas/Error" /v1/me/password/reset: post: summary: Request a password reset email operationId: requestPasswordReset tags: [account] description: | Sends a password reset link to the given email. Always returns 200 regardless of whether the email exists, to prevent account enumeration. The reset token expires in 15 minutes. requestBody: required: true content: application/json: schema: type: object required: [email] properties: email: type: string format: email responses: "204": description: Request accepted (email sent if account exists) /v1/me/password/reset/confirm: post: summary: Confirm password reset operationId: confirmPasswordReset tags: [account] description: | Consumes a password reset token and sets a new password. The token is single-use and expires after 15 minutes. requestBody: required: true content: application/json: schema: type: object required: [token, new_password] properties: token: type: string description: Raw reset token from the email link new_password: type: string minLength: 8 responses: "204": description: Password reset successful "400": description: Invalid or expired token, or password too short content: application/json: schema: $ref: "#/components/schemas/Error" /v1/me/providers/{provider}/connect: parameters: - name: provider in: path required: true schema: type: string enum: [github] description: OAuth provider name get: summary: Initiate OAuth provider link operationId: connectProvider tags: [account] security: - sessionAuth: [] description: | Sets OAuth state and link cookies, then returns the provider's authorization URL. The frontend navigates to this URL to start the OAuth flow. On callback, the provider is linked to the current account (not a new registration). responses: "200": description: Authorization URL content: application/json: schema: type: object properties: auth_url: type: string format: uri "404": description: Provider not found or not configured content: application/json: schema: $ref: "#/components/schemas/Error" /v1/me/providers/{provider}: parameters: - name: provider in: path required: true schema: type: string enum: [github] description: OAuth provider name delete: summary: Disconnect an OAuth provider operationId: disconnectProvider tags: [account] security: - sessionAuth: [] description: | Unlinks the OAuth provider from the current account. Blocked if this is the user's only login method (no password and no other providers). responses: "204": description: Provider disconnected "400": description: Cannot disconnect last login method content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Provider not connected content: application/json: schema: $ref: "#/components/schemas/Error" /v1/api-keys: post: summary: Create an API key operationId: createAPIKey tags: [api-keys] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateAPIKeyRequest" responses: "201": description: API key created (plaintext key only shown once) content: application/json: schema: $ref: "#/components/schemas/APIKeyResponse" "401": description: Unauthorized content: application/json: schema: $ref: "#/components/schemas/Error" get: summary: List API keys for your team operationId: listAPIKeys tags: [api-keys] security: - sessionAuth: [] responses: "200": description: List of API keys (plaintext keys are never returned) content: application/json: schema: type: array items: $ref: "#/components/schemas/APIKeyResponse" /v1/api-keys/{id}: parameters: - name: id in: path required: true schema: type: string delete: summary: Delete an API key operationId: deleteAPIKey tags: [api-keys] security: - sessionAuth: [] responses: "204": description: API key deleted /v1/users/search: get: summary: Search users by email prefix operationId: searchUsers tags: [users] security: - sessionAuth: [] description: | Returns up to 10 users whose email starts with the given prefix. The prefix must contain "@". Intended for the add-member UI autocomplete. parameters: - name: email in: query required: true schema: type: string description: Email prefix (must contain "@", e.g. "alice@") responses: "200": description: Matching users content: application/json: schema: type: array items: $ref: "#/components/schemas/UserSearchResult" "400": description: Prefix does not contain "@" content: application/json: schema: $ref: "#/components/schemas/Error" /v1/teams: get: summary: List teams for the authenticated user operationId: listTeams tags: [teams] security: - sessionAuth: [] responses: "200": description: Teams the user belongs to, each with their role content: application/json: schema: type: array items: $ref: "#/components/schemas/TeamWithRole" post: summary: Create a new team operationId: createTeam tags: [teams] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: type: string description: 1-128 chars; A-Z a-z 0-9 space _ responses: "201": description: Team created (caller is owner) content: application/json: schema: $ref: "#/components/schemas/TeamWithRole" "400": description: Invalid team name content: application/json: schema: $ref: "#/components/schemas/Error" /v1/teams/{id}: parameters: - name: id in: path required: true schema: type: string description: Team ID (must match the JWT's team_id) get: summary: Get team info and member list operationId: getTeam tags: [teams] security: - sessionAuth: [] responses: "200": description: Team details with members content: application/json: schema: $ref: "#/components/schemas/TeamDetail" "403": description: JWT team does not match requested team content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Team not found content: application/json: schema: $ref: "#/components/schemas/Error" patch: summary: Rename the team operationId: renameTeam tags: [teams] security: - sessionAuth: [] description: Admin or owner role required (verified from DB). requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: type: string responses: "204": description: Renamed "400": description: Invalid team name content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Insufficient role content: application/json: schema: $ref: "#/components/schemas/Error" delete: summary: Delete the team operationId: deleteTeam tags: [teams] security: - sessionAuth: [] description: | Owner only. Soft-deletes the team and destroys all running/paused/starting capsules. All DB records are preserved. The team slug is permanently reserved. responses: "204": description: Team deleted "403": description: Caller is not the owner content: application/json: schema: $ref: "#/components/schemas/Error" /v1/teams/{id}/members: parameters: - name: id in: path required: true schema: type: string get: summary: List team members operationId: listTeamMembers tags: [teams] security: - sessionAuth: [] responses: "200": description: Members with roles content: application/json: schema: type: array items: $ref: "#/components/schemas/TeamMember" post: summary: Add a member by email operationId: addTeamMember tags: [teams] security: - sessionAuth: [] description: Admin or owner role required. User is added instantly as a member. requestBody: required: true content: application/json: schema: type: object required: [email] properties: email: type: string format: email responses: "201": description: Member added content: application/json: schema: $ref: "#/components/schemas/TeamMember" "403": description: Insufficient role content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: No account with that email content: application/json: schema: $ref: "#/components/schemas/Error" "400": description: User is already a member content: application/json: schema: $ref: "#/components/schemas/Error" /v1/teams/{id}/members/{uid}: parameters: - name: id in: path required: true schema: type: string - name: uid in: path required: true schema: type: string description: Target user ID patch: summary: Update member role operationId: updateMemberRole tags: [teams] security: - sessionAuth: [] description: | Admin or owner required. Valid target roles: admin, member. The owner's role cannot be changed. requestBody: required: true content: application/json: schema: type: object required: [role] properties: role: type: string enum: [admin, member] responses: "204": description: Role updated "403": description: Insufficient role or attempt to modify owner content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: User is not a member content: application/json: schema: $ref: "#/components/schemas/Error" delete: summary: Remove a member operationId: removeTeamMember tags: [teams] security: - sessionAuth: [] description: Admin or owner required. Owner cannot be removed. responses: "204": description: Member removed "403": description: Insufficient role or attempt to remove owner content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: User is not a member content: application/json: schema: $ref: "#/components/schemas/Error" /v1/teams/{id}/leave: parameters: - name: id in: path required: true schema: type: string post: summary: Leave the team operationId: leaveTeam tags: [teams] security: - sessionAuth: [] description: The owner cannot leave; they must delete the team instead. responses: "204": description: Left the team "403": description: Owner cannot leave content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules: post: summary: Create a capsule operationId: createCapsule tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateCapsuleRequest" responses: "202": description: Capsule creation initiated (status will be "starting") content: application/json: schema: $ref: "#/components/schemas/Capsule" "502": description: Host agent error content: application/json: schema: $ref: "#/components/schemas/Error" get: summary: List capsules for your team operationId: listCapsules tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] responses: "200": description: List of capsules content: application/json: schema: type: array items: $ref: "#/components/schemas/Capsule" /v1/capsules/stats: get: summary: Get capsule usage stats for your team operationId: getCapsuleStats tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] parameters: - name: range in: query required: false schema: type: string enum: [5m, 1h, 6h, 24h, 30d] default: 1h description: Time window for the time-series data. responses: "200": description: Capsule stats for the team content: application/json: schema: $ref: "#/components/schemas/CapsuleStats" "400": $ref: "#/components/responses/BadRequest" /v1/capsules/usage: get: summary: Get daily CPU and RAM usage for your team operationId: getCapsuleUsage tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] parameters: - name: from in: query required: false schema: type: string format: date description: Start date (YYYY-MM-DD). Defaults to 30 days ago. - name: to in: query required: false schema: type: string format: date description: End date (YYYY-MM-DD). Defaults to today. responses: "200": description: Daily usage data for the team content: application/json: schema: $ref: "#/components/schemas/UsageResponse" "400": $ref: "#/components/responses/BadRequest" /v1/capsules/{id}: parameters: - name: id in: path required: true schema: type: string get: summary: Get capsule details operationId: getCapsule tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] responses: "200": description: Capsule details content: application/json: schema: $ref: "#/components/schemas/Capsule" "404": description: Capsule not found content: application/json: schema: $ref: "#/components/schemas/Error" delete: summary: Destroy a capsule operationId: destroyCapsule tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] responses: "202": description: Capsule destruction initiated /v1/capsules/{id}/exec: parameters: - name: id in: path required: true schema: type: string post: summary: Execute a command operationId: execCommand tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ExecRequest" responses: "200": description: Command output (foreground exec) content: application/json: schema: $ref: "#/components/schemas/ExecResponse" "202": description: Background process started content: application/json: schema: $ref: "#/components/schemas/BackgroundExecResponse" "404": description: Capsule not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Capsule not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/processes: parameters: - name: id in: path required: true schema: type: string get: summary: List running processes operationId: listProcesses tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] description: | Returns all running processes inside the capsule, including background processes and any processes started by templates or init scripts. responses: "200": description: Process list content: application/json: schema: $ref: "#/components/schemas/ProcessListResponse" "404": description: Capsule not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Capsule not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/processes/{selector}: parameters: - name: id in: path required: true schema: type: string - name: selector in: path required: true description: Process PID (numeric) or tag (string) schema: type: string delete: summary: Kill a process operationId: killProcess tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] parameters: - name: signal in: query required: false description: Signal to send (SIGKILL or SIGTERM, default SIGKILL) schema: type: string enum: [SIGKILL, SIGTERM] default: SIGKILL responses: "204": description: Process killed "404": description: Capsule or process not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Capsule not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/processes/{selector}/stream: parameters: - name: id in: path required: true schema: type: string - name: selector in: path required: true description: Process PID (numeric) or tag (string) schema: type: string get: summary: Stream process output via WebSocket operationId: connectProcess tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] description: | Opens a WebSocket connection to stream stdout/stderr from a running background process. The selector can be a numeric PID or a string tag. Server sends JSON messages: - `{"type": "start", "pid": 42}` — connected to process - `{"type": "stdout", "data": "..."}` — stdout output - `{"type": "stderr", "data": "..."}` — stderr output - `{"type": "exit", "exit_code": 0}` — process exited - `{"type": "error", "data": "..."}` — error message responses: "101": description: WebSocket upgrade /v1/capsules/{id}/ping: parameters: - name: id in: path required: true schema: type: string post: summary: Reset capsule inactivity timer operationId: pingCapsule tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] description: | Resets the last_active_at timestamp for a running capsule, preventing the auto-pause TTL from expiring. Use this as a keepalive for capsules that are idle but should remain running. responses: "204": description: Ping acknowledged, inactivity timer reset "404": description: Capsule not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Capsule not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/metrics: parameters: - name: id in: path required: true schema: type: string get: summary: Get per-capsule resource metrics operationId: getCapsuleMetrics tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] description: | Returns time-series CPU, memory, and disk metrics for a capsule. Three tiers are available with different granularity and retention: - `10m`: 500ms samples, last 10 minutes - `2h`: 30-second averages, last 2 hours - `24h`: 5-minute averages, last 24 hours For running capsules, data comes from the host agent's in-memory ring buffer. For paused capsules, data is read from persisted snapshots in the database. Stopped/destroyed capsules return 404. parameters: - name: range in: query required: false schema: type: string enum: ["5m", "10m", "1h", "2h", "6h", "12h", "24h"] default: "10m" description: Time range filter to query responses: "200": description: Metrics retrieved content: application/json: schema: $ref: "#/components/schemas/CapsuleMetrics" "400": description: Invalid range parameter content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Capsule not found or metrics not available content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/pause: parameters: - name: id in: path required: true schema: type: string post: summary: Pause a running capsule operationId: pauseCapsule tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] description: | Takes a snapshot of the capsule (VM state + memory + rootfs), then destroys all running resources. The capsule exists only as files on disk and can be resumed later. responses: "202": description: Capsule pause initiated (status will be "pausing") content: application/json: schema: $ref: "#/components/schemas/Capsule" "409": description: Capsule not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/resume: parameters: - name: id in: path required: true schema: type: string post: summary: Resume a paused capsule operationId: resumeCapsule tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] description: | Restores a paused capsule from its snapshot. Cloud Hypervisor is relaunched in --restore mode with memory_restore_mode=ondemand so guest pages fault in lazily via userfaultfd. The original network slot (and host-reachable IP) is preserved across pause/resume. responses: "202": description: Capsule resume initiated (status will be "resuming") content: application/json: schema: $ref: "#/components/schemas/Capsule" "409": description: Capsule not paused content: application/json: schema: $ref: "#/components/schemas/Error" /v1/snapshots: post: summary: Create a snapshot template operationId: createSnapshot tags: [snapshots] security: - apiKeyAuth: [] - sessionAuth: [] description: | Live snapshot: briefly pauses the capsule, writes its VM state + memory + flattened rootfs to a new template directory, then resumes the capsule. The source capsule keeps running after the snapshot; the resulting template can be used to create new capsules. Snapshots are immutable: each call must use a fresh name. Re-using an existing name returns 409 Conflict. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateSnapshotRequest" responses: "201": description: Snapshot created content: application/json: schema: $ref: "#/components/schemas/Template" "409": description: Name already exists or capsule not running content: application/json: schema: $ref: "#/components/schemas/Error" get: summary: List templates for your team operationId: listSnapshots tags: [snapshots] security: - apiKeyAuth: [] - sessionAuth: [] parameters: - name: type in: query required: false schema: type: string enum: [base, snapshot] description: Filter by template type. responses: "200": description: List of templates content: application/json: schema: type: array items: $ref: "#/components/schemas/Template" /v1/snapshots/{name}: parameters: - name: name in: path required: true schema: type: string delete: summary: Delete a snapshot template operationId: deleteSnapshot tags: [snapshots] security: - apiKeyAuth: [] - sessionAuth: [] description: Removes the snapshot files from disk and deletes the database record. responses: "204": description: Snapshot deleted "404": description: Template not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/files/write: parameters: - name: id in: path required: true schema: type: string post: summary: Upload a file operationId: uploadFile tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] requestBody: required: true content: multipart/form-data: schema: type: object required: [path, file] properties: path: type: string description: Absolute destination path inside the capsule file: type: string format: binary description: File content responses: "204": description: File uploaded "409": description: Capsule not running content: application/json: schema: $ref: "#/components/schemas/Error" "413": description: File too large content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/files/read: parameters: - name: id in: path required: true schema: type: string post: summary: Download a file operationId: downloadFile tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ReadFileRequest" responses: "200": description: File content content: application/octet-stream: schema: type: string format: binary "404": description: Capsule or file not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/files/list: parameters: - name: id in: path required: true schema: type: string post: summary: List directory contents operationId: listDir tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ListDirRequest" responses: "200": description: Directory listing content: application/json: schema: $ref: "#/components/schemas/ListDirResponse" "404": description: Capsule not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Capsule not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/files/mkdir: parameters: - name: id in: path required: true schema: type: string post: summary: Create a directory operationId: makeDir tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MakeDirRequest" responses: "200": description: Directory created content: application/json: schema: $ref: "#/components/schemas/MakeDirResponse" "404": description: Capsule not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: > Capsule not running, or a directory already exists at the target path (error code `already_exists`). content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/files/remove: parameters: - name: id in: path required: true schema: type: string post: summary: Remove a file or directory operationId: removePath tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RemoveRequest" responses: "204": description: File or directory removed "404": description: Capsule not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Capsule not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/exec/stream: parameters: - name: id in: path required: true schema: type: string get: summary: Stream command execution via WebSocket operationId: execStream tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] description: | Opens a WebSocket connection for streaming command execution. **Client sends** (first message to start the process): ```json {"type": "start", "cmd": "tail", "args": ["-f", "/var/log/syslog"]} ``` **Client sends** (to stop the process): ```json {"type": "stop"} ``` **Server sends** (process events as they arrive): ```json {"type": "start", "pid": 1234} {"type": "stdout", "data": "line of output\n"} {"type": "stderr", "data": "warning message\n"} {"type": "exit", "exit_code": 0} {"type": "error", "data": "description of error"} ``` The connection closes automatically after the process exits. responses: "101": description: WebSocket upgrade "404": description: Capsule not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Capsule not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/pty: parameters: - name: id in: path required: true schema: type: string get: summary: Interactive PTY session via WebSocket operationId: ptySession tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] description: | Opens a WebSocket connection for an interactive PTY (terminal) session. Supports creating new sessions, sending input, resizing, killing, and reconnecting to existing sessions. **Client sends** (first message — start a new PTY): ```json { "type": "start", "cmd": "/bin/bash", "args": [], "cols": 80, "rows": 24, "envs": {"TERM": "xterm-256color"}, "cwd": "/home/user", "user": "user" } ``` All fields except `type` are optional. Defaults: cmd="/bin/bash", cols=80, rows=24. **Client sends** (first message — reconnect to existing PTY): ```json {"type": "connect", "tag": "pty-abc123de"} ``` **Client sends** (after session is established): ```json {"type": "input", "data": ""} {"type": "resize", "cols": 120, "rows": 40} {"type": "kill"} ``` **Server sends**: ```json {"type": "started", "tag": "pty-abc123de", "pid": 42} {"type": "output", "data": ""} {"type": "exit", "exit_code": 0} {"type": "error", "data": "description", "fatal": true} {"type": "ping"} ``` PTY data (input and output) is base64-encoded because it contains raw terminal bytes (escape sequences, control codes) that are not valid UTF-8. Sessions persist across WebSocket disconnections — the process keeps running in the capsule. Use the `tag` from the "started" response to reconnect later. responses: "101": description: WebSocket upgrade "404": description: Capsule not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Capsule not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/files/stream/write: parameters: - name: id in: path required: true schema: type: string post: summary: Upload a file (streaming) operationId: streamUploadFile tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] description: | Streams file content to the capsule without buffering in memory. Suitable for large files. Uses the same multipart/form-data format as the non-streaming upload endpoint. requestBody: required: true content: multipart/form-data: schema: type: object required: [path, file] properties: path: type: string description: Absolute destination path inside the capsule file: type: string format: binary description: File content responses: "204": description: File uploaded "404": description: Capsule not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Capsule not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/capsules/{id}/files/stream/read: parameters: - name: id in: path required: true schema: type: string post: summary: Download a file (streaming) operationId: streamDownloadFile tags: [capsules] security: - apiKeyAuth: [] - sessionAuth: [] description: | Streams file content from the capsule without buffering in memory. Suitable for large files. Returns raw bytes with chunked transfer encoding. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ReadFileRequest" responses: "200": description: File content streamed in chunks content: application/octet-stream: schema: type: string format: binary "404": description: Capsule or file not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Capsule not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts: post: summary: Create a host operationId: createHost tags: [hosts] security: - sessionAuth: [] description: | Creates a new host record and returns a one-time registration token. Regular hosts can only be created by admins. BYOC hosts can be created by admins or team owners. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateHostRequest" responses: "201": description: Host created with registration token content: application/json: schema: $ref: "#/components/schemas/CreateHostResponse" "400": description: Invalid request content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Insufficient permissions content: application/json: schema: $ref: "#/components/schemas/Error" get: summary: List hosts operationId: listHosts tags: [hosts] security: - sessionAuth: [] description: | Admins see all hosts. Non-admins see only BYOC hosts belonging to their team. responses: "200": description: List of hosts content: application/json: schema: type: array items: $ref: "#/components/schemas/Host" /v1/hosts/{id}: parameters: - name: id in: path required: true schema: type: string get: summary: Get host details operationId: getHost tags: [hosts] security: - sessionAuth: [] responses: "200": description: Host details content: application/json: schema: $ref: "#/components/schemas/Host" "404": description: Host not found content: application/json: schema: $ref: "#/components/schemas/Error" delete: summary: Delete a host operationId: deleteHost tags: [hosts] security: - sessionAuth: [] description: | Admins can delete any host. Team owners and admins can delete BYOC hosts belonging to their team. Without `?force=true`, returns 409 if the host has active capsules. With `?force=true`, destroys all capsules first. parameters: - name: force in: query required: false schema: type: boolean description: If true, destroy all capsules on the host before deleting. responses: "204": description: Host deleted "403": description: Insufficient permissions content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Host has active capsules (only when force is not set) content: application/json: schema: $ref: "#/components/schemas/HostHasCapsulesError" /v1/hosts/{id}/token: parameters: - name: id in: path required: true schema: type: string post: summary: Regenerate registration token operationId: regenerateHostToken tags: [hosts] security: - sessionAuth: [] description: | Issues a new registration token for a host still in "pending" status. Use this when a previous registration attempt failed after consuming the original token. Same permission model as host creation. responses: "201": description: New registration token issued content: application/json: schema: $ref: "#/components/schemas/CreateHostResponse" "403": description: Insufficient permissions content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Host is not in pending status content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts/register: post: summary: Register a host agent operationId: registerHost tags: [hosts] description: | Called by the host agent on first startup. Validates the one-time registration token, records machine specs, sets the host status to "online", and returns a long-lived JWT for subsequent API calls (heartbeats). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RegisterHostRequest" responses: "201": description: Host registered, JWT returned content: application/json: schema: $ref: "#/components/schemas/RegisterHostResponse" "400": description: Invalid request content: application/json: schema: $ref: "#/components/schemas/Error" "401": description: Invalid or expired registration token content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts/{id}/heartbeat: parameters: - name: id in: path required: true schema: type: string post: summary: Host agent heartbeat operationId: hostHeartbeat tags: [hosts] security: - hostTokenAuth: [] description: | Updates the host's last_heartbeat_at timestamp. The host ID in the URL must match the host ID in the JWT. responses: "204": description: Heartbeat recorded "401": description: Invalid or missing host token content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Host ID mismatch content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts/sandbox-events: post: summary: Sandbox lifecycle event callback operationId: sandboxEventCallback tags: [hosts] security: - hostTokenAuth: [] description: | Receives autonomous lifecycle events from host agents (e.g. auto-pause from the TTL reaper). The event is published to an internal Redis stream for the control plane's event consumer to process. requestBody: required: true content: application/json: schema: type: object required: [event, sandbox_id, host_id] properties: event: type: string description: | Lifecycle event type. Known values: * `sandbox.auto_paused` — TTL reaper paused the capsule * `sandbox.stopped` — autonomous destroy (crash/eviction) * `sandbox.error` — VMM/crash watcher reported error Unknown event names are accepted and forwarded to the stream consumer as-is (future-compatible). sandbox_id: type: string host_id: type: string timestamp: type: integer format: int64 responses: "204": description: Event accepted "400": description: Invalid request content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Host ID mismatch content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts/auth/refresh: post: summary: Refresh host JWT operationId: refreshHostToken tags: [hosts] description: | Exchanges a refresh token for a new JWT and rotated refresh token. The old refresh token is immediately revoked. No authentication required — the refresh token itself is the credential. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RefreshHostTokenRequest" responses: "200": description: New JWT and rotated refresh token content: application/json: schema: $ref: "#/components/schemas/RefreshHostTokenResponse" "401": description: Invalid, expired, or revoked refresh token content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts/{id}/delete-preview: parameters: - name: id in: path required: true schema: type: string get: summary: Preview host deletion operationId: getHostDeletePreview tags: [hosts] security: - sessionAuth: [] description: | Returns the list of capsule IDs that would be destroyed if the host were deleted with `?force=true`. No state is modified. responses: "200": description: Deletion preview content: application/json: schema: $ref: "#/components/schemas/HostDeletePreview" "403": description: Insufficient permissions content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Host not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts/{id}/tags: parameters: - name: id in: path required: true schema: type: string get: summary: List host tags operationId: listHostTags tags: [hosts] security: - sessionAuth: [] responses: "200": description: List of tags content: application/json: schema: type: array items: type: string post: summary: Add a tag to a host operationId: addHostTag tags: [hosts] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AddTagRequest" responses: "204": description: Tag added "404": description: Host not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts/{id}/tags/{tag}: parameters: - name: id in: path required: true schema: type: string - name: tag in: path required: true schema: type: string delete: summary: Remove a tag from a host operationId: removeHostTag tags: [hosts] security: - sessionAuth: [] responses: "204": description: Tag removed "404": description: Host not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/channels: post: summary: Create a notification channel operationId: createChannel tags: [channels] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateChannelRequest" responses: "201": description: Channel created content: application/json: schema: $ref: "#/components/schemas/ChannelResponse" "400": $ref: "#/components/responses/BadRequest" get: summary: List notification channels operationId: listChannels tags: [channels] security: - sessionAuth: [] responses: "200": description: Channels list content: application/json: schema: type: array items: $ref: "#/components/schemas/ChannelResponse" /v1/channels/test: post: summary: Test a channel configuration description: > Sends a test notification using the provided provider and config without saving anything. Use this to verify credentials before creating a channel. operationId: testChannel tags: [channels] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TestChannelRequest" responses: "200": description: Test notification sent successfully content: application/json: schema: type: object properties: status: type: string example: ok "400": $ref: "#/components/responses/BadRequest" /v1/channels/{id}: parameters: - name: id in: path required: true schema: type: string get: summary: Get a notification channel operationId: getChannel tags: [channels] security: - sessionAuth: [] responses: "200": description: Channel details content: application/json: schema: $ref: "#/components/schemas/ChannelResponse" "404": description: Channel not found content: application/json: schema: $ref: "#/components/schemas/Error" patch: summary: Update a notification channel operationId: updateChannel tags: [channels] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateChannelRequest" responses: "200": description: Channel updated content: application/json: schema: $ref: "#/components/schemas/ChannelResponse" "400": $ref: "#/components/responses/BadRequest" "404": description: Channel not found content: application/json: schema: $ref: "#/components/schemas/Error" delete: summary: Delete a notification channel operationId: deleteChannel tags: [channels] security: - sessionAuth: [] responses: "204": description: Channel deleted /v1/channels/{id}/config: parameters: - name: id in: path required: true schema: type: string put: summary: Rotate channel secrets description: > Replaces the channel's provider configuration entirely with new secrets. The previous config is discarded. Config fields must match the provider's required fields. operationId: rotateChannelConfig tags: [channels] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RotateConfigRequest" responses: "200": description: Config rotated content: application/json: schema: $ref: "#/components/schemas/ChannelResponse" "400": $ref: "#/components/responses/BadRequest" "404": description: Channel not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/admin/users/{id}/admin: put: summary: Grant or revoke platform admin operationId: setUserAdmin tags: [admin] description: | Sets the platform admin flag on a user. Cannot remove the last admin. Requires platform admin access. Session caches for the target user are invalidated immediately so the flag flip takes effect on the user's next request. security: - sessionAuth: [] parameters: - name: id in: path required: true schema: type: string example: "usr-a1b2c3d4" requestBody: required: true content: application/json: schema: type: object required: [admin] properties: admin: type: boolean description: true to grant admin, false to revoke. responses: "204": description: Admin status updated "400": $ref: "#/components/responses/BadRequest" "403": description: Caller is not a platform admin content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: User not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/events/stream: get: summary: Real-time lifecycle event stream operationId: streamEvents tags: [events] description: | Server-Sent Events stream of capsule, template, and host lifecycle events scoped to the caller's active team. Browsers send the wrenn_sid cookie automatically on EventSource connections; SDKs authenticate via X-API-Key. Frame format follows the standard SSE protocol: ``` event: capsule.create data: {"event":"capsule.create","outcome":"success","resource":{"id":"sb-..."},"sandbox":{...},"timestamp":"2026-05-19T02:00:00Z"} : keepalive ``` A `: keepalive` comment is emitted every 30s. security: - apiKeyAuth: [] - sessionAuth: [] responses: "200": description: SSE stream opened content: text/event-stream: schema: $ref: "#/components/schemas/SSEEvent" "401": description: Missing or invalid auth content: application/json: schema: $ref: "#/components/schemas/Error" /v1/audit-logs: get: summary: List team audit log entries operationId: listAuditLogs tags: [audit] description: Paginated cursor list of audit events for the caller's team. security: - sessionAuth: [] parameters: - name: before in: query required: false schema: type: string format: date-time - name: before_id in: query required: false schema: type: string - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 200 default: 50 responses: "200": description: Audit log page content: application/json: schema: type: object properties: entries: type: array items: $ref: "#/components/schemas/AuditLogEntry" next_cursor: type: object nullable: true properties: before: type: string format: date-time before_id: type: string /v1/admin/events/stream: get: summary: Admin SSE event stream (all teams) operationId: adminStreamEvents tags: [admin, events] description: | Admin variant of /v1/events/stream that emits events across all teams. Requires an admin session cookie. security: - sessionAuth: [] responses: "200": description: SSE stream opened content: text/event-stream: schema: $ref: "#/components/schemas/SSEEvent" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /v1/admin/audit-logs: get: summary: List audit log entries (all teams) operationId: adminListAuditLogs tags: [admin, audit] security: - sessionAuth: [] parameters: - name: before in: query schema: {type: string, format: date-time} - name: before_id in: query schema: {type: string} - name: limit in: query schema: {type: integer, minimum: 1, maximum: 200, default: 50} responses: "200": description: Audit log page (all teams) content: application/json: schema: type: object properties: entries: type: array items: $ref: "#/components/schemas/AuditLogEntry" /v1/admin/teams: get: summary: List all teams (admin) operationId: adminListTeams tags: [admin] security: - sessionAuth: [] responses: "200": description: Teams list content: application/json: schema: type: array items: {type: object} /v1/admin/teams/{id}/byoc: put: summary: Toggle BYOC for a team (admin) operationId: adminSetTeamBYOC tags: [admin] security: - sessionAuth: [] parameters: - name: id in: path required: true schema: {type: string} requestBody: required: true content: application/json: schema: type: object required: [byoc] properties: byoc: {type: boolean} responses: "204": description: Updated /v1/admin/teams/{id}: delete: summary: Delete a team (admin) operationId: adminDeleteTeam tags: [admin] security: - sessionAuth: [] parameters: - name: id in: path required: true schema: {type: string} responses: "204": description: Deleted /v1/admin/users: get: summary: List all users (admin) operationId: adminListUsers tags: [admin] security: - sessionAuth: [] responses: "200": description: Users list content: application/json: schema: type: array items: {type: object} /v1/admin/users/{id}/active: put: summary: Activate or deactivate a user (admin) operationId: adminSetUserActive tags: [admin] security: - sessionAuth: [] parameters: - name: id in: path required: true schema: {type: string} requestBody: required: true content: application/json: schema: type: object required: [active] properties: active: {type: boolean} responses: "204": description: Updated /v1/admin/templates: get: summary: List all templates (admin) operationId: adminListTemplates tags: [admin] security: - sessionAuth: [] responses: "200": description: Templates list content: application/json: schema: type: array items: $ref: "#/components/schemas/Template" /v1/admin/templates/{name}: delete: summary: Delete a template (admin) operationId: adminDeleteTemplate tags: [admin] security: - sessionAuth: [] parameters: - name: name in: path required: true schema: {type: string} responses: "204": description: Deleted /v1/admin/builds: post: summary: Submit a template build (admin) operationId: adminCreateBuild tags: [admin] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: {type: object} responses: "202": description: Build queued content: application/json: schema: {type: object} get: summary: List builds (admin) operationId: adminListBuilds tags: [admin] security: - sessionAuth: [] responses: "200": description: Builds list content: application/json: schema: type: array items: {type: object} /v1/admin/builds/{id}: get: summary: Get build detail (admin) operationId: adminGetBuild tags: [admin] security: - sessionAuth: [] parameters: - name: id in: path required: true schema: {type: string} responses: "200": description: Build detail content: application/json: schema: {type: object} /v1/admin/builds/{id}/cancel: post: summary: Cancel a build (admin) operationId: adminCancelBuild tags: [admin] security: - sessionAuth: [] parameters: - name: id in: path required: true schema: {type: string} responses: "204": description: Cancelled /v1/admin/capsules: post: summary: Create a capsule on behalf of any team (admin) operationId: adminCreateCapsule tags: [admin] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateCapsuleRequest" responses: "201": description: Capsule created content: application/json: schema: $ref: "#/components/schemas/Capsule" get: summary: List capsules across all teams (admin) operationId: adminListCapsules tags: [admin] security: - sessionAuth: [] responses: "200": description: Capsules list content: application/json: schema: type: array items: $ref: "#/components/schemas/Capsule" /v1/admin/capsules/{id}: parameters: - name: id in: path required: true schema: {type: string} get: summary: Get capsule detail (admin) operationId: adminGetCapsule tags: [admin] security: - sessionAuth: [] responses: "200": description: Capsule detail content: application/json: schema: $ref: "#/components/schemas/Capsule" delete: summary: Destroy capsule (admin) operationId: adminDestroyCapsule tags: [admin] security: - sessionAuth: [] responses: "204": description: Destroyed /v1/admin/capsules/{id}/snapshot: post: summary: Create snapshot from any capsule (admin) operationId: adminCreateSnapshotFromCapsule tags: [admin] security: - sessionAuth: [] parameters: - name: id in: path required: true schema: {type: string} requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: {type: string} responses: "201": description: Snapshot created content: application/json: schema: $ref: "#/components/schemas/Template" /v1/admin/capsules/{id}/exec: parameters: - name: id in: path required: true schema: {type: string} post: summary: Execute a command on any capsule (admin) operationId: adminExecCommand tags: [admin] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ExecRequest" responses: "200": description: Command output (foreground exec) content: application/json: schema: $ref: "#/components/schemas/ExecResponse" "202": description: Background process started content: application/json: schema: $ref: "#/components/schemas/BackgroundExecResponse" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/FailedPrecondition" /v1/admin/capsules/{id}/metrics: parameters: - name: id in: path required: true schema: {type: string} get: summary: Get per-capsule resource metrics (admin) operationId: adminGetCapsuleMetrics tags: [admin] security: - sessionAuth: [] parameters: - name: range in: query required: false schema: type: string enum: ["5m", "10m", "1h", "2h", "6h", "12h", "24h"] default: "10m" responses: "200": description: Metrics retrieved content: application/json: schema: $ref: "#/components/schemas/CapsuleMetrics" "404": $ref: "#/components/responses/NotFound" /v1/admin/capsules/{id}/processes: parameters: - name: id in: path required: true schema: {type: string} get: summary: List running processes on any capsule (admin) operationId: adminListProcesses tags: [admin] security: - sessionAuth: [] responses: "200": description: Process list content: application/json: schema: $ref: "#/components/schemas/ProcessListResponse" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/FailedPrecondition" /v1/admin/capsules/{id}/processes/{selector}: parameters: - name: id in: path required: true schema: {type: string} - name: selector in: path required: true schema: {type: string} description: Process PID (numeric) or tag (string) delete: summary: Kill a process on any capsule (admin) operationId: adminKillProcess tags: [admin] security: - sessionAuth: [] parameters: - name: signal in: query required: false schema: type: string enum: [SIGKILL, SIGTERM] default: SIGKILL responses: "204": description: Process killed "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/FailedPrecondition" /v1/admin/capsules/{id}/files/write: parameters: - name: id in: path required: true schema: {type: string} post: summary: Upload a file to any capsule (admin) operationId: adminUploadFile tags: [admin] security: - sessionAuth: [] requestBody: required: true content: multipart/form-data: schema: type: object required: [path, file] properties: path: {type: string} file: {type: string, format: binary} responses: "204": description: File uploaded "409": $ref: "#/components/responses/FailedPrecondition" /v1/admin/capsules/{id}/files/read: parameters: - name: id in: path required: true schema: {type: string} post: summary: Download a file from any capsule (admin) operationId: adminDownloadFile tags: [admin] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ReadFileRequest" responses: "200": description: File content content: application/octet-stream: schema: type: string format: binary "404": $ref: "#/components/responses/NotFound" /v1/admin/capsules/{id}/files/list: parameters: - name: id in: path required: true schema: {type: string} post: summary: List directory contents on any capsule (admin) operationId: adminListDir tags: [admin] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ListDirRequest" responses: "200": description: Directory listing content: application/json: schema: $ref: "#/components/schemas/ListDirResponse" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/FailedPrecondition" /v1/admin/capsules/{id}/files/mkdir: parameters: - name: id in: path required: true schema: {type: string} post: summary: Create a directory on any capsule (admin) operationId: adminMakeDir tags: [admin] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MakeDirRequest" responses: "200": description: Directory created content: application/json: schema: $ref: "#/components/schemas/MakeDirResponse" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/FailedPrecondition" /v1/admin/capsules/{id}/files/remove: parameters: - name: id in: path required: true schema: {type: string} post: summary: Remove a file or directory on any capsule (admin) operationId: adminRemovePath tags: [admin] security: - sessionAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RemoveRequest" responses: "204": description: File or directory removed "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/FailedPrecondition" /v1/admin/capsules/{id}/exec/stream: parameters: - name: id in: path required: true schema: {type: string} get: summary: Stream command execution on any capsule via WebSocket (admin) operationId: adminExecStream tags: [admin] security: - sessionAuth: [] description: | Admin variant of /v1/capsules/{id}/exec/stream. Same protocol — WebSocket upgrade, client sends `{"type":"start", "cmd":..., "args":...}` to start; server streams stdout/stderr/exit frames. responses: "101": description: WebSocket upgrade "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/FailedPrecondition" /v1/admin/capsules/{id}/pty: parameters: - name: id in: path required: true schema: {type: string} get: summary: Interactive PTY session on any capsule via WebSocket (admin) operationId: adminPtySession tags: [admin] security: - sessionAuth: [] description: | Admin variant of /v1/capsules/{id}/pty. Same protocol — base64-encoded PTY bytes, start/connect/input/resize/kill control messages, persistent sessions reconnectable via tag. responses: "101": description: WebSocket upgrade "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/FailedPrecondition" /v1/admin/capsules/{id}/processes/{selector}/stream: parameters: - name: id in: path required: true schema: {type: string} - name: selector in: path required: true schema: {type: string} description: Process PID (numeric) or tag (string) get: summary: Stream process output on any capsule via WebSocket (admin) operationId: adminConnectProcess tags: [admin] security: - sessionAuth: [] responses: "101": description: WebSocket upgrade "404": $ref: "#/components/responses/NotFound" components: responses: BadRequest: description: Invalid request parameters content: application/json: schema: $ref: "#/components/schemas/Error" Unauthorized: description: Missing or invalid auth content: application/json: schema: $ref: "#/components/schemas/Error" Forbidden: description: Authenticated but not permitted (e.g. non-admin on /v1/admin/*) content: application/json: schema: $ref: "#/components/schemas/Error" NotFound: description: Resource not found content: application/json: schema: $ref: "#/components/schemas/Error" FailedPrecondition: description: Resource state does not allow this operation (e.g. exec on a paused capsule) content: application/json: schema: $ref: "#/components/schemas/Error" securitySchemes: apiKeyAuth: type: apiKey in: header name: X-API-Key description: API key for capsule lifecycle operations. Create via POST /v1/api-keys. sessionAuth: type: apiKey in: cookie name: wrenn_sid description: | Opaque session cookie set by POST /v1/auth/login, /v1/auth/activate, or the OAuth callback. HttpOnly, Secure, SameSite=Strict. Idle window 6h, absolute lifetime 24h. State-changing requests also require an X-CSRF-Token header matching the wrenn_csrf cookie (double-submit). csrfHeader: type: apiKey in: header name: X-CSRF-Token description: | Double-submit CSRF token whose value must match the wrenn_csrf cookie. Required on all non-GET requests authenticated via session cookie. Not required for API key auth. hostTokenAuth: type: apiKey in: header name: X-Host-Token description: Host JWT returned from POST /v1/hosts/register or POST /v1/hosts/auth/refresh. Valid for 7 days. schemas: SignupRequest: type: object required: [email, password, name] properties: email: type: string format: email password: type: string minLength: 8 name: type: string maxLength: 100 LoginRequest: type: object required: [email, password] properties: email: type: string format: email password: type: string SignupResponse: type: object properties: message: type: string description: Confirmation message instructing user to check email SessionResponse: type: object description: | 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. properties: user_id: type: string team_id: type: string email: type: string name: type: string role: type: string is_admin: type: boolean CreateAPIKeyRequest: type: object properties: name: type: string default: Unnamed API Key APIKeyResponse: type: object properties: id: type: string team_id: type: string name: type: string key_prefix: type: string description: Display prefix (e.g. "wrn_ab12cd34...") created_at: type: string format: date-time last_used: type: string format: date-time nullable: true key: type: string description: Full plaintext key. Only returned on creation, never again. nullable: true CreateCapsuleRequest: type: object properties: template: type: string default: minimal vcpus: type: integer default: 1 memory_mb: type: integer default: 512 disk_size_mb: type: integer default: 5120 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). timeout_sec: type: integer minimum: 0 default: 0 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). UsageResponse: type: object properties: from: type: string format: date to: type: string format: date points: type: array items: type: object properties: date: type: string format: date cpu_minutes: type: number ram_mb_minutes: type: number CapsuleStats: type: object properties: range: type: string enum: [5m, 1h, 6h, 24h, 30d] current: type: object properties: running_count: type: integer vcpus_reserved: 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. properties: running_count: type: integer vcpus: type: integer memory_mb: type: integer series: type: object description: Parallel arrays for chart rendering. properties: labels: type: array items: type: string format: date-time running: type: array items: type: integer vcpus: type: array items: type: integer memory_mb: type: array items: type: integer Capsule: type: object properties: id: type: string status: type: string enum: [pending, starting, running, pausing, paused, resuming, stopping, hibernated, stopped, missing, error] template: type: string vcpus: type: integer memory_mb: type: integer timeout_sec: type: integer guest_ip: type: string host_ip: type: string created_at: type: string format: date-time started_at: type: string format: date-time nullable: true last_active_at: type: string format: date-time nullable: true last_updated: type: string format: date-time metadata: type: object additionalProperties: {type: string} nullable: true description: | Free-form key/value labels attached at create-time. Also carries agent-side version info (kernel_version, vmm_version, agent_version, envd_version) when running. disk_size_mb: type: integer nullable: true CreateSnapshotRequest: type: object required: [sandbox_id] properties: sandbox_id: type: string description: ID of the running capsule to snapshot. name: type: string description: Name for the snapshot template. Auto-generated if omitted. Template: type: object properties: name: type: string type: type: string enum: [base, snapshot] vcpus: type: integer nullable: true memory_mb: type: integer nullable: true size_bytes: type: integer format: int64 created_at: type: string format: date-time platform: type: boolean description: | True when the template is platform-managed (visible to all teams, e.g. the built-in `minimal` rootfs). False for team-owned snapshot templates. metadata: type: object additionalProperties: {type: string} nullable: true ExecRequest: type: object required: [cmd] properties: cmd: type: string args: type: array items: type: string timeout_sec: type: integer default: 30 description: Timeout in seconds (foreground exec only, default 30) background: type: boolean default: false description: If true, starts the process in the background and returns immediately with a PID and tag (HTTP 202) tag: type: string description: Optional user-chosen tag for the background process. Auto-generated if omitted. Only used when background is true. envs: type: object additionalProperties: type: string description: Environment variables for the process (background exec only) cwd: type: string description: Working directory for the process (background exec only) BackgroundExecResponse: type: object properties: sandbox_id: type: string cmd: type: string pid: type: integer tag: type: string ProcessEntry: type: object properties: pid: type: integer tag: type: string cmd: type: string args: type: array items: type: string ProcessListResponse: type: object properties: processes: type: array items: $ref: "#/components/schemas/ProcessEntry" ExecResponse: type: object properties: sandbox_id: type: string cmd: type: string stdout: type: string stderr: type: string exit_code: type: integer duration_ms: type: integer encoding: type: string enum: [utf-8, base64] description: Output encoding. "base64" when stdout/stderr contain binary data. ReadFileRequest: type: object required: [path] properties: path: type: string description: Absolute file path inside the capsule ListDirRequest: type: object required: [path] properties: path: type: string description: Directory path inside the capsule depth: type: integer default: 1 description: Recursion depth (0 = non-recursive, 1 = immediate children) ListDirResponse: type: object properties: entries: type: array items: $ref: "#/components/schemas/FileEntry" FileEntry: type: object properties: name: type: string path: type: string type: type: string enum: [file, directory, symlink] size: type: integer format: int64 mode: type: integer permissions: type: string description: Human-readable permissions (e.g. "-rwxr-xr-x") owner: type: string group: type: string modified_at: type: integer format: int64 description: Unix timestamp (seconds) symlink_target: type: string nullable: true MakeDirRequest: type: object required: [path] properties: path: type: string description: Directory path to create inside the capsule MakeDirResponse: type: object properties: entry: $ref: "#/components/schemas/FileEntry" RemoveRequest: type: object required: [path] properties: path: type: string description: Path to remove inside the capsule CreateHostRequest: type: object required: [type] properties: type: type: string enum: [regular, byoc] description: Host type. Regular hosts are shared; BYOC hosts belong to a team. team_id: type: string description: Required for BYOC hosts. provider: type: string description: Cloud provider (e.g. aws, gcp, hetzner, bare-metal). availability_zone: type: string description: Availability zone (e.g. us-east, eu-west). CreateHostResponse: type: object properties: host: $ref: "#/components/schemas/Host" registration_token: type: string description: One-time registration token for the host agent. Expires in 1 hour. RegisterHostRequest: type: object required: [token, address] properties: token: type: string description: One-time registration token from POST /v1/hosts. arch: type: string description: CPU architecture (e.g. x86_64, aarch64). cpu_cores: type: integer memory_mb: type: integer disk_gb: type: integer address: type: string description: Host agent address (ip:port). RegisterHostResponse: type: object properties: host: $ref: "#/components/schemas/Host" token: type: string description: Host JWT for X-Host-Token header. Valid for 7 days. refresh_token: type: string description: Refresh token for obtaining new JWTs. Valid for 60 days; rotated on each use. Host: type: object properties: id: type: string type: type: string enum: [regular, byoc] team_id: type: string nullable: true provider: type: string nullable: true availability_zone: type: string nullable: true arch: type: string nullable: true cpu_cores: type: integer nullable: true memory_mb: type: integer nullable: true disk_gb: type: integer nullable: true address: type: string nullable: true status: type: string enum: [pending, online, offline, draining, unreachable] last_heartbeat_at: type: string format: date-time nullable: true created_by: type: string created_at: type: string format: date-time updated_at: type: string format: date-time RefreshHostTokenRequest: type: object required: [refresh_token] properties: refresh_token: type: string description: Refresh token obtained from registration or a previous refresh. RefreshHostTokenResponse: type: object properties: host: $ref: "#/components/schemas/Host" token: type: string description: New host JWT. Valid for 7 days. refresh_token: type: string description: New refresh token. Valid for 60 days; old token is revoked. HostDeletePreview: type: object properties: host: $ref: "#/components/schemas/Host" sandbox_ids: type: array items: type: string description: IDs of capsules that would be destroyed on force-delete. HostHasCapsulesError: type: object properties: error: type: object properties: code: type: string example: host_has_sandboxes message: type: string sandbox_ids: type: array items: type: string description: IDs of active capsules blocking deletion. AddTagRequest: type: object required: [tag] properties: tag: type: string UserSearchResult: type: object properties: user_id: type: string email: type: string Team: type: object properties: id: type: string name: type: string slug: type: string description: Immutable 12-char hex slug (e.g. a1b2c3-d1e2f3) created_at: type: string format: date-time TeamWithRole: allOf: - $ref: "#/components/schemas/Team" - type: object properties: role: type: string enum: [owner, admin, member] TeamMember: type: object properties: user_id: type: string email: type: string role: type: string enum: [owner, admin, member] joined_at: type: string format: date-time TeamDetail: type: object properties: team: $ref: "#/components/schemas/Team" members: type: array items: $ref: "#/components/schemas/TeamMember" CapsuleMetrics: type: object properties: sandbox_id: type: string range: type: string enum: ["5m", "10m", "1h", "2h", "6h", "12h", "24h"] points: type: array items: $ref: "#/components/schemas/MetricPoint" MetricPoint: type: object properties: timestamp_unix: type: integer format: int64 cpu_pct: type: number format: double description: "CPU utilization percentage (0-100), normalized to vCPU count" mem_bytes: type: integer format: int64 description: "Resident memory in bytes (VmRSS of Cloud Hypervisor process)" disk_bytes: type: integer format: int64 description: "Allocated disk bytes for the CoW sparse file" CreateChannelRequest: type: object required: [name, provider, config, events] properties: name: type: string description: Unique channel name within the team. provider: type: string enum: [discord, slack, teams, googlechat, telegram, matrix, webhook] config: type: object additionalProperties: type: string 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). events: type: array items: type: string enum: - capsule.create - capsule.pause - capsule.resume - capsule.destroy - template.snapshot.create - template.snapshot.delete - host.up - host.down TestChannelRequest: type: object required: [provider, config] properties: provider: type: string enum: [discord, slack, teams, googlechat, telegram, matrix, webhook] config: type: object additionalProperties: type: string description: Provider-specific configuration fields (same as CreateChannelRequest.config). RotateConfigRequest: type: object required: [config] properties: config: type: object additionalProperties: type: string description: > New provider configuration fields. Must include all required fields for the channel's provider. Replaces the existing config entirely. UpdateChannelRequest: type: object required: [name, events] properties: name: type: string events: type: array items: type: string enum: - capsule.create - capsule.pause - capsule.resume - capsule.destroy - template.snapshot.create - template.snapshot.delete - host.up - host.down ChannelResponse: type: object properties: id: type: string team_id: type: string name: type: string provider: type: string enum: [discord, slack, teams, googlechat, telegram, matrix, webhook] events: type: array items: type: string created_at: type: string format: date-time updated_at: type: string format: date-time secret: type: string nullable: true description: Webhook secret. Only returned on creation, never again. MeResponse: type: object properties: name: type: string email: type: string format: email has_password: type: boolean description: Whether the user has a password set (false for OAuth-only accounts) providers: type: array items: type: string description: List of linked OAuth provider names (e.g. ["github"]) ChangePasswordRequest: type: object required: [new_password] properties: current_password: type: string description: Required when changing an existing password new_password: type: string minLength: 8 confirm_password: type: string description: Required when adding a password to an OAuth-only account (must match new_password) Error: type: object properties: error: type: object properties: code: type: string message: type: string AuditLogEntry: type: object properties: id: {type: string} actor_type: {type: string, enum: [user, api_key, host, system]} actor_id: {type: string} actor_name: {type: string} resource_type: {type: string} resource_id: {type: string} action: {type: string} scope: {type: string} status: {type: string, enum: [success, failure]} metadata: type: object additionalProperties: true created_at: type: string format: date-time SSEEvent: type: object description: | Wire format of one SSE message body. The event name (`event:` line) is the `kind` and the JSON below is the `data:` line. properties: event: type: string enum: - connected - capsule.create - capsule.pause - capsule.resume - capsule.destroy - capsule.state.changed - template.snapshot.create - template.snapshot.delete - host.up - host.down outcome: type: string enum: [success, error] description: | Present for action events (capsule.* except state.changed, template.snapshot.*). Absent for host.up/down, capsule.state.changed, and the connected sentinel. resource: type: object properties: id: {type: string} type: {type: string} actor: type: object properties: type: {type: string, enum: [user, api_key, system]} id: {type: string} name: {type: string} metadata: type: object additionalProperties: {type: string} description: | Event-specific context. Examples: `reason` (ttl_expired, host_failure, cleanup_after_create_error, orphaned), `host_ip`, `from`/`to` (for capsule.state.changed). error: type: string description: Failure reason; only set when outcome=error. sandbox: allOf: - $ref: "#/components/schemas/Capsule" nullable: true description: Populated for capsule.* events; null if DB lookup failed. timestamp: type: string format: date-time