Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
356 changes: 356 additions & 0 deletions api-reference/openapi/sandbox.json
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,
Copy link
Copy Markdown

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 with type unions including "null".

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api-reference/openapi/sandbox.json, line 300:

<comment>OpenAPI 3.1 schema uses `nullable`, which should be replaced with `type` unions including `"null"`.</comment>

<file context>
@@ -0,0 +1,356 @@
+              },
+              "state": {
+                "type": "string",
+                "nullable": true,
+                "enum": [
+                  "provisioning",
</file context>

"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."
}
}
}
}
}
}
4 changes: 4 additions & 0 deletions api-reference/sandbox/create.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
title: 'Create or restore session sandbox'
openapi: 'POST /api/sandbox'
---
4 changes: 4 additions & 0 deletions api-reference/sandbox/status.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
title: 'Get session sandbox status'
openapi: 'GET /api/sandbox/status'
---
7 changes: 7 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,13 @@
"api-reference/sessions/create",
"api-reference/sessions/get"
]
},
{
"group": "Session Sandboxes",
"pages": [
"api-reference/sandbox/create",
"api-reference/sandbox/status"
]
}
]
},
Expand Down