Download OpenAPI specification:
Run AI coding agents in governed, disposable sandboxes.
fluidbox is a control plane that runs AI coding agents in governed,
disposable sandboxes. You register a versioned agent definition; each
run freezes an immutable RunSpec, provisions a fresh sandbox, streams
a live event timeline, pauses for human approval (or auto-decides in
autonomous mode), and ends with a diff and a cost report.
This description covers every HTTP surface the control plane exposes. They are separate audiences with separate credentials, and mixing them up is the most common integration mistake:
| Plane | Base path | Who calls it | Credential |
|---|---|---|---|
| Public API | /v1 |
Your code, the CLI, the dashboard | Admin token, PAT, or browser session |
| Runner contract | /internal |
Only the in-sandbox runner | Audience-scoped session token |
| Operator | /v1/admin |
Break-glass operator tooling | Admin token only |
| Ingress | /v1/ingress |
GitHub and other connected services | Webhook signature |
On Kubernetes the runner contract is served on a separate listener
(:8788) and the /internal routes do not exist on the public listener at
all — route absence is a stronger boundary than bearer auth. On the
single-host Docker path both planes ride :8787 and the bearer token
separates them.
Every failure across every plane returns the same body: a single error
string. There are no per-endpoint error envelopes.
{ "error": "agent not found" }
Two invariants shape most of this API and will surprise you if you miss them:
RunSpec is frozen at run creation, including a full policy
snapshot. Editing an agent or a policy affects only future runs. This
is what makes the audit trail trustworthy.A run is one governed execution of an agent. Creating a run freezes an
immutable RunSpec — agent revision, policy snapshot, workspace,
budgets, and the frozen capability surface — and everything downstream is
judged against that snapshot rather than against current configuration.
| limit | integer <int64> Default: 50 Maximum items to return. |
Array of objects (Session) |
{- "sessions": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978",
- "agent_revision_id": "3c2ee4c4-f591-413e-a2b2-834f483cc40b",
- "status": "created",
- "trust_tier": "trusted",
- "task": "string",
- "autonomous": true,
- "run_spec": { },
- "created_at": "2019-08-24T14:15:22Z",
- "ended_at": "2019-08-24T14:15:22Z"
}
]
}Freezes an immutable RunSpec and begins provisioning. The response
returns immediately with the new run in created; watch
/v1/sessions/{id}/events/stream for progress.
What gets frozen: the agent revision's model and system prompt, a full policy snapshot, the resolved workspace, the effective budgets, and the capability surface after any per-run narrowing. Nothing you change afterwards affects this run.
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| agent required | string The agent's name or id. |
| task required | string What to do this time. Distinct from the agent's system prompt. |
Scratch (object) or Local copy (object) or Git repository (object) (WorkspaceInput) | |
| autonomous | boolean Default: false Auto-decides approvals using the policy fallback. The permission gate stays wired either way — this is not a bypass, and both the original and the rewritten verdict are recorded. |
object (Budgets) Spending limits. Anything omitted falls back to the policy's value. Budgets can be tightened per run but never loosened past the policy. | |
| capabilities | Array of strings A keep-list of bundle names, intersected with the revision's attachments. Remove-only — it can never add a capability. |
object Explicit requirement-slot to connection-id overrides. Each entry is verified for tenant, caller permission, connector match, and snapshot before the run starts. Unknown slots are rejected. |
object (Session) A run. The |
{- "agent": "reviewer",
- "task": "Summarize the OWASP top ten for a Rust web service."
}{- "session": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978",
- "agent_revision_id": "3c2ee4c4-f591-413e-a2b2-834f483cc40b",
- "status": "created",
- "trust_tier": "trusted",
- "task": "string",
- "autonomous": true,
- "run_spec": { },
- "created_at": "2019-08-24T14:15:22Z",
- "ended_at": "2019-08-24T14:15:22Z"
}
}Returns the run alongside its accumulated usage totals.
| id required | string <uuid> The run (session) identifier. |
object (Session) A run. The | |
object (UsageTotals) |
{- "session": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978",
- "agent_revision_id": "3c2ee4c4-f591-413e-a2b2-834f483cc40b",
- "status": "created",
- "trust_tier": "trusted",
- "task": "string",
- "autonomous": true,
- "run_spec": { },
- "created_at": "2019-08-24T14:15:22Z",
- "ended_at": "2019-08-24T14:15:22Z"
}, - "usage": {
- "input_tokens": 0,
- "output_tokens": 0,
- "cost_usd": 0.1,
- "tool_calls": 0
}
}Records the intent to cancel and starts finalization. cancelled is
false when the run was already terminal — that is a success, not an
error.
Cancellation is deliberately stricter than run visibility: being able to see a run does not mean being able to stop it.
| id required | string <uuid> The run (session) identifier. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| cancelled | boolean False when the run had already reached a terminal state. |
{- "cancelled": true
}The metered cost of the run. Usage is teed off the streaming LLM response by the facade, so this is measured rather than estimated.
| id required | string <uuid> The run (session) identifier. |
| input_tokens | integer <int64> |
| output_tokens | integer <int64> |
| cost_usd | number <double> |
| tool_calls | integer <int64> |
{- "input_tokens": 0,
- "output_tokens": 0,
- "cost_usd": 0.1,
- "tool_calls": 0
}The outputs a finished run produced — most usefully, the diff.
| id required | string <uuid> The run (session) identifier. |
Array of objects (Artifact) |
{- "artifacts": [
- {
- "id": "string",
- "kind": "diff",
- "size_bytes": 0,
- "digest": "string"
}
]
}| id required | string <uuid> The run (session) identifier. |
| aid required | string The artifact identifier. |
| id | string |
| kind | string |
| size_bytes | integer <int64> |
| digest | string |
{- "id": "string",
- "kind": "diff",
- "size_bytes": 0,
- "digest": "string"
}Result delivery is decoupled from the run lifecycle: a failing webhook
or a dead GitHub can never mutate a run. Deliveries are at-least-once
and are retried with backoff from 5 seconds to 1 hour over 6 attempts —
receivers must deduplicate on the x-fluidbox-delivery header.
| id required | string <uuid> The run (session) identifier. |
Array of objects (Delivery) |
{- "deliveries": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "destination": "string",
- "status": "pending",
- "attempts": 0,
- "next_attempt_at": "2019-08-24T14:15:22Z"
}
]
}The append-only run timeline. Events carry a gapless per-session seq
assigned under a row lock, which is what makes both catch-up polling and
Last-Event-ID stream resume exact. Model prompts never reach the
ledger — only digests, usage, and cost.
Returns events after seq. This same query backs the live stream, so
polling and streaming are exact about each other — you can switch
between them without gaps or duplicates.
| id required | string <uuid> The run (session) identifier. |
| after | integer <int64> Default: 0 Return events with |
| limit | integer <int64> Default: 200 |
Array of objects (Event) |
{- "events": [
- {
- "seq": 0,
- "session_id": "1ffd059c-17ea-40a8-8aef-70fd0307db82",
- "kind": "tool.requested",
- "actor": "string",
- "body": { },
- "created_at": "2019-08-24T14:15:22Z"
}
]
}A text/event-stream of the run timeline.
Fanout is hybrid on purpose: a database NOTIFY is only a wakeup, and
the seq catch-up query is the delivery source of truth. That makes
the stream immune to missed notifications and to the database scaling
to zero.
Send Last-Event-ID to resume exactly where you left off.
curl -N -H "Authorization: Bearer $FLUIDBOX_TOKEN" \
"$FLUIDBOX_URL/v1/sessions/$RUN/events/stream"
| id required | string <uuid> The run (session) identifier. |
| Last-Event-ID | string The last |
{- "error": "agent not found"
}Human-in-the-loop decisions. Approvals are idempotent by
(session_id, tool_call_id): the database row is the source of truth, so
a runner retry after a restart re-attaches to the pending row rather than
duplicating or hanging.
Every approval currently waiting on a human decision.
| limit | integer <int64> Default: 50 Maximum items to return. |
Array of objects (Approval) |
{- "approvals": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "session_id": "1ffd059c-17ea-40a8-8aef-70fd0307db82",
- "tool_call_id": "string",
- "tool": "string",
- "status": "pending",
- "requested_at": "2019-08-24T14:15:22Z",
- "decided_at": "2019-08-24T14:15:22Z",
- "decided_by": "string"
}
]
}| id required | string <uuid> The run (session) identifier. |
Array of objects (Approval) |
{- "approvals": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "session_id": "1ffd059c-17ea-40a8-8aef-70fd0307db82",
- "tool_call_id": "string",
- "tool": "string",
- "status": "pending",
- "requested_at": "2019-08-24T14:15:22Z",
- "decided_at": "2019-08-24T14:15:22Z",
- "decided_by": "string"
}
]
}Unblocks a run waiting at the permission gate.
The decision is settled by a compare-and-swap, and the resulting
approval.decided and tool.decision events are appended inside the
deciding transaction — so only the CAS winner emits, and a
double-submit produces exactly one decision and one pair of events.
Authority is not uniform: a call against a personal connection is decidable only by the owner who invoked it. There is no role, admin, or operator override, symmetric across approve and deny.
| id required | string <uuid> The approval identifier. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| decision required | string Enum: "approved_once" "approved_session" "denied"
|
| id | string <uuid> |
| session_id | string <uuid> |
| tool_call_id | string |
| tool | string |
| status | string Enum: "pending" "approved_once" "approved_session" "denied" "expired" |
| requested_at | string <date-time> |
| decided_at | string <date-time> |
| decided_by | string |
{- "decision": "approved_once"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "session_id": "1ffd059c-17ea-40a8-8aef-70fd0307db82",
- "tool_call_id": "string",
- "tool": "string",
- "status": "pending",
- "requested_at": "2019-08-24T14:15:22Z",
- "decided_at": "2019-08-24T14:15:22Z",
- "decided_by": "string"
}Versioned agent definitions. Append-only: an edit is a new revision. The system prompt lives on the revision (who the agent is); the task is supplied per run (what to do this time).
| limit | integer <int64> Default: 50 Maximum items to return. |
Array of objects (Agent) |
{- "agents": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "description": "string",
- "current_revision_id": "e5714292-57d7-4936-a019-416c89b9b32b",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}Creates the agent and its first revision. The model, system prompt, policy, and budgets all live on the revision — to change any of them later, append a revision rather than mutating this one.
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| name required | string |
| description | string |
| harness | string From |
| model | string |
| system_prompt | string Who the agent is. Distinct from the per-run task. |
| policy | string Policy name. |
| runner_image | string Defaults to the configured sandbox image. Note that a seeded agent pins the image reference from its creation time. |
object (Budgets) Spending limits. Anything omitted falls back to the policy's value. Budgets can be tightened per run but never loosened past the policy. | |
Scratch (object) or Local copy (object) or Git repository (object) (WorkspaceInput) | |
| capability_bundles | Array of strings
|
Array of objects (ConnectionRequirement) |
object (Agent) |
{- "name": "reviewer",
- "system_prompt": "You review diffs for correctness and security."
}{- "agent": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "description": "string",
- "current_revision_id": "e5714292-57d7-4936-a019-416c89b9b32b",
- "created_at": "2019-08-24T14:15:22Z"
}
}Returns the agent with its revision history.
| id required | string The agent's id or its name. |
object (Agent) | |
Array of objects (AgentRevision) |
{- "agent": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "description": "string",
- "current_revision_id": "e5714292-57d7-4936-a019-416c89b9b32b",
- "created_at": "2019-08-24T14:15:22Z"
}, - "revisions": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978",
- "revision": 0,
- "harness": "string",
- "model": "string",
- "system_prompt": "string",
- "policy_name": "string",
- "runner_image": "string",
- "budgets": {
- "max_wall_clock_secs": 1800,
- "max_tokens": 1000000,
- "max_cost_usd": 2.5,
- "max_tool_calls": 200
}, - "capability_bundles": [
- "string"
], - "connection_requirements": [
- {
- "slot": "string",
- "required_tools": [
- "string"
], - "binding_mode": "invoking_user"
}
], - "created_at": "2019-08-24T14:15:22Z"
}
]
}The only way to change an agent. Omitted fields inherit from the latest
revision; an explicit empty array clears a list (this is how you drop
every capability pin, and how a bundle upgrade lands — re-resolving
"name" pins the newest version as of now).
In-flight runs are unaffected: they are governed by the snapshot they froze at creation.
| id required | string The agent's id or its name. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| harness | string |
| model | string |
| system_prompt | string |
| policy | string |
| runner_image | string |
object (Budgets) Spending limits. Anything omitted falls back to the policy's value. Budgets can be tightened per run but never loosened past the policy. | |
Scratch (object) or Local copy (object) or Git repository (object) (WorkspaceInput) | |
| capability_bundles | Array of strings |
Array of objects (ConnectionRequirement) |
object (AgentRevision) Immutable once written. |
{- "model": "claude-haiku-4-5"
}{- "revision": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978",
- "revision": 0,
- "harness": "string",
- "model": "string",
- "system_prompt": "string",
- "policy_name": "string",
- "runner_image": "string",
- "budgets": {
- "max_wall_clock_secs": 1800,
- "max_tokens": 1000000,
- "max_cost_usd": 2.5,
- "max_tool_calls": 200
}, - "capability_bundles": [
- "string"
], - "connection_requirements": [
- {
- "slot": "string",
- "required_tools": [
- "string"
], - "binding_mode": "invoking_user"
}
], - "created_at": "2019-08-24T14:15:22Z"
}
}The governance rules a run is judged against. Publishing is
optimistically concurrent — you send the base_version you loaded, and a
publish over a moved head is a 409 rather than a silent overwrite.
Array of objects (Policy) |
{- "policies": [
- {
- "name": "string",
- "version": 0,
- "content": { },
- "yaml": "string",
- "summary": "string",
- "published_by": "string",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| name required | string |
| yaml required | string The policy document. |
| name | string |
| version | integer <int32> The head version. Send this back as |
| content | object The policy document as structure. |
| yaml | string The document's YAML form, when it was authored as YAML. |
| summary | string The review note recorded with this version. |
| published_by | string |
| created_at | string <date-time> |
{- "name": "string",
- "yaml": "string"
}{- "name": "string",
- "version": 0,
- "content": { },
- "yaml": "string",
- "summary": "string",
- "published_by": "string",
- "created_at": "2019-08-24T14:15:22Z"
}| name required | string The policy name. |
| name | string |
| version | integer <int32> The head version. Send this back as |
| content | object The policy document as structure. |
| yaml | string The document's YAML form, when it was authored as YAML. |
| summary | string The review note recorded with this version. |
| published_by | string |
| created_at | string <date-time> |
{- "name": "string",
- "version": 0,
- "content": { },
- "yaml": "string",
- "summary": "string",
- "published_by": "string",
- "created_at": "2019-08-24T14:15:22Z"
}Runs that already froze this policy keep their snapshot and continue to be governed by it.
| name required | string The policy name. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| deleted | boolean |
{- "deleted": true
}| name required | string The policy name. |
| version required | integer <int32> |
| name | string |
| version | integer <int32> The head version. Send this back as |
| content | object The policy document as structure. |
| yaml | string The document's YAML form, when it was authored as YAML. |
| summary | string The review note recorded with this version. |
| published_by | string |
| created_at | string <date-time> |
{- "name": "string",
- "version": 0,
- "content": { },
- "yaml": "string",
- "summary": "string",
- "published_by": "string",
- "created_at": "2019-08-24T14:15:22Z"
}Publishes a new head version. base_version is the head you loaded the
draft from — publishing over a moved head is a 409 rather than a
silent overwrite of the other editor's intent.
That same 409 is what makes this safe to retry: a post-commit retry
of your own publish also lands on 409, so a network failure cannot
produce two versions.
| name required | string The policy name. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| content required | object The whole draft as structure. The server validates it; the browser never resolves a verdict. |
| summary required | string What changed and why. Required and non-blank. |
| base_version required | integer <int32> |
| name | string |
| version | integer <int32> The head version. Send this back as |
| content | object The policy document as structure. |
| yaml | string The document's YAML form, when it was authored as YAML. |
| summary | string The review note recorded with this version. |
| published_by | string |
| created_at | string <date-time> |
{- "content": { },
- "summary": "string",
- "base_version": 0
}{- "name": "string",
- "version": 0,
- "content": { },
- "yaml": "string",
- "summary": "string",
- "published_by": "string",
- "created_at": "2019-08-24T14:15:22Z"
}Publishes the content of version as a new head. History is never
rewritten — a revert moves forward.
| name required | string The policy name. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| version required | integer <int32> The version to restore. |
| base_version required | integer <int32> The head you were looking at. Same guard as publish. |
| name | string |
| version | integer <int32> The head version. Send this back as |
| content | object The policy document as structure. |
| yaml | string The document's YAML form, when it was authored as YAML. |
| summary | string The review note recorded with this version. |
| published_by | string |
| created_at | string <date-time> |
{- "version": 0,
- "base_version": 0
}{- "name": "string",
- "version": 0,
- "content": { },
- "yaml": "string",
- "summary": "string",
- "published_by": "string",
- "created_at": "2019-08-24T14:15:22Z"
}Parses and checks a policy document without storing anything.
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| yaml required | string |
| valid | boolean |
| errors | Array of strings |
{- "yaml": "string"
}{- "valid": true,
- "errors": [
- "string"
]
}Resolves a draft into the per-tool verdict matrix without publishing — this is how you see what a rule change will actually do before it governs a run.
Passing an existing policy's name folds that policy's agents' mcp__*
tool roster into the matrix. A brand-new draft previews the canonical
tool vocabulary only.
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| content required | object |
| name | string An existing policy name, to fold in its agents' tools. |
Array of objects |
{- "content": { },
- "name": "string"
}{- "matrix": [
- {
- "tool": "string",
- "verdict": "allow"
}
]
}Creates a new policy from an existing one. Omitting from starts blank
— an empty rule set under the fail-safe defaults, where everything asks
a human.
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| name required | string The new policy's name. |
| from | string Source policy name. Omit to start blank. |
| from_version | integer <int32> Pin the exact source version. Omit for its latest. |
| name | string |
| version | integer <int32> The head version. Send this back as |
| content | object The policy document as structure. |
| yaml | string The document's YAML form, when it was authored as YAML. |
| summary | string The review note recorded with this version. |
| published_by | string |
| created_at | string <date-time> |
{- "name": "string",
- "from": "string",
- "from_version": 0
}{- "name": "string",
- "version": 0,
- "content": { },
- "yaml": "string",
- "summary": "string",
- "published_by": "string",
- "created_at": "2019-08-24T14:15:22Z"
}A trigger subscription invokes an agent from outside the dashboard: an API call, a clock, or a connected-service event. A schedule is a trigger subscription with a clock attached, never a separate object.
| limit | integer <int64> Default: 50 Maximum items to return. |
Array of objects (Trigger) |
{- "triggers": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978",
- "trigger_kind": "api",
- "enabled": true,
- "concurrency_policy": "allow",
- "schedule": {
- "cron": "0 9 * * 1-5",
- "timezone": "Europe/London",
- "missed_run_policy": "skip"
}, - "next_fire_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}One object covers all three invocation shapes:
schedule with a cron expression.connection to listen to a connected service.schedule and connection are mutually exclusive.
The response includes the subscription's trigger token exactly once. Store it now; it is stored hashed and is never returned again. That token can invoke this one subscription and poll its runs — never the admin API, and the admin token can never invoke.
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| agent required | string |
| name required | string |
| task_template | string |
| allow_task_override | boolean Default: false |
| allow_workspace_override | boolean Default: false |
| autonomous | boolean Default: false |
object (Budgets) Spending limits. Anything omitted falls back to the policy's value. Budgets can be tightened per run but never loosened past the policy. | |
Scratch (object) or Local copy (object) or Git repository (object) (WorkspaceInput) | |
| callback_url | string <uri> A pre-registered signed-webhook destination. Destinations are approved at subscription time and can never be invented by a caller. Validated against the egress policy on save. |
| pinned_revision_id | string <uuid> Pin runs to one revision. Omit to always use the latest. |
| concurrency_policy | string Default: "allow" Enum: "allow" "skip_if_running" "replace" |
object (ScheduleInput) | |
| connection | string Listen to a connection's events. Mutually exclusive with |
| repositories | Array of strings Only these repositories match. Omit for every repository the connection sees. |
| events | Array of strings Omit for the connector's defaults — |
| publish | Array of strings Items Enum: "pr_comment" "check" Omit for |
| capabilities | Array of strings Keep-list, intersected with the revision's attachments. Remove-only. |
| id | string <uuid> |
| name | string |
| agent_id | string <uuid> |
| trigger_kind | string Enum: "api" "schedule" "event" |
| enabled | boolean |
| concurrency_policy | string Enum: "allow" "skip_if_running" "replace" |
| callback_url | string <uri> |
object (ScheduleInput) | |
| next_fire_at | string <date-time> |
| created_at | string <date-time> |
| token | string The subscription-scoped trigger token, shown once. |
{- "agent": "fixer",
- "name": "nightly-fix",
- "task_template": "Fix whatever is failing on main.",
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978",
- "trigger_kind": "api",
- "enabled": true,
- "concurrency_policy": "allow",
- "schedule": {
- "cron": "0 9 * * 1-5",
- "timezone": "Europe/London",
- "missed_run_policy": "skip"
}, - "next_fire_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "token": "fbx_trig_2f9c…"
}| id required | string <uuid> The trigger subscription identifier. |
| id | string <uuid> |
| name | string |
| agent_id | string <uuid> |
| trigger_kind | string Enum: "api" "schedule" "event" |
| enabled | boolean |
| concurrency_policy | string Enum: "allow" "skip_if_running" "replace" |
| callback_url | string <uri> |
object (ScheduleInput) | |
| next_fire_at | string <date-time> |
| created_at | string <date-time> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978",
- "trigger_kind": "api",
- "enabled": true,
- "concurrency_policy": "allow",
- "schedule": {
- "cron": "0 9 * * 1-5",
- "timezone": "Europe/London",
- "missed_run_policy": "skip"
}, - "next_fire_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}Starts a run through this subscription. Authenticate with the trigger token, not the admin token — the admin token can never invoke.
Overrides can only ever narrow authority, and only when the
subscription opted in (allow_task_override,
allow_workspace_override, both off by default). A workspace override
must stay inside the subscription's existing repository and connection:
it can pick a different ref or commit, never a new connection, clone
URL, or local path.
| id required | string <uuid> The trigger subscription identifier. |
| task | string Requires |
| context | object Arbitrary context frozen into the run's |
object Requires |
object (Session) A run. The |
{ }{- "session": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978",
- "agent_revision_id": "3c2ee4c4-f591-413e-a2b2-834f483cc40b",
- "status": "created",
- "trust_tier": "trusted",
- "task": "string",
- "autonomous": true,
- "run_spec": { },
- "created_at": "2019-08-24T14:15:22Z",
- "ended_at": "2019-08-24T14:15:22Z"
}
}The trigger token's read side. Polling is scoped to the subscription, not to the token: rotation replaces the credential, not the authority, so a replacement token can still poll runs created before the rotation.
| id required | string <uuid> The trigger subscription identifier. |
| sid required | string <uuid> The run identifier. |
object (Session) A run. The |
{- "session": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978",
- "agent_revision_id": "3c2ee4c4-f591-413e-a2b2-834f483cc40b",
- "status": "created",
- "trust_tier": "trusted",
- "task": "string",
- "autonomous": true,
- "run_spec": { },
- "created_at": "2019-08-24T14:15:22Z",
- "ended_at": "2019-08-24T14:15:22Z"
}
}A disabled subscription's schedule does not advance while it is off, so
re-enabling goes through the missed-run path — with the default skip
policy that records exactly one skip row rather than firing a backlog.
| id required | string <uuid> The trigger subscription identifier. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| id | string <uuid> |
| name | string |
| agent_id | string <uuid> |
| trigger_kind | string Enum: "api" "schedule" "event" |
| enabled | boolean |
| concurrency_policy | string Enum: "allow" "skip_if_running" "replace" |
| callback_url | string <uri> |
object (ScheduleInput) | |
| next_fire_at | string <date-time> |
| created_at | string <date-time> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978",
- "trigger_kind": "api",
- "enabled": true,
- "concurrency_policy": "allow",
- "schedule": {
- "cron": "0 9 * * 1-5",
- "timezone": "Europe/London",
- "missed_run_policy": "skip"
}, - "next_fire_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}| id required | string <uuid> The trigger subscription identifier. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| id | string <uuid> |
| name | string |
| agent_id | string <uuid> |
| trigger_kind | string Enum: "api" "schedule" "event" |
| enabled | boolean |
| concurrency_policy | string Enum: "allow" "skip_if_running" "replace" |
| callback_url | string <uri> |
object (ScheduleInput) | |
| next_fire_at | string <date-time> |
| created_at | string <date-time> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978",
- "trigger_kind": "api",
- "enabled": true,
- "concurrency_policy": "allow",
- "schedule": {
- "cron": "0 9 * * 1-5",
- "timezone": "Europe/London",
- "missed_run_policy": "skip"
}, - "next_fire_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}Mints a replacement and retires the old one. The new token is returned exactly once. Rotation replaces the credential, not the authority — the replacement still polls runs created before it existed.
| id required | string <uuid> The trigger subscription identifier. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| token | string |
{- "token": "string"
}Custodied credentials for external services. The credential is sealed at rest and is only ever used control-plane-side — it never enters a sandbox.
Personal connections are visible only to their owner — administrators are excluded by design.
Array of objects (Connection) |
{- "connections": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "provider": "string",
- "display_name": "string",
- "status": "pending",
- "auth_kind": "static",
- "owner_type": "organization",
- "authorization_generation": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
]
}Custodies a credential for an external service. Every secret in this request is consumed here, sealed at rest, and never returned by any endpoint.
Requires FLUIDBOX_CREDENTIAL_KEY to be configured — without it the
server boots fine but connections are disabled.
For MCP servers with auth_kind: oauth, the connection starts
pending; run /v1/connections/{id}/oauth/start to complete it.
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| provider required | string |
| token | string <password> Personal-access-token flavor. |
| app_id | string |
| installation_id | string |
| private_key | string <password> |
| webhook_secret | string <password> |
| display_name | string |
| base_url | string <uri> For |
| header_name | string Default: "authorization" |
| scheme | string Default: "Bearer"
|
| auth_kind | string Default: "static" Enum: "static" "oauth" "none" |
| scopes | Array of strings |
| client_id | string |
| client_secret | string <password> |
| owner | string Default: "organization" Enum: "organization" "personal"
|
| id | string <uuid> |
| provider | string |
| display_name | string |
| status | string Enum: "pending" "active" "error" "revoked"
|
| auth_kind | string Enum: "static" "oauth" "none" |
| owner_type | string Enum: "organization" "personal" |
| base_url | string <uri> |
| authorization_generation | integer Bumped when an ever-activated OAuth connection is reconnected. Bindings from an earlier generation refuse mid-run. Ordinary token rotation within a generation is fine, and GitHub App lifecycle never bumps it. |
| created_at | string <date-time> |
{- "provider": "github",
- "token": "ghp_xxxxxxxxxxxxxxxxxxxx",
- "display_name": "acme org"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "provider": "string",
- "display_name": "string",
- "status": "pending",
- "auth_kind": "static",
- "owner_type": "organization",
- "authorization_generation": 0,
- "created_at": "2019-08-24T14:15:22Z"
}Admin intent for a connection that GitHub-initiated discovery created
as pending. Approving a previously revoked installation revives the
same connection id, which keeps its dedup history continuous.
| id required | string <uuid> The connection identifier. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| id | string <uuid> |
| provider | string |
| display_name | string |
| status | string Enum: "pending" "active" "error" "revoked"
|
| auth_kind | string Enum: "static" "oauth" "none" |
| owner_type | string Enum: "organization" "personal" |
| base_url | string <uri> |
| authorization_generation | integer Bumped when an ever-activated OAuth connection is reconnected. Bindings from an earlier generation refuse mid-run. Ordinary token rotation within a generation is fine, and GitHub App lifecycle never bumps it. |
| created_at | string <date-time> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "provider": "string",
- "display_name": "string",
- "status": "pending",
- "auth_kind": "static",
- "owner_type": "organization",
- "authorization_generation": 0,
- "created_at": "2019-08-24T14:15:22Z"
}Marks the connection revoked and evicts any cached tokens. In-flight runs holding a binding to it fail closed on their next brokered call — every credential access re-verifies status before the secret is touched.
| id required | string <uuid> The connection identifier. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| id | string <uuid> |
| provider | string |
| display_name | string |
| status | string Enum: "pending" "active" "error" "revoked"
|
| auth_kind | string Enum: "static" "oauth" "none" |
| owner_type | string Enum: "organization" "personal" |
| base_url | string <uri> |
| authorization_generation | integer Bumped when an ever-activated OAuth connection is reconnected. Bindings from an earlier generation refuse mid-run. Ordinary token rotation within a generation is fine, and GitHub App lifecycle never bumps it. |
| created_at | string <date-time> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "provider": "string",
- "display_name": "string",
- "status": "pending",
- "auth_kind": "static",
- "owner_type": "organization",
- "authorization_generation": 0,
- "created_at": "2019-08-24T14:15:22Z"
}Returns only a go_url. Navigate a browser to it — that navigation
is what sets the flow cookie and claims the one-time flow row, with the
browser hash inside the atomic single-use predicate. A leaked
authorization URL can therefore neither complete nor burn a flow.
The flow freezes the authorization and token endpoints, the resolved
client, the resource parameter, and the PKCE verifier at start, so
the callback exchanges against the frozen row rather than
re-discovering anything.
The
go_urlitself is transferable — whichever browser opens it becomes the initiating browser. Treat it as a secret and hand it straight to the intended user.
| id required | string <uuid> The connection identifier. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| go_url | string <uri> |
{
}The latest append-only snapshot of the tools this connection exposes,
taken by a forced initialize handshake at connect time.
A run freezes the snapshot it saw. If the upstream server later changes
its tools, calls against the drifted set are denied rather than
silently re-negotiated — re-photograph with /tools/refresh and start
a new run.
| id required | string <uuid> The connection identifier. |
| connection_id | string <uuid> |
| version | integer |
| protocol_version | string The negotiated MCP version. At call time this must match the frozen surface exactly — drift denies the call rather than silently re-negotiating. |
Array of objects (Tool) | |
| digest | string |
| created_at | string <date-time> |
{- "connection_id": "d3547de1-d1f2-4344-b4c2-17169b7526f9",
- "version": 0,
- "protocol_version": "2025-11-25",
- "tools": [
- {
- "name": "string",
- "description": "string",
- "input_schema": { },
- "output_schema": { }
}
], - "digest": "string",
- "created_at": "2019-08-24T14:15:22Z"
}Takes a new snapshot. Existing runs keep the surface they froze; only runs created after this call see the new one.
| id required | string <uuid> The connection identifier. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| connection_id | string <uuid> |
| version | integer |
| protocol_version | string The negotiated MCP version. At call time this must match the frozen surface exactly — drift denies the call rather than silently re-negotiating. |
Array of objects (Tool) | |
| digest | string |
| created_at | string <date-time> |
{- "connection_id": "d3547de1-d1f2-4344-b4c2-17169b7526f9",
- "version": 0,
- "protocol_version": "2025-11-25",
- "tools": [
- {
- "name": "string",
- "description": "string",
- "input_schema": { },
- "output_schema": { }
}
], - "digest": "string",
- "created_at": "2019-08-24T14:15:22Z"
}| id required | string <uuid> The connection identifier. |
| page | integer Default: 1 |
| per_page | integer Default: 30 |
Array of objects |
{- "repositories": [
- {
- "full_name": "acme/widgets",
- "default_branch": "string",
- "private": true
}
]
}What this connection received and what it fanned out to. Deliveries are deduplicated at two levels, which is what makes a webhook retry heal a partial fan-out rather than duplicate runs or comments.
| id required | string <uuid> The connection identifier. |
| limit | integer <int64> Default: 50 Maximum items to return. |
| deliveries | Array of objects |
{- "deliveries": [
- { }
]
}Untrusted reference data describing connectors you can connect to. The catalog suggests; the permission gate decides.
Reference data only. Curated and imported entries are deployment-global; custom entries are tenant-scoped, and a tenant entry shadows a global one with the same slug.
tool_hints are display defaults — they are policy seeds, never
decisions. The permission gate stays the judge.
Array of objects (CatalogEntry) |
{- "entries": [
- {
- "slug": "string",
- "name": "string",
- "icon": "string",
- "description": "string",
- "tier": "curated",
- "transport": "string",
- "auth_mode": "none",
- "auth_hints": { },
- "scopes": [
- "string"
], - "tool_hints": { }
}
]
}Custom entries are forced to tier: custom. Requires an admin or owner role.
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| slug required | string |
| name required | string |
| icon | string |
| description | string |
| categories | Array of strings |
| url | string <uri> |
| transport | string |
| auth_mode | string Enum: "none" "api_key" "oauth" |
| auth_hints | object |
| scopes | Array of strings |
| egress | object |
| tool_hints | object |
| sandbox_launch | object |
| slug | string |
| name | string |
| icon | string |
| description | string |
| tier | string Enum: "curated" "imported" "custom" |
| url | string <uri> |
| transport | string |
| auth_mode | string Enum: "none" "api_key" "oauth" |
| auth_hints | object |
| scopes | Array of strings |
| tool_hints | object Policy-default seeds for display. The gate stays the judge. |
{- "slug": "string",
- "name": "string",
- "icon": "string",
- "description": "string",
- "categories": [
- "string"
], - "transport": "string",
- "auth_mode": "none",
- "auth_hints": { },
- "scopes": [
- "string"
], - "egress": { },
- "tool_hints": { },
- "sandbox_launch": { }
}{- "slug": "string",
- "name": "string",
- "icon": "string",
- "description": "string",
- "tier": "curated",
- "transport": "string",
- "auth_mode": "none",
- "auth_hints": { },
- "scopes": [
- "string"
], - "tool_hints": { }
}| slug required | string The connector slug. |
| slug | string |
| name | string |
| icon | string |
| description | string |
| tier | string Enum: "curated" "imported" "custom" |
| url | string <uri> |
| transport | string |
| auth_mode | string Enum: "none" "api_key" "oauth" |
| auth_hints | object |
| scopes | Array of strings |
| tool_hints | object Policy-default seeds for display. The gate stays the judge. |
{- "slug": "string",
- "name": "string",
- "icon": "string",
- "description": "string",
- "tier": "curated",
- "transport": "string",
- "auth_mode": "none",
- "auth_hints": { },
- "scopes": [
- "string"
], - "tool_hints": { }
}The one-call path from catalog entry to usable connection.
api_key entries seal the token and immediately photograph the tool
surface. If the photograph is refused, the whole connect rolls back.oauth entries return a go_url and photograph on callback.none) entries register a sandbox capability bundle
instead — no credential is involved.| slug required | string The connector slug. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| display_name | string |
| token | string <password> For |
| bundle_name | string |
| client_id | string |
| client_secret | string <password> |
| scopes | Array of strings |
| owner | string Default: "organization" Enum: "organization" "personal" |
object (Connection) | |
| go_url | string <uri> Present for OAuth entries. Navigate a browser to it. |
{- "token": "sk-xxxxxxxx",
- "display_name": "Acme production"
}{- "connection": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "provider": "string",
- "display_name": "string",
- "status": "pending",
- "auth_kind": "static",
- "owner_type": "organization",
- "authorization_generation": 0,
- "created_at": "2019-08-24T14:15:22Z"
},
}Paste a URL, detect its authentication mode, and preview its tools — without committing anything. Nothing is stored.
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| url required | string <uri> |
| auth_mode | string Enum: "none" "api_key" "oauth" |
| protocol_version | string |
Array of objects (Tool) |
{
}{- "auth_mode": "none",
- "protocol_version": "string",
- "tools": [
- {
- "name": "string",
- "description": "string",
- "input_schema": { },
- "output_schema": { }
}
]
}Creates a tier: custom catalog entry and connects it in one call.
The catalog entry stays organization reference data regardless of
owner — only the resulting connection carries personal custody.
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| url required | string <uri> |
| name required | string |
| auth_mode | string Default: "none" Enum: "none" "api_key" "oauth" |
| token | string <password> |
| display_name | string |
| icon | string |
| description | string |
| header_name | string Default: "authorization" |
| scheme | string Default: "Bearer" |
| scopes | Array of strings |
| client_id | string |
| client_secret | string <password> |
| owner | string Default: "organization" Enum: "organization" "personal" |
object (CatalogEntry) | |
object (Connection) | |
| go_url | string <uri> |
{- "name": "acme-tools",
- "auth_mode": "api_key",
- "token": "sk-xxxxxxxx",
- "header_name": "authorization",
- "scheme": "Bearer"
}{- "entry": {
- "slug": "string",
- "name": "string",
- "icon": "string",
- "description": "string",
- "tier": "curated",
- "transport": "string",
- "auth_mode": "none",
- "auth_hints": { },
- "scopes": [
- "string"
], - "tool_hints": { }
}, - "connection": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "provider": "string",
- "display_name": "string",
- "status": "pending",
- "auth_kind": "static",
- "owner_type": "organization",
- "authorization_generation": 0,
- "created_at": "2019-08-24T14:15:22Z"
},
}Sandbox-class MCP servers — credential-free stdio subprocesses packaged in the runner image. Brokered (credentialed) servers are not capability bundles; they are connections.
Array of objects (CapabilityBundle) |
{- "bundles": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "version": 0,
- "description": "string",
- "servers": [
- {
- "alias": "string",
- "command": "string",
- "args": [
- "string"
], - "tools": [
- {
- "name": "string",
- "description": "string",
- "input_schema": { },
- "output_schema": { }
}
]
}
], - "created_at": "2019-08-24T14:15:22Z"
}
]
}Bundles describe sandbox-class MCP servers only: credential-free stdio subprocesses packaged in the runner image, contained by the container.
A brokered (credentialed) server is refused here — those are connections, and the whole point of the split is that their credential never enters a sandbox.
The registry is append-only: re-registering a name publishes the next version.
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| name required | string |
| description | string |
required | Array of objects (CapabilityServer) |
| id | string <uuid> |
| name | string |
| version | integer |
| description | string |
Array of objects (CapabilityServer) | |
| created_at | string <date-time> |
{- "name": "string",
- "description": "string",
- "servers": [
- {
- "alias": "string",
- "command": "string",
- "args": [
- "string"
], - "tools": [
- {
- "name": "string",
- "description": "string",
- "input_schema": { },
- "output_schema": { }
}
]
}
]
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "version": 0,
- "description": "string",
- "servers": [
- {
- "alias": "string",
- "command": "string",
- "args": [
- "string"
], - "tools": [
- {
- "name": "string",
- "description": "string",
- "input_schema": { },
- "output_schema": { }
}
]
}
], - "created_at": "2019-08-24T14:15:22Z"
}| id required | string The bundle id, or |
| id | string <uuid> |
| name | string |
| version | integer |
| description | string |
Array of objects (CapabilityServer) | |
| created_at | string <date-time> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "version": 0,
- "description": "string",
- "servers": [
- {
- "alias": "string",
- "command": "string",
- "args": [
- "string"
], - "tools": [
- {
- "name": "string",
- "description": "string",
- "input_schema": { },
- "output_schema": { }
}
]
}
], - "created_at": "2019-08-24T14:15:22Z"
}Login, sessions, and personal access tokens. Multi-user identity is off
by default and enabled with FLUIDBOX_REQUIRE_SSO=1.
Redirects to the organization's configured identity provider. The
browser-bound one-time flow row and the sealed state are the
authentication here — there is no bearer token to present.
| slug required | string The organization slug. |
| redirect_to | string |
{- "error": "agent not found"
}The one stable redirect URI for every organization. Unauthenticated by
design — the sealed state plus the per-flow cookie are the
authentication, the same pattern as webhook signatures.
| code | string |
| state | string |
| error | string |
| error_description | string |
{- "error": "agent not found"
}Resolves the caller. Useful for confirming which of the three principal kinds you are actually authenticating as — operator, user, or PAT.
object | |
object | |
| roles | Array of strings |
{- "user": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "email": "user@example.com"
}, - "org": {
- "slug": "string"
}, - "roles": [
- "string"
]
}| id required | string <uuid> The pending organization-switch identifier. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
{- "error": "agent not found"
}Metadata only — token values are stored as SHA-256 digests and cannot be read back.
Array of objects (PersonalAccessToken) |
{- "tokens": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z",
- "last_used_at": "2019-08-24T14:15:22Z"
}
]
}Machine access without a browser flow. Requires a browser session — a PAT can never mint another PAT.
The token value is returned exactly once.
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| name required | string |
| expires_in | integer <int64> Lifetime in seconds. Omit for the deployment default. |
| id | string <uuid> |
| name | string |
| created_at | string <date-time> |
| expires_at | string <date-time> |
| last_used_at | string <date-time> |
| token | string |
{- "name": "ci-runner",
- "expires_in": 2592000
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z",
- "last_used_at": "2019-08-24T14:15:22Z",
- "token": "fbx_pat_9c2e…"
}| id required | string <uuid> |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
{- "error": "agent not found"
}Sets the flow cookie and redirects to the authorization server. Unauthenticated by design — a browser redirect cannot carry a bearer token, so the sealed boot token plus the one-time flow claim are the authentication.
Navigate a browser here; do not call it programmatically.
| f required | string The sealed boot token from |
{- "error": "agent not found"
}The single redirect URI for every connector OAuth flow. The AEAD-sealed
state carrying the connection id and PKCE verifier is the
authentication.
The exchange runs against the endpoints frozen at start (closing authorization-server mix-up) and refuses a moved authorization generation. A successful exchange requires a refresh token.
| code | string |
| state | string |
| error | string |
{- "error": "agent not found"
}A registration custodies an app identity — its private key, webhook secret, and client secret, all sealed. There is one registration per GitHub account or organization, because a private app installs only on its owner.
Array of objects (GithubAppRegistration) |
{- "registrations": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "app_slug": "string",
- "owner_login": "string",
- "status": "pending",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}Mints a one-time flow and returns a go_url. Requires admin intent —
activation is never something GitHub can initiate on its own.
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| go_url | string <uri> |
{
}| id required | string <uuid> The GitHub App registration identifier. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
| go_url | string <uri> |
{
}Where GitHub sends the browser after an installation. A state-less hit
performs zero writes and zero GitHub calls — installation_id from
a query string is never trusted. Use sync or approve to record
intent.
| id required | string <uuid> The GitHub App registration identifier. |
| installation_id | string |
| state | string |
{- "error": "agent not found"
}Reconciles local state against GitHub's truth. Webhook ordering never wins over a sync — an installation id is only ever trusted after it resolves under our own app's JWT.
| id required | string <uuid> The GitHub App registration identifier. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
Array of objects (Connection) |
{- "connections": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "provider": "string",
- "display_name": "string",
- "status": "pending",
- "auth_kind": "static",
- "owner_type": "organization",
- "authorization_generation": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
]
}Cascades to every connection the registration custodies and evicts their cached tokens. Custody resolution is fail-closed: a connection whose registration is missing or inactive is refused rather than falling back to per-connection credentials.
| id required | string <uuid> The GitHub App registration identifier. |
| x-fluidbox-csrf | string Examples: 1 Required on non-safe methods when authenticating with the session
cookie. Bearer principals are exempt. Send |
{- "error": "agent not found"
}Reports whether the control plane can serve real traffic, which
includes reaching the database. Use this one for load-balancer
readiness; use /v1/health for liveness.
| status | string |
{- "status": "ready"
}The single source of truth for which agent harnesses this deployment supports and which models each one accepts. The dashboard pickers read exactly this; build yours from it too rather than hardcoding names.
Array of objects |
{- "harnesses": [
- {
- "id": "string",
- "models": [
- "string"
], - "default_model": "string"
}
]
}The CIMD document (MCP spec 2025-11-25). This document's URL is the
control plane's OAuth client_id — authorization servers fetch it,
so it is public by nature.
It is only used when FLUIDBOX_PUBLIC_URL is HTTPS and non-loopback,
because the authorization server has to be able to reach it. Local
deployments always fall back to dynamic client registration.
{ }The contract every runner image implements. This is how a sandbox talks to the control plane, and implementing it is how you add a new agent harness.
The sandbox holds four audience-scoped tokens, not one bearer. Each
guarded route checks the audience as its first statement and answers
403 {"error":"wrong_audience"} otherwise — that body code is
load-bearing, because runners key their fatal abort on it.
| Audience | Routes |
|---|---|
tool |
/permission, /tools/call |
control |
/events, /heartbeat, /result, /token/renew |
workspace |
/workspace |
llm |
/llm/* |
The heart of the system. Every tool the agent wants to run comes through here, and the answer is authoritative.
The gate runs a fixed sequence, and the order is the security model:
source=capability;
a missing or stale binding denies with source=binding.source=schema.read_only run (any fork pull request) is
refused write and secret-reaching tools here, above policy and above
human approval. There is no approval escape from this tier.The permission callback stays wired in both autonomy modes. An
autonomous run rewrites a require_approval verdict to the policy
fallback inside the evaluation and records both the original and the
rewritten verdict — it never bypasses the gate.
Decisions are idempotent by (session_id, tool_call_id), so retrying
after a restart re-attaches to the pending row.
Tool names must use the canonical vocabulary — Bash{command},
Edit/Write/MultiEdit{file_path}, Read/Glob/Grep/LS, and
mcp__<server>__<tool>. Canonicalization is the runner's job.
| id required | string <uuid> The run (session) identifier. |
| tool_call_id required | string |
| tool required | string |
| input | object |
| allow | boolean |
| verdict | string (Verdict) Enum: "allow" "deny" "require_approval" What the policy engine decided for a tool call. |
| source | string Enum: "budget" "capability" "binding" "schema" "trust_tier" "policy" "approval" Which gate stage produced a denial — useful for diagnosing why a call was refused. |
| reason | string |
{- "tool_call_id": "toolu_01ABC",
- "tool": "Bash",
- "input": {
- "command": "cargo test -p fluidbox-core"
}
}{- "allow": true,
- "verdict": "allow",
- "source": "budget",
- "reason": "string"
}Intent in, governed result out. The sealed credential turns server-side — it never enters the sandbox, the same inversion as the LLM facade and the credentialed git fetch.
Runners auto-allow brokered mcp__* calls in their own permission
callback precisely because the broker runs the identical gate here.
Every dispatch is wrapped in a durable four-state execution claim keyed
(session_id, tool_call_id, input_digest). A reused id with different
arguments is a new claim, never an adoption. Only
failed_before_send — which requires positive proof nothing was
written — is re-claimable; a definitive upstream response is terminal.
The ledger records tool.requested → tool.decision →
tool.brokered, carrying latency and a result digest, never payloads
or secrets.
| id required | string <uuid> The run (session) identifier. |
| tool_call_id required | string |
| tool required | string The prefixed name, e.g. |
| input | object |
| ok | boolean |
object |
{- "tool_call_id": "string",
- "tool": "string",
- "input": { }
}{- "ok": true,
- "result": {
- "content": [
- { }
], - "is_error": true
}
}Appends to the run ledger. The ledger only accepts redacted envelopes —
model prompts never reach it, only digests, usage, and cost. Session
tokens (fbx_sess_), web tokens, and PATs are all scrubbed by the
redactor.
seq is assigned server-side, gaplessly, under a row lock.
| id required | string <uuid> The run (session) identifier. |
| actor required | string |
| body required | object |
| seq | integer <int64> |
{- "actor": "string",
- "body": { }
}{- "seq": 0
}Keeps the watchdog satisfied. A run that stops heartbeating is reaped by the heartbeat worker — the server is the single status writer, and the runner only ever reports.
| id required | string <uuid> The run (session) identifier. |
{- "error": "agent not found"
}The runner's last call. The server decides the terminal status from this report; the runner never writes status itself.
The audience is checked before the revoked-token leniency, so a
revoked control token still acknowledges here — but an llm or
tool token never does.
| id required | string <uuid> The run (session) identifier. |
| outcome required | string |
| summary | string |
{- "outcome": "completed",
- "summary": "string"
}{- "error": "agent not found"
}The immutable workspace archive the Kubernetes init container pulls. Credential-free and digest-verified.
Workspace initialization is control-plane-side by design: the
credentialed fetch happens in the orchestrator before the agent starts,
so the original repository is never touched and the sandbox stays
egress-free. The agent only ever sees a copy at /workspace.
| id required | string <uuid> The run (session) identifier. |
{- "error": "agent not found"
}Extends the calling token's lifetime for a long-running run.
| ttl_secs | integer <int64> |
| token | string |
| expires_at | string <date-time> |
{- "ttl_secs": 0
}{- "token": "string",
- "expires_at": "2019-08-24T14:15:22Z"
}The sandbox's ANTHROPIC_API_KEY is its session token — there is no
real provider key inside a sandbox, ever.
The facade validates the token, enforces the budget stop, swaps in the real upstream credential, forwards to the gateway, and tees the streaming response to meter usage. It dispatches on the run's harness, speaking the Anthropic Messages dialect or the OpenAI Responses dialect as appropriate.
Admission books a durable, request-keyed reservation whose primary key becomes the usage entry's external id — which is what makes a retry and a late drain idempotent.
| rest required | string The provider path the harness appends, e.g. |
{ }{ }The LiteLLM usage callback. Called by the gateway, not by a runner.
{ }{- "error": "agent not found"
}Break-glass and deployment-lifecycle surfaces. Under
FLUIDBOX_REQUIRE_SSO=1 this is the only surface the admin token
reaches — everywhere else it is refused in favour of user sessions and
PATs.
| slug required | string |
| name | string |
| id | string <uuid> |
| slug | string |
| name | string |
| created_at | string <date-time> |
{- "slug": "string",
- "name": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "slug": "string",
- "name": "string",
- "created_at": "2019-08-24T14:15:22Z"
}| slug required | string The organization slug. |
Array of objects (IdpConfig) |
{- "idps": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "client_id": "string",
- "status": "inactive",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}Created inactive. Activate it explicitly once discovery has been verified — the whole lifecycle is deliberately multi-step so a misconfigured issuer cannot lock an organization out.
| slug required | string The organization slug. |
| issuer required | string <uri> |
| client_id required | string |
| client_secret | string <password> |
| scopes | Array of strings |
| id | string <uuid> |
| issuer | string <uri> |
| client_id | string |
| status | string Enum: "inactive" "active" "disabled" |
| created_at | string <date-time> |
{- "client_id": "string",
- "client_secret": "pa$$word",
- "scopes": [
- "string"
]
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "client_id": "string",
- "status": "inactive",
- "created_at": "2019-08-24T14:15:22Z"
}| slug required | string The organization slug. |
| id required | string <uuid> The identity provider configuration identifier. |
| issuer required | string <uri> |
| client_id required | string |
| client_secret | string <password> |
| scopes | Array of strings |
| id | string <uuid> |
| issuer | string <uri> |
| client_id | string |
| status | string Enum: "inactive" "active" "disabled" |
| created_at | string <date-time> |
{- "client_id": "string",
- "client_secret": "pa$$word",
- "scopes": [
- "string"
]
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "client_id": "string",
- "status": "inactive",
- "created_at": "2019-08-24T14:15:22Z"
}Moves an organization's users to a new issuer without re-inviting them.
| slug required | string The organization slug. |
| id required | string <uuid> The identity provider configuration identifier. |
| issuer required | string <uri> |
{
}{- "error": "agent not found"
}The recovery path when an organization has locked itself out. Every accepted mutation audits inside its own transaction; rejected attempts audit separately.
| slug required | string The organization slug. |
| email required | string <email> |
{- "email": "user@example.com"
}{- "error": "agent not found"
}| slug required | string The organization slug. |
Array of objects (Membership) |
{- "members": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
- "email": "user@example.com",
- "roles": [
- "string"
], - "active": true
}
]
}| slug required | string The organization slug. |
| membership_id required | string <uuid> |
| roles required | Array of strings |
| id | string <uuid> |
| user_id | string <uuid> |
string <email> | |
| roles | Array of strings |
| active | boolean |
{- "roles": [
- "string"
]
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
- "email": "user@example.com",
- "roles": [
- "string"
], - "active": true
}This is also the kill switch for that member's personal connections: every brokered call re-verifies owner-membership before touching a credential, so deactivation cuts them off mid-run.
| slug required | string The organization slug. |
| membership_id required | string <uuid> |
{- "error": "agent not found"
}Mints a fresh gateway virtual key, swaps the sealed row, and retires the old key upstream. The key itself is never returned.
| slug required | string The organization slug. |
{- "error": "agent not found"
}Progress of the legacy-to-envelope re-seal, including the v1 row count
that gates retiring FLUIDBOX_CREDENTIAL_KEY.
| state | string Enum: "idle" "running" "completed" "failed" |
| v1_rows_remaining | integer <int64> The retirement gate. |
| v2_rows | integer <int64> |
| started_at | string <date-time> |
{- "state": "idle",
- "v1_rows_remaining": 0,
- "v2_rows": 0,
- "started_at": "2019-08-24T14:15:22Z"
}Re-seals every legacy (v1) sealed column under per-tenant envelope encryption. Resumable, compare-and-swap guarded, count-parity checked, and a singleton.
This is the supported path to retiring the legacy credential key.
Do not drop FLUIDBOX_CREDENTIAL_KEY until this job proves zero v1
rows — and be aware that from the moment any v2 row exists, custody
roots on the KEK, so losing the KEK is unrecoverable. Back it up.
| state | string Enum: "idle" "running" "completed" "failed" |
| v1_rows_remaining | integer <int64> The retirement gate. |
| v2_rows | integer <int64> |
| started_at | string <date-time> |
{- "state": "idle",
- "v1_rows_remaining": 0,
- "v2_rows": 0,
- "started_at": "2019-08-24T14:15:22Z"
}Bounded-cardinality counters, gauges, and histograms. There are deliberately no per-tenant labels — per-tenant accounting lives in the usage ledger, not in metrics.
The optional FLUIDBOX_METRICS_BIND listener serves this same body
unauthenticated on its own port; point it at a private interface only.
{- "error": "agent not found"
}The same body as /v1/admin/metrics, served unauthenticated on the
optional listener configured by FLUIDBOX_METRICS_BIND.
This path exists only on that listener — it is not mounted on the public or sandbox planes. Because it carries no authentication, bind it to a private interface only. A bad address fails boot by design, but a reachable one is your responsibility.
{- "error": "agent not found"
}Webhook receivers for connected services. Deliberately unauthenticated in the bearer sense: the signature verified against the connection's sealed secret is the authentication, and nothing is stored before it verifies.
Unauthenticated in the bearer sense by design: the signature verified against the connection's sealed secret is the authentication, and nothing is stored before it verifies.
The pipeline is provider-ignorant — ingress, verify, normalize, match, create run, publish — with all provider knowledge behind a single dispatch.
Retries are safe and in fact healing: two database-unique dedup levels (delivery per connection, dispatch per subscription) mean a retry completes a partial fan-out rather than duplicating runs or comments.
A pull request from a fork freezes trust_tier: read_only, enforced at
the permission gate above policy and above human approval. The check
fails toward "fork" when the head repository is hidden.
| provider required | string The connector provider, e.g. |
| connection_id required | string <uuid> |
| X-Hub-Signature-256 | string The HMAC signature over the raw body. |
{ }{- "error": "agent not found"
}App-level ingress, verified against the registration's sealed webhook secret. The connection is resolved from the verified payload's installation id and fed into the same pipeline as connection-level ingress.
Lifecycle database failures answer 5xx on purpose so GitHub retries —
never swallow-and-acknowledge.
| registration_id required | string <uuid> The GitHub App registration identifier. |
| X-Hub-Signature-256 | string |
{ }{- "error": "agent not found"
}