-
Notifications
You must be signed in to change notification settings - Fork 4
docs(sandbox): add POST /api/sandbox + GET /api/sandbox/status reference #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,356 @@ | ||
| { | ||
| "openapi": "3.1.0", | ||
| "info": { | ||
| "title": "Recoup API - Session Sandboxes", | ||
| "description": "Per-session sandbox lifecycle for agent runs. These endpoints provision, restore, and report status on the Sandbox bound to a single agent session. Distinct from the legacy `/api/sandboxes` (plural) account-scoped endpoints — these are session-scoped and used by the open-agents-style chat UI to drive the \"loading sandbox…\" state on session entry.", | ||
| "license": { | ||
| "name": "MIT" | ||
| }, | ||
| "version": "1.0.0" | ||
| }, | ||
| "servers": [ | ||
| { | ||
| "url": "https://api.recoupable.com" | ||
| } | ||
| ], | ||
| "security": [ | ||
| { | ||
| "ApiKeyAuth": [] | ||
| }, | ||
| { | ||
| "BearerAuth": [] | ||
| } | ||
| ], | ||
| "paths": { | ||
| "/api/sandbox": { | ||
| "post": { | ||
| "summary": "Create or restore session sandbox", | ||
| "description": "Provisions a Sandbox for the given session. If a per-org base snapshot exists, the sandbox boots from it (skipping the full repo clone, ~75s saved). Otherwise the sandbox boots from the default base snapshot and a background workflow builds an org-specific snapshot for next time. When the session has prior runtime state (a paused or running sandbox under the same `sandboxName`), the call resumes it instead of creating a new one. On success, the session row is updated with the new `sandboxState` and lifecycle is bumped to `active`; the lifecycle workflow is kicked to manage hibernation and expiry from there.", | ||
| "requestBody": { | ||
| "required": true, | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/CreateSandboxRequest" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "responses": { | ||
| "200": { | ||
| "description": "Sandbox provisioned and bound to the session.", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/CreateSandboxResponse" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "400": { | ||
| "description": "Invalid request body — malformed JSON, missing required fields, or invalid GitHub repository URL.", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/Error" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "401": { | ||
| "description": "Unauthorized — invalid or missing API key / Bearer token.", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/Error" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "403": { | ||
| "description": "Forbidden — the authenticated account does not own the supplied `sessionId`.", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/Error" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "404": { | ||
| "description": "Not found — no session exists with the given `sessionId`.", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/Error" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "502": { | ||
| "description": "Upstream error — the sandbox provider failed to provision a sandbox.", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/Error" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "/api/sandbox/status": { | ||
| "get": { | ||
| "summary": "Get session sandbox status", | ||
| "description": "Returns the current lifecycle and runtime state for the sandbox bound to a session. The chat UI polls this endpoint while showing the \"loading sandbox…\" state and flips to \"ready\" when `status` becomes `active`. The response includes `lifecycleVersion` (an optimistic concurrency token) and a `lifecycle` envelope with `serverTime`, the lifecycle FSM `state`, and timestamps for last activity, hibernation deadline, and sandbox expiry. As a side effect, if the runtime state is stale (expired or overdue for hibernation), the lifecycle workflow is kicked to clean up — callers do not need to do this themselves.", | ||
| "parameters": [ | ||
| { | ||
| "name": "sessionId", | ||
| "in": "query", | ||
| "required": true, | ||
| "description": "The id of the session whose sandbox status to read.", | ||
| "schema": { | ||
| "type": "string" | ||
| } | ||
| } | ||
| ], | ||
| "responses": { | ||
| "200": { | ||
| "description": "Sandbox status retrieved successfully.", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/SandboxStatusResponse" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "400": { | ||
| "description": "Missing `sessionId` query parameter.", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/Error" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "401": { | ||
| "description": "Unauthorized — invalid or missing API key / Bearer token.", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/Error" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "403": { | ||
| "description": "Forbidden — the authenticated account does not own this session.", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/Error" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "404": { | ||
| "description": "Not found — no session exists with the given `sessionId`.", | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/Error" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "components": { | ||
| "securitySchemes": { | ||
| "ApiKeyAuth": { | ||
| "type": "apiKey", | ||
| "in": "header", | ||
| "name": "x-api-key" | ||
| }, | ||
| "BearerAuth": { | ||
| "type": "http", | ||
| "scheme": "bearer" | ||
| } | ||
| }, | ||
| "schemas": { | ||
| "CreateSandboxRequest": { | ||
| "type": "object", | ||
| "required": [ | ||
| "repoUrl" | ||
| ], | ||
| "properties": { | ||
| "repoUrl": { | ||
| "type": "string", | ||
| "description": "GitHub repository URL the sandbox should clone (e.g. `https://github.com/owner/repo`). Validated against GitHub URL rules; private repos require the service GitHub token configured server-side." | ||
| }, | ||
| "sessionId": { | ||
| "type": "string", | ||
| "description": "Owning session id. Required for the chat UX flow — the sandbox is named deterministically from the session id, enabling resume across reconnects. When omitted, a one-shot ephemeral sandbox is created (legacy)." | ||
| }, | ||
| "branch": { | ||
| "type": "string", | ||
| "description": "Branch the sandbox should check out. Ignored when `isNewBranch` is true. Defaults to `main`.", | ||
| "default": "main" | ||
| }, | ||
| "isNewBranch": { | ||
| "type": "boolean", | ||
| "description": "When true, the sandbox creates a fresh branch with the supplied `branch` name instead of checking out an existing one.", | ||
| "default": false | ||
| } | ||
| } | ||
| }, | ||
| "CreateSandboxResponse": { | ||
| "type": "object", | ||
| "required": [ | ||
| "createdAt", | ||
| "timeout", | ||
| "currentBranch", | ||
| "mode", | ||
| "timing" | ||
| ], | ||
| "properties": { | ||
| "createdAt": { | ||
| "type": "integer", | ||
| "format": "int64", | ||
| "description": "Epoch milliseconds when the sandbox handle was returned." | ||
| }, | ||
| "timeout": { | ||
| "type": "integer", | ||
| "format": "int64", | ||
| "description": "Sandbox idle-timeout in milliseconds. The lifecycle workflow uses this to schedule hibernation." | ||
| }, | ||
| "currentBranch": { | ||
| "type": "string", | ||
| "description": "Branch the sandbox checked out." | ||
| }, | ||
| "mode": { | ||
| "type": "string", | ||
| "enum": [ | ||
| "vercel" | ||
| ], | ||
| "description": "Sandbox provider. Currently always `vercel`." | ||
| }, | ||
| "timing": { | ||
| "type": "object", | ||
| "required": [ | ||
| "readyMs" | ||
| ], | ||
| "properties": { | ||
| "readyMs": { | ||
| "type": "integer", | ||
| "format": "int64", | ||
| "description": "Wall-clock milliseconds from request receipt until the sandbox was ready. Useful for tracking cold-start vs warm-resume performance." | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "SandboxStatusResponse": { | ||
| "type": "object", | ||
| "required": [ | ||
| "status", | ||
| "hasSnapshot", | ||
| "lifecycleVersion", | ||
| "lifecycle" | ||
| ], | ||
| "properties": { | ||
| "status": { | ||
| "type": "string", | ||
| "enum": [ | ||
| "active", | ||
| "no_sandbox" | ||
| ], | ||
| "description": "`active` when a non-expired sandbox is bound to the session; `no_sandbox` otherwise. The chat UI flips out of its loading state when this becomes `active`." | ||
| }, | ||
| "hasSnapshot": { | ||
| "type": "boolean", | ||
| "description": "True when a paused/snapshotted sandbox exists and can be resumed. Used by the UI to decide whether to show \"resume\" vs \"create\" affordances when `status` is `no_sandbox`." | ||
| }, | ||
| "lifecycleVersion": { | ||
| "type": "integer", | ||
| "description": "Optimistic concurrency token for lifecycle transitions. Clients can pass this back to lifecycle-mutating endpoints to detect races." | ||
| }, | ||
| "lifecycle": { | ||
| "type": "object", | ||
| "required": [ | ||
| "serverTime", | ||
| "state", | ||
| "lastActivityAt", | ||
| "hibernateAfter", | ||
| "sandboxExpiresAt" | ||
| ], | ||
| "properties": { | ||
| "serverTime": { | ||
| "type": "integer", | ||
| "format": "int64", | ||
| "description": "Server's current epoch milliseconds. Use this — not the client clock — when computing how much time is left before `hibernateAfter` or `sandboxExpiresAt`." | ||
| }, | ||
| "state": { | ||
| "type": "string", | ||
| "nullable": true, | ||
| "enum": [ | ||
| "provisioning", | ||
| "active", | ||
| "hibernating", | ||
| "hibernated", | ||
| "restoring", | ||
| "archived", | ||
| "failed" | ||
| ], | ||
| "description": "Lifecycle FSM state. `null` for sessions that have never had a sandbox." | ||
| }, | ||
| "lastActivityAt": { | ||
| "type": "integer", | ||
| "format": "int64", | ||
| "nullable": true, | ||
| "description": "Epoch milliseconds of the last recorded sandbox activity, or null if there has been none." | ||
| }, | ||
| "hibernateAfter": { | ||
| "type": "integer", | ||
| "format": "int64", | ||
| "nullable": true, | ||
| "description": "Epoch milliseconds after which the sandbox is eligible for hibernation, or null when not applicable." | ||
| }, | ||
| "sandboxExpiresAt": { | ||
| "type": "integer", | ||
| "format": "int64", | ||
| "nullable": true, | ||
| "description": "Epoch milliseconds when the sandbox runtime expires, or null when not applicable." | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "Error": { | ||
| "type": "object", | ||
| "required": [ | ||
| "status", | ||
| "error" | ||
| ], | ||
| "properties": { | ||
| "status": { | ||
| "type": "string", | ||
| "enum": [ | ||
| "error" | ||
| ], | ||
| "description": "Always `\"error\"` for error responses." | ||
| }, | ||
| "error": { | ||
| "type": "string", | ||
| "description": "Human-readable error message." | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| --- | ||
| title: 'Create or restore session sandbox' | ||
| openapi: 'POST /api/sandbox' | ||
| --- |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| --- | ||
| title: 'Get session sandbox status' | ||
| openapi: 'GET /api/sandbox/status' | ||
| --- |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: OpenAPI 3.1 schema uses
nullable, which should be replaced withtypeunions including"null".Prompt for AI agents