{"components":{"responses":{},"schemas":{"SandboxResponse":{"properties":{"data":{"$ref":"#/components/schemas/SandboxDetail"}},"required":["data"],"title":"SandboxResponse","type":"object"},"InferenceCredentialSetResponse":{"properties":{"data":{"$ref":"#/components/schemas/InferenceCredentialSet"}},"required":["data"],"title":"InferenceCredentialSetResponse","type":"object"},"ChatGPTLinkAttemptCreateRequest":{"description":"Exactly one of the two: `name` links a new subscription under that name, `grant_id` reconnects the one it names.","properties":{"grant_id":{"format":"uuid","type":"string"},"name":{"maxLength":200,"minLength":1,"type":"string"}},"title":"ChatGPTLinkAttemptCreateRequest","type":"object"},"ApiKeyRequest":{"properties":{"name":{"description":"What this key is for.","minLength":1,"type":"string"}},"required":["name"],"title":"ApiKeyRequest","type":"object"},"SandboxChange":{"description":"One path `git status` reports, relative to `repo_root` (ADR 0039). An untracked file reads `untracked` on both sides, which is what git reports for it and the one state a diff cannot show.","properties":{"index":{"description":"The staged side: what a commit would record.","enum":["added","copied","deleted","ignored","modified","renamed","type_changed","unchanged","unmerged","untracked"],"type":"string"},"path":{"description":"Relative to `repo_root`.","type":"string"},"renamed_from":{"description":"The old path, when that side is a rename or a copy; null otherwise.","nullable":true,"type":"string"},"worktree":{"description":"The unstaged side: what the disk holds beyond the index.","enum":["added","copied","deleted","ignored","modified","renamed","type_changed","unchanged","unmerged","untracked"],"type":"string"}},"required":["path","index","worktree"],"title":"SandboxChange","type":"object"},"RegisterRequest":{"properties":{"access_code":{"description":"The instance's signup access code. Required only when the operator set one; otherwise ignored.","type":"string"},"email":{"format":"email","type":"string"},"password":{"format":"password","type":"string"}},"required":["email","password"],"title":"RegisterRequest","type":"object"},"SandboxFile":{"description":"One file on a sandbox, redacted: the sandbox's environment and vault values read `[REDACTED]`.","properties":{"content":{"type":"string"},"encoding":{"description":"`utf-8` when `content` is the text itself; `base64` when it is not valid UTF-8.","enum":["utf-8","base64"],"type":"string"},"path":{"description":"The file read, absolute.","type":"string"},"size":{"description":"The whole file, in bytes.","type":"integer"},"truncated":{"description":"True when `content` is not the whole file: either the file is longer than `max_bytes`, or redaction grew what was read past it. False means `content` is everything.","type":"boolean"}},"required":["path","size","truncated","encoding","content"],"title":"SandboxFile","type":"object"},"TeamSchedule":{"description":"A scheduled prompt for a teammate: on `cron` (five fields, UTC), send `prompt` to the agent — into its team conversation, or (`one_off`) on a fresh computer.","properties":{"agent_id":{"format":"uuid","type":"string"},"cron":{"example":"0 9 * * 1-5","type":"string"},"enabled":{"type":"boolean"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"last_conversation_id":{"description":"The conversation the last run went to; null when it failed or never ran.","format":"uuid","nullable":true,"type":"string"},"last_error":{"description":"Why the last run did not go out (`teammate was busy`, ...); null after a good run.","nullable":true,"type":"string"},"last_run_at":{"format":"date-time","nullable":true,"type":"string"},"name":{"maxLength":120,"nullable":true,"type":"string"},"next_run_at":{"description":"The next fire time (UTC); null while disabled.","format":"date-time","nullable":true,"type":"string"},"one_off":{"description":"false: the prompt goes into the teammate's own conversation. true: each run opens a fresh conversation with the teammate's agent, environment and vault.","type":"boolean"},"prompt":{"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","agent_id","cron","prompt","one_off","enabled"],"title":"TeamSchedule","type":"object"},"SecretListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Secret"},"type":"array"}},"required":["data"],"title":"SecretListResponse","type":"object"},"BuzzProvisionRequest":{"description":"Provision (or converge on) a hosted Buzz agent. The Nostr secret key is accepted here and stored server-side in the identity's vault; it is never returned and never enters a sandbox. Idempotent on `pubkey`.","properties":{"agent_id":{"description":"The Fountain agent to run","format":"uuid","type":"string"},"auth_tag":{"description":"NIP-OA owner attestation tag (JSON array)","type":"string"},"display_name":{"nullable":true,"type":"string"},"environment_id":{"description":"Optional environment to provision this identity's conversations from instead of the agent's own — one agent config, one environment per identity. Must be owned by the caller (404 otherwise) and, when the agent sets allowed_environment_ids, on that list (checked at conversation start). Omitted on a re-provision clears a previously set one.","format":"uuid","nullable":true,"type":"string"},"name":{"maxLength":200,"minLength":1,"type":"string"},"private_key_nsec":{"description":"Nostr secret key (nsec/hex). Stored, never returned","type":"string"},"pubkey":{"description":"Nostr public key (hex) — the convergence key","type":"string"},"relay_url":{"description":"wss:// relay URL","type":"string"},"respond_to":{"description":"Who may @-mention the agent and fire a turn — buzz-acp's --respond-to mode, set as BUZZ_ACP_RESPOND_TO on the hosted harness. Omitted means owner-only. A re-provision that changes it restarts a running harness.","enum":["owner-only","allowlist","anyone","nobody"],"nullable":true,"type":"string"},"respond_to_allowlist":{"description":"64-hex pubkeys admitted in allowlist mode (BUZZ_ACP_RESPOND_TO_ALLOWLIST). Required non-empty when respond_to is allowlist; ignored otherwise.","items":{"type":"string"},"nullable":true,"type":"array"},"sandbox_mode":{"description":"Where this identity's conversations run (ADR 0023), passed to the harness as fountain acp --sandbox-mode. Omitted means the agent's own default; omitted on a re-provision clears a previously set one. A re-provision that changes it restarts a running harness.","enum":["ephemeral","persistent"],"nullable":true,"type":"string"}},"required":["name","relay_url","agent_id","pubkey","private_key_nsec","auth_tag"],"title":"BuzzProvisionRequest","type":"object"},"ConversationCreateRequest":{"properties":{"agent_id":{"format":"uuid","type":"string"},"channel_id":{"description":"Opaque key for the external channel this conversation is bound to (for example a Buzz channel id). When set, the latest live conversation for the same agent, vault and channel is resumed (200) instead of a new one being opened (201).","maxLength":255,"nullable":true,"type":"string"},"client_request_id":{"description":"Your own name for the first prompt. Ignored when the request carries no `prompt`. An immediate `channel_id` resume does not deliver the prompt; send the value with it on the prompts route. If a queued start resumes a channel bound while it waited, the queue delivers the prompt and value. Fountain stores it on the turn the prompt opens and sends it on that turn's `started` stage event, beside the `turn_id`, so a client can bind its work item to the exact turn without inferring it from turn order. Use the event to find a candidate turn and the turn itself to confirm it: the event's copy has been through event redaction, and the turn's is what you sent. It is a correlation and not an idempotency key: a second prompt with the same value opens a second turn that carries it too. Make it unique within the conversation.","maxLength":200,"minLength":1,"nullable":true,"pattern":"^[^\\x00]*$","type":"string"},"environment_id":{"description":"Optional environment to provision from instead of the agent's own; the conversation stays pinned to it across wakes. Must be owned by the caller (404 otherwise) and satisfy the agent's allowed_environment_ids when that allowlist is set (422 environment_not_allowed). Part of the channel_id resume key.","format":"uuid","nullable":true,"type":"string"},"fresh":{"description":"With channel_id: skip the resume and open a new conversation (201), which then becomes the channel's binding. Sent by a chat harness relaying its owner's rotate command. Ignored without channel_id.","nullable":true,"type":"boolean"},"images":{"description":"Optional images to attach to the initial prompt. They require that prompt: images with no opening text are refused with 422 invalid_prompt, and an image whose media_type is unsupported or whose decoded bytes are empty or over the 10MB ceiling is refused with 422 invalid_images. Both refusals happen before a sandbox is reserved.","items":{"$ref":"#/components/schemas/ImageInput"},"nullable":true,"type":"array"},"inference_credential_id":{"description":"Optional credential set to run on instead of the agent's; the conversation stays pinned to it across wakes. Must be owned by the caller (404 inference_credential_not_found otherwise) and satisfy the agent's allowed_inference_credential_ids when that allowlist is set (422 inference_credential_not_allowed). An unusable selected set is 422 inference_credential_unusable. The resolved source and revision stay bound across wakes; replacing or deleting the source returns 409 inference_source_changed. Not part of sandbox identity, but a shared Codex sandbox requires the same resolved source and revision (409 codex_inference_conflict otherwise). The exception is a set that names a ChatGPT subscription: Codex keeps that sign-in in a home of its own, so it shares a sandbox with any other source, unless the sandbox was first bound before Fountain prepared such homes.","format":"uuid","nullable":true,"type":"string"},"labels":{"additionalProperties":{"type":"string"},"description":"Key/value strings to stamp on the conversation. At most 32 entries; a key is at most 64 bytes and a value at most 256 bytes, and a 422 names the offending key under `errors.labels`. With channel_id, a resume merges these into the conversation it hands back rather than dropping them.","nullable":true,"type":"object"},"model":{"description":"Optional model to run instead of the agent's (ADR 0061), in canonical provider/model_id form, for this conversation only. Checked as the agent's own model is: a provider the runtime does not drive, or any model on the acp runtime, is 422 model_invalid. The model id itself is not checked against a catalog. Change it later with POST /api/conversations/{id}/reapply. Not part of the channel_id resume key: a request that resumes a conversation running a different model is 409 conversation_model_differs, before any prompt is sent; reapply the model, or set fresh: true.","nullable":true,"type":"string"},"permission_policy":{"allOf":[{"$ref":"#/components/schemas/PermissionPolicy"}],"description":"Per-launch permission override (#939). Keys are matched against the tool card's title first and then ACP's kind (execute, edit, read, fetch, …); \"default\" covers the rest. Prefer a kind: claude titles a tool call with the command it is about to run, so a title matches one invocation only. Merged with the agent's own policy, taking the stricter of the two. It may only narrow: a policy that would loosen any tool is refused with 422 permission_policy_widens rather than silently clamped, and one the runtime never consults is refused with 422 permission_policy_unenforceable. If a queued request with a prompt and a nonempty policy resumes an existing channel, it fails with permission_policy_requires_fresh_conversation before sending the prompt. Set fresh: true to create a conversation for that policy.","nullable":true},"prompt":{"description":"Optional first turn prompt. A launch may open with no prompt at all, but a prompt that is present must carry words: blank and whitespace-only text is refused with 422 invalid_prompt, before the launch reserves a sandbox.","type":"string"},"queue":{"description":"When a fresh start reaches the tenant or the fleet concurrency ceiling, wait in the bounded sandbox queue and return 202 with a SandboxRequest instead of 429 or 503 (ADR 0042). Starts carrying images or an explicit sandbox_id are never queued, and a full queue keeps the immediate error.","nullable":true,"type":"boolean"},"sandbox_api_access":{"description":"none omits the sandbox Fountain credential on provision and every wake. Requires a fresh ephemeral sandbox; unavailable on attach or policy-changing channel resume.","enum":["owner","none"],"type":"string"},"sandbox_id":{"description":"Attach the conversation to a sandbox you already have instead of provisioning one (ADR 0023). The sandbox must be yours (404 sandbox_not_found), ready or suspended (409 sandbox_not_attachable), and built from the same environment and vault as this launch (422 sandbox_identity_mismatch). It is normally the same agent's (422 sandbox_runtime_mismatch if the agent's runtime changed since). An agent other than the one the sandbox was built for may attach to that agent's persistent sandbox as a guest only to make a claude and codex pair: a codex agent on a claude agent's home, or a claude agent on a codex agent's, where no other agent of either runtime has run. Any other guest is 422 sandbox_identity_mismatch. Attaching a guest needs a full-scope API key; a sandbox's own token, or any key below full scope, is refused with 403 guest_attach_requires_full_scope (reason insufficient_scope), a channel rotation (fresh) of a guest included. The sandbox stays its own agent's, and a guest's reapply is refused with 409 rebuild_required: field shared_sandbox while another conversation is on the sandbox, field guest when the guest is alone there. A claude home created before guests were admitted refuses a codex guest with 409 codex_inference_conflict until it is reset. The conversation opens idle on that machine; a prompt here wakes it. Several conversations then run on one disk at once, except on opencode and gemini, where a second turn of the same runtime is refused with 409 sandbox_at_capacity while one runs.","format":"uuid","nullable":true,"type":"string"},"sandbox_mode":{"description":"Where this conversation runs (ADR 0023); null takes the agent's sandbox_mode. persistent lands on the agent identity's home — provisioning it if this is the first launch of that (agent, environment, vault), attaching to it otherwise, or 503 provisioning while the first launch is still building it. ephemeral provisions a sandbox for this conversation alone. Ignored when sandbox_id names a machine.","enum":["ephemeral","persistent"],"nullable":true,"type":"string"},"session_config":{"allOf":[{"$ref":"#/components/schemas/SessionConfig"}],"description":"ACP session config options for this conversation (ADR 0062), over the agent's, such as {\"reasoning_effort\": \"high\"}. Applied after the model on every turn. The ids and values are the adapter's own; one it does not advertise is skipped, and a value it refuses fails the turn."},"sprite_name":{"description":"Name the conversation's machine instead of taking a generated name. The value is the suffix of an account-scoped name: the server keeps the fountain-<account>- prefix every generated name carries, so a name you choose lands in your own namespace rather than another account's. 1 to 40 characters of letters, digits, - and _, starting with a letter or digit; a name that already carries this account's prefix is taken as it stands, so a name from an earlier launch resolves to the same machine. 422 invalid_sprite_name otherwise. Refused with 422 sprite_name_not_supported on an agent that runs on a self-hosted runner, where the name carries the runner instead. Refused with sandbox_api_access none, which requires a machine no other conversation can reach.","type":"string"},"title":{"description":"Optional display title. The team page names a teammate with it.","maxLength":120,"nullable":true,"type":"string"},"vault_id":{"description":"Optional vault whose secrets override the environment's baseline at sprite spawn. Must satisfy the agent's allowed_vault_ids when that allowlist is set.","format":"uuid","nullable":true,"type":"string"}},"required":["agent_id"],"title":"ConversationCreateRequest","type":"object"},"Secret":{"description":"A named secret. Values are write-only — the API never returns them.","properties":{"environment_id":{"format":"uuid","type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"key":{"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","key","environment_id"],"title":"Secret","type":"object"},"SupportReport":{"description":"A problem report a client filed, with the context it had and where it was forwarded.","properties":{"category":{"enum":["bug","stuck","question","idea","other"],"type":"string"},"client":{"nullable":true,"type":"string"},"context":{"additionalProperties":true,"type":"object"},"external_url":{"description":"The GitHub issue, when one was created.","nullable":true,"type":"string"},"forward_error":{"nullable":true,"type":"string"},"forwarded_at":{"format":"date-time","nullable":true,"type":"string"},"has_screenshot":{"type":"boolean"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"message":{"type":"string"},"screenshot_media_type":{"nullable":true,"type":"string"},"status":{"enum":["new","forwarded","failed"],"type":"string"}},"required":["id","category","message","status","inserted_at"],"title":"SupportReport","type":"object"},"BuzzIdentityListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/BuzzIdentity"},"type":"array"}},"required":["data"],"title":"BuzzIdentityListResponse","type":"object"},"SandboxDiffResponse":{"properties":{"data":{"$ref":"#/components/schemas/SandboxDiff"}},"required":["data"],"title":"SandboxDiffResponse","type":"object"},"BuzzIdentity":{"description":"A hosted Buzz agent: a Nostr identity (key held server-side in a vault) bound to a Fountain agent. Its harness runs on the gateway (ADR 0020).","properties":{"agent_id":{"format":"uuid","type":"string"},"display_name":{"nullable":true,"type":"string"},"enabled":{"type":"boolean"},"environment_id":{"description":"Environment this identity's conversations are provisioned from instead of the agent's own; null means the agent's.","format":"uuid","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"name":{"type":"string"},"pubkey":{"description":"Nostr public key (hex)","nullable":true,"type":"string"},"relay_url":{"description":"wss:// relay URL","type":"string"},"respond_to":{"description":"The harness's inbound author gate (buzz-acp --respond-to).","enum":["owner-only","allowlist","anyone","nobody"],"type":"string"},"respond_to_allowlist":{"description":"64-hex pubkeys admitted in allowlist mode.","items":{"type":"string"},"type":"array"},"sandbox_mode":{"description":"Where this identity's conversations run (ADR 0023): a sandbox per conversation, or the agent's one persistent machine. null means the agent's own default.","enum":["ephemeral","persistent"],"nullable":true,"type":"string"},"updated_at":{"format":"date-time","type":"string"},"vault_id":{"format":"uuid","type":"string"}},"required":["id","name","agent_id","vault_id","enabled"],"title":"BuzzIdentity","type":"object"},"EmailChangeRequest":{"properties":{"current_password":{"format":"password","type":"string"},"new_email":{"format":"email","type":"string"}},"required":["new_email","current_password"],"title":"EmailChangeRequest","type":"object"},"SessionConfig":{"additionalProperties":{"anyOf":[{"maxLength":200,"minLength":1,"type":"string"},{"type":"boolean"}]},"description":"ACP session config options to request (ADR 0062): a map of the adapter's option id to a value, a string or a boolean. The ids and values are the adapter's own and are not checked against a list: claude-agent-acp offers `effort` and `fast`, codex-acp `reasoning_effort` and `fast-mode`, and which exist depends on the model. They are applied after the model and before each prompt. An id the adapter does not advertise is skipped, and a `config` stage event reports it (`done`, outcome `skipped`). A value it refuses fails the turn. `model` is refused here; use the model field. `session_config_options` on the conversation lists what the adapter offers.","example":{"effort":"high","fast":true},"maxProperties":16,"nullable":true,"title":"SessionConfig","type":"object"},"VaultUpdate":{"properties":{"description":{"type":"string"},"metadata":{"additionalProperties":true,"type":"object"},"name":{"maxLength":200,"minLength":1,"type":"string"}},"title":"VaultUpdate","type":"object"},"AccountDeleteRequest":{"properties":{"confirm":{"description":"The account's own email address, exactly.","type":"string"}},"required":["confirm"],"title":"AccountDeleteRequest","type":"object"},"NegotiationError":{"properties":{"errors":{"properties":{"detail":{"type":"string"}},"required":["detail"],"type":"object"}},"required":["errors"],"title":"NegotiationError","type":"object"},"OAuthTokenResponse":{"properties":{"access_token":{"description":"A Fountain API key; use it as the bearer token.","type":"string"},"expires_in":{"description":"Seconds until the key expires.","type":"integer"},"token_type":{"example":"bearer","type":"string"}},"required":["access_token","token_type","expires_in"],"title":"OAuthTokenResponse","type":"object"},"StreamLogEvent":{"description":"One frame of the conversation, events or team SSE log stream (#2297). Same fields as `LogEvent` minus `id` — the frame's id travels in the SSE `id:` line, never the JSON body — plus `conversation_id` and `agent_id`, which the REST log feed never sends because its URL or list item already names the conversation.\n\nNot every stream sends every optional field here; see each property's own description for which of `GET /api/conversations/:id/stream`, `GET /api/events/stream` and `GET /api/team/stream` include it. The events and team streams also send `StreamSignal` frames on the same connection.","properties":{"agent_id":{"description":"The teammate whose conversation this is. Sent only on `GET /api/team/stream`, to route the event to a roster row.","format":"uuid","type":"string"},"blocks":{"description":"Only with `?blocks=true`: `data` parsed server-side into the blocks a transcript renders. Empty for non-output events.","items":{"$ref":"#/components/schemas/Block"},"type":"array"},"conversation_id":{"description":"Which conversation this event belongs to. Sent on `GET /api/events/stream` and `GET /api/team/stream`; not sent on `GET /api/conversations/:id/stream`, whose URL already names the conversation.","format":"uuid","type":"string"},"data":{"description":"Output text, or JSON-encoded metadata for stage events.","type":"string"},"duration_ms":{"description":"Sent on `GET /api/events/stream`. Not sent on `GET /api/conversations/:id/stream` or `GET /api/team/stream`.","nullable":true,"type":"integer"},"kind":{"enum":["output","stage"],"type":"string"},"stage":{"description":"Lifecycle stage name. null on an event that has no stage.","nullable":true,"type":"string"},"state":{"description":"Lifecycle state of the stage. null on an event that has no state.","enum":["started","done","failed","interrupted"],"nullable":true,"type":"string"},"stream":{"description":"`stdout` / `stderr` for output events; empty for stage events.","type":"string"},"ts":{"format":"date-time","type":"string"},"turn_id":{"format":"uuid","nullable":true,"type":"string"}},"required":["kind","ts"],"title":"StreamLogEvent","type":"object"},"PermissionPolicy":{"additionalProperties":{"oneOf":[{"enum":["auto_allow","ask","auto_deny"],"type":"string"},{"minimum":1,"type":"integer"}]},"description":"The wire shape of a permission policy: a map of key to verdict, plus the optional `ask_timeout` key, whose value is a number rather than a verdict. Every place a policy travels shares this shape and says there what a policy means at that door. Null everywhere a policy is absent.","nullable":true,"properties":{"ask_timeout":{"description":"Seconds a permission request that outlived its turn waits before it is denied (#1635). Names no tool, so it is the one key whose value is a number rather than a verdict, which is why the value schema below is a union. Absent leaves the global ask timeout. A request may shorten it with `_meta.fountain.timeout` on its own session/request_permission, and may not lengthen it. A launch may only shorten what the agent set, or the global ask timeout where the agent set nothing. Capped at a year, which is where the deadline stops fitting in a timestamp rather than a limit on how long a wait is useful.","maximum":31536000,"minimum":1,"type":"integer"}},"title":"PermissionPolicy","type":"object"},"OAuthClientUpdateRequest":{"example":{"name":"Notes for Fountain"},"properties":{"name":{"minLength":1,"type":"string"},"redirect_uris":{"items":{"type":"string"},"minItems":1,"type":"array"}},"title":"OAuthClientUpdateRequest","type":"object"},"ImageInput":{"description":"A base64-encoded image to attach to a prompt.","properties":{"data":{"description":"Base64-encoded image bytes.","type":"string"},"media_type":{"description":"MIME type of the image.","enum":["image/png","image/jpeg","image/gif","image/webp"],"type":"string"}},"required":["data","media_type"],"title":"ImageInput","type":"object"},"OAuthClientListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/OAuthClient"},"type":"array"}},"required":["data"],"title":"OAuthClientListResponse","type":"object"},"Repository":{"properties":{"mount_path":{"pattern":"^/","type":"string"},"ref":{"description":"Optional branch or tag to clone (`git clone -b`). Without it the default branch is cloned.","type":"string"},"secret_key":{"description":"Name of a secret — on the environment or on a vault attached at launch — holding a token to clone with. The clone uses it as HTTPS `x-access-token` auth. Required for a private repository; omit it for a public one.","type":"string"},"url":{"format":"uri","pattern":"^https://","type":"string"}},"required":["url","mount_path"],"title":"Repository","type":"object"},"AgentVersionListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/AgentVersion"},"type":"array"}},"required":["data"],"title":"AgentVersionListResponse","type":"object"},"ClaimableUserCreatedResponse":{"properties":{"data":{"$ref":"#/components/schemas/ClaimableUserCreated"}},"required":["data"],"title":"ClaimableUserCreatedResponse","type":"object"},"RunnerListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Runner"},"type":"array"}},"required":["data"],"title":"RunnerListResponse","type":"object"},"EnvironmentUpdate":{"properties":{"env_vars":{"additionalProperties":{"type":"string"},"type":"object"},"metadata":{"additionalProperties":true,"type":"object"},"name":{"maxLength":200,"minLength":1,"type":"string"},"networking_config":{"additionalProperties":true,"description":"Refines networking_type: limited. allowed_hosts is the only key honored today; unknown keys are ignored. Where the policy is enforced depends on the account: on a brokered account (`brokered: true` on GET /api/auth/me) the sandbox can reach only the egress broker, and under limited the broker refuses any host not in allowed_hosts with a 403 that names it, while a host with a bound credential needs no entry. On an unbrokered account the sandbox itself allows only the allowlisted domains. Either way, limited with no allowed_hosts (or an empty list) is a deny-all, not an allow-all.","properties":{"allowed_hosts":{"description":"Domains the sandbox may reach when networking_type is limited.","items":{"type":"string"},"type":"array"}},"type":"object"},"networking_type":{"enum":["unrestricted","limited"],"type":"string"},"packages":{"additionalProperties":true,"type":"object"},"repositories":{"items":{"$ref":"#/components/schemas/Repository"},"type":"array"},"setup_script":{"type":"string"},"setup_timeout_seconds":{"description":"Setup exec timeout in seconds; defaults to 120. The overall provisioning deadline still applies.","maximum":900,"minimum":1,"type":"integer"}},"title":"EnvironmentUpdate","type":"object"},"SandboxDiff":{"description":"`git diff` of a repository on a sandbox, redacted like a file.","properties":{"diff":{"description":"Unified diff, no colour.","type":"string"},"path":{"description":"The directory the diff was asked for, absolute.","type":"string"},"ref":{"description":"The revision diffed against, or null when the caller named none.","nullable":true,"type":"string"},"repo_root":{"description":"The repository's top-level directory.","type":"string"},"staged":{"description":"True when the index was diffed (`--cached`).","type":"boolean"},"truncated":{"description":"True when `diff` is not the whole diff: either it is longer than `max_bytes`, or redaction grew what was read past it. False means `diff` is everything.","type":"boolean"}},"required":["path","repo_root","staged","diff","truncated"],"title":"SandboxDiff","type":"object"},"VerifyEmailResponse":{"properties":{"email_verified":{"type":"boolean"},"message":{"type":"string"},"user_id":{"format":"uuid","type":"string"}},"required":["user_id","email_verified","message"],"title":"VerifyEmailResponse","type":"object"},"WebhookDeliveryListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/WebhookDelivery"},"type":"array"}},"required":["data"],"title":"WebhookDeliveryListResponse","type":"object"},"VaultSecret":{"description":"A named secret in a vault. Values are write-only — the API never returns them.","properties":{"expires_at":{"format":"date-time","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"key":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"vault_id":{"format":"uuid","type":"string"}},"required":["id","key","vault_id"],"title":"VaultSecret","type":"object"},"OnboardingResponse":{"properties":{"data":{"properties":{"completed":{"type":"boolean"},"completed_at":{"format":"date-time","nullable":true,"type":"string"}},"required":["completed"],"type":"object"}},"required":["data"],"title":"OnboardingResponse","type":"object"},"TeammateConversationListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/TeammateConversation"},"type":"array"}},"required":["data"],"title":"TeammateConversationListResponse","type":"object"},"PermissionAnswerRequest":{"properties":{"option_id":{"description":"One of the `optionId` values from the request's own `options` list, as carried on the `permission_request` block. An id the agent did not offer is refused (422 unknown_option) rather than forwarded.","type":"string"}},"required":["option_id"],"title":"PermissionAnswerRequest","type":"object"},"SandboxDetail":{"description":"A sandbox with the conversations on it.","properties":{"agent_id":{"description":"The agent the machine was built for. With environment_id and vault_id it is the identity a conversation must match to attach (sandbox_id on create); an agent of another runtime may attach to a persistent machine as a guest, which leaves this field unchanged.","format":"uuid","nullable":true,"type":"string"},"checkpoint":{"description":"The checkpoint Fountain took of this home the last time it parked (ADR 0023). It is scoped to this machine: it can roll the machine back, not rebuild a machine that is gone. Null for an ephemeral sandbox, a provider without checkpoints, or a home that has not parked yet.","nullable":true,"properties":{"at":{"format":"date-time","nullable":true,"type":"string"},"id":{"type":"string"}},"type":"object"},"conversations":{"description":"Every conversation ever opened on this machine, newest first.","items":{"$ref":"#/components/schemas/SandboxConversation"},"type":"array"},"environment_id":{"format":"uuid","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"last_resumed_at":{"format":"date-time","nullable":true,"type":"string"},"mode":{"description":"ephemeral: one conversation's machine, reclaimed with it. persistent: the agent identity's home, shared by its conversations and kept when one ends.","enum":["ephemeral","persistent"],"type":"string"},"provider":{"description":"The sandbox backend this row lives on.","enum":["sprites","e2b","daytona","runner"],"type":"string"},"runner":{"description":"For `provider: runner` — the user's own machine the sandbox lives on, and its directory there (#834). Null for hosted providers; the inner fields are null when the runner row was forgotten.","nullable":true,"properties":{"hostname":{"nullable":true,"type":"string"},"id":{"format":"uuid","nullable":true,"type":"string"},"name":{"nullable":true,"type":"string"},"online":{"description":"Whether the runner daemon is connected right now.","type":"boolean"},"path":{"description":"The sandbox directory on the machine (`<root>/<name>`).","nullable":true,"type":"string"}},"required":["online"],"type":"object"},"sprite_name":{"type":"string"},"status":{"enum":["pending","starting","ready","suspended","terminated","failed"],"type":"string"},"url":{"description":"The sandbox's own HTTP endpoint, where a service the agent starts is reachable. Null for providers that expose no such URL. The same value is available inside the sandbox as `SANDBOX_URL`.","nullable":true,"type":"string"},"vault_id":{"format":"uuid","nullable":true,"type":"string"}},"required":["id","sprite_name","status","conversations"],"title":"SandboxDetail","type":"object"},"AvatarRequest":{"description":"JSON form of an avatar upload. The raw-bytes form sends the image directly with an image content-type instead.","properties":{"data":{"description":"Base64-encoded image bytes.","type":"string"},"media_type":{"enum":["image/png","image/jpeg","image/gif","image/webp"],"type":"string"}},"required":["data","media_type"],"title":"AvatarRequest","type":"object"},"TeamScheduleCreateRequest":{"properties":{"cron":{"description":"Five fields, UTC. `@daily`-style names work; `@reboot` does not.","example":"0 9 * * 1-5","type":"string"},"enabled":{"default":true,"type":"boolean"},"name":{"maxLength":120,"nullable":true,"type":"string"},"one_off":{"default":false,"type":"boolean"},"prompt":{"maxLength":20000,"minLength":1,"type":"string"}},"required":["cron","prompt"],"title":"TeamScheduleCreateRequest","type":"object"},"ChatGPTLinkAttempt":{"description":"One device-code sign-in, for a new subscription or to reconnect one. Show `user_code` and `verification_url` to the person, then read the attempt again every `poll_interval` seconds until `state` leaves `pending`. The server does the polling of ChatGPT; reading this costs nothing upstream.","properties":{"auth_unreachable":{"description":"True while a pending attempt's last poll of ChatGPT's sign-in service went unanswered. The attempt is still open and Fountain asks again, less often.","type":"boolean"},"expires_at":{"format":"date-time","type":"string"},"failure":{"description":"Why a `failed` attempt failed. Null otherwise.","nullable":true,"properties":{"grant":{"nullable":true,"type":"string"},"grant_id":{"format":"uuid","nullable":true,"type":"string"},"reason":{"description":"`stale_grant`: the subscription was reconnected or disconnected after this attempt began, and what is there now was left alone. `account_already_linked`: the account holds this ChatGPT account already, as `grant`; reconnect that one instead. `linking_disabled`: linking was turned off for the account while this new link was open.","enum":["stale_grant","account_already_linked","grant_limit_reached","grant_not_found","name_taken","owner_ineligible","linking_disabled","tenant_key_unavailable","invalid_sign_in","authorization_failed","exchange_failed","internal_error"],"type":"string"}},"required":["reason","grant_id","grant"],"type":"object"},"grant_id":{"description":"The subscription being reconnected. Null for a new link.","format":"uuid","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"kind":{"enum":["link","reconnect"],"type":"string"},"name":{"description":"The name a new subscription will take. Null for a reconnect.","nullable":true,"type":"string"},"poll_interval":{"description":"Seconds to wait between reads of this attempt.","type":"integer"},"result_grant_id":{"description":"The subscription a completed attempt linked or reconnected.","format":"uuid","nullable":true,"type":"string"},"state":{"description":"`pending`, then exactly one of the other four, which are final.","enum":["pending","completed","cancelled","expired","failed"],"type":"string"},"updated_at":{"format":"date-time","type":"string"},"user_code":{"description":"The code to type at `verification_url`. Null once the attempt ends.","nullable":true,"type":"string"},"verification_url":{"nullable":true,"type":"string"}},"required":["id","kind","name","grant_id","state","user_code","verification_url","poll_interval","auth_unreachable","expires_at","result_grant_id","failure","inserted_at","updated_at"],"title":"ChatGPTLinkAttempt","type":"object"},"TurnListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Turn"},"type":"array"}},"required":["data"],"title":"TurnListResponse","type":"object"},"EgressEvent":{"description":"One outbound HTTP request the sandbox made through the egress broker (ADR 0019). The row is written when the request ends, so a streamed response is recorded when the stream finishes rather than when it starts.","properties":{"at":{"format":"date-time","nullable":true,"type":"string"},"credential_keys":{"items":{"type":"string"},"type":"array"},"error":{"description":"Why forwarding did not complete, such as `upstream_closed`, `credential_missing` or `request_timeout`. Null on a request that did.","nullable":true,"type":"string"},"host":{"description":"Host and port, as the sandbox dialed it.","type":"string"},"id":{"type":"integer"},"latency_ms":{"description":"How long the whole request took, request head through response body, not time to first byte.","nullable":true,"type":"integer"},"method":{"type":"string"},"path":{"description":"Always /[REDACTED]. URL paths, queries and fragments are withheld because they can contain credentials.","type":"string"},"service":{"description":"The binding that matched, and so which credential was attached. Null when none was: a passthrough host, or a host allowed under `limited` with no credential bound to it.","nullable":true,"type":"string"},"status":{"description":"The upstream status, or the broker's own refusal. Null where no answer arrived.","nullable":true,"type":"integer"}},"required":["id","method","host","path","credential_keys"],"title":"EgressEvent","type":"object"},"SandboxStatusResponse":{"properties":{"data":{"$ref":"#/components/schemas/SandboxStatus"}},"required":["data"],"title":"SandboxStatusResponse","type":"object"},"Export":{"description":"An account data export. Built asynchronously; the payload is fetched from the download endpoint, never embedded here.","properties":{"byte_size":{"description":"Uncompressed size.","nullable":true,"type":"integer"},"downloadable":{"description":"Completed and not yet expired — the only state the download serves.","type":"boolean"},"error":{"nullable":true,"type":"string"},"expires_at":{"format":"date-time","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"status":{"enum":["pending","completed","failed"],"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","status"],"title":"Export","type":"object"},"Sandbox":{"description":"One machine. Provisioned for a conversation; several conversations may run on it at once (ADR 0023).","properties":{"agent_id":{"description":"The agent the machine was built for. With environment_id and vault_id it is the identity a conversation must match to attach (sandbox_id on create); an agent of another runtime may attach to a persistent machine as a guest, which leaves this field unchanged.","format":"uuid","nullable":true,"type":"string"},"checkpoint":{"description":"The checkpoint Fountain took of this home the last time it parked (ADR 0023). It is scoped to this machine: it can roll the machine back, not rebuild a machine that is gone. Null for an ephemeral sandbox, a provider without checkpoints, or a home that has not parked yet.","nullable":true,"properties":{"at":{"format":"date-time","nullable":true,"type":"string"},"id":{"type":"string"}},"type":"object"},"environment_id":{"format":"uuid","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"mode":{"description":"ephemeral: one conversation's machine, reclaimed with it. persistent: the agent identity's home, shared by its conversations and kept when one ends.","enum":["ephemeral","persistent"],"type":"string"},"provider":{"description":"The sandbox backend this row lives on.","enum":["sprites","e2b","daytona","runner"],"type":"string"},"runner":{"description":"For `provider: runner` — the user's own machine the sandbox lives on, and its directory there (#834). Null for hosted providers; the inner fields are null when the runner row was forgotten.","nullable":true,"properties":{"hostname":{"nullable":true,"type":"string"},"id":{"format":"uuid","nullable":true,"type":"string"},"name":{"nullable":true,"type":"string"},"online":{"description":"Whether the runner daemon is connected right now.","type":"boolean"},"path":{"description":"The sandbox directory on the machine (`<root>/<name>`).","nullable":true,"type":"string"}},"required":["online"],"type":"object"},"sprite_name":{"type":"string"},"status":{"enum":["pending","starting","ready","suspended","terminated","failed"],"type":"string"},"url":{"description":"The sandbox's own HTTP endpoint, where a service the agent starts is reachable. Null for providers that expose no such URL. The same value is available inside the sandbox as `SANDBOX_URL`.","nullable":true,"type":"string"},"vault_id":{"format":"uuid","nullable":true,"type":"string"}},"required":["id","sprite_name","status"],"title":"Sandbox","type":"object"},"LogEventListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/LogEvent"},"type":"array"},"meta":{"properties":{"has_more":{"description":"More matching events exist past this page: newer ones for `asc`, older ones for `desc`.","type":"boolean"},"limit":{"type":"integer"},"next_cursor":{"description":"The page's last event id: pass as `after` (`asc`) or `before` (`desc`) to fetch the next page. null when the page is empty.","nullable":true,"type":"integer"}},"required":["limit","has_more"],"type":"object"},"page":{"description":"The window this page covers (#2531).","properties":{"newest_cursor":{"description":"The largest event id on the page; on the first `order=desc` page, the `Last-Event-ID` an SSE follow resumes from. null when the page is empty.","nullable":true,"type":"integer"},"oldest_cursor":{"description":"The smallest event id on the page. null when the page is empty.","nullable":true,"type":"integer"},"order":{"description":"The order of `data`, as requested.","enum":["asc","desc"],"type":"string"},"turn_split":{"description":"`whole_turns=true` reached its ceiling inside a turn: the page's oldest turn continues on the next page. Always false otherwise.","type":"boolean"}},"required":["order","oldest_cursor","newest_cursor","turn_split"],"type":"object"}},"required":["data","meta"],"title":"LogEventListResponse","type":"object"},"StreamSignal":{"description":"A change-signal frame on the events or team SSE stream (#2297) — not a log event, and no `conversation_id`/`agent_id`/anything else `StreamLogEvent` declares. The event name says what changed: `conversations` on `GET /api/events/stream` (the caller's conversation list changed — created, titled, read, deleted, finished), `team` on `GET /api/team/stream` (the roster changed) and `schedule` on `GET /api/team/stream` (a team schedule was created, updated, deleted or fired). The client re-lists rather than reading anything from the body.","properties":{"reason":{"enum":["changed"],"type":"string"}},"required":["reason"],"title":"StreamSignal","type":"object"},"PasswordChangeRequest":{"properties":{"current_password":{"format":"password","type":"string"},"new_password":{"format":"password","type":"string"}},"required":["current_password","new_password"],"title":"PasswordChangeRequest","type":"object"},"Error":{"description":"The one body every JSON error status carries (#2324). `error` is the code to branch on; the other keys accompany particular codes and are absent otherwise. A `406` is the exception: content negotiation fails before any controller runs and renders `NegotiationError` instead.","properties":{"active_sandboxes":{"description":"Sandboxes the account has in use, on `sandbox_quota_exceeded` (429).","type":"integer"},"attempt_id":{"description":"The sign-in already open on that subscription, on `chatgpt_link_attempt_pending` (409): read or cancel that one.","format":"uuid","type":"string"},"count":{"description":"How many the account has against `limit`, on `chatgpt_grant_limit_reached` and `chatgpt_link_attempts_exceeded` (409).","type":"integer"},"error":{"description":"The machine-readable code: `validation_failed`, `not_found`, `insufficient_credits`, `sandbox_quota_exceeded`, `expired`, `invalid_token` and the rest. On the key-authentication and scope refusals it is a sentence and `reason` carries the code.","example":"validation_failed","type":"string"},"errors":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"description":"Field validation messages keyed by field, beside `error: \"validation_failed\"`, whether the request died at the OpenAPI cast or in a changeset (#1431).","type":"object"},"grant":{"description":"That subscription's name, on `chatgpt_grant_unusable` (409).","nullable":true,"type":"string"},"grant_id":{"description":"The ChatGPT subscription the refusal is about, on `chatgpt_grant_unusable` (409).","format":"uuid","type":"string"},"limit":{"description":"The account's concurrent-sandbox cap, on `sandbox_quota_exceeded` (429); how many ChatGPT subscriptions it may hold, on `chatgpt_grant_limit_reached` (409); how many sign-ins it may have open, on `chatgpt_link_attempts_exceeded` (409); how many it may start in an hour, on `chatgpt_link_attempts_rate_limited` (429).","type":"integer"},"message":{"description":"A sentence for a human, when there is one.","type":"string"},"reason":{"description":"A second stable word. On the 401 and 403 refusals from key authentication and scope checks, `error` is prose and this is the code (`api_key_invalid`, `api_key_expired`, `insufficient_scope`). On `broker_unavailable`, `sandbox_not_resettable` and `credential_set_is_default`, `error` is the code and this narrows it (`econnrefused`, `timeout`, `is_default`, ...). On `chatgpt_grant_unusable` it says why the named subscription cannot serve: `disconnected`, `revoked`, `expired`, `reconnect_required`, `exhausted`, `not_found`, `broker_required` or `owner_ineligible`.","type":"string"},"retry_after_seconds":{"description":"Seconds until another sign-in may be started, on `chatgpt_link_attempts_rate_limited` (429); the `Retry-After` header says the same.","type":"integer"},"sets":{"description":"The credential sets that still name the subscription, by name, on `chatgpt_grant_named_by_sets` (409): point them elsewhere first.","items":{"type":"string"},"type":"array"},"state":{"description":"What the attempt had already become, on `chatgpt_link_attempt_not_pending` (409).","type":"string"},"until":{"description":"When the subscription's Codex usage resets, on `chatgpt_grant_unusable` with `reason: \"exhausted\"`; null otherwise.","format":"date-time","nullable":true,"type":"string"},"upgrade_url":{"description":"Where to buy credit, on `insufficient_credits` (402).","type":"string"}},"required":["error"],"title":"Error","type":"object"},"SandboxConversation":{"description":"A conversation on a sandbox, as the sandbox lists it.","properties":{"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"mid_turn":{"description":"True while this conversation is running a turn on the machine.","type":"boolean"},"runtime":{"enum":["claude","codex","gemini","opencode","acp"],"type":"string"},"status":{"enum":["pending","running","idle","failed","terminated"],"type":"string"},"title":{"nullable":true,"type":"string"}},"required":["id","status","mid_turn"],"title":"SandboxConversation","type":"object"},"ChatGPTSubscriptionListResponse":{"description":"Every subscription the account holds, by name. Not paginated: it is capped.","properties":{"count":{"description":"How many the account holds. A disconnected one counts until removed.","type":"integer"},"data":{"items":{"$ref":"#/components/schemas/ChatGPTSubscription"},"type":"array"},"limit":{"description":"How many it may hold.","type":"integer"},"linking_enabled":{"description":"Whether this account may link a new subscription now. False leaves every other operation here working.","type":"boolean"}},"required":["data","count","limit","linking_enabled"],"title":"ChatGPTSubscriptionListResponse","type":"object"},"AdminCompRequest":{"properties":{"comped":{"type":"boolean"}},"required":["comped"],"title":"AdminCompRequest","type":"object"},"ReadinessResponse":{"properties":{"checks":{"additionalProperties":{"enum":["ok","error"],"type":"string"},"description":"Per-dependency result. `ok` or `error`, with no further detail.","example":{"broker_listener":"ok","database":"ok"},"type":"object"},"status":{"enum":["ok","error"],"example":"ok","type":"string"}},"required":["status","checks"],"title":"ReadinessResponse","type":"object"},"InferenceCredentialStatus":{"description":"Whether a provider credential is set for the tenant. Values are write-only — the API never returns a credential, truncated or otherwise.","properties":{"provider":{"enum":["anthropic_api_key","claude_code_oauth_token","openai_api_key","gemini_api_key"],"type":"string"},"set":{"type":"boolean"}},"required":["provider","set"],"title":"InferenceCredentialStatus","type":"object"},"InferenceCredentialSet":{"description":"One named set of a tenant's inference credentials. An account holds one or more and exactly one is the default; an agent or a launch may name another. Values are never returned — `providers` reports only which of them this set holds.","properties":{"chatgpt_grant":{"description":"The named subscription's name and status, so a set that will fail says so here. Read-only; the whole subscription is at `/api/account/chatgpt-subscriptions`.","nullable":true,"properties":{"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"status":{"enum":["active","revoked","expired","disconnected"],"type":"string"}},"required":["id","name","status"],"type":"object"},"chatgpt_grant_id":{"description":"The ChatGPT subscription this set's codex runs use, or null. A named subscription is used or the run fails with 409 `chatgpt_grant_unusable`: nothing falls back to the set's `openai_api_key`, to another subscription or to the platform. Every other OpenAI consumer still needs the key.","format":"uuid","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"is_default":{"description":"The set every surface reads unless something names another. Exactly one per account, and it cannot be deleted.","type":"boolean"},"name":{"type":"string"},"providers":{"description":"The credentials this set holds, by name. Never the values.","items":{"enum":["anthropic_api_key","claude_code_oauth_token","openai_api_key","gemini_api_key"],"type":"string"},"type":"array"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","is_default","providers","chatgpt_grant_id","chatgpt_grant","inserted_at","updated_at"],"title":"InferenceCredentialSet","type":"object"},"WebhookEndpointCreateRequest":{"properties":{"description":{"maxLength":500,"nullable":true,"type":"string"},"event_types":{"description":"Defaults to conversation.turn.done, conversation.turn.failed and conversation.provision.failed when absent.","example":["conversation.turn.done"],"items":{"type":"string"},"type":"array"},"url":{"description":"https:// only, unless the instance permits http. Loopback, link-local (including the cloud metadata address) and RFC1918 targets are refused, at request time as well as here.","example":"https://example.com/hooks/fountain","type":"string"}},"required":["url"],"title":"WebhookEndpointCreateRequest","type":"object"},"VaultSecretResponse":{"properties":{"data":{"$ref":"#/components/schemas/VaultSecret"}},"required":["data"],"title":"VaultSecretResponse","type":"object"},"WebhookEndpointListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/WebhookEndpoint"},"type":"array"}},"required":["data"],"title":"WebhookEndpointListResponse","type":"object"},"WebhookEndpointResponse":{"properties":{"data":{"$ref":"#/components/schemas/WebhookEndpoint"}},"required":["data"],"title":"WebhookEndpointResponse","type":"object"},"TurnUsage":{"description":"The turn's token usage as the runtime reported it when the turn ended (the ACP `session/prompt` response's `usage`). The cache fields appear only when the runtime reports them. Accounting is absent for unqualified or historical reports. A metadata-only report has no measured token counts; missing counts are not zero.","properties":{"accounting":{"$ref":"#/components/schemas/UsageAccounting"},"cache_read":{"minimum":0,"nullable":true,"type":"integer"},"cache_write":{"minimum":0,"nullable":true,"type":"integer"},"input":{"minimum":0,"type":"integer"},"output":{"minimum":0,"type":"integer"}},"title":"TurnUsage","type":"object"},"CatalogResponse":{"properties":{"data":{"properties":{"apps":{"description":"Where this instance sends a human to watch a conversation or message a teammate. Null for an app this deployment does not have.","properties":{"conversations":{"nullable":true,"type":"string"},"team":{"nullable":true,"type":"string"}},"required":["conversations","team"],"type":"object"},"first_request":{"description":"The one onboarding request this deployment hands out (ADR 0038), the same text the verified landing and the manual print. The base URL is already in it. The caller's key and agent are not: the server stores only a hash of a key, so a client substitutes the key it holds and an agent from `GET /api/agents`.","properties":{"curl":{"description":"The `curl`, with placeholders.","type":"string"},"placeholders":{"description":"Every token a client must substitute before it runs the request.","items":{"type":"string"},"type":"array"},"prompt":{"description":"The prompt both snippets send, for a client that runs it.","type":"string"},"typescript":{"description":"The TypeScript SDK equivalent, with placeholders.","type":"string"}},"required":["curl","typescript","prompt","placeholders"],"type":"object"},"mcp_servers":{"description":"Remote MCP servers verified to complete the MCP authorization discovery chain, each with the date it was last verified. Suggestions, not an allowlist — any URL can be discovered.","items":{"properties":{"dcr":{"description":"Whether the authorization server offers dynamic client registration (RFC 7591). False means the tenant pastes a client id from their own app registration.","type":"boolean"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"verified_on":{"format":"date","type":"string"}},"required":["slug","name","url","dcr","verified_on"],"type":"object"},"type":"array"},"model_providers":{"items":{"type":"string"},"type":"array"},"models":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"description":"Suggested `provider/model` ids per runtime. Suggestions, not an allowlist.","type":"object"},"package_managers":{"description":"The managers provisioning installs from an environment's `packages`.","items":{"type":"string"},"type":"array"},"runtimes":{"items":{"type":"string"},"type":"array"},"sandbox_api_access":{"items":{"enum":["owner","none"],"type":"string"},"type":"array"},"sandbox_providers":{"properties":{"default":{"type":"string"},"enabled":{"items":{"type":"string"},"type":"array"}},"required":["enabled","default"],"type":"object"}},"required":["runtimes","models","sandbox_providers","package_managers","apps","first_request"],"type":"object"}},"required":["data"],"title":"CatalogResponse","type":"object"},"WebhookEndpoint":{"description":"A URL of yours that Fountain POSTs lifecycle events to. The signing secret is returned only by create and rotate, and never appears here.","properties":{"consecutive_failures":{"description":"Events in a row that exhausted their retries. Any accepted delivery clears it.","type":"integer"},"description":{"maxLength":500,"nullable":true,"type":"string"},"disabled_at":{"format":"date-time","nullable":true,"type":"string"},"disabled_reason":{"nullable":true,"type":"string"},"event_types":{"description":"What this endpoint is subscribed to. An exact type (`conversation.turn.done`), one stage (`conversation.turn.*`), or `*` for everything. `GET /api/catalog` is not the source for these; the docs page is.","example":["conversation.turn.done","conversation.turn.failed"],"items":{"type":"string"},"type":"array"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"status":{"description":"`disabled` means Fountain stopped delivering, either because you switched it off or because deliveries failed for long enough.","enum":["active","disabled"],"type":"string"},"updated_at":{"format":"date-time","type":"string"},"url":{"example":"https://example.com/hooks/fountain","type":"string"}},"required":["id","url","event_types","status"],"title":"WebhookEndpoint","type":"object"},"Teammate":{"description":"One agent on the team: the agent, its current team conversation (the newest live one, else the newest finished one), and what the roster shows for it.","properties":{"agent":{"$ref":"#/components/schemas/Agent"},"agent_id":{"format":"uuid","type":"string"},"conversation":{"$ref":"#/components/schemas/Conversation"},"last_turn":{"nullable":true,"properties":{"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"prompt":{"type":"string"},"status":{"type":"string"},"turn_number":{"type":"integer"},"usage":{"anyOf":[{"allOf":[{"$ref":"#/components/schemas/TurnUsage"}]},{"enum":[null],"nullable":true}],"nullable":true}},"type":"object"},"name":{"description":"What the teammate is called: the conversation's title, else the agent's name.","type":"string"},"presence":{"properties":{"label":{"type":"string"},"state":{"enum":["working","starting","online","asleep","away","machine_offline","failed","offline"],"type":"string"}},"required":["state","label"],"type":"object"},"preview":{"description":"The roster line: `you` (the last prompt, no reply yet), `them` (the last reply), or `typing` (a turn is in flight). Null with no messages.","nullable":true,"properties":{"kind":{"enum":["you","them","typing"],"type":"string"},"text":{"nullable":true,"type":"string"}},"type":"object"},"unread":{"type":"boolean"},"usage_total":{"allOf":[{"$ref":"#/components/schemas/UsageTotal"}],"description":"Summed over every conversation this agent has had under the team channel, not just the current one — the per-teammate figure."}},"required":["agent_id","name","agent","conversation","presence","unread"],"title":"Teammate","type":"object"},"EnvironmentResponse":{"properties":{"data":{"$ref":"#/components/schemas/Environment"}},"required":["data"],"title":"EnvironmentResponse","type":"object"},"ClaimedPrincipalResponse":{"properties":{"data":{"$ref":"#/components/schemas/ClaimedPrincipal"}},"required":["data"],"title":"ClaimedPrincipalResponse","type":"object"},"ConversationLabelsRequest":{"description":"Labels to merge into a conversation. A key not named is left alone; a key whose value is null is removed.","properties":{"labels":{"additionalProperties":{"nullable":true,"type":"string"},"description":"The pairs to merge. null removes a key. At most 32 entries survive the merge; a key is at most 64 bytes and a value at most 256 bytes. A 422 names the offending key under `errors.labels`.","type":"object"}},"required":["labels"],"title":"ConversationLabelsRequest","type":"object"},"TokenRequest":{"description":"A token lifted out of an emailed link.","properties":{"token":{"type":"string"}},"required":["token"],"title":"TokenRequest","type":"object"},"SandboxStatus":{"description":"`git status` of a repository on a sandbox, one entry per changed path.","properties":{"branch":{"description":"The checked-out branch, or null on a detached HEAD.","nullable":true,"type":"string"},"entries":{"items":{"$ref":"#/components/schemas/SandboxChange"},"type":"array"},"path":{"description":"The directory the status was asked for, absolute.","type":"string"},"repo_root":{"description":"The repository's top-level directory.","type":"string"},"truncated":{"description":"True when the repository holds more changes than were returned.","type":"boolean"},"untracked":{"description":"What was asked about untracked paths.","enum":["normal","all","no"],"type":"string"}},"required":["path","repo_root","untracked","entries","truncated"],"title":"SandboxStatus","type":"object"},"InferenceCredentialSetListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/InferenceCredentialSet"},"type":"array"}},"required":["data"],"title":"InferenceCredentialSetListResponse","type":"object"},"AdminCreditsRequest":{"example":{"cents":1000,"note":"won dispute dp_123"},"properties":{"cents":{"description":"Credit to add, in cents. Never expires and is spent last.","minimum":1,"type":"integer"},"note":{"description":"Why, for the audit trail. A won dispute, a goodwill credit, an outage.","nullable":true,"type":"string"}},"required":["cents"],"title":"AdminCreditsRequest","type":"object"},"TeamAddRequest":{"properties":{"agent_id":{"format":"uuid","type":"string"},"environment_id":{"description":"Provision the teammate's computer from this environment instead of the agent's own. Must satisfy the agent's allowed_environment_ids.","format":"uuid","nullable":true,"type":"string"},"name":{"description":"What to call the teammate. Blank means the agent's name.","maxLength":120,"nullable":true,"type":"string"},"vault_id":{"description":"Layer this vault's secrets on top. Must satisfy allowed_vault_ids.","format":"uuid","nullable":true,"type":"string"}},"required":["agent_id"],"title":"TeamAddRequest","type":"object"},"TeammateConversation":{"allOf":[{"$ref":"#/components/schemas/Conversation"},{"properties":{"current":{"type":"boolean"}},"required":["current"],"type":"object"}],"description":"A conversation object plus `current`: whether it is the teammate's live one.","title":"TeammateConversation"},"EmailChangedResponse":{"properties":{"email":{"description":"The new address.","format":"email","type":"string"},"message":{"type":"string"}},"required":["email","message"],"title":"EmailChangedResponse","type":"object"},"SandboxListingResponse":{"properties":{"data":{"$ref":"#/components/schemas/SandboxListing"}},"required":["data"],"title":"SandboxListingResponse","type":"object"},"SandboxListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/SandboxDetail"},"type":"array"}},"required":["data"],"title":"SandboxListResponse","type":"object"},"ConnectionListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Connection"},"type":"array"}},"required":["data"],"title":"ConnectionListResponse","type":"object"},"InferenceCredentialListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/InferenceCredentialStatus"},"type":"array"}},"required":["data"],"title":"InferenceCredentialListResponse","type":"object"},"TeamMessageRequest":{"properties":{"images":{"items":{"$ref":"#/components/schemas/ImageInput"},"nullable":true,"type":"array"},"labels":{"additionalProperties":{"nullable":true,"type":"string"},"description":"Labels to merge into the conversation this message lands on, whether that is the teammate's current one or the fresh one a retired thread is replaced by. Same limits as everywhere else; null removes a key.","nullable":true,"type":"object"},"prompt":{"type":"string"}},"required":["prompt"],"title":"TeamMessageRequest","type":"object"},"SecretBindingPreset":{"description":"A known service from the broker's catalog, to prefill a binding from.","properties":{"auth_type":{"type":"string"},"description":{"nullable":true,"type":"string"},"header":{"nullable":true,"type":"string"},"headers":{"additionalProperties":{"type":"string"},"type":"object"},"host":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"prefix":{"nullable":true,"type":"string"},"suggested_key":{"nullable":true,"type":"string"},"usable":{"description":"False for the few presets a binding cannot express on its own (request signing, a username of yours).","type":"boolean"}},"required":["id","name","host","auth_type","usable"],"title":"SecretBindingPreset","type":"object"},"AdminSandbox":{"description":"A live sandbox, cross-tenant. Metadata only — never contents.","properties":{"conversation_count":{"type":"integer"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"provider":{"description":"Sandbox backend that owns this row","type":"string"},"sprite_name":{"type":"string"},"status":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"user_email":{"nullable":true,"type":"string"},"user_id":{"format":"uuid","nullable":true,"type":"string"}},"required":["id","status"],"title":"AdminSandbox","type":"object"},"SecretBindingListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/SecretBinding"},"type":"array"}},"required":["data"],"title":"SecretBindingListResponse","type":"object"},"ChatGPTSubscriptionResponse":{"properties":{"data":{"$ref":"#/components/schemas/ChatGPTSubscription"}},"required":["data"],"title":"ChatGPTSubscriptionResponse","type":"object"},"AgentVersion":{"description":"One immutable snapshot of an agent's config (ADR 0029), written on create and on every update that changes a config field. `config` holds the full values, keyed by the agent fields `Agent.changeset/2` casts (everything but ownership and the avatar). Versions are read-only over the API; rollback is a console action.","properties":{"agent_id":{"format":"uuid","type":"string"},"config":{"additionalProperties":true,"type":"object"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"version":{"description":"1-based, monotonic per agent.","type":"integer"}},"required":["id","agent_id","version","config","inserted_at"],"title":"AgentVersion","type":"object"},"Agent":{"description":"An AI agent definition: runtime, model, skills, MCP, env.","properties":{"acp":{"description":"Whether this agent's runtime speaks the Agent Client Protocol. Derived from the runtime, never stored. When false, the conversation's output is the runtime's own dialect on the `stdout` stream rather than ACP `session/update` notifications on the `acp` stream, and a protocol client such as `fountain acp` cannot render it.","readOnly":true,"type":"boolean"},"allowed_environment_ids":{"description":"Environments a conversation may launch this agent under instead of its own (environment_id on create). Same shape as allowed_vault_ids: null (default) allows any environment the tenant owns; an empty list forbids overriding; a non-empty list is an allowlist. The agent's own environment always passes.","items":{"format":"uuid","type":"string"},"nullable":true,"type":"array"},"allowed_inference_credential_ids":{"description":"Credential sets a conversation may launch this agent on instead of the agent's (inference_credential_id on create). Same shape as allowed_vault_ids: null (default) allows any set the tenant owns; an empty list forbids overriding; a non-empty list is an allowlist. The agent's own set always passes.","items":{"format":"uuid","type":"string"},"nullable":true,"type":"array"},"allowed_vault_ids":{"description":"Vaults a conversation may attach to this agent. null (default) allows any vault the tenant owns; an empty list forbids attaching any vault; a non-empty list is an allowlist. Vault values override the agent's environment on key collision, so this scopes who can override reviewed config.","items":{"format":"uuid","type":"string"},"nullable":true,"type":"array"},"avatar_media_type":{"description":"Set when the agent has an avatar; fetch the bytes at `GET /api/agents/:id/avatar`. Null means no avatar.","enum":["image/png","image/jpeg","image/gif","image/webp"],"nullable":true,"type":"string"},"conversation_count":{"description":"Conversations started from this agent.","type":"integer"},"description":{"type":"string"},"environment_id":{"format":"uuid","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inference_credential_id":{"description":"The credential set this agent's conversations run on. null (default) is the account's default set, which is what every agent had before an account could hold more than one.","format":"uuid","nullable":true,"type":"string"},"inserted_at":{"format":"date-time","type":"string"},"mcp_servers":{"additionalProperties":true,"type":"object"},"metadata":{"additionalProperties":true,"type":"object"},"model":{"description":"Canonical provider/model_id (e.g. anthropic/claude-sonnet-5). The provider must match the runtime — anthropic for claude, openai for codex, google for gemini; opencode accepts any of the three. Other providers are rejected: Fountain has no credentials to export for them. The model id is not checked against a list, so a newly released model works without a Fountain release. Null on the acp runtime, which resolves no inference credential and reads no model.","nullable":true,"pattern":"^[a-z0-9_-]+/[a-z0-9._-]+$","type":"string"},"name":{"type":"string"},"permission_policy":{"allOf":[{"$ref":"#/components/schemas/PermissionPolicy"}],"description":"Per-tool permission policy: a map of key to verdict, plus an optional \"default\" key. A key is matched against the tool card's title first and then ACP's kind (execute, edit, read, fetch, …); prefer a kind, because claude titles a tool call with the command it is about to run. Unset keys fall back to the default, and an unset default is auto_allow — today's behaviour. \"ask\" holds the tool until a human answers it on the conversation stream, and denies if nobody does before the timeout. A runtime that never asks (opencode) refuses anything stricter than auto_allow with 422 permission_policy_unenforceable.","nullable":true},"runtime":{"enum":["claude","codex","gemini","opencode","acp"],"type":"string"},"runtime_command":{"description":"The command the acp runtime launches inside the sandbox, as a shell line resolved there (for example `chant acp`). Required when runtime is acp, and rejected on every other runtime, which resolves its own executable. A free string by design: it runs under the same isolation as an environment's setup script.","nullable":true,"type":"string"},"sandbox_mode":{"description":"Where a conversation of this agent runs by default (ADR 0023). ephemeral: a sandbox per conversation, reclaimed with it. persistent: one sandbox per agent identity (agent, environment, vault) — the agent's computer — that every conversation of that identity lands on and shares; it survives a conversation ending and is parked, not destroyed, at the ceiling. A launch may name the other with sandbox_mode on POST /api/conversations.","enum":["ephemeral","persistent"],"type":"string"},"sandbox_provider":{"description":"Sandbox backend override; null inherits the instance default (SANDBOX_PROVIDER). Only providers configured on this instance are accepted","enum":["sprites","e2b","daytona","runner"],"nullable":true,"type":"string"},"session_config":{"allOf":[{"$ref":"#/components/schemas/SessionConfig"}],"description":"ACP session config options every conversation of this agent requests (ADR 0062), such as {\"effort\": \"high\"}. A conversation's own session_config overrides these keys, and a prompt's overrides both for that turn. Null or {} requests none."},"skills":{"description":"Each entry is either inline (`{name, content}` — full SKILL.md text written to the sprite) or github (`{source, ref?, name?}` — installed on the sprite via the skills.sh CLI, optionally pinned to a tag/branch/sha via `ref`). Exactly one of `content` or `source` must be set on each entry.","items":{"properties":{"content":{"description":"Full SKILL.md body for inline entries.","type":"string"},"name":{"description":"Skill name (required for inline entries).","type":"string"},"ref":{"description":"Optional tag, branch, or sha pinning a github-sourced skill (installed as `owner/repo@ref`). Without it the default branch is fetched at spawn time.","pattern":"^[A-Za-z0-9._/-]+$","type":"string"},"source":{"description":"GitHub `owner/repo` for skills.sh-sourced entries.","pattern":"^[A-Za-z0-9._/-]+$","type":"string"}},"type":"object"},"type":"array"},"system":{"description":"System prompt.","type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","model","runtime"],"title":"Agent","type":"object"},"AdminSandboxLimitRequest":{"properties":{"limit":{"description":"Override of the balance-funded concurrent-sandbox cap. Null clears it, handing the cap back to the balance rule.","minimum":0,"nullable":true,"type":"integer"}},"required":["limit"],"title":"AdminSandboxLimitRequest","type":"object"},"BuzzIdentityResponse":{"properties":{"data":{"$ref":"#/components/schemas/BuzzIdentity"}},"required":["data"],"title":"BuzzIdentityResponse","type":"object"},"ApplyResult":{"properties":{"action":{"description":"`unchanged` means the record already matched the document, so nothing was written to it and no audit event was recorded. Inline `spec.secrets` are re-encrypted on every apply and still report `upserted` under `secrets`.","enum":["created","updated","unchanged","error"],"type":"string"},"errors":{"additionalProperties":true,"nullable":true,"type":"object"},"kind":{"type":"string"},"name":{"type":"string"},"secret":{"description":"A Webhook endpoint's HMAC-SHA256 signing secret, on the apply that created it. Store it; it is not shown again, and an update never returns it. Null on every other row.","example":"whsec_Zm91bnRhaW4tZXhhbXBsZS1zZWNyZXQtdmFsdWU","nullable":true,"type":"string"},"secrets":{"items":{"$ref":"#/components/schemas/ApplySecretResult"},"type":"array"}},"required":["kind","name","action"],"title":"ApplyResult","type":"object"},"AuthMeResponse":{"description":"Identity of the account the bearer token belongs to.","properties":{"brokered":{"description":"Whether this account's conversations run behind the egress credential broker (ADR 0019): secrets with bindings stay at the broker, a limited environment is enforced there, and /api/conversations/:id/egress has content. Connections availability is separate. Read-only; an operator sets it.","type":"boolean"},"chatgpt_subscriptions_enabled":{"description":"Whether this account may link a new ChatGPT subscription. Listing, renaming, reconnecting, disconnecting and removing the ones it holds never depend on it.","type":"boolean"},"comped":{"description":"Null when billing is off.","nullable":true,"type":"boolean"},"connections_enabled":{"description":"Whether this account may add connections, providers, or credential bindings. Use connections_manageable to show existing credentials and their removal controls.","type":"boolean"},"connections_manageable":{"description":"Whether this account may list, revoke, and delete existing connections, providers, and credential bindings. Remains true when the connections rollout flag is off but the deployment still brokers credentials.","type":"boolean"},"email":{"format":"email","type":"string"},"email_verified":{"type":"boolean"},"expires_at":{"format":"date-time","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"onboarding_completed":{"type":"boolean"},"role":{"enum":["user","admin"],"type":"string"}},"required":["id","email","role","email_verified"],"title":"AuthMeResponse","type":"object"},"SupportReportCreateRequest":{"properties":{"category":{"enum":["bug","stuck","question","idea","other"],"type":"string"},"client":{"example":"fountain-team 2026-08-19 a1db945","maxLength":200,"nullable":true,"type":"string"},"context":{"additionalProperties":true,"description":"What the client knew: conversation_id, agent_id/agent_name/runtime/model, sandbox, presence, recent events, url, app version. 64 KB max. Never secrets.","nullable":true,"type":"object"},"message":{"maxLength":20000,"minLength":1,"type":"string"},"screenshot":{"nullable":true,"properties":{"data":{"description":"base64","type":"string"},"media_type":{"enum":["image/png","image/jpeg","image/gif","image/webp"],"type":"string"}},"required":["data","media_type"],"type":"object"}},"required":["category","message"],"title":"SupportReportCreateRequest","type":"object"},"WebhookTestResponse":{"description":"The test event was queued. Its delivery shows up in `GET /api/webhooks/{id}/deliveries` once the worker has run.","properties":{"event_type":{"example":"webhook.test","type":"string"},"queued":{"type":"boolean"}},"required":["queued","event_type"],"title":"WebhookTestResponse","type":"object"},"Conversation":{"description":"One chat with one agent inside one sandbox.","properties":{"acp":{"description":"Whether this conversation's runtime speaks the Agent Client Protocol. When true its output is stored as ACP `session/update` notifications on the `acp` event stream, which is what a protocol client replays; when false the output is the runtime's own dialect on `stdout`.","readOnly":true,"type":"boolean"},"agent_id":{"format":"uuid","nullable":true,"type":"string"},"agent_version":{"description":"The version number behind agent_version_id, resolved on the list and get endpoints; null elsewhere and wherever agent_version_id is null.","nullable":true,"readOnly":true,"type":"integer"},"agent_version_id":{"description":"The agent config version this conversation launched under (ADR 0029): provenance only, the live agent still drives the sandbox. Null for a conversation that predates versioning. Resolve it at GET /api/agents/{agent_id}/versions/{agent_version}.","format":"uuid","nullable":true,"type":"string"},"channel_id":{"description":"The external channel key this conversation is bound to, if it was created with one.","nullable":true,"type":"string"},"environment_id":{"description":"Per-launch environment override; null means the agent's environment.","format":"uuid","nullable":true,"type":"string"},"first_prompt":{"description":"The first turn's prompt — what to title an untitled conversation with. Null until the first turn exists.","nullable":true,"readOnly":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"labels":{"additionalProperties":{"type":"string"},"description":"Free-form key/value strings on the conversation. A program stamps its own run with them (`env=prod`, `drift=true`) and `GET /api/conversations?label=env:prod` filters on them. At most 32 entries; a key is at most 64 bytes and a value at most 256 bytes. Always an object, empty when nothing set one.","type":"object"},"last_active_at":{"description":"Most recent runtime output, falling back to creation time. Stage events (reconnects, sandbox lifecycle) do not count.","format":"date-time","nullable":true,"type":"string"},"last_read_at":{"description":"Set by `POST /api/conversations/:id/read`. Null if never read.","format":"date-time","nullable":true,"type":"string"},"model":{"description":"This conversation's model override (ADR 0061), set at launch or by reapply; null means it runs the agent's model. Each turn's model_selection records the model that turn actually asked for.","nullable":true,"type":"string"},"parent_conversation_id":{"format":"uuid","nullable":true,"type":"string"},"pending_requests":{"description":"Permission requests that outlived a turn and are still waiting for an answer (#1635). Only GET /api/conversations/{id} can report one; every other response carrying this schema, including the list and the create response, sends an empty array rather than querying for it.","items":{"$ref":"#/components/schemas/PendingPermissionRequest"},"type":"array"},"permission_policy":{"allOf":[{"$ref":"#/components/schemas/PermissionPolicy"}],"description":"The per-launch permission override this conversation was started with, or null if it had none. The policy actually in force is this merged with the agent's, taking the stricter of the two per tool.","nullable":true},"runtime":{"enum":["claude","codex","gemini","opencode","acp"],"type":"string"},"runtime_session_id":{"nullable":true,"type":"string"},"sandbox":{"anyOf":[{"allOf":[{"$ref":"#/components/schemas/Sandbox"}]},{"enum":[null],"nullable":true}],"nullable":true},"sandbox_api_access":{"description":"Immutable sandbox callback credential policy. none never issues a callback token.","enum":["owner","none"],"type":"string"},"sandbox_id":{"format":"uuid","nullable":true,"type":"string"},"session_config":{"allOf":[{"$ref":"#/components/schemas/SessionConfig"}],"description":"This conversation's ACP session config options (ADR 0062), over its agent's: a key here replaces the agent's value for that key. Set at launch or by reapply. {} follows the agent."},"session_config_options":{"description":"The config options the adapter advertised before the latest prompt, after the model and the requested options were applied: what the current model offers and what is in force. Null until a turn reports them.","items":{"$ref":"#/components/schemas/SessionConfigOption"},"nullable":true,"type":"array"},"source":{"enum":["ui","api","agent"],"type":"string"},"status":{"enum":["pending","running","idle","failed","terminated"],"type":"string"},"title":{"description":"Optional display title, set explicitly or supplied by the harness over ACP. Harness titles may be revised or cleared; null when no title is set.","nullable":true,"type":"string"},"turn_count":{"type":"integer"},"unread":{"description":"last_active_at is later than last_read_at (true if never read).","type":"boolean"},"updated_at":{"format":"date-time","type":"string"},"usage_total":{"$ref":"#/components/schemas/UsageTotal"},"vault_id":{"format":"uuid","nullable":true,"type":"string"}},"required":["id","runtime","status"],"title":"Conversation","type":"object"},"ClaimableUserCreated":{"description":"The grant plus its two secrets. Both are shown once and stored only as hashes. Replaying the create with the same `Idempotency-Key` returns the same principal with a fresh pair, which invalidates this one.","properties":{"api_key":{"description":"A `principal`-scoped API key for the principal, expiring with it. It reaches agents, environments, vaults, conversations and sandboxes, and nothing that manages an account.","type":"string"},"application_id":{"type":"string"},"claim_token":{"description":"The one-time capability that claims this principal.","type":"string"},"created_at":{"format":"date-time","type":"string"},"expires_at":{"format":"date-time","type":"string"},"grant_cents":{"type":"integer"},"id":{"format":"uuid","type":"string"},"max_live_sandboxes":{"nullable":true,"type":"integer"},"metadata":{"type":"object"},"principal_id":{"format":"uuid","type":"string"},"status":{"enum":["unclaimed","claimed","expired","released"],"type":"string"}},"required":["id","principal_id","status","expires_at","api_key","claim_token"],"title":"ClaimableUserCreated","type":"object"},"EnvironmentListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Environment"},"type":"array"}},"required":["data"],"title":"EnvironmentListResponse","type":"object"},"SandboxEntry":{"description":"One entry of a directory on a sandbox (ADR 0039).","properties":{"name":{"type":"string"},"size":{"description":"Bytes, for a regular file; null otherwise.","nullable":true,"type":"integer"},"type":{"enum":["file","directory","symlink","other"],"type":"string"}},"required":["name","type"],"title":"SandboxEntry","type":"object"},"ApiKeyListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/ApiKey"},"type":"array"}},"required":["data"],"title":"ApiKeyListResponse","type":"object"},"DeviceTokenRequest":{"properties":{"device_code":{"description":"The `device_code` from `POST /api/auth/device`.","type":"string"}},"required":["device_code"],"title":"DeviceTokenRequest","type":"object"},"Runner":{"description":"A self-hosted runner: a machine of yours running `fountain runner`, serving sandboxes for the `runner` provider (ADR 0022). `online` is live — whether the daemon holds a connection right now.","properties":{"arch":{"nullable":true,"type":"string"},"connected_at":{"format":"date-time","nullable":true,"type":"string"},"created_at":{"format":"date-time","type":"string"},"hostname":{"nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"last_seen_at":{"format":"date-time","nullable":true,"type":"string"},"name":{"description":"The `--name` the daemon connected with.","type":"string"},"online":{"type":"boolean"},"os":{"nullable":true,"type":"string"},"root":{"description":"The directory on the machine that holds its sandboxes.","nullable":true,"type":"string"},"version":{"description":"The daemon's CLI version.","nullable":true,"type":"string"}},"required":["id","name","online","created_at"],"title":"Runner","type":"object"},"ClaimedPrincipal":{"description":"The result of a claim. The principal's resources are untouched: the same sandbox, agent, environment, vault and conversations, under the same ids.","properties":{"api_key":{"description":"A fresh `principal`-scoped key that expires 30 days after issuance. The claim revokes the application credential. Owners can replace the key from API keys in the console, including after expiry.","type":"string"},"claimed_at":{"format":"date-time","type":"string"},"principal_id":{"format":"uuid","type":"string"},"status":{"enum":["unclaimed","claimed","expired","released"],"type":"string"},"user":{"description":"The account that now owns the principal.","properties":{"email":{"nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"}},"type":"object"}},"required":["user","principal_id","status","api_key"],"title":"ClaimedPrincipal","type":"object"},"ExportResponse":{"properties":{"data":{"$ref":"#/components/schemas/Export"}},"required":["data"],"title":"ExportResponse","type":"object"},"VaultSecretRequest":{"properties":{"expires_at":{"description":"When the value stops working, as recorded by the owner. Advisory: the owner is emailed before this instant; nothing is enforced.","format":"date-time","nullable":true,"type":"string"},"key":{"type":"string"},"value":{"description":"Secret value (write-only).","type":"string"}},"required":["key","value"],"title":"VaultSecretRequest","type":"object"},"InferenceCredentialSetCreateRequest":{"properties":{"name":{"description":"Unique within the account.","maxLength":200,"minLength":1,"type":"string"}},"required":["name"],"title":"InferenceCredentialSetCreateRequest","type":"object"},"ApiKey":{"description":"Key metadata. Never the key itself, and never its hash.","properties":{"created_at":{"format":"date-time","type":"string"},"expires_at":{"format":"date-time","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"last_used_at":{"format":"date-time","nullable":true,"type":"string"},"name":{"type":"string"},"prefix":{"type":"string"},"scopes":{"description":"`full` for a key a person minted; `sprite:<conversation_id>` for the auto-issued token a sandbox holds.","items":{"type":"string"},"type":"array"}},"required":["id","name","prefix","created_at"],"title":"ApiKey","type":"object"},"PromptRequest":{"properties":{"client_request_id":{"description":"Your own name for this prompt. Fountain stores it on the turn the prompt opens and sends it on that turn's `started` stage event, beside the `turn_id`, so a client can bind its work item to the exact turn without inferring it from turn order. Use the event to find a candidate turn and the turn itself to confirm it: the event's copy has been through event redaction, and the turn's is what you sent. It is a correlation and not an idempotency key: a second prompt with the same value opens a second turn that carries it too. Make it unique within the conversation.","maxLength":200,"minLength":1,"nullable":true,"pattern":"^[^\\x00]*$","type":"string"},"images":{"description":"Optional images to attach to this prompt.","items":{"$ref":"#/components/schemas/ImageInput"},"nullable":true,"type":"array"},"prompt":{"type":"string"},"session_config":{"allOf":[{"$ref":"#/components/schemas/SessionConfig"}],"description":"ACP session config options for this turn only (ADR 0062), over the conversation's and the agent's, such as {\"effort\": \"high\", \"fast\": true}. The next turn goes back to the conversation's. The turn's config_selection records what was requested, applied and skipped."}},"required":["prompt"],"title":"PromptRequest","type":"object"},"Turn":{"description":"One prompt → exit_code cycle within a conversation.","properties":{"client_request_id":{"description":"The `client_request_id` of the prompt that opened this turn (#1406), or null: the caller sent none, or the turn is `autonomous`. Not unique. This is the value the caller sent, byte for byte, and it is the one to compare against: the copy on the turn's `started` stage event has been through event redaction, which rewrites any registered environment value it contains.","nullable":true,"type":"string"},"config_selection":{"additionalProperties":true,"description":"The ACP session config options of this turn (ADR 0062). `requested` is what the turn asked for (agent, then conversation, then prompt). `applied` maps each id the adapter took to the value it confirmed. `skipped` lists ids the adapter did not advertise. On a refusal, `status` is `failed` with `error`. Null on a turn that requested none.","nullable":true,"type":"object"},"ended_at":{"format":"date-time","nullable":true,"type":"string"},"exit_code":{"nullable":true,"type":"integer"},"id":{"format":"uuid","type":"string"},"image_count":{"description":"Number of images attached to this turn.","type":"integer"},"inference":{"description":"Which inference source served this turn, recorded once when the turn started and never rewritten: repointing the credential set, reconnecting the subscription or restarting the conversation changes later turns only. Null on a turn that started before the source was recorded.","nullable":true,"properties":{"chatgpt_grant_id":{"description":"The ChatGPT subscription that served the turn when `scope` is `grant`, as listed by `GET /api/account/chatgpt-subscriptions`; null otherwise. It keeps naming that subscription after the subscription is removed.","format":"uuid","nullable":true,"type":"string"},"origin":{"description":"`own` for a credential of the account's, `platform` for the deployment's.","enum":["own","platform"],"type":"string"},"scope":{"description":"Where the credential came from: the account's credential set (`credential`), an environment or vault value (`tenant_secret`), a ChatGPT subscription the set names (`grant`), the deployment (`platform`), or nowhere because the provider needs none (`none`) or none was found (`missing`).","enum":["credential","tenant_secret","grant","platform","none","missing"],"type":"string"}},"readOnly":true,"required":["origin","scope","chatgpt_grant_id"],"type":"object"},"inserted_at":{"format":"date-time","type":"string"},"limit_reason":{"description":"The service-enforced limit that ended this turn, or null. Set independently of exit_code: a runtime that exits zero after its deadline is still an incomplete turn, so a client must read this before treating a turn as successful.","nullable":true,"type":"string"},"model_selection":{"description":"ACP model selection evidence; null for turns without a selection report.","nullable":true,"properties":{"effective_model":{"nullable":true,"type":"string"},"error":{"type":"string"},"requested_model":{"nullable":true,"type":"string"},"source":{"enum":["runtime","selection_ack"],"nullable":true,"type":"string"},"status":{"enum":["selected","failed"],"type":"string"}},"type":"object"},"origin":{"description":"Who opened the turn: `user` for a prompt somebody sent, `autonomous` for a turn the server opened for a background cycle the agent ran after its prompt was answered (#817).","enum":["user","autonomous"],"type":"string"},"prompt":{"type":"string"},"started_at":{"format":"date-time","nullable":true,"type":"string"},"status":{"enum":["pending","running","completed","failed","interrupted"],"type":"string"},"turn_number":{"type":"integer"},"usage":{"anyOf":[{"allOf":[{"$ref":"#/components/schemas/TurnUsage"}]},{"enum":[null],"nullable":true}],"description":"The end-of-turn token figure; null while the turn runs, when the runtime reported none, or on turns that predate the field.","nullable":true},"waiting":{"description":"The turn ended with a permission request still open (#1635): the agent answered with stop reason `waiting`, the turn is `completed` and the request is on the conversation as a `pending_requests` entry.","type":"boolean"}},"required":["id","turn_number","prompt","status"],"title":"Turn","type":"object"},"VaultSecretListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/VaultSecret"},"type":"array"}},"required":["data"],"title":"VaultSecretListResponse","type":"object"},"AuthTokenResponse":{"description":"A freshly minted full-scope key. `api_key` is shown once and never again.","properties":{"api_key":{"example":"ftn_live_...","type":"string"},"key_id":{"format":"uuid","type":"string"},"prefix":{"description":"Leading characters of the key, the only part stored in the clear.","type":"string"}},"required":["api_key","key_id","prefix"],"title":"AuthTokenResponse","type":"object"},"HealthResponse":{"properties":{"status":{"example":"ok","type":"string"}},"required":["status"],"title":"HealthResponse","type":"object"},"Block":{"additionalProperties":true,"description":"One structured piece of a log event's output — the same parse the web UI renders (`Fountain.Conversations.Blocks`). `kind` decides the other fields: `text`/`thinking` carry `body`; `plan` carries the full ordered checklist in `body` (entries with `content`, `status`, and optional `priority`, `id`, `activeForm`); `tool_use` carries `id`, `name`, `summary`, `body` (the input); `tool_result` carries `tool_id`, `body`, `error` and pairs with the `tool_use` of the same id; `init` carries `summary`, `body`; `result` carries `body`, `raw`; `error` carries `body`; `raw` carries `body`, `summary`; `permission_request` carries `request_id`, `name`, `summary` and `options` — the agent is blocked on it, and a client answers with POST /api/conversations/{id}/requests/{request_id}. Render only the options in `options`; never synthesise one the agent did not offer. `prompt` carries `body` — the prompt that opened the turn, which is the one kind not parsed out of a runtime's output. It appears only on a turn's `turn`/`started` stage event, and only when the events feed was asked for it with `blocks=true&prompts=true`.","properties":{"body":{"oneOf":[{"nullable":true,"type":"string"},{"items":{"additionalProperties":true,"properties":{"activeForm":{"type":"string"},"content":{"type":"string"},"id":{"type":"string"},"priority":{"type":"string"},"status":{"enum":["pending","in_progress","completed"],"type":"string"}},"required":["content","status"],"type":"object"},"type":"array"}]},"error":{"nullable":true,"type":"boolean"},"id":{"nullable":true,"type":"string"},"kind":{"enum":["text","thinking","tool_use","tool_result","init","result","error","raw","permission_request","plan","prompt"],"type":"string"},"name":{"nullable":true,"type":"string"},"options":{"description":"permission_request only: the options the agent offered, in its order. Each carries at least `optionId` and `kind` (allow_once, allow_always, reject_once, reject_always, ...).","items":{"additionalProperties":true,"type":"object"},"nullable":true,"type":"array"},"raw":{"nullable":true,"type":"string"},"request_id":{"description":"permission_request only: the id to answer with.","nullable":true,"type":"string"},"summary":{"nullable":true,"type":"string"},"tool_id":{"nullable":true,"type":"string"}},"required":["kind"],"title":"Block","type":"object"},"ApiKeyCreatedResponse":{"description":"The one and only response that carries key material.","properties":{"created_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"key":{"description":"Plaintext key. Not recoverable afterwards.","type":"string"},"name":{"type":"string"},"prefix":{"type":"string"}},"required":["id","name","key","prefix"],"title":"ApiKeyCreatedResponse","type":"object"},"TeammateListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Teammate"},"type":"array"}},"required":["data"],"title":"TeammateListResponse","type":"object"},"AdminUserListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/AdminUser"},"type":"array"},"meta":{"properties":{"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"}},"required":["page","per_page","total"],"type":"object"}},"required":["data","meta"],"title":"AdminUserListResponse","type":"object"},"EgressListResponse":{"properties":{"brokered":{"description":"False when the conversation was not brokered; `data` is then empty.","type":"boolean"},"data":{"items":{"$ref":"#/components/schemas/EgressEvent"},"type":"array"},"next":{"description":"Pass as `before` for the next page; null at the end.","nullable":true,"type":"integer"}},"required":["data","brokered"],"title":"EgressListResponse","type":"object"},"VaultResponse":{"properties":{"data":{"$ref":"#/components/schemas/Vault"}},"required":["data"],"title":"VaultResponse","type":"object"},"ManifestResource":{"description":"One compiled document from a fountain.yml manifest. `spec` matches the create/update schema for the kind, plus an inline `secrets` map (Environment and Vault). Specs reference other documents by name, and the server resolves each to an id: an Agent's `environment`, a Teammate's `agent`, `environment` and `vault`, and a Schedule's `teammate`. Teammate specs take `agent`, `environment` and `vault`, and a Teammate document is read as a whole declaration, so an absent `environment` or `vault` clears that binding. Schedule specs take `teammate` plus the TeamScheduleCreateRequest fields `cron`, `prompt`, `one_off` and `enabled`; Webhook specs take the WebhookEndpointCreateRequest fields `url`, `description` and `event_types`, and are keyed by `url` rather than by `name`.","properties":{"kind":{"enum":["Environment","Vault","Agent","Teammate","Schedule","Webhook"],"type":"string"},"name":{"maxLength":200,"minLength":1,"type":"string"},"spec":{"additionalProperties":true,"type":"object"}},"required":["kind","name"],"title":"ManifestResource","type":"object"},"SecretBindingPresetListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/SecretBindingPreset"},"type":"array"}},"required":["data"],"title":"SecretBindingPresetListResponse","type":"object"},"AdminSandboxListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/AdminSandbox"},"type":"array"}},"required":["data"],"title":"AdminSandboxListResponse","type":"object"},"AuditEvent":{"description":"One entry in the account's append-only audit trail.","properties":{"action":{"example":"vault.secret.write","type":"string"},"actor":{"description":"Which surface acted: `ui` (browser session), `api` (bearer key), `sprite` (a per-conversation token held by a sandbox), `system`.","nullable":true,"type":"string"},"id":{"description":"Cursor for `before`.","type":"integer"},"inserted_at":{"format":"date-time","type":"string"},"metadata":{"additionalProperties":true,"type":"object"},"request_ip":{"nullable":true,"type":"string"},"resource_id":{"nullable":true,"type":"string"},"resource_type":{"nullable":true,"type":"string"}},"required":["id","action","inserted_at"],"title":"AuditEvent","type":"object"},"ApplySecretResult":{"description":"Outcome for one secret key. Values are never echoed back.","properties":{"action":{"enum":["upserted","error"],"type":"string"},"errors":{"additionalProperties":true,"nullable":true,"type":"object"},"key":{"type":"string"}},"required":["key","action"],"title":"ApplySecretResult","type":"object"},"PromptResponse":{"properties":{"client_request_id":{"description":"The `client_request_id` the request carried, or null when it carried none. The response cannot name the turn: a conversation that has to be woken is answered before its turn exists. Find the turn by this value instead.","nullable":true,"type":"string"},"status":{"example":"queued","type":"string"}},"required":["status"],"title":"PromptResponse","type":"object"},"AgentListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Agent"},"type":"array"}},"required":["data"],"title":"AgentListResponse","type":"object"},"WebhookDelivery":{"description":"One HTTP attempt at one event. Retained for 30 days by default, then pruned.","properties":{"attempt":{"description":"1 for the first try.","type":"integer"},"duration_ms":{"nullable":true,"type":"integer"},"error":{"nullable":true,"type":"string"},"event_id":{"description":"The log_events row id, which is also the SSE event id.","type":"string"},"event_type":{"example":"conversation.turn.done","type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"response_body":{"description":"The first few KB of what the receiver said.","nullable":true,"type":"string"},"status_code":{"description":"null when the request never got a response.","nullable":true,"type":"integer"}},"required":["id","event_id","event_type","attempt"],"title":"WebhookDelivery","type":"object"},"PendingPermissionRequest":{"description":"A permission request that outlived its turn (#1635). The agent ended the turn with stop reason `waiting` while this request was open, so the conversation is idle, the sandbox may be suspended, and the request is still waiting for an answer. Answer it at POST /api/conversations/{id}/requests/{request_id}, which resolves it and opens a new turn carrying the outcome to the agent.","properties":{"asked_at":{"format":"date-time","nullable":true,"type":"string"},"deadline":{"description":"When the request is denied for want of an answer. Set from the request's own `_meta.fountain.timeout`, else the policy's `ask_timeout`, else the global ask timeout.","format":"date-time","nullable":true,"type":"string"},"options":{"description":"The options the agent offered, verbatim. `option_id` must be one of these `optionId` values; an id from another runtime is refused.","items":{"additionalProperties":true,"type":"object"},"type":"array"},"request_id":{"type":"string"},"tool":{"description":"The tool the agent asked about, as the transcript labels it.","nullable":true,"type":"string"},"turn_id":{"format":"uuid","type":"string"}},"required":["request_id","options"],"title":"PendingPermissionRequest","type":"object"},"ChatGPTSubscriptionUpdateRequest":{"description":"A new name. A name is a label: renaming changes no credential.","properties":{"name":{"maxLength":200,"minLength":1,"type":"string"}},"required":["name"],"title":"ChatGPTSubscriptionUpdateRequest","type":"object"},"SandboxListing":{"description":"A directory on a sandbox, directories first then by name.","properties":{"entries":{"items":{"$ref":"#/components/schemas/SandboxEntry"},"type":"array"},"path":{"description":"The directory listed, absolute.","type":"string"},"truncated":{"description":"True when the directory holds more entries than were returned.","type":"boolean"}},"required":["path","entries","truncated"],"title":"SandboxListing","type":"object"},"AgentUpdate":{"properties":{"allowed_environment_ids":{"description":"Environments a conversation may launch this agent under instead of its own (environment_id on create). Same shape as allowed_vault_ids: null (default) allows any environment the tenant owns; an empty list forbids overriding; a non-empty list is an allowlist. The agent's own environment always passes.","items":{"format":"uuid","type":"string"},"nullable":true,"type":"array"},"allowed_inference_credential_ids":{"description":"Credential sets a conversation may launch this agent on instead of the agent's (inference_credential_id on create). Same shape as allowed_vault_ids: null (default) allows any set the tenant owns; an empty list forbids overriding; a non-empty list is an allowlist. The agent's own set always passes.","items":{"format":"uuid","type":"string"},"nullable":true,"type":"array"},"allowed_vault_ids":{"description":"Vaults a conversation may attach to this agent. null (default) allows any vault the tenant owns; an empty list forbids attaching any vault; a non-empty list is an allowlist.","items":{"format":"uuid","type":"string"},"nullable":true,"type":"array"},"description":{"type":"string"},"environment_id":{"format":"uuid","nullable":true,"type":"string"},"inference_credential_id":{"description":"The credential set this agent's conversations run on. null (default) is the account's default set, which is what every agent had before an account could hold more than one.","format":"uuid","nullable":true,"type":"string"},"mcp_servers":{"additionalProperties":true,"type":"object"},"metadata":{"additionalProperties":true,"type":"object"},"model":{"nullable":true,"pattern":"^[a-z0-9_-]+/[a-z0-9._-]+$","type":"string"},"name":{"maxLength":200,"minLength":1,"type":"string"},"permission_policy":{"allOf":[{"$ref":"#/components/schemas/PermissionPolicy"}],"description":"Per-tool permission policy: a map of key to verdict, plus an optional \"default\" key. A key is matched against the tool card's title first and then ACP's kind (execute, edit, read, fetch, …); prefer a kind, because claude titles a tool call with the command it is about to run. Unset keys fall back to the default, and an unset default is auto_allow. \"ask\" holds the tool until a human answers it on the conversation stream, and denies if nobody does before the timeout. A conversation may narrow this at launch, never widen it. A runtime that never asks (opencode) refuses anything stricter than auto_allow with 422 permission_policy_unenforceable.","nullable":true},"runtime":{"enum":["claude","codex","gemini","opencode","acp"],"type":"string"},"runtime_command":{"description":"The command the acp runtime launches inside the sandbox, as a shell line resolved there (for example `chant acp`). Required when runtime is acp, and rejected on every other runtime, which resolves its own executable. A free string by design: it runs under the same isolation as an environment's setup script.","nullable":true,"type":"string"},"sandbox_mode":{"description":"Where a conversation of this agent runs by default (ADR 0023). ephemeral: a sandbox per conversation, reclaimed with it. persistent: one sandbox per agent identity (agent, environment, vault) — the agent's computer — that every conversation of that identity lands on and shares; it survives a conversation ending and is parked, not destroyed, at the ceiling. A launch may name the other with sandbox_mode on POST /api/conversations.","enum":["ephemeral","persistent"],"type":"string"},"sandbox_provider":{"description":"Sandbox backend override; null inherits the instance default (SANDBOX_PROVIDER). Only providers configured on this instance are accepted","enum":["sprites","e2b","daytona","runner"],"nullable":true,"type":"string"},"session_config":{"allOf":[{"$ref":"#/components/schemas/SessionConfig"}],"description":"ACP session config options every conversation of this agent requests (ADR 0062), such as {\"effort\": \"high\"}. A conversation's own session_config overrides these keys, and a prompt's overrides both for that turn. Null or {} requests none."},"skills":{"description":"Each entry is either inline (`{name, content}` — full SKILL.md text written to the sprite) or github (`{source, ref?, name?}` — installed on the sprite via the skills.sh CLI, optionally pinned to a tag/branch/sha via `ref`). Exactly one of `content` or `source` must be set on each entry.","items":{"properties":{"content":{"description":"Full SKILL.md body for inline entries.","type":"string"},"name":{"description":"Skill name (required for inline entries).","type":"string"},"ref":{"description":"Optional tag, branch, or sha pinning a github-sourced skill (installed as `owner/repo@ref`). Without it the default branch is fetched at spawn time.","pattern":"^[A-Za-z0-9._/-]+$","type":"string"},"source":{"description":"GitHub `owner/repo` for skills.sh-sourced entries.","pattern":"^[A-Za-z0-9._/-]+$","type":"string"}},"type":"object"},"type":"array"},"system":{"type":"string"}},"title":"AgentUpdate","type":"object"},"InferenceCredentialRequest":{"properties":{"validate":{"default":true,"description":"Ping the provider to check the credential before storing it. false stores it unchecked.","type":"boolean"},"value":{"description":"The provider token (write-only).","type":"string"}},"required":["value"],"title":"InferenceCredentialRequest","type":"object"},"PasswordChangeResponse":{"properties":{"api_keys_revoked":{"description":"Always false. API keys are separate credentials with their own expiries; revoke them yourself at `DELETE /api/auth/api-keys/{id}`.","type":"boolean"},"message":{"type":"string"},"sessions_invalidated":{"type":"boolean"}},"required":["message","sessions_invalidated","api_keys_revoked"],"title":"PasswordChangeResponse","type":"object"},"ConversationListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Conversation"},"type":"array"}},"required":["data"],"title":"ConversationListResponse","type":"object"},"StripeUrlResponse":{"description":"A Stripe-hosted URL to open in a browser. Single-use and short-lived.","properties":{"data":{"properties":{"url":{"format":"uri","type":"string"}},"required":["url"],"type":"object"}},"required":["data"],"title":"StripeUrlResponse","type":"object"},"SecretBindingRequest":{"example":{"auth_type":"bearer","host":"api.stripe.com","key":"STRIPE_SECRET_KEY"},"properties":{"auth_type":{"enum":["substitute","bearer","basic","api_key","custom"],"type":"string"},"enabled":{"type":"boolean"},"header":{"nullable":true,"type":"string"},"headers":{"additionalProperties":{"type":"string"},"type":"object"},"host":{"type":"string"},"key":{"type":"string"},"prefix":{"nullable":true,"type":"string"},"username":{"nullable":true,"type":"string"}},"required":["key","host","auth_type"],"title":"SecretBindingRequest","type":"object"},"VaultRequest":{"properties":{"description":{"type":"string"},"metadata":{"additionalProperties":true,"type":"object"},"name":{"maxLength":200,"minLength":1,"type":"string"}},"required":["name"],"title":"VaultRequest","type":"object"},"Connection":{"description":"A provider account the tenant signed in to once, whose credential Fountain holds (#1178). Agents get the capability, never the token: an agent's `mcp_servers` names the connection (`{\"gmail\": {\"connection\": \"<id>\"}}`) and Fountain serves the Gmail tools; and the access token is brokered under `env_key` for an MCP server the tenant runs. Only on a deployment that runs the egress broker.","properties":{"account_email":{"description":"The connected account: an address, or the label the provider gave.","type":"string"},"created_at":{"format":"date-time","type":"string"},"env_key":{"description":"The env var name the access token is brokered under (`GOOGLE_ACCESS_TOKEN`).","type":"string"},"expires_at":{"description":"When the cached access token expires. Refreshed on use.","format":"date-time","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"provider":{"description":"The provider's slug: `google`, or a tenant provider's.","type":"string"},"provider_id":{"description":"The tenant provider (#1186); null for the platform provider.","format":"uuid","nullable":true,"type":"string"},"revoked_at":{"format":"date-time","nullable":true,"type":"string"},"scopes":{"items":{"type":"string"},"type":"array"},"status":{"description":"`revoked` once the tenant cut it or the provider refused the refresh token; `expired` when the access token lapsed and the provider issued no refresh token. The row stays so the console can say why the tools stopped. Reconnect to replace it.","enum":["active","revoked","expired"],"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","provider","account_email","scopes","env_key","status"],"title":"Connection","type":"object"},"AdminAuditListResponse":{"description":"Cross-tenant audit events; each carries the tenant it belongs to.","properties":{"data":{"items":{"$ref":"#/components/schemas/AuditEvent"},"type":"array"}},"required":["data"],"title":"AdminAuditListResponse","type":"object"},"CreditsCheckoutRequest":{"example":{"cents":2500},"properties":{"cents":{"description":"The pack to buy, in cents. Must be one of credits.packs_cents.","type":"integer"}},"required":["cents"],"title":"CreditsCheckoutRequest","type":"object"},"SandboxRequestResponse":{"properties":{"data":{"$ref":"#/components/schemas/SandboxRequest"}},"required":["data"],"title":"SandboxRequestResponse","type":"object"},"TeamScheduleResponse":{"properties":{"data":{"$ref":"#/components/schemas/TeamSchedule"}},"required":["data"],"title":"TeamScheduleResponse","type":"object"},"ConnectionProviderListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/ConnectionProvider"},"type":"array"}},"required":["data"],"title":"ConnectionProviderListResponse","type":"object"},"DeviceAuthResponse":{"description":"A fresh device-authorization grant (#1305). `device_code` stays on the polling machine and never gets typed; `user_code` is what the human enters at `verification_uri`.","properties":{"device_code":{"description":"High-entropy code the CLI polls the token endpoint with. Shown once.","type":"string"},"expires_in":{"description":"Seconds until the grant expires.","type":"integer"},"interval":{"description":"Minimum seconds between polls; faster gets `slow_down`.","type":"integer"},"user_code":{"description":"Short code for the human to type into the console.","example":"BCDF-GHJK","type":"string"},"verification_uri":{"description":"The console page where the user approves the grant.","type":"string"},"verification_uri_complete":{"description":"`verification_uri` with the user code prefilled.","type":"string"}},"required":["device_code","user_code","verification_uri","verification_uri_complete","expires_in","interval"],"title":"DeviceAuthResponse","type":"object"},"EmailRequest":{"properties":{"email":{"format":"email","type":"string"}},"required":["email"],"title":"EmailRequest","type":"object"},"InferenceCredentialResponse":{"properties":{"data":{"$ref":"#/components/schemas/InferenceCredentialStatus"}},"required":["data"],"title":"InferenceCredentialResponse","type":"object"},"ConversationTreeNode":{"description":"One conversation in a spawn tree, flat with a parent pointer.","properties":{"id":{"format":"uuid","type":"string"},"parent_id":{"format":"uuid","nullable":true,"type":"string"},"source":{"enum":["ui","api","agent"],"type":"string"},"status":{"enum":["pending","running","idle","failed","terminated"],"type":"string"}},"required":["id"],"title":"ConversationTreeNode","type":"object"},"SupportReportResponse":{"properties":{"data":{"$ref":"#/components/schemas/SupportReport"}},"required":["data"],"title":"SupportReportResponse","type":"object"},"ExportListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Export"},"type":"array"}},"required":["data"],"title":"ExportListResponse","type":"object"},"UsageTotal":{"description":"Running sums of `input` and `output` over the turns that reported a usage.","properties":{"input":{"minimum":0,"type":"integer"},"output":{"minimum":0,"type":"integer"}},"required":["input","output"],"title":"UsageTotal","type":"object"},"ConversationResponse":{"properties":{"data":{"$ref":"#/components/schemas/Conversation"},"meta":{"description":"Present when creating or resuming a conversation.","properties":{"resumed":{"description":"Whether the channel resumed an existing conversation.","type":"boolean"}},"required":["resumed"],"type":"object"}},"required":["data"],"title":"ConversationResponse","type":"object"},"Vault":{"description":"A free-floating bag of env-var overrides selected at conversation creation. Vault values override an environment's baseline secrets when the same key is set on both.","properties":{"description":{"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"metadata":{"additionalProperties":true,"type":"object"},"name":{"type":"string"},"secret_count":{"description":"Secrets stored in this vault.","type":"integer"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name"],"title":"Vault","type":"object"},"AdminReapResponse":{"properties":{"data":{"properties":{"outcome":{"enum":["terminated","released","already_terminal"],"type":"string"},"sandbox_id":{"format":"uuid","type":"string"}},"required":["outcome"],"type":"object"}},"required":["data"],"title":"AdminReapResponse","type":"object"},"OAuthTokenRequest":{"properties":{"client_id":{"type":"string"},"code":{"description":"The code from the `/oauth/authorize` redirect.","type":"string"},"code_verifier":{"description":"The PKCE verifier whose S256 challenge was sent to `/oauth/authorize`.","type":"string"},"grant_type":{"description":"`authorization_code` (anything else is 400 `unsupported_grant_type`).","type":"string"},"redirect_uri":{"description":"Exactly the redirect_uri the authorization request used.","type":"string"}},"required":["grant_type","code","code_verifier","client_id","redirect_uri"],"title":"OAuthTokenRequest","type":"object"},"RegisterResponse":{"properties":{"message":{"type":"string"},"user_id":{"format":"uuid","type":"string"}},"required":["user_id","message"],"title":"RegisterResponse","type":"object"},"ApplyResponse":{"properties":{"data":{"properties":{"results":{"items":{"$ref":"#/components/schemas/ApplyResult"},"type":"array"}},"required":["results"],"type":"object"}},"required":["data"],"title":"ApplyResponse","type":"object"},"SecretResponse":{"properties":{"data":{"$ref":"#/components/schemas/Secret"}},"required":["data"],"title":"SecretResponse","type":"object"},"SecretRequest":{"properties":{"key":{"type":"string"},"value":{"description":"Secret value (write-only).","type":"string"}},"required":["key","value"],"title":"SecretRequest","type":"object"},"TeamMessageResponse":{"properties":{"conversation_id":{"description":"The conversation the message went to — a fresh one when the teammate's previous conversation was past resuming.","format":"uuid","type":"string"},"status":{"example":"queued","type":"string"}},"required":["status","conversation_id"],"title":"TeamMessageResponse","type":"object"},"PasswordResetRequest":{"properties":{"password":{"format":"password","type":"string"},"token":{"description":"From the reset email.","type":"string"}},"required":["token","password"],"title":"PasswordResetRequest","type":"object"},"ConversationReapplyRequest":{"description":"A selection of Agent, Environment, Vault, model and session config to apply to the machine an existing conversation already runs on. An omitted field keeps its current selection. An explicit null clears the Environment override, the Vault, the model override or the session config. An empty object reapplies the current selection.","properties":{"agent_id":{"description":"Agent to use; omitted keeps the current Agent.","format":"uuid","type":"string"},"environment_id":{"description":"Environment override to use; null returns to the selected Agent's Environment.","format":"uuid","nullable":true,"type":"string"},"model":{"description":"Model to run from the next turn (ADR 0061), in canonical provider/model_id form; null returns to the selected Agent's model. Checked against the selected Agent's runtime (422 model_invalid). The conversation keeps its credential: a model that credential does not serve is 409 inference_source_changed and nothing changes. The runtime session is kept, so the next turn continues it on the new model.","nullable":true,"type":"string"},"session_config":{"allOf":[{"$ref":"#/components/schemas/SessionConfig"}],"description":"ACP session config options to request from the next turn (ADR 0062), replacing the conversation's current ones; null or {} returns to the Agent's. Only the shape is checked here. The adapter decides which ids and values exist."},"vault_id":{"description":"Vault to use; null detaches the current Vault.","format":"uuid","nullable":true,"type":"string"}},"title":"ConversationReapplyRequest","type":"object"},"WebhookEndpointCreatedResponse":{"description":"The endpoint, plus the signing secret. This is the only response that carries the secret; it is not recoverable afterwards, only replaceable.","properties":{"data":{"$ref":"#/components/schemas/WebhookEndpoint"},"secret":{"description":"The HMAC-SHA256 signing secret. Store it; it is not shown again.","example":"whsec_Zm91bnRhaW4tZXhhbXBsZS1zZWNyZXQtdmFsdWU","type":"string"}},"required":["data","secret"],"title":"WebhookEndpointCreatedResponse","type":"object"},"ConnectionProviderRequest":{"description":"`kind: oauth2` needs `name`, `authorize_url`, `token_url`, `client_id` and `client_secret` (unless `token_endpoint_auth` is `none`). `kind: mcp` needs `mcp_url`; the rest comes from discovery. `slug` defaults from the name or the server host, `env_key` from the slug (`GITHUB_ACCESS_TOKEN`).","example":{"account_label_path":"login","authorize_url":"https://github.com/login/oauth/authorize","client_id":"Iv1.abc","client_secret":"…","kind":"oauth2","name":"GitHub","scopes":["repo","read:user"],"slug":"github","token_hosts":["api.github.com"],"token_url":"https://github.com/login/oauth/access_token","userinfo_url":"https://api.github.com/user"},"properties":{"account_label_path":{"nullable":true,"type":"string"},"authorize_url":{"type":"string"},"client_id":{"type":"string"},"client_secret":{"description":"Write-only.","type":"string"},"env_key":{"type":"string"},"kind":{"enum":["oauth2","mcp"],"type":"string"},"mcp_url":{"type":"string"},"name":{"type":"string"},"pkce":{"type":"boolean"},"revoke_url":{"nullable":true,"type":"string"},"scopes":{"items":{"type":"string"},"type":"array"},"slug":{"type":"string"},"token_endpoint_auth":{"enum":["client_secret_post","client_secret_basic","none"],"type":"string"},"token_hosts":{"items":{"type":"string"},"type":"array"},"token_url":{"type":"string"},"userinfo_url":{"nullable":true,"type":"string"}},"title":"ConnectionProviderRequest","type":"object"},"AdminSuspendRequest":{"properties":{"suspended":{"type":"boolean"}},"required":["suspended"],"title":"AdminSuspendRequest","type":"object"},"ChatGPTLinkAttemptResponse":{"properties":{"data":{"$ref":"#/components/schemas/ChatGPTLinkAttempt"}},"required":["data"],"title":"ChatGPTLinkAttemptResponse","type":"object"},"AgentRequest":{"properties":{"allowed_environment_ids":{"description":"Environments a conversation may launch this agent under instead of its own (environment_id on create). Same shape as allowed_vault_ids: null (default) allows any environment the tenant owns; an empty list forbids overriding; a non-empty list is an allowlist. The agent's own environment always passes.","items":{"format":"uuid","type":"string"},"nullable":true,"type":"array"},"allowed_inference_credential_ids":{"description":"Credential sets a conversation may launch this agent on instead of the agent's (inference_credential_id on create). Same shape as allowed_vault_ids: null (default) allows any set the tenant owns; an empty list forbids overriding; a non-empty list is an allowlist. The agent's own set always passes.","items":{"format":"uuid","type":"string"},"nullable":true,"type":"array"},"allowed_vault_ids":{"description":"Vaults a conversation may attach to this agent. null (default) allows any vault the tenant owns; an empty list forbids attaching any vault; a non-empty list is an allowlist.","items":{"format":"uuid","type":"string"},"nullable":true,"type":"array"},"description":{"type":"string"},"environment_id":{"format":"uuid","nullable":true,"type":"string"},"inference_credential_id":{"description":"The credential set this agent's conversations run on. null (default) is the account's default set, which is what every agent had before an account could hold more than one.","format":"uuid","nullable":true,"type":"string"},"mcp_servers":{"additionalProperties":true,"type":"object"},"metadata":{"additionalProperties":true,"type":"object"},"model":{"nullable":true,"pattern":"^[a-z0-9_-]+/[a-z0-9._-]+$","type":"string"},"name":{"maxLength":200,"minLength":1,"type":"string"},"permission_policy":{"allOf":[{"$ref":"#/components/schemas/PermissionPolicy"}],"description":"Per-tool permission policy: a map of key to verdict, plus an optional \"default\" key. A key is matched against the tool card's title first and then ACP's kind (execute, edit, read, fetch, …); prefer a kind, because claude titles a tool call with the command it is about to run. Unset keys fall back to the default, and an unset default is auto_allow. \"ask\" holds the tool until a human answers it on the conversation stream, and denies if nobody does before the timeout. A conversation may narrow this at launch, never widen it. A runtime that never asks (opencode) refuses anything stricter than auto_allow with 422 permission_policy_unenforceable.","nullable":true},"runtime":{"enum":["claude","codex","gemini","opencode","acp"],"type":"string"},"runtime_command":{"description":"The command the acp runtime launches inside the sandbox, as a shell line resolved there (for example `chant acp`). Required when runtime is acp, and rejected on every other runtime, which resolves its own executable. A free string by design: it runs under the same isolation as an environment's setup script.","nullable":true,"type":"string"},"sandbox_mode":{"description":"Where a conversation of this agent runs by default (ADR 0023). ephemeral: a sandbox per conversation, reclaimed with it. persistent: one sandbox per agent identity (agent, environment, vault) — the agent's computer — that every conversation of that identity lands on and shares; it survives a conversation ending and is parked, not destroyed, at the ceiling. A launch may name the other with sandbox_mode on POST /api/conversations.","enum":["ephemeral","persistent"],"type":"string"},"sandbox_provider":{"description":"Sandbox backend override; null inherits the instance default (SANDBOX_PROVIDER). Only providers configured on this instance are accepted","enum":["sprites","e2b","daytona","runner"],"nullable":true,"type":"string"},"session_config":{"allOf":[{"$ref":"#/components/schemas/SessionConfig"}],"description":"ACP session config options every conversation of this agent requests (ADR 0062), such as {\"effort\": \"high\"}. A conversation's own session_config overrides these keys, and a prompt's overrides both for that turn. Null or {} requests none."},"skills":{"description":"Each entry is either inline (`{name, content}` — full SKILL.md text written to the sprite) or github (`{source, ref?, name?}` — installed on the sprite via the skills.sh CLI, optionally pinned to a tag/branch/sha via `ref`). Exactly one of `content` or `source` must be set on each entry.","items":{"properties":{"content":{"description":"Full SKILL.md body for inline entries.","type":"string"},"name":{"description":"Skill name (required for inline entries).","type":"string"},"ref":{"description":"Optional tag, branch, or sha pinning a github-sourced skill (installed as `owner/repo@ref`). Without it the default branch is fetched at spawn time.","pattern":"^[A-Za-z0-9._/-]+$","type":"string"},"source":{"description":"GitHub `owner/repo` for skills.sh-sourced entries.","pattern":"^[A-Za-z0-9._/-]+$","type":"string"}},"type":"object"},"type":"array"},"system":{"type":"string"}},"required":["name","runtime"],"title":"AgentRequest","type":"object"},"VaultSecretMetadataRequest":{"additionalProperties":false,"properties":{"expires_at":{"description":"Advisory expiry. Null clears it; omission keeps the current value.","format":"date-time","nullable":true,"type":"string"}},"title":"VaultSecretMetadataRequest","type":"object"},"InferenceCredentialSetUpdateRequest":{"description":"Rename a set, make it the default, name a ChatGPT subscription, or any of them. Omitting a field leaves it alone. `is_default: false` is refused: a set stops being the default when another becomes it, never on its own.","properties":{"chatgpt_grant_id":{"description":"One of the account's ChatGPT subscriptions, or null to stop naming one. Naming one ends the set's running codex conversations.","format":"uuid","nullable":true,"type":"string"},"is_default":{"type":"boolean"},"name":{"maxLength":200,"minLength":1,"type":"string"}},"title":"InferenceCredentialSetUpdateRequest","type":"object"},"AdminUser":{"description":"An account as the operator surface sees it. Metadata only.","properties":{"active_sandboxes":{"type":"integer"},"comped":{"description":"A free account: the balance is never checked (ADR 0031).","type":"boolean"},"credit_balance_cents":{"description":"Prepaid balance in cents (ADR 0030). May be negative. Zero while credits are not active on this deployment.","type":"integer"},"email":{"type":"string"},"email_verified":{"type":"boolean"},"email_verified_at":{"format":"date-time","nullable":true,"type":"string"},"has_stripe_customer":{"type":"boolean"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"last_activity_at":{"format":"date-time","nullable":true,"type":"string"},"max_concurrent_sandboxes":{"description":"The concurrency cap actually enforced: the override, or the balance rule's.","nullable":true,"type":"integer"},"onboarding_completed_at":{"format":"date-time","nullable":true,"type":"string"},"role":{"enum":["admin","user"],"type":"string"},"sandbox_limit_override":{"description":"Admin override of the cap. Null means the balance rule applies.","nullable":true,"type":"integer"},"suspended":{"type":"boolean"},"suspended_at":{"format":"date-time","nullable":true,"type":"string"}},"required":["id","email","role"],"title":"AdminUser","type":"object"},"EnvironmentRequest":{"properties":{"env_vars":{"additionalProperties":{"type":"string"},"type":"object"},"metadata":{"additionalProperties":true,"type":"object"},"name":{"maxLength":200,"minLength":1,"type":"string"},"networking_config":{"additionalProperties":true,"description":"Refines networking_type: limited. allowed_hosts is the only key honored today; unknown keys are ignored. Where the policy is enforced depends on the account: on a brokered account (`brokered: true` on GET /api/auth/me) the sandbox can reach only the egress broker, and under limited the broker refuses any host not in allowed_hosts with a 403 that names it, while a host with a bound credential needs no entry. On an unbrokered account the sandbox itself allows only the allowlisted domains. Either way, limited with no allowed_hosts (or an empty list) is a deny-all, not an allow-all.","properties":{"allowed_hosts":{"description":"Domains the sandbox may reach when networking_type is limited.","items":{"type":"string"},"type":"array"}},"type":"object"},"networking_type":{"enum":["unrestricted","limited"],"type":"string"},"packages":{"additionalProperties":true,"type":"object"},"repositories":{"items":{"$ref":"#/components/schemas/Repository"},"type":"array"},"setup_script":{"type":"string"},"setup_timeout_seconds":{"description":"Setup exec timeout in seconds; defaults to 120. The overall provisioning deadline still applies.","maximum":900,"minimum":1,"type":"integer"}},"required":["name"],"title":"EnvironmentRequest","type":"object"},"ChatGPTLinkAttemptListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/ChatGPTLinkAttempt"},"type":"array"}},"required":["data"],"title":"ChatGPTLinkAttemptListResponse","type":"object"},"OAuthClient":{"description":"A tenant-owned OAuth client. Unpublished clients are in development mode and can sign in only their owner. Their redirect origins are also admitted by CORS.","properties":{"client_id":{"description":"The client_id sent to /oauth/authorize.","type":"string"},"created_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"name":{"description":"Shown on the consent page.","type":"string"},"origins":{"description":"Origins derived from redirect_uris and admitted by CORS. A loopback origin is admitted on any port, not only the port shown here.","items":{"type":"string"},"type":"array"},"published":{"description":"False means development mode with owner-only sign-in.","type":"boolean"},"redirect_uris":{"description":"Exact match, except that an unpublished loopback URI matches on any port.","items":{"type":"string"},"type":"array"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","client_id","name","redirect_uris","origins","published","created_at","updated_at"],"title":"OAuthClient","type":"object"},"AdminUserResponse":{"properties":{"data":{"$ref":"#/components/schemas/AdminUser"}},"required":["data"],"title":"AdminUserResponse","type":"object"},"SearchResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/SearchHit"},"type":"array"},"meta":{"properties":{"has_more":{"type":"boolean"},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["limit","offset","has_more"],"type":"object"}},"required":["data","meta"],"title":"SearchResponse","type":"object"},"SearchHit":{"description":"One search hit: what matched, and where to jump.","properties":{"agent_id":{"format":"uuid","nullable":true,"type":"string"},"conversation_id":{"format":"uuid","type":"string"},"kind":{"enum":["title","prompt","reply"],"type":"string"},"snippet":{"description":"The best-matching fragment, plain text — no markup to escape.","type":"string"},"ts":{"description":"The turn's creation time, or the conversation's for a title hit.","format":"date-time","type":"string"},"turn_id":{"description":"The turn (prompt / reply hits); null for a title hit.","format":"uuid","nullable":true,"type":"string"},"turn_number":{"nullable":true,"type":"integer"}},"required":["kind","conversation_id","snippet","ts"],"title":"SearchHit","type":"object"},"AgentVersionResponse":{"properties":{"data":{"$ref":"#/components/schemas/AgentVersion"}},"required":["data"],"title":"AgentVersionResponse","type":"object"},"TeamScheduleListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/TeamSchedule"},"type":"array"}},"required":["data"],"title":"TeamScheduleListResponse","type":"object"},"SandboxRequest":{"description":"Work waiting for sandbox capacity (ADR 0042).","properties":{"agent_id":{"format":"uuid","type":"string"},"conversation_id":{"description":"The conversation the request started, or the target when a failed request reports prompt_delivery_unknown. Inspect it before resubmitting: after a timeout or connection loss, the prompt may have run or may still execute.","format":"uuid","nullable":true,"type":"string"},"error":{"nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"kind":{"enum":["start","schedule_run"],"type":"string"},"position":{"description":"One-based place in the tenant's queue; null once it stops waiting.","nullable":true,"type":"integer"},"source":{"nullable":true,"type":"string"},"status":{"enum":["queued","starting","started","cancelled","expired","failed"],"type":"string"}},"required":["id","agent_id","kind","status"],"title":"SandboxRequest","type":"object"},"AdminEventListResponse":{"description":"The privilege trail: who did what to whom.","properties":{"data":{"items":{"properties":{"actor_user_id":{"format":"uuid","nullable":true,"type":"string"},"event_type":{"example":"admin.account.suspended","type":"string"},"id":{"type":"integer"},"inserted_at":{"format":"date-time","type":"string"},"metadata":{"additionalProperties":true,"type":"object"},"target_user_id":{"format":"uuid","nullable":true,"type":"string"}},"required":["event_type"],"type":"object"},"type":"array"}},"required":["data"],"title":"AdminEventListResponse","type":"object"},"SandboxFileResponse":{"properties":{"data":{"$ref":"#/components/schemas/SandboxFile"}},"required":["data"],"title":"SandboxFileResponse","type":"object"},"Environment":{"description":"A reusable sandbox environment: packages, env vars, repos, networking.","properties":{"agent_count":{"description":"Agents referencing this environment — 0 means safe to delete.","type":"integer"},"env_vars":{"additionalProperties":{"type":"string"},"type":"object"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"metadata":{"additionalProperties":true,"type":"object"},"name":{"type":"string"},"networking_config":{"additionalProperties":true,"description":"Refines networking_type: limited. allowed_hosts is the only key honored today; unknown keys are ignored. Where the policy is enforced depends on the account: on a brokered account (`brokered: true` on GET /api/auth/me) the sandbox can reach only the egress broker, and under limited the broker refuses any host not in allowed_hosts with a 403 that names it, while a host with a bound credential needs no entry. On an unbrokered account the sandbox itself allows only the allowlisted domains. Either way, limited with no allowed_hosts (or an empty list) is a deny-all, not an allow-all.","properties":{"allowed_hosts":{"description":"Domains the sandbox may reach when networking_type is limited.","items":{"type":"string"},"type":"array"}},"type":"object"},"networking_type":{"enum":["unrestricted","limited"],"type":"string"},"packages":{"additionalProperties":true,"type":"object"},"repositories":{"items":{"$ref":"#/components/schemas/Repository"},"type":"array"},"secret_count":{"description":"Secrets stored on this environment.","type":"integer"},"setup_script":{"type":"string"},"setup_timeout_seconds":{"description":"Setup exec timeout in seconds; defaults to 120. The overall provisioning deadline still applies.","maximum":900,"minimum":1,"type":"integer"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name"],"title":"Environment","type":"object"},"PermissionAnswerResponse":{"properties":{"ok":{"example":true,"type":"boolean"}},"required":["ok"],"title":"PermissionAnswerResponse","type":"object"},"ConversationTreeResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/ConversationTreeNode"},"type":"array"}},"required":["data"],"title":"ConversationTreeResponse","type":"object"},"AuthTokenRequest":{"properties":{"email":{"format":"email","type":"string"},"password":{"format":"password","type":"string"}},"required":["email","password"],"title":"AuthTokenRequest","type":"object"},"UsageAccounting":{"description":"The adapter's accounting claim, not independently verified billing. Interpret source, version and scope together. Reported does not imply whole-conversation coverage.","properties":{"completeness":{"enum":["reported","partial"],"type":"string"},"scope":{"maxLength":256,"minLength":1,"type":"string"},"source":{"maxLength":256,"minLength":1,"type":"string"},"version":{"minimum":1,"type":"integer"}},"required":["version","source","scope","completeness"],"title":"UsageAccounting","type":"object"},"AuditEventListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/AuditEvent"},"type":"array"},"meta":{"properties":{"has_more":{"type":"boolean"},"limit":{"type":"integer"},"next_cursor":{"description":"Pass as `before` for the next (older) page.","nullable":true,"type":"integer"}},"required":["limit","has_more"],"type":"object"}},"required":["data","meta"],"title":"AuditEventListResponse","type":"object"},"WebhookEndpointUpdateRequest":{"description":"Any subset. `status` toggles delivery; the secret is rotated separately.","properties":{"description":{"maxLength":500,"nullable":true,"type":"string"},"event_types":{"items":{"type":"string"},"type":"array"},"status":{"enum":["active","disabled"],"type":"string"},"url":{"type":"string"}},"title":"WebhookEndpointUpdateRequest","type":"object"},"AdminRoleRequest":{"properties":{"role":{"enum":["admin","user"],"type":"string"}},"required":["role"],"title":"AdminRoleRequest","type":"object"},"ChatGPTSubscription":{"description":"One ChatGPT subscription linked to the account, for the codex runtime. Fountain holds and renews its tokens; none is ever returned, and neither is the provider's account id. A credential set names one by `id`.","properties":{"access_expires_at":{"format":"date-time","nullable":true,"type":"string"},"account_email":{"description":"The ChatGPT account's email, shown to its owner and nobody else.","nullable":true,"type":"string"},"exhausted_until":{"description":"When the subscription's Codex usage resets, while that is in the future. Nothing records one for an account's subscription yet.","format":"date-time","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"last_refreshed_at":{"format":"date-time","nullable":true,"type":"string"},"name":{"description":"Unique within the account.","type":"string"},"plan_type":{"nullable":true,"type":"string"},"refreshable":{"description":"Whether a refresh token is stored. False once disconnected.","type":"boolean"},"revoked_reason":{"description":"The auth server's reason code when `status` is `revoked`.","nullable":true,"type":"string"},"status":{"description":"`active` serves runs. `revoked`: the auth server refused the refresh token; reconnect it. `disconnected`: the account disconnected it; the row holds no token, still counts against the limit, and can be reconnected or removed. A set that names a subscription that is not `active` fails its codex runs by name; nothing is substituted.","enum":["active","revoked","expired","disconnected"],"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","status","plan_type","account_email","refreshable","access_expires_at","last_refreshed_at","revoked_reason","exhausted_until","inserted_at","updated_at"],"title":"ChatGPTSubscription","type":"object"},"SandboxRequestListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/SandboxRequest"},"type":"array"}},"required":["data"],"title":"SandboxRequestListResponse","type":"object"},"MessageResponse":{"properties":{"message":{"type":"string"}},"required":["message"],"title":"MessageResponse","type":"object"},"TeammateResponse":{"properties":{"data":{"$ref":"#/components/schemas/Teammate"}},"required":["data"],"title":"TeammateResponse","type":"object"},"AccountDeletedResponse":{"properties":{"deleted":{"type":"boolean"},"sprites_destroyed":{"type":"integer"},"user_id":{"format":"uuid","type":"string"}},"required":["deleted"],"title":"AccountDeletedResponse","type":"object"},"BillingResponse":{"properties":{"data":{"properties":{"credits":{"description":"The prepaid balance, in cents. Null when this deployment has billing off; a client must not show a zero balance then. At zero, new work is refused with 402 `insufficient_credits`.","nullable":true,"properties":{"balance_cents":{"description":"May be negative: an in-flight turn that crosses zero finishes.","type":"integer"},"expires_at":{"format":"date-time","nullable":true,"type":"string"},"expiring_cents":{"description":"Unspent credit from the earliest live grant, which expires at expires_at.","type":"integer"},"packs_cents":{"description":"The packs on sale, ascending. Pass one to the credits checkout.","items":{"type":"integer"},"type":"array"},"purchased_cents":{"description":"The part of the balance that was bought. It never expires and is spent last.","type":"integer"},"turn_hour_cents":{"description":"What one hour of turn time costs.","type":"integer"}},"type":"object"},"has_stripe_customer":{"type":"boolean"},"period":{"description":"The calendar month the usage numbers cover, half-open: `end` is the first instant of the next month.","properties":{"end":{"format":"date-time","type":"string"},"start":{"format":"date-time","type":"string"}},"type":"object"},"sandbox_cap":{"description":"How many sandboxes this account may run at once: an admin override, or what the balance funds (ADR 0031).","type":"integer"},"usage":{"properties":{"conversations":{"description":"Conversations that ran a turn in the month, deleted or not.","type":"integer"},"credit_burned_cents":{"description":"Cents the ledger took this month: turns and platform inference. The charged number, where turn_hours is the metered one. Null with billing off.","nullable":true,"type":"integer"},"sandbox_minutes":{"description":"Active sandbox minutes inside the period, parked time excluded.","type":"number"},"sandbox_minutes_by_provider":{"additionalProperties":{"type":"number"},"description":"sandbox_minutes split by sandbox provider (sprites, e2b, daytona, runner). Providers not used in the period are absent.","type":"object"},"turn_hours":{"description":"Hours with a prompt in flight, on providers Fountain pays for; what burns credit. An idle sandbox spends none of these; sandbox_minutes counts it.","type":"number"},"turns":{"type":"integer"}},"type":"object"}},"required":["usage"],"type":"object"}},"required":["data"],"title":"BillingResponse","type":"object"},"VaultListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Vault"},"type":"array"}},"required":["data"],"title":"VaultListResponse","type":"object"},"ConnectionProvider":{"description":"Where a connection's tokens come from (#1186, #1299): a platform provider (`platform: true`, id `google`, `microsoft` or `slack` — Fountain's own OAuth client), a tenant's own OAuth app at a service (`kind: oauth2`), or a remote MCP server whose authorization Fountain discovered (`kind: mcp`). The client secret is never returned.","properties":{"account_label_path":{"description":"A dotted path into the userinfo body that names the account.","nullable":true,"type":"string"},"authorize_url":{"nullable":true,"type":"string"},"client_id":{"nullable":true,"type":"string"},"client_source":{"description":"`dcr` when the client came from RFC 7591 registration.","enum":["dcr","manual",null],"nullable":true,"type":"string"},"configured":{"description":"True when the provider has a client Fountain can start a flow with.","type":"boolean"},"connect_url":{"description":"Where to send the account owner, in a browser signed in to the console, to connect an account.","type":"string"},"created_at":{"format":"date-time","nullable":true,"type":"string"},"env_key":{"description":"The env var a connection's access token is brokered under.","type":"string"},"has_client_secret":{"type":"boolean"},"id":{"description":"A uuid, or a platform slug.","type":"string"},"issuer":{"description":"The authorization server discovery found (`mcp`).","nullable":true,"type":"string"},"kind":{"enum":["oauth2","mcp"],"type":"string"},"mcp_url":{"nullable":true,"type":"string"},"name":{"type":"string"},"pkce":{"type":"boolean"},"platform":{"description":"True for a platform provider, which has no row.","type":"boolean"},"redirect_uri":{"description":"The callback to register at the service.","type":"string"},"registration_endpoint":{"nullable":true,"type":"string"},"revoke_url":{"nullable":true,"type":"string"},"scopes":{"items":{"type":"string"},"type":"array"},"slug":{"type":"string"},"token_endpoint_auth":{"enum":["client_secret_post","client_secret_basic","none"],"type":"string"},"token_hosts":{"description":"The hosts the brokered token is attached to as a bearer.","items":{"type":"string"},"type":"array"},"token_url":{"nullable":true,"type":"string"},"updated_at":{"format":"date-time","nullable":true,"type":"string"},"userinfo_url":{"nullable":true,"type":"string"}},"required":["id","slug","name","kind","platform","configured","scopes","env_key","token_hosts","redirect_uri","connect_url"],"title":"ConnectionProvider","type":"object"},"SupportReportListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/SupportReport"},"type":"array"}},"required":["data"],"title":"SupportReportListResponse","type":"object"},"OAuthClientRequest":{"example":{"name":"Notes","redirect_uris":["https://abc123.sprites.app/callback","http://localhost:5173/callback"]},"properties":{"name":{"minLength":1,"type":"string"},"redirect_uris":{"items":{"type":"string"},"minItems":1,"type":"array"}},"required":["name","redirect_uris"],"title":"OAuthClientRequest","type":"object"},"SessionConfigOption":{"additionalProperties":true,"description":"One config option as the adapter advertised it (ACP `SessionConfigOption`), passed through unchanged. `category` is `thought_level` for reasoning effort and `model_config` for fast mode on the pinned adapters. `options` lists a select's values, possibly in groups. `currentValue` is the value in force.","properties":{"category":{"nullable":true,"type":"string"},"currentValue":{"anyOf":[{"type":"string"},{"type":"boolean"}]},"description":{"nullable":true,"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"options":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"type":{"description":"`select` or `boolean`.","type":"string"}},"required":["id"],"title":"SessionConfigOption","type":"object"},"ClaimableUserResponse":{"properties":{"data":{"$ref":"#/components/schemas/ClaimableUser"}},"required":["data"],"title":"ClaimableUserResponse","type":"object"},"LogEvent":{"description":"One line of a conversation's log feed: runtime output or a lifecycle stage transition. Same fields the SSE stream sends, plus `id`.","properties":{"blocks":{"description":"Only with `?blocks=true`: `data` parsed server-side into the blocks a transcript renders. Empty for non-output events.","items":{"$ref":"#/components/schemas/Block"},"type":"array"},"data":{"description":"Output text, or JSON-encoded metadata for stage events.","type":"string"},"duration_ms":{"nullable":true,"type":"integer"},"id":{"description":"Monotonic id. Pagination cursor here, `Last-Event-ID` on the SSE route.","type":"integer"},"kind":{"enum":["output","stage"],"type":"string"},"stage":{"description":"Lifecycle stage name. null on an event that has no stage.","nullable":true,"type":"string"},"state":{"description":"Lifecycle state of the stage. null on an event that has no state.","enum":["started","done","failed","interrupted"],"nullable":true,"type":"string"},"stream":{"description":"`stdout` / `stderr` for output events; empty for stage events.","type":"string"},"ts":{"format":"date-time","type":"string"},"turn_id":{"format":"uuid","nullable":true,"type":"string"}},"required":["id","kind","ts"],"title":"LogEvent","type":"object"},"ClaimableUser":{"description":"A claimable principal (ADR 0044): the anonymous tenant an application opens for a visitor who has no Fountain account yet. `principal_id` is the tenant every resource it builds belongs to, and it never changes — claiming attaches an owner rather than moving anything.","properties":{"application_id":{"description":"The application's own label for itself, as given at creation.","type":"string"},"budget_exhausted_at":{"description":"When the introductory grant first refused a spend, if it has.","format":"date-time","nullable":true,"type":"string"},"claimed_at":{"format":"date-time","nullable":true,"type":"string"},"claimed_by_user_id":{"description":"The account that claimed it. Only shown to that account and to the application.","format":"uuid","nullable":true,"type":"string"},"created_at":{"format":"date-time","type":"string"},"expires_at":{"format":"date-time","type":"string"},"grant_cents":{"description":"The introductory grant, in cents. Zero when credits are off.","type":"integer"},"id":{"description":"The grant's id.","format":"uuid","type":"string"},"max_live_sandboxes":{"nullable":true,"type":"integer"},"metadata":{"description":"Whatever the application stored here.","type":"object"},"principal_id":{"description":"The tenant id. Baked into every sandbox name the principal creates, and identical before and after a claim.","format":"uuid","type":"string"},"status":{"enum":["unclaimed","claimed","expired","released"],"type":"string"}},"required":["id","principal_id","application_id","status","expires_at","created_at"],"title":"ClaimableUser","type":"object"},"TeamRenameRequest":{"properties":{"name":{"description":"What to call the teammate. Null or blank means the agent's name.","maxLength":120,"nullable":true,"type":"string"}},"title":"TeamRenameRequest","type":"object"},"AgentResponse":{"properties":{"data":{"$ref":"#/components/schemas/Agent"}},"required":["data"],"title":"AgentResponse","type":"object"},"ApplyRequest":{"properties":{"resources":{"items":{"$ref":"#/components/schemas/ManifestResource"},"type":"array"}},"required":["resources"],"title":"ApplyRequest","type":"object"},"SecretBinding":{"description":"Which host a secret is attached to at the egress broker, and how (ADR 0019). Keyed by the secret's name: it applies wherever an environment or vault holds a secret of that name. A secret with at least one enabled binding reaches the sandbox as a placeholder (`__key__`); one with none reaches it in the clear.","properties":{"auth_type":{"enum":["substitute","bearer","basic","api_key","custom"],"type":"string"},"created_at":{"format":"date-time","type":"string"},"enabled":{"type":"boolean"},"header":{"description":"api_key only: the header the value is sent in. Defaults to Authorization.","nullable":true,"type":"string"},"headers":{"additionalProperties":{"type":"string"},"description":"custom only: header name to template, `{{ KEY }}` is replaced by the secret.","type":"object"},"host":{"description":"Host pattern: `api.example.com`, a one-level wildcard `*.example.com`, optionally `:port` and a `/path/*` glob.","type":"string"},"id":{"format":"uuid","type":"string"},"key":{"description":"The secret's name, UPPER_SNAKE_CASE.","type":"string"},"prefix":{"description":"api_key only: text placed before the value, such as `Token `.","nullable":true,"type":"string"},"updated_at":{"format":"date-time","type":"string"},"username":{"description":"basic only: the username; the secret is the password.","nullable":true,"type":"string"}},"required":["id","key","host","auth_type","headers","enabled","created_at","updated_at"],"title":"SecretBinding","type":"object"},"TeamScheduleUpdateRequest":{"properties":{"cron":{"example":"0 9 * * 1-5","type":"string"},"enabled":{"type":"boolean"},"name":{"maxLength":120,"nullable":true,"type":"string"},"one_off":{"type":"boolean"},"prompt":{"maxLength":20000,"minLength":1,"type":"string"}},"title":"TeamScheduleUpdateRequest","type":"object"},"BuzzAccessUpdateRequest":{"description":"Change who may @-mention a hosted Buzz agent. Sets buzz-acp's inbound author gate on the identity and restarts its harness. At least one field is required. A later provider deploy from the desktop resends the desktop's record and overwrites this.","properties":{"respond_to":{"enum":["owner-only","allowlist","anyone","nobody"],"nullable":true,"type":"string"},"respond_to_allowlist":{"description":"64-hex pubkeys; required non-empty when respond_to is allowlist.","items":{"type":"string"},"nullable":true,"type":"array"}},"title":"BuzzAccessUpdateRequest","type":"object"}},"securitySchemes":{"bearer":{"description":"Per-user API key (`ftn_...`), minted at `POST /api/auth/api-keys` or exchanged at `POST /api/auth/token`. Keys carry scopes and an expiry.","scheme":"bearer","type":"http"}}},"info":{"description":"HTTP API for Fountain. The same surface backs the LiveView UI and the\n`fountain` CLI (`brew install managoat/tap/fountain`); if it's not\nhere, it doesn't exist yet.\n\nAll `/api/*` endpoints require a per-user API key (`ftn_...`) passed as\na bearer token. Mint one at `POST /api/auth/api-keys` (or exchange\nemail + password at `POST /api/auth/token`); keys carry scopes and an\nexpiry.\n","title":"Fountain","version":"0.21.0"},"openapi":"3.0.0","paths":{"/api/catalog":{"get":{"callbacks":{},"description":"Runtimes with model suggestions per runtime (suggestions, not an allowlist — any `provider/model` under a known provider is accepted), sandbox providers usable on this instance and the default, package managers an environment accepts, the URLs of the browser apps this instance sends people to for conversations and the team, and remote MCP servers verified to complete connection discovery (again suggestions — any URL can be discovered), each with the date it was last verified. Also `first_request`, the one onboarding snippet this deployment hands out, with its base URL already in it and the caller's own key and agent left as placeholders.","operationId":"FountainWeb.CatalogController.show","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogResponse"}}},"description":"Catalog"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"The instance's form vocabulary","tags":["Catalog"]}},"/api/environments/{environment_id}/secrets/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.SecretController.delete","parameters":[{"description":"","in":"path","name":"environment_id","required":true,"schema":{"type":"string"}},{"description":"Secret key.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Delete a secret by key","tags":["Secrets"]}},"/api/secret-bindings":{"get":{"callbacks":{},"description":"Every binding on the account: a secret name, the host it is attached to at the egress broker, and the auth shape. A secret with at least one enabled binding reaches the sandbox as a placeholder; one with none reaches it in the clear. Only on a deployment that runs the broker (ADR 0019); 404 otherwise.","operationId":"FountainWeb.SecretBindingController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretBindingListResponse"}}},"description":"Bindings"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Brokerage is not enabled for this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List secret bindings","tags":["Secret bindings"]},"post":{"callbacks":{},"operationId":"FountainWeb.SecretBindingController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretBindingRequest"}}},"description":"Binding","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretBinding"}}},"description":"Binding"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Brokerage is not enabled for this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Bind a secret to a host","tags":["Secret bindings"]}},"/api/sandboxes":{"get":{"callbacks":{},"description":"Every sandbox the caller has provisioned, newest first, each with the conversations on it and which of them is mid-turn. `status` filters by a comma-separated list; without it every status is listed, terminated ones included.","operationId":"FountainWeb.SandboxController.index","parameters":[{"description":"Comma-separated: pending, starting, ready, suspended, terminated, failed.","in":"query","name":"status","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxListResponse"}}},"description":"Sandboxes"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown status"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List sandboxes","tags":["Sandboxes"]}},"/api/vaults/{vault_id}/secrets/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.VaultSecretController.delete","parameters":[{"description":"","in":"path","name":"vault_id","required":true,"schema":{"type":"string"}},{"description":"Secret key.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Delete a vault secret by key","tags":["Vault Secrets"]},"patch":{"callbacks":{},"description":"Changes expiry without replacing the value. Null clears expiry; omission keeps it. Expiry is advisory and does not revoke the credential.","operationId":"FountainWeb.VaultSecretController.update","parameters":[{"description":"","in":"path","name":"vault_id","required":true,"schema":{"type":"string"}},{"description":"Secret key.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretMetadataRequest"}}},"description":"Secret metadata","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretResponse"}}},"description":"Vault Secret"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Update a vault secret's expiry","tags":["Vault Secrets"]}},"/api/auth/verify":{"post":{"callbacks":{},"description":"Idempotent: an already-verified account is a 200, which is what a CLI retrying a request needs. No session is issued — mint a key at `POST /api/auth/token` once the account is live. Tokens last 24 hours.","operationId":"FountainWeb.EmailVerificationController.api_verify","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}},"description":"The emailed token","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEmailResponse"}}},"description":"Verified (or already verified)"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`expired`, `invalid_token`, or a missing token"}},"security":[],"summary":"Activate an account from a verification token","tags":["Auth"]}},"/api/sandbox-queue":{"get":{"callbacks":{},"description":"The caller's waiting requests, oldest first, each with its one-based position. Only requests that are still waiting appear here. One the drainer has already claimed is not listed, and neither is one that finished; read either by id instead.","operationId":"FountainWeb.SandboxQueueController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxRequestListResponse"}}},"description":"Sandbox requests"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List queued sandbox requests","tags":["Sandbox Queue"]}},"/api/team/schedules":{"get":{"callbacks":{},"description":"Every team schedule the caller owns, across teammates, soonest next run first. Times are UTC: `cron` is a five-field expression evaluated in UTC.","operationId":"FountainWeb.TeamScheduleController.index_all","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamScheduleListResponse"}}},"description":"Schedules"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List every schedule of the caller","tags":["Team"]}},"/health":{"get":{"callbacks":{},"description":"Public, unauthenticated. Returns `{\"status\": \"ok\"}` if the app is up. Checks no dependencies by design — ask `/health/ready` whether this instance can actually serve.","operationId":"FountainWeb.HealthController.show","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"description":"Health response"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"}},"security":[],"summary":"Liveness probe","tags":["Health"]}},"/api/sandbox-queue/{id}":{"delete":{"callbacks":{},"description":"Gives up a request that is still waiting. A request the drainer has already claimed reads as 404 rather than being cancelled out from under a start in flight.","operationId":"FountainWeb.SandboxQueueController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Cancelled"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found or no longer queued"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Cancel a queued sandbox request","tags":["Sandbox Queue"]},"get":{"callbacks":{},"description":"The request's current status. `conversation_id` is set once it started; `error` says why if it failed. A request nobody else owns reads as 404.","operationId":"FountainWeb.SandboxQueueController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxRequestResponse"}}},"description":"Sandbox request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get a sandbox request","tags":["Sandbox Queue"]}},"/api/secret-bindings/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.SecretBindingController.delete","parameters":[{"description":"Binding id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such binding, or brokerage is not enabled"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Unbind a secret from a host","tags":["Secret bindings"]},"patch":{"callbacks":{},"operationId":"FountainWeb.SecretBindingController.update","parameters":[{"description":"Binding id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretBindingRequest"}}},"description":"Binding","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretBinding"}}},"description":"Binding"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such binding, or brokerage is not enabled"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Change a binding","tags":["Secret bindings"]}},"/api/environments/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"An agent is mid-turn on a persistent sandbox built on this environment"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Delete an environment","tags":["Environments"]},"get":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get an environment","tags":["Environments"]},"patch":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.EnvironmentController.update (2)","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentUpdate"}}},"description":"Partial environment attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Update an environment (partial)","tags":["Environments"]},"put":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.EnvironmentController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentUpdate"}}},"description":"Partial environment attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Update an environment (partial)","tags":["Environments"]}},"/api/account/exports":{"get":{"callbacks":{},"description":"At most one export exists per account, so this is a zero- or one-element list — the shape matches the rest of the API rather than 404-ing when no export has been requested.","operationId":"FountainWeb.AccountDataController.index_exports","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportListResponse"}}},"description":"Exports"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List account data exports","tags":["Account"]},"post":{"callbacks":{},"description":"Builds asynchronously. At most one export exists per account and one request per hour; a request inside that window is refused with 429 and a `Retry-After` header. Poll `GET /api/account/exports` for status.","operationId":"FountainWeb.AccountDataController.create_export","parameters":[],"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportResponse"}}},"description":"Pending export"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited"}},"summary":"Request an account data export","tags":["Account"]}},"/api/conversations/{conversation_id}/terminate":{"post":{"callbacks":{},"description":"Tears down the sprite and marks the conversation `terminated`. Idempotent for already-dead conversations.","operationId":"FountainWeb.ConversationController.terminate","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Terminated"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox or fleet unavailable"}},"summary":"Terminate a conversation","tags":["Conversations"]}},"/api/sandboxes/{id}":{"delete":{"callbacks":{},"description":"Destroy a persistent sandbox — the agent's home — so the next launch on the same agent, environment and vault builds a clean machine. The conversations on it are kept, idle; each one's next prompt lands on the fresh home. Only a `persistent` sandbox that is not `terminated` or `failed` resets (`422 sandbox_not_resettable`), and not while any conversation on it is mid-turn (`409 sandbox_mid_turn`). A reset the provider does not confirm keeps its fence and the sandbox's capacity, and answers `409 sandbox_reset_pending`; retrying it is safe.","operationId":"FountainWeb.SandboxController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Reset"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"A conversation on it is mid-turn"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a live persistent sandbox"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Another teardown of this sandbox is running"}},"summary":"Reset a sandbox","tags":["Sandboxes"]},"get":{"callbacks":{},"operationId":"FountainWeb.SandboxController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxResponse"}}},"description":"Sandbox"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get a sandbox","tags":["Sandboxes"]}},"/api/account/inference-credential-sets/{id}/credentials/{provider}":{"delete":{"callbacks":{},"operationId":"FountainWeb.InferenceCredentialController.delete_in_set","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"","in":"path","name":"provider","required":true,"schema":{"enum":["anthropic_api_key","claude_code_oauth_token","openai_api_key","gemini_api_key"],"type":"string"}}],"responses":{"204":{"description":"Cleared"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such set"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown provider"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Clear a provider credential inside a named set","tags":["Inference credentials"]},"put":{"callbacks":{},"description":"The same validate-then-store path as PUT /inference-credentials/:provider, against a set the caller names instead of the account's default (ADR 0053 decision 1). This is what puts a second subscription's key somewhere an agent can point at.","operationId":"FountainWeb.InferenceCredentialController.update_in_set","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"","in":"path","name":"provider","required":true,"schema":{"enum":["anthropic_api_key","claude_code_oauth_token","openai_api_key","gemini_api_key"],"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InferenceCredentialRequest"}}},"description":"Credential","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InferenceCredentialResponse"}}},"description":"Provider status for the set"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such set"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rejected credential, blank value, or unknown provider"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"502":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Provider unreachable"},"504":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Provider timed out"}},"summary":"Set a provider credential inside a named set","tags":["Inference credentials"]}},"/api/conversations/{conversation_id}/requests/{request_id}":{"post":{"callbacks":{},"description":"Answers a `session/request_permission` the agent is blocked on (#940). The request and its options arrive as a `permission_request` block on the conversation's event stream; `option_id` must be one of the `optionId` values that block carried. Never send an option the agent did not offer.\n\nFirst answer wins: another attached client, the timeout, or the turn ending may already have resolved it, and all of those return 409. The resolution appears on the stream as a `request` stage event with state `done`.\n\nA request that outlived its turn (#1635) is answered here too. The agent ended that turn with stop reason `waiting`, so the conversation is idle and the sandbox may be suspended; GET /api/conversations/{id} lists such requests as `pending_requests`. Answering one resolves it and opens a new turn carrying the request id and the option, which wakes the sandbox.","operationId":"FountainWeb.ConversationController.answer_request","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}},{"description":"","in":"path","name":"request_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermissionAnswerRequest"}}},"description":"Answer","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermissionAnswerResponse"}}},"description":"Answered"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Busy"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"402":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient credits"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The sandbox may not answer"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Already resolved, or resolved but not delivered"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown option"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Answer a permission request","tags":["Conversations"]}},"/api/account/exports/{id}/download":{"get":{"callbacks":{},"description":"The gzipped JSON payload, served with `content-encoding: gzip`. 404 covers every not-downloadable case identically — wrong tenant, missing id, still pending, failed, expired — so nothing about other tenants' artifacts is inferable.","operationId":"FountainWeb.AccountDataController.download_export","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Export payload"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Download a completed export","tags":["Account"]}},"/api/admin/audit":{"get":{"callbacks":{},"description":"Every tenant's events, newest first — the unscoped view behind `/audit` for admins. Use `GET /api/audit` for the caller's own trail.","operationId":"FountainWeb.AdminController.index_audit","parameters":[{"description":"1..500, default 100.","in":"query","name":"limit","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminAuditListResponse"}}},"description":"Audit events"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Admin required"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Cross-tenant audit events","tags":["Admin"]}},"/api/agents":{"get":{"callbacks":{},"operationId":"FountainWeb.AgentController.index","parameters":[{"description":"Case-insensitive substring match on the agent name.","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Comma-separated runtimes, e.g. `claude,codex`.","in":"query","name":"runtime","required":false,"schema":{"type":"string"}},{"description":"Comma-separated environment ids.","in":"query","name":"environment_id","required":false,"schema":{"type":"string"}},{"description":"Only agents with at least one skill.","in":"query","name":"has_skills","required":false,"schema":{"type":"boolean"}},{"description":"Only agents with at least one MCP server.","in":"query","name":"has_mcp","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentListResponse"}}},"description":"Agents"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List agents","tags":["Agents"]},"post":{"callbacks":{},"operationId":"FountainWeb.AgentController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRequest"}}},"description":"Agent attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Create an agent","tags":["Agents"]}},"/api/secret-bindings/presets":{"get":{"callbacks":{},"description":"The broker's service catalog: known hosts with their auth shape and the secret name each usually goes by. Suggestions the console prefills from; a binding is still yours to save.","operationId":"FountainWeb.SecretBindingController.presets","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretBindingPresetListResponse"}}},"description":"Presets"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Brokerage is not enabled for this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List binding presets","tags":["Secret bindings"]}},"/api/environments/{environment_id}/secrets":{"get":{"callbacks":{},"operationId":"FountainWeb.SecretController.index","parameters":[{"description":"","in":"path","name":"environment_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretListResponse"}}},"description":"Secrets"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List secrets in an environment","tags":["Secrets"]},"post":{"callbacks":{},"description":"Sets the value for `key`. If the key exists, the value is overwritten. Values are write-only — subsequent reads never return them.","operationId":"FountainWeb.SecretController.create","parameters":[{"description":"","in":"path","name":"environment_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretRequest"}}},"description":"Secret","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretResponse"}}},"description":"Secret"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Upsert a secret","tags":["Secrets"]}},"/api/conversations/{conversation_id}/prompts":{"post":{"callbacks":{},"description":"Queues a new turn. If the ConversationServer has been GC'd (e.g. across a BEAM restart) a fresh sprite is provisioned and the runtime resumes via its session id.","operationId":"FountainWeb.ConversationController.prompt","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptRequest"}}},"description":"Prompt","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptResponse"}}},"description":"Queued"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Busy"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"402":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient credits"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox is being reset or deleted"},"410":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Conversation is terminal"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid request parameters"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox or fleet unavailable"}},"summary":"Send another prompt","tags":["Conversations"]}},"/api/vaults/{id}/copy":{"post":{"callbacks":{},"description":"Creates a new vault holding a copy of every secret in the source vault. Values are copied server-side and re-encrypted; no value is ever returned. `name` is required; `description` and `metadata` default to the source's. Atomic: on any failure no new vault exists. A source vault the caller does not own reads as not found.","operationId":"FountainWeb.VaultController.copy","parameters":[{"description":"Source vault.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultRequest"}}},"description":"New vault attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Copy a vault","tags":["Vaults"]}},"/api/support/reports/{id}":{"get":{"callbacks":{},"operationId":"FountainWeb.SupportReportController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportReportResponse"}}},"description":"Report"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid API key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Show one report","tags":["Support"]}},"/api/account/exports/{id}":{"get":{"callbacks":{},"operationId":"FountainWeb.AccountDataController.show_export","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportResponse"}}},"description":"Export"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get an export's status","tags":["Account"]}},"/api/conversations/{conversation_id}/read":{"post":{"callbacks":{},"description":"Sets the conversation's `last_read_at` to now, which is what clears its unread state. Idempotent.","operationId":"FountainWeb.ConversationController.read","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Marked read"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Mark a conversation read","tags":["Conversations"]}},"/api/connection-providers":{"get":{"callbacks":{},"description":"The platform providers (Google, Microsoft, Slack) followed by the tenant's own. Each carries the redirect URI to register at the service and the env var its tokens are brokered under. Only for accounts the egress broker is on for (ADR 0019); 404 otherwise.","operationId":"FountainWeb.ConnectionProviderController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionProviderListResponse"}}},"description":"Providers"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Connections are not enabled for this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List connection providers","tags":["Connections"]},"post":{"callbacks":{},"description":"`kind: oauth2` takes the tenant's own app registration: authorize and token URLs, scopes, client id and secret. `kind: mcp` takes only `mcp_url`: Fountain fetches the server's protected-resource metadata (RFC 9728), the authorization server's metadata (RFC 8414) and registers a client there (RFC 7591) where it can; pass `client_id` and `client_secret` for a server without registration. Every URL must be https and public.","operationId":"FountainWeb.ConnectionProviderController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionProviderRequest"}}},"description":"Provider","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionProvider"}}},"description":"Provider"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Connections are not enabled for this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation or discovery failed"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Define a connection provider","tags":["Connections"]}},"/api/vaults":{"get":{"callbacks":{},"operationId":"FountainWeb.VaultController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultListResponse"}}},"description":"Vaults"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List vaults","tags":["Vaults"]},"post":{"callbacks":{},"operationId":"FountainWeb.VaultController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultRequest"}}},"description":"Vault attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Create a vault","tags":["Vaults"]}},"/api/connections/providers":{"get":{"callbacks":{},"description":"Every provider this account can connect — the platform ones (Google, Microsoft, Slack; #1299), and the tenant's own (#1186) — with the scopes each asks for, the env var its token is brokered under, and the console URL that starts the flow (a browser signed in as the account owner). The same list as `GET /api/connection-providers`.","operationId":"FountainWeb.ConnectionController.providers","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionProviderListResponse"}}},"description":"Providers"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Connections are not enabled for this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List connectable providers","tags":["Connections"]}},"/api/auth/resend-verification":{"post":{"callbacks":{},"description":"Always 200 with the same message whether the address is unknown, unverified, or already verified — this endpoint is not an account oracle. Rate-limited to 5 per IP per hour in its own bucket, so retrying resend does not burn the registration budget.","operationId":"FountainWeb.RegistrationController.api_resend","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailRequest"}}},"description":"Address to resend to","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"Accepted"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"}},"security":[],"summary":"Resend the verification email","tags":["Auth"]}},"/api/conversations/{conversation_id}/turns/{turn_id}/images/{position}":{"get":{"callbacks":{},"description":"The image bytes, with the stored media type. `position` is the zero-based index into the turn's images; `image_count` on the turn (from `GET /api/conversations/{id}/turns`) says how many there are. 404 covers every miss — unknown conversation, a turn belonging to a different conversation, an absent position, and a stored media type that is not an image — so this is not a probe for ids.","operationId":"FountainWeb.TurnImageController.api_show","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}},{"description":"","in":"path","name":"turn_id","required":true,"schema":{"type":"string"}},{"description":"Zero-based index into the turn's images.","in":"path","name":"position","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"content":{"image/*":{"schema":{"format":"binary","type":"string"}}},"description":"Image bytes"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such image"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Fetch an image attached to a turn","tags":["Conversations"]}},"/api/connections/{id}":{"delete":{"callbacks":{},"description":"Tells the provider to forget the grant, then deletes the row. The next tool call from an agent that names it fails with `connection revoked`.","operationId":"FountainWeb.ConnectionController.delete","parameters":[{"description":"Connection id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Revoke and delete a connection","tags":["Connections"]},"get":{"callbacks":{},"operationId":"FountainWeb.ConnectionController.show","parameters":[{"description":"Connection id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Connection"}}},"description":"Connection"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get a connection","tags":["Connections"]}},"/api/account/inference-credential-sets/{id}":{"delete":{"callbacks":{},"description":"Refused for the default set: something has to answer which credential runs this account. Promote another first. Deleting a set removes its credentials; conversations bound to that source refuse to resume with inference_source_changed.","operationId":"FountainWeb.InferenceCredentialSetController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such set"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The default set cannot be deleted"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Delete an inference credential set","tags":["Inference credentials"]},"patch":{"callbacks":{},"description":"Omitting a field leaves it alone. `is_default: false` is refused: a set stops being the default when another becomes it, never on its own, because an account with no default has nothing to read a credential from. `chatgpt_grant_id` names one of the account's ChatGPT subscriptions for the set's codex runs, and `null` stops naming one. Naming one ends the set's running codex conversations with `inference_source_changed`: their source is now the subscription. An id that is not one of the account's subscriptions, and a disconnected one, are 422; sending the id the set already names changes nothing.","operationId":"FountainWeb.InferenceCredentialSetController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InferenceCredentialSetUpdateRequest"}}},"description":"Changes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InferenceCredentialSetResponse"}}},"description":"Credential set"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such set"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or duplicate name"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Rename an inference credential set, make it the default, or name a ChatGPT subscription","tags":["Inference credentials"]}},"/api/auth/forgot":{"post":{"callbacks":{},"description":"Always 200 with the same message, registered address or not — this is not an enumeration oracle. Rate-limited to 5 per IP per hour. The emailed link points at the browser page; the token in it also works at `POST /api/auth/reset`.","operationId":"FountainWeb.PasswordResetController.api_forgot","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailRequest"}}},"description":"Address to reset","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"Accepted"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"}},"security":[],"summary":"Request a password-reset email","tags":["Auth"]}},"/api/admin/users/{id}/credits":{"post":{"callbacks":{},"description":"A `grant_admin` ledger row (ADR 0030): goodwill, a won dispute, an outage. It never expires and is spent after the opening grant. There is deliberately no negative form here; a clawback is what a refund or dispute does through Stripe.","operationId":"FountainWeb.AdminController.grant_credits","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCreditsRequest"}}},"description":"Credit","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserResponse"}}},"description":"Account"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Admin required"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Refused"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Add prepaid credit to an account","tags":["Admin"]}},"/api/team/{agent_id}/messages":{"post":{"callbacks":{},"description":"A turn on the teammate's conversation. A parked or reaped sandbox wakes; a conversation past resuming is replaced by a fresh one under the same binding, seeded with this message, so the response names the conversation the message went to. 400 `conversation_busy` while the previous turn is still running (the same shape as `POST /api/conversations/:id/prompts`), 503 while the computer is still starting.\n\n`labels` merges onto the conversation the message lands on (#1637), before the turn is queued, so a label the limits refuse leaves the message unsent.","operationId":"FountainWeb.TeamController.message","parameters":[{"description":"","in":"path","name":"agent_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamMessageRequest"}}},"description":"Message","required":false},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamMessageResponse"}}},"description":"Queued"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"A turn is still running"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"A sandbox token labelling another conversation"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not on the team"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid labels"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Message a teammate","tags":["Team"]}},"/api/auth/reset":{"post":{"callbacks":{},"description":"Takes the token out of the reset email, so a CLI can prompt for it instead of opening a browser. A successful reset bumps `session_version`: every browser session dies, and so does every other outstanding reset token for the account. Rate-limited to 10 per IP per hour.","operationId":"FountainWeb.PasswordResetController.api_reset","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetRequest"}}},"description":"Token and new password","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"Password updated"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`expired`, `invalid_token`, missing fields, or a password that fails validation"}},"security":[],"summary":"Set a new password from a reset token","tags":["Auth"]}},"/api/account":{"delete":{"callbacks":{},"description":"Irreversible. Cancels billing, destroys sandboxes, deletes every resource and the tenant encryption key. Requires `{\"confirm\": \"<your account email>\"}` in the body — the API equivalent of the UI's typed-email gate — and a `full`-scoped key.","operationId":"FountainWeb.AccountDataController.delete_account","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountDeleteRequest"}}},"description":"Confirmation","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountDeletedResponse"}}},"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Confirmation mismatch"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"502":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Billing cancellation failed"}},"summary":"Delete the account and everything in it","tags":["Account"]}},"/api/team":{"get":{"callbacks":{},"description":"One entry per agent on the team, most recently active first: the agent, its current conversation (the newest live one, or the newest finished one when none is live), presence, unread state and the roster preview. A teammate is a conversation bound to the reserved channel `fountain:team`.","operationId":"FountainWeb.TeamController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeammateListResponse"}}},"description":"Team"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List the team","tags":["Team"]},"post":{"callbacks":{},"description":"Opens the agent's team conversation — which provisions its sandbox — with an optional name (the conversation title), environment (provision from it instead of the agent's own; must satisfy `allowed_environment_ids`) and vault (must satisfy `allowed_vault_ids`). 201 with the teammate; 200 when the agent was already on the team (its live conversation is returned, the attributes ignored).","operationId":"FountainWeb.TeamController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamAddRequest"}}},"description":"Add attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeammateResponse"}}},"description":"Teammate (already on the team)"},"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeammateResponse"}}},"description":"Teammate"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown agent, environment or vault"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not allowed by the agent"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Add an agent to the team","tags":["Team"]}},"/api/runners":{"get":{"callbacks":{},"description":"Every machine that has ever connected as `fountain runner` for this account, newest connection first, with `online` reflecting whether it holds a connection right now. Sandboxes for an agent whose `sandbox_provider` is `runner` are placed on the most recently connected online runner.","operationId":"FountainWeb.RunnerController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunnerListResponse"}}},"description":"Runners"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List self-hosted runners","tags":["Runners"]}},"/health/ready":{"get":{"callbacks":{},"description":"Public, unauthenticated. Returns 200 when this instance can serve requests, or 503 when a dependency it cannot work without is unavailable. Individual checks report `ok` or `error` and nothing further.","operationId":"FountainWeb.HealthController.ready","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadinessResponse"}}},"description":"Ready"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadinessResponse"}}},"description":"Not ready"}},"security":[],"summary":"Readiness probe","tags":["Health"]}},"/api/account/inference-credentials":{"get":{"callbacks":{},"description":"Reports which providers have a credential set. Values are never returned — not even truncated.","operationId":"FountainWeb.InferenceCredentialController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InferenceCredentialListResponse"}}},"description":"Provider status"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List inference-credential status per provider","tags":["Inference credentials"]}},"/api/account/chatgpt-subscriptions/{id}/disconnect":{"post":{"callbacks":{},"description":"Forgets the subscription's tokens and keeps its row. From the moment this returns no sandbox request may use it, inside a connection that was already open too. A credential set that names it keeps naming it, and its codex runs fail by name until the subscription is reconnected or the set is pointed elsewhere; nothing is substituted. Already disconnected is a 200. The token is not revoked at ChatGPT.","operationId":"FountainWeb.ChatGPTSubscriptionController.disconnect","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatGPTSubscriptionResponse"}}},"description":"Subscription"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such subscription"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Disconnect a ChatGPT subscription","tags":["ChatGPT subscriptions"]}},"/api/agents/{id}/versions/{version}":{"get":{"callbacks":{},"description":"One version by number, with its full config. A conversation's `agent_version` names the number to look up here.","operationId":"FountainWeb.AgentVersionController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"1-based.","in":"path","name":"version","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersionResponse"}}},"description":"Version"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Agent or version not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid request parameters"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get one config version of an agent","tags":["Agents"]}},"/api/webhooks/{id}":{"delete":{"callbacks":{},"description":"Its delivery log goes with it. Queued deliveries are dropped.","operationId":"FountainWeb.WebhookEndpointController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such endpoint on this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Delete a webhook endpoint","tags":["Webhooks"]},"get":{"callbacks":{},"operationId":"FountainWeb.WebhookEndpointController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointResponse"}}},"description":"The endpoint"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such endpoint on this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get one webhook endpoint","tags":["Webhooks"]},"patch":{"callbacks":{},"description":"Any subset of url, description, event_types and status. Setting `status` to `active` on an endpoint that was auto-disabled also clears its failure count.","operationId":"FountainWeb.WebhookEndpointController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointUpdateRequest"}}},"description":"Fields to change","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointResponse"}}},"description":"The endpoint"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such endpoint on this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid URL or event filter"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Update a webhook endpoint","tags":["Webhooks"]}},"/api/auth/register":{"post":{"callbacks":{},"description":"Creates the account and sends the verification email. The account cannot mint an API key until it is verified, so a headless bootstrap is register → `POST /api/auth/verify` with the emailed token → `POST /api/auth/token`. Rate-limited to 5 per IP per hour. On an instance with open registration disabled this answers 403 with a reason code; an instance that asks for an access code answers 403 `access_code_required` until `access_code` carries it.","operationId":"FountainWeb.RegistrationController.api_create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"description":"Credentials","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterResponse"}}},"description":"Account created"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Registration refused"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing fields or validation errors"}},"security":[],"summary":"Register an account","tags":["Auth"]}},"/api/claimable-users":{"post":{"callbacks":{},"description":"Creates an anonymous tenant an application can build a computer inside before its visitor has a Fountain account, and returns a scoped API key for it plus a one-time claim token. The principal is funded out of the calling account's credit balance and expires on its own. Send an `Idempotency-Key` header: replaying it returns the same principal with a fresh key and claim token.","operationId":"FountainWeb.ClaimableUserController.create","parameters":[{"description":"Replay-safe creation key, unique per application.","in":"header","name":"Idempotency-Key","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"application_id":{"description":"The application's own label for itself, for the audit trail.","type":"string"},"expires_in":{"description":"Seconds until the principal expires. Clamped to the deployment's maximum.","type":"integer"},"limits":{"properties":{"max_cost_usd":{"type":"number"},"max_live_sandboxes":{"type":"integer"}},"type":"object"},"metadata":{"type":"object"}},"required":["application_id"],"type":"object"}}},"description":"Grant","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimableUserCreatedResponse"}}},"description":"Opened"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Malformed grant"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"402":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The application cannot fund it"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a full-scope key"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too many principals, or too fast"}},"summary":"Open a claimable principal","tags":["Claimable principals"]}},"/api/conversations":{"get":{"callbacks":{},"operationId":"FountainWeb.ConversationController.index","parameters":[{"description":"Exclude sub-conversations (those with a `parent_conversation_id`), leaving only top-level sessions. Defaults to false.","in":"query","name":"roots_only","required":false,"schema":{"type":"boolean"}},{"description":"Only this agent's conversations (#832).","in":"query","name":"agent_id","required":false,"schema":{"type":"string"}},{"description":"Only conversations on this sandbox. Combined with the other filters.","in":"query","name":"sandbox_id","required":false,"schema":{"format":"uuid","type":"string"}},{"description":"Only conversations bound to this channel — `fountain:team` for the team's. A conversation unbound by removing its teammate no longer matches; a teammate's full history is `GET /api/team/:agent_id/conversations`.","in":"query","name":"channel_id","required":false,"schema":{"type":"string"}},{"description":"Comma-separated statuses to keep (`idle,terminated`); 400 on a value outside the vocabulary.","in":"query","name":"status","required":false,"schema":{"type":"string"}},{"description":"Return at most this many conversations, most recently updated first (1 to 500; 400 outside that range). Without it the whole list is returned, which on a busy account is hundreds of rows per call — a client that needs one conversation should filter (`agent_id`, `sandbox_id`, `channel_id`) and cap.","in":"query","name":"limit","required":false,"schema":{"maximum":500,"minimum":1,"type":"integer"}},{"description":"Only conversations carrying these `key:value` labels (#1637). Repeat the parameter to combine them with AND: `?label=env:prod&label=drift:true` keeps the conversations that carry both. `label[]=` is accepted as well. Each value splits on its first colon only, so `label=path:a:b` matches the label `path` with the value `a:b`. 400 `invalid_label_filter` on a value with no colon or an empty key.","explode":true,"in":"query","name":"label","required":false,"schema":{"items":{"type":"string"},"type":"array"},"style":"form"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationListResponse"}}},"description":"Conversations"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown status or limit out of range"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid filter"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List conversations","tags":["Conversations"]},"post":{"callbacks":{},"description":"Creates a sandbox + conversation pair, starts the runtime in a fresh sprite, and (if `prompt` is supplied) sends it as turn 1. With `channel_id`, resumes the latest live conversation already bound to that channel for the same agent and vault (200, `meta.resumed: true`) instead of opening a new one (201). `labels` (#1637) are stamped on the new conversation; with `channel_id`, a resume merges them into the conversation it hands back, and a sandbox callback token resuming a conversation it was not minted for is refused with 403. Pass `X-Fountain-Parent-Conversation-Id` header to record which conversation spawned this one.","operationId":"FountainWeb.ConversationController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationCreateRequest"}}},"description":"Conversation attrs","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}},"description":"Conversation (resumed by channel_id)"},"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}},"description":"Conversation"},"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxRequestResponse"}}},"description":"Queued for sandbox capacity"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"402":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient credits"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"A sandbox token labelling the conversation a resume landed on, or a key below full scope attaching another agent's conversation to a sandbox (guest_attach_requires_full_scope)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Agent not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Conflicting state, changed inference source, or incompatible Codex sandbox credential"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Tenant concurrency cap reached"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox or fleet unavailable"}},"summary":"Start a conversation","tags":["Conversations"]}},"/api/audit":{"get":{"callbacks":{},"description":"Newest first, cursor-paginated: pass the previous page's `meta.next_cursor` as `before`. Only this tenant's events; system and cross-tenant rows are not visible here.","operationId":"FountainWeb.AuditController.index","parameters":[{"description":"Page size, 1..500. Defaults to 100.","in":"query","name":"limit","required":false,"schema":{"type":"integer"}},{"description":"Return events older than this event id.","in":"query","name":"before","required":false,"schema":{"type":"integer"}},{"description":"Match actions starting with this string, e.g. `vault.` for every vault event. Treated as a literal — LIKE metacharacters do not apply.","in":"query","name":"action_prefix","required":false,"schema":{"type":"string"}},{"description":"Exact match, e.g. `secret`, `vault_secret`, `conversation`.","in":"query","name":"resource_type","required":false,"schema":{"type":"string"}},{"description":"ISO 8601 timestamp; events at or after it.","in":"query","name":"since","required":false,"schema":{"type":"string"}},{"description":"ISO 8601 timestamp; events at or before it.","in":"query","name":"until","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditEventListResponse"}}},"description":"Audit events"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Malformed since/until"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List the account's audit events","tags":["Audit"]}},"/api/team/{agent_id}/schedules":{"get":{"callbacks":{},"operationId":"FountainWeb.TeamScheduleController.index","parameters":[{"description":"","in":"path","name":"agent_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamScheduleListResponse"}}},"description":"Schedules"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List one teammate's schedules","tags":["Team"]},"post":{"callbacks":{},"description":"`cron` is five fields in UTC (`0 9 * * 1-5` is 09:00 UTC on weekdays; `@daily`-style names work, `@reboot` does not). `one_off: false` (the default) sends the prompt into the teammate's own conversation as a typed message would; `one_off: true` opens a fresh conversation on a new computer each run, with the teammate's agent, environment and vault. The agent must be the caller's; it need not be on the team yet (an in-thread schedule then fails each run with `agent is not on the team` until it is). Audited as `team.schedule.created`.","operationId":"FountainWeb.TeamScheduleController.create","parameters":[{"description":"","in":"path","name":"agent_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamScheduleCreateRequest"}}},"description":"Schedule attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamScheduleResponse"}}},"description":"Schedule"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown agent"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation errors"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Create a schedule for a teammate","tags":["Team"]}},"/api/buzz/agents/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.BuzzAgentController.delete","parameters":[{"description":"Buzz agent id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid API key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Tear down a hosted Buzz agent","tags":["Buzz"]},"patch":{"callbacks":{},"operationId":"FountainWeb.BuzzAgentController.update (2)","parameters":[{"description":"Buzz agent id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuzzAccessUpdateRequest"}}},"description":"Access attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuzzIdentityResponse"}}},"description":"Buzz agent"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid API key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Change who may talk to a hosted Buzz agent","tags":["Buzz"]},"put":{"callbacks":{},"operationId":"FountainWeb.BuzzAgentController.update","parameters":[{"description":"Buzz agent id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuzzAccessUpdateRequest"}}},"description":"Access attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuzzIdentityResponse"}}},"description":"Buzz agent"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid API key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Change who may talk to a hosted Buzz agent","tags":["Buzz"]}},"/api/account/billing/credits/checkout":{"post":{"callbacks":{},"description":"A one-time payment for one of the packs this deployment sells (`GET /api/account/billing` lists them under `credits.packs_cents`). The balance moves when Stripe's webhook confirms payment, not when this returns. Refused for a comped account with `comped`, and for an amount that is not a pack with `unknown_pack`.","operationId":"FountainWeb.BillingApiController.credits_checkout","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditsCheckoutRequest"}}},"description":"Pack","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeUrlResponse"}}},"description":"Stripe URL"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Billing disabled"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Refused"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"502":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Stripe unreachable"}},"summary":"Mint a Stripe Checkout URL for a credit pack","tags":["Billing"]}},"/api/admin/users":{"get":{"callbacks":{},"operationId":"FountainWeb.AdminController.index_users","parameters":[{"description":"Email substring.","in":"query","name":"q","required":false,"schema":{"type":"string"}},{"description":"Only comped accounts (`true`) or only billed ones (`false`).","in":"query","name":"comped","required":false,"schema":{"type":"boolean"}},{"description":"","in":"query","name":"role","required":false,"schema":{"enum":["admin","user"],"type":"string"}},{"description":"","in":"query","name":"verified","required":false,"schema":{"type":"boolean"}},{"description":"","in":"query","name":"sort","required":false,"schema":{"enum":["email","joined","last_activity"],"type":"string"}},{"description":"","in":"query","name":"dir","required":false,"schema":{"enum":["asc","desc"],"type":"string"}},{"description":"","in":"query","name":"page","required":false,"schema":{"type":"integer"}},{"description":"1..100, default 25.","in":"query","name":"per_page","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserListResponse"}}},"description":"Accounts"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Admin required"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid request parameters"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List accounts (admin)","tags":["Admin"]}},"/api/claimable-users/{id}":{"delete":{"callbacks":{},"description":"Revokes the principal's credentials, destroys its sandboxes and refunds what its introductory grant still holds. The grant stays readable so a lost response can still be reconciled; the principal itself is deleted a retention window later. A claimed principal belongs to the account that claimed it and cannot be released this way.","operationId":"FountainWeb.ClaimableUserController.delete","parameters":[{"description":"The grant's id.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Released"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such grant"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Already claimed"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Abandon a claimable principal","tags":["Claimable principals"]},"get":{"callbacks":{},"description":"The grant's current status, for an application recovering from a lost response. Readable by the application that opened it and by the account that claimed it; anyone else gets 404, so an id cannot be probed.","operationId":"FountainWeb.ClaimableUserController.show","parameters":[{"description":"The grant's id.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimableUserResponse"}}},"description":"The grant"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such grant"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Read a claimable principal","tags":["Claimable principals"]}},"/api/agents/{id}/versions":{"get":{"callbacks":{},"description":"The agent's config history (ADR 0029), newest first. A version is written on create and on every update that changes a config field, so version 1 is the config the agent was created with. Read-only: rollback is a console action, and applies a version's config as a new edit rather than rewriting history.","operationId":"FountainWeb.AgentVersionController.index","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersionListResponse"}}},"description":"Versions"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Agent not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List an agent's config versions","tags":["Agents"]}},"/api/auth/me":{"get":{"callbacks":{},"description":"What the presented bearer token resolves to. `fountain auth whoami` calls this to show which account a key belongs to.","operationId":"FountainWeb.AuthMeController.show","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthMeResponse"}}},"description":"The account"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Identity of the authenticated account","tags":["Auth"]}},"/api/runners/{id}":{"delete":{"callbacks":{},"description":"Removes the row and disconnects a live daemon. The machine is not touched — a daemon left running reconnects and re-registers under the same name — and sandbox rows that lived on it are left alone.","operationId":"FountainWeb.RunnerController.delete","parameters":[{"description":"Runner id.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Forgotten"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such runner"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Forget a runner","tags":["Runners"]}},"/api/agents/{id}/avatar":{"delete":{"callbacks":{},"description":"Idempotent — an agent with no avatar is still a 204.","operationId":"FountainWeb.AgentAvatarController.api_delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Delete an agent's avatar","tags":["Agents"]},"get":{"callbacks":{},"description":"The image bytes, with the stored media type. 404 when the agent has no avatar — the same answer as an agent that does not exist, so this is not a probe for ids.","operationId":"FountainWeb.AgentAvatarController.api_show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"image/*":{"schema":{"format":"binary","type":"string"}}},"description":"Image bytes"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No avatar"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Fetch an agent's avatar","tags":["Agents"]},"put":{"callbacks":{},"description":"Send the raw image bytes with an image content-type (`image/png`, `image/jpeg`, `image/gif`, `image/webp`), or JSON with base64 `data` and a `media_type`. Replaces any existing avatar. 5 MB maximum.","operationId":"FountainWeb.AgentAvatarController.api_update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvatarRequest"}}},"description":"Image","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"413":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too large"},"415":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unsupported type"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid image"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Upload an agent's avatar","tags":["Agents"]}},"/api/oauth/token":{"post":{"callbacks":{},"description":"OAuth 2.0 authorization code grant with PKCE (S256), for registered public clients — Fountain's own browser apps on other origins. The user consented at `/oauth/authorize`; this exchanges the resulting `code` plus the `code_verifier` for a full-scope API key that expires in 30 days and lists under Account → API keys as `oauth:<client_id>`. Every way a grant can be wrong (unknown, used, expired, wrong client, wrong redirect_uri, wrong verifier) is one answer: 400 `invalid_grant`. Rate-limited to 30 attempts per IP per hour.","operationId":"FountainWeb.OAuthTokenController.token","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthTokenRequest"}}},"description":"Token request","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthTokenResponse"}}},"description":"Token"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"invalid_grant / unsupported_grant_type"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"}},"summary":"Exchange an authorization code for an API key","tags":["Auth"]}},"/api/admin/sandboxes/{id}/reap":{"post":{"callbacks":{},"description":"`terminated` when live conversations were ended with it, `released` when the sprite went and the conversations stay resumable, `already_terminal` when there was nothing to do.","operationId":"FountainWeb.AdminController.reap_sandbox","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminReapResponse"}}},"description":"Outcome"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Admin required"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Another teardown holds this machine; retry"}},"summary":"Reap a sandbox","tags":["Admin"]}},"/api/oauth/clients/{id}":{"delete":{"callbacks":{},"description":"Deleting a client stops new sign-ins through it. Keys it already issued are ordinary API keys and outlive it — revoke those under the API keys endpoint. A published client can only be removed by an operator, because every account signs in through it.","operationId":"FountainWeb.OAuthClientController.delete","parameters":[{"description":"Client record id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such client"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Refused"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Unregister an OAuth client","tags":["OAuth clients"]},"get":{"callbacks":{},"operationId":"FountainWeb.OAuthClientController.show","parameters":[{"description":"Client record id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthClient"}}},"description":"Client"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such client"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get an OAuth client","tags":["OAuth clients"]},"patch":{"callbacks":{},"description":"Rename the client or replace its redirect URIs. `client_id` never changes. Publishing is not a self-serve operation, and a published client can only be changed by an operator.","operationId":"FountainWeb.OAuthClientController.update","parameters":[{"description":"Client record id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthClientUpdateRequest"}}},"description":"Client changes","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthClient"}}},"description":"Client"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such client"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Change an OAuth client","tags":["OAuth clients"]}},"/api/admin/users/{id}/role":{"post":{"callbacks":{},"operationId":"FountainWeb.AdminController.set_role","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRoleRequest"}}},"description":"Role","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserResponse"}}},"description":"Account"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Admin required"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Refused"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Grant or revoke the admin role","tags":["Admin"]}},"/api/conversations/{id}":{"delete":{"callbacks":{},"description":"Tears down the sprite if alive, then deletes the conversation row (cascades to turns and log events).","operationId":"FountainWeb.ConversationController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Delete a conversation","tags":["Conversations"]},"get":{"callbacks":{},"operationId":"FountainWeb.ConversationController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}},"description":"Conversation"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get a conversation","tags":["Conversations"]}},"/api/claimable-users/{id}/inference-credentials/{provider}":{"delete":{"callbacks":{},"operationId":"FountainWeb.ClaimableUserController.delete_inference_credential","parameters":[{"description":"The grant's id.","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"","in":"path","name":"provider","required":true,"schema":{"enum":["anthropic_api_key","claude_code_oauth_token","openai_api_key","gemini_api_key"],"type":"string"}}],"responses":{"204":{"description":"Cleared"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a full-scope key"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such grant"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown provider"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Clear a provider credential on a principal","tags":["Claimable principals"]},"put":{"callbacks":{},"description":"Stores the credential encrypted under the principal's own tenant key. A principal is a first-class tenant and a `principal`-scoped key cannot write account state. Only the current owner may write: the opening application before claim, and the claiming account afterward. Expired and released grants refuse writes. The principal's own credential gains nothing from this route.","operationId":"FountainWeb.ClaimableUserController.put_inference_credential","parameters":[{"description":"The grant's id.","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"","in":"path","name":"provider","required":true,"schema":{"enum":["anthropic_api_key","claude_code_oauth_token","openai_api_key","gemini_api_key"],"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InferenceCredentialRequest"}}},"description":"Credential","required":false},"responses":{"204":{"description":"Stored"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a full-scope key"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such grant"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Blank value or unknown provider"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Set a provider credential on a principal","tags":["Claimable principals"]}},"/api/webhooks/{id}/deliveries/{delivery_id}/redeliver":{"post":{"callbacks":{},"description":"A fresh job with a fresh attempt counter, against the endpoint's current URL and current secret. The payload is the one that was recorded.","operationId":"FountainWeb.WebhookEndpointController.redeliver","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"","in":"path","name":"delivery_id","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookTestResponse"}}},"description":"Queued"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such endpoint or delivery on this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Send one recorded event again","tags":["Webhooks"]}},"/api/sandboxes/{sandbox_id}/files":{"get":{"callbacks":{},"description":"The entries of one directory, directories first then by name. Without `path`, the agent's working directory. Only a `ready` sandbox answers (`409 sandbox_not_ready`): a parked one is not woken for a read. A sandbox being parked or destroyed is `503 sandbox_unavailable`; retry after the `Retry-After`. Full scope.","operationId":"FountainWeb.SandboxFilesController.index","parameters":[{"description":"","in":"path","name":"sandbox_id","required":true,"schema":{"type":"string"}},{"description":"In-sandbox path, absolute or relative to the agent's working directory (`/home/sprite` for claude and codex, `/tmp/gemini-workspace` and `/tmp/opencode-workspace` for the others). Confined to those directories: anything else is `422 path_outside_sandbox`.","in":"query","name":"path","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxListingResponse"}}},"description":"Directory listing"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such sandbox or path"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox is not ready"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a directory, or outside the sandbox"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox provider unreachable, or the sandbox is being parked or destroyed (`sandbox_unavailable`, with `Retry-After`)"}},"summary":"List a directory on a sandbox","tags":["Sandboxes"]}},"/api/events/stream":{"get":{"callbacks":{},"description":"One `text/event-stream` carrying the log events of every conversation the caller owns, including conversations that finish before discovery, each payload the shape of `GET /api/conversations/:id/stream` plus `conversation_id`. A `conversations` event (data `{reason: changed}`) is sent, debounced, when the list changes — created, titled, read, deleted, finished — and the stream follows a new conversation on its own; the client re-lists. `Last-Event-ID` replays what was missed across all owned conversations, including finished ones. Without a cursor, only events recorded after connection are sent. `?streams=` filters as elsewhere; `?blocks=true` adds server-parsed blocks. Heartbeats every 15 s; closes after 60 s idle so the client reconnects. The first byte is a `: connected` comment.","operationId":"FountainWeb.EventsController.stream","parameters":[{"description":"Resume after this event id (integer as string). Missing or unparseable values are treated as 0.","in":"header","name":"Last-Event-ID","required":false,"schema":{"type":"string"}},{"description":"Comma-separated stream allow-list (`stdout,stderr,acp,stage`).","in":"query","name":"streams","required":false,"schema":{"type":"string"}},{"description":"Add `blocks` to each event payload. Defaults to false.","in":"query","name":"blocks","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"text/event-stream":{"schema":{"oneOf":[{"$ref":"#/components/schemas/StreamLogEvent"},{"$ref":"#/components/schemas/StreamSignal"}]}}},"description":"SSE stream"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Stream every conversation's events (SSE)","tags":["Conversations"]}},"/api/support/reports":{"get":{"callbacks":{},"description":"Newest first, with forwarding status and the issue URL when one was created.","operationId":"FountainWeb.SupportReportController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportReportListResponse"}}},"description":"Reports"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid API key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List the caller's reports","tags":["Support"]},"post":{"callbacks":{},"description":"Stores the report with whatever `context` the client attaches (conversation, agent, sandbox, presence, recent events, app version — the facts triage needs; never secrets) and forwards it to the operator: a GitHub issue when the instance configures `SUPPORT_GITHUB_REPO`, and/or mail to `SUPPORT_EMAIL`. Forwarding is asynchronous; `status` is `new` until it lands. Audited as `support.report.created` (category, sizes and context keys — not the message).","operationId":"FountainWeb.SupportReportController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportReportCreateRequest"}}},"description":"Report","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportReportResponse"}}},"description":"Report"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid API key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation errors"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"File a problem report","tags":["Support"]}},"/api/auth/api-keys/{id}":{"delete":{"callbacks":{},"description":"Immediate — the key stops authenticating on the next request. Revoking the key you are presenting is allowed and is the last thing that key does.","operationId":"FountainWeb.ApiKeyController.delete","parameters":[{"description":"Key id.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Revoked"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The presented key lacks key-management scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such key on this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Revoke an API key","tags":["Auth"]}},"/api/auth/email":{"post":{"callbacks":{},"description":"Sends a confirmation link to the new address; the address only changes when that token comes back to `POST /api/auth/email/confirm`. The response is identical whether or not the address was free — this is not an availability oracle. Requires the current password and `full` scope.","operationId":"FountainWeb.AccountSecurityController.api_request_email_change","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailChangeRequest"}}},"description":"New address and current password","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"Confirmation link sent (if the address was available)"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`invalid_current_password`, or a key without full scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`no_password`, `invalid_email`, `same_email`, or missing fields"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Start an email change","tags":["Auth"]}},"/api/team/{agent_id}/schedules/{id}/run":{"post":{"callbacks":{},"description":"The same path as the page's \"Run now\": the prompt goes where the schedule says (the teammate's thread, or a one-off computer) and `last_run_at`, `last_conversation_id` and `last_error` are stamped either way. The cron is untouched. 400 `conversation_busy` while the teammate's previous turn is still running, 503 while its computer is still starting, 404 when an in-thread schedule's agent is not on the team; the sandbox quota and credit refusals are the same as `POST /api/conversations`. Audited as `team.schedule.fired`.","operationId":"FountainWeb.TeamScheduleController.run","parameters":[{"description":"","in":"path","name":"agent_id","required":true,"schema":{"type":"string"}},{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamMessageResponse"}}},"description":"Queued"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"A turn is still running"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found, or not on the team"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Run a schedule now","tags":["Team"]}},"/api/auth/token":{"post":{"callbacks":{},"description":"The front door: every other `/api/*` endpoint needs the bearer token this returns. Each call mints a **new** full-scope key rather than returning an existing one, so a client that calls it on every run accumulates keys — store the result. The account must be verified; an unverified one is refused with 403 and `reason: email_unverified`. Rate-limited to 10 attempts per IP per hour.","operationId":"FountainWeb.AuthTokenController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenRequest"}}},"description":"Credentials","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenResponse"}}},"description":"A new API key"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid email or password"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Email not verified"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing email or password"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited"}},"security":[],"summary":"Exchange email and password for an API key","tags":["Auth"]}},"/api/account/inference-credentials/{provider}":{"delete":{"callbacks":{},"operationId":"FountainWeb.InferenceCredentialController.delete","parameters":[{"description":"","in":"path","name":"provider","required":true,"schema":{"enum":["anthropic_api_key","claude_code_oauth_token","openai_api_key","gemini_api_key"],"type":"string"}}],"responses":{"204":{"description":"Cleared"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown provider"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Clear a provider credential","tags":["Inference credentials"]},"put":{"callbacks":{},"description":"Validates the credential against the provider, then stores it encrypted under the tenant DEK. Set `validate: false` to skip the provider ping — useful when the provider is unreachable from this instance, at the cost of finding out about a typo mid-conversation instead of here.","operationId":"FountainWeb.InferenceCredentialController.update","parameters":[{"description":"","in":"path","name":"provider","required":true,"schema":{"enum":["anthropic_api_key","claude_code_oauth_token","openai_api_key","gemini_api_key"],"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InferenceCredentialRequest"}}},"description":"Credential","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InferenceCredentialResponse"}}},"description":"Provider status"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rejected credential, blank value, or unknown provider"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"502":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Provider unreachable"},"504":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Provider timed out"}},"summary":"Set a provider credential","tags":["Inference credentials"]}},"/api/webhooks/{id}/test":{"post":{"callbacks":{},"description":"Queues one `webhook.test` delivery, signed like any other. Delivered whatever the endpoint's filter says, and deliberately outside the `conversation.*` namespace so a receiver switching on type cannot mistake it for a real transition.","operationId":"FountainWeb.WebhookEndpointController.test","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookTestResponse"}}},"description":"Queued"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such endpoint on this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Send a test event","tags":["Webhooks"]}},"/api/conversations/{conversation_id}/egress":{"get":{"callbacks":{},"description":"The broker's request log for this conversation, newest first: each outbound HTTP request the sandbox made, the host, the service that matched (and so which credential was attached), the status and latency (ADR 0019 gate 4). Empty, with `brokered: false`, for a conversation that was not brokered. The log outlives the conversation for `BROKER_LOG_RETENTION_HOURS`.","operationId":"FountainWeb.ConversationController.egress","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}},{"description":"1 to 500, default 100","in":"query","name":"limit","required":false,"schema":{"type":"integer"}},{"description":"Page: the `next` value of the previous page","in":"query","name":"before","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EgressListResponse"}}},"description":"Egress"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The key lacks full scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"502":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The broker did not answer"}},"summary":"List what the conversation sent out through the egress broker","tags":["Conversations"]}},"/api/conversations/{conversation_id}/events":{"get":{"callbacks":{},"description":"The read-model behind the SSE stream. Same rows, same fields, as JSON — fetching or archiving a conversation's output no longer requires an SSE parser. Oldest first, cursor-paginated: pass the previous page's `meta.next_cursor` as `after`. SSE remains the tail/follow mechanism. With `order=desc` the page is the newest events instead, newest first, and `meta.next_cursor` is the page's oldest id, to pass as `before` for the page older than it; add `whole_turns=true` so no page ends inside a turn. `page.newest_cursor` of the first such page is where an SSE follow resumes (`Last-Event-ID`).","operationId":"FountainWeb.ConversationController.events","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}},{"description":"Comma-separated allow-list of `stdout`, `stderr`, `stage`. Same semantics as the SSE route; omitted means everything.","in":"query","name":"streams","required":false,"schema":{"type":"string"}},{"description":"Return events with an id greater than this. Defaults to 0.","in":"query","name":"after","required":false,"schema":{"type":"integer"}},{"description":"Return events with an id less than this: the previous `order=desc` page's `meta.next_cursor`. Applies in either order; omitted means no upper bound.","in":"query","name":"before","required":false,"schema":{"type":"integer"}},{"description":"`asc` (the default) pages forward from `after`, oldest first. `desc` pages backward from `before` (or from the newest event), and `data` is newest first.","in":"query","name":"order","required":false,"schema":{"enum":["asc","desc"],"type":"string"}},{"description":"With `order=desc`, never end a page inside a turn: when the `limit`th event belongs to a turn that began earlier, the page extends past `limit` to that turn's first event, keeping any events interleaved with it, so a client renders complete turns and `before=<meta.next_cursor>` returns only earlier ones. At most 5000 events: a turn larger than that is cut there, with `page.turn_split: true`, and continues on the next page. Turn-less events (the setup before turn 1) page by `limit` alone. Refused with 422 without `order=desc`. Defaults to false.","in":"query","name":"whole_turns","required":false,"schema":{"type":"boolean"}},{"description":"Page size, 1..1000. Defaults to 100. `whole_turns=true` can return more.","in":"query","name":"limit","required":false,"schema":{"type":"integer"}},{"description":"Add `blocks` to each event: its `data` parsed server-side into the structured blocks a transcript renders (text, thinking, tool_use, tool_result, init, result, error, raw) — the same parse the web UI uses, so no client re-implements a runtime's dialect. Defaults to false.","in":"query","name":"blocks","required":false,"schema":{"type":"boolean"}},{"description":"With `blocks=true`, fill each turn's `turn`/`started` stage event — whose `blocks` is otherwise always `[]` — with one `prompt` block carrying the prompt that opened that turn. Without it the feed holds only what the runtime wrote, so a client replaying a conversation renders it as a monologue in the agent's voice. No event is added, removed or reordered, so `meta.next_cursor`, `has_more` and the page size are unchanged. A turn whose `origin` is `autonomous` gets no block: nobody typed it. Note that `streams=acp` excludes stage events, and so excludes these prompts with them. Ignored without `blocks=true`. Defaults to false.","in":"query","name":"prompts","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogEventListResponse"}}},"description":"Log events"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid request parameters"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List a conversation's log events","tags":["Conversations"]}},"/api/conversations/{conversation_id}/interrupt":{"post":{"callbacks":{},"description":"Ends the turn in flight. Wakes a conversation whose server has died so a turn left `running` behind it can still be closed.\n\n`404` means no such conversation. `409` means the conversation exists but has nothing to interrupt: `no_turn_running` when it is idle between turns, `not_running` when it is terminated or could not be woken.","operationId":"FountainWeb.ConversationController.interrupt","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Interrupted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Nothing to interrupt"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox or fleet unavailable"}},"summary":"Interrupt the running turn","tags":["Conversations"]}},"/api/conversations/{conversation_id}/labels":{"patch":{"callbacks":{},"description":"Merges `labels` into the conversation's own (#1637). A key the body does not name is left alone, and a key whose value is `null` is removed, so a run can stamp one outcome without reading the rest first.\n\nAt most 32 labels survive the merge; a key is at most 64 bytes and a value at most 256 bytes. A 422 names the offending key.\n\nThe account's own key may label any of its conversations. A sandbox callback token may label **only the conversation it was minted for**; another id is refused with 403 `sprite_may_not_label_another_conversation`. An agent inside a turn does not need this route at all: it sends the `_fountain/labels` ACP extension update instead.","operationId":"FountainWeb.ConversationController.labels","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationLabelsRequest"}}},"description":"Labels","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}},"description":"Conversation"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"A sandbox token labelling another conversation"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid labels"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Set a conversation's labels","tags":["Conversations"]}},"/api/runners/ws":{"get":{"callbacks":{},"description":"The `fountain runner` daemon's socket. Upgrades to a WebSocket over which Fountain sends sandbox requests and the daemon streams command output; the frame protocol is documented in `Managoat.Runner.Connection` and in the self-hosted runners guide. Not for other clients: it is not a stream of anything.","operationId":"FountainWeb.RunnerController.connect","parameters":[{"description":"The runner's name (lowercase, unique per account).","in":"query","name":"name","required":true,"schema":{"type":"string"}},{"description":"","in":"query","name":"hostname","required":false,"schema":{"type":"string"}},{"description":"","in":"query","name":"os","required":false,"schema":{"type":"string"}},{"description":"","in":"query","name":"arch","required":false,"schema":{"type":"string"}},{"description":"Daemon version.","in":"query","name":"version","required":false,"schema":{"type":"string"}},{"description":"Sandbox root on the host.","in":"query","name":"root","required":false,"schema":{"type":"string"}}],"responses":{"101":{"description":"Upgraded"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a WebSocket upgrade, or a bad name"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The presented key lacks full scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Runners are disabled on this instance"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"A runner with this name is already connected"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Connect a runner (WebSocket)","tags":["Runners"]}},"/api/admin/users/{id}/comp":{"post":{"callbacks":{},"operationId":"FountainWeb.AdminController.set_comp","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCompRequest"}}},"description":"Comped","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserResponse"}}},"description":"Account"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Admin required"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Refused"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Comp or un-comp an account","tags":["Admin"]}},"/api/conversations/{conversation_id}/stream":{"get":{"callbacks":{},"description":"Server-Sent Events stream of the conversation's log events. The `Last-Event-ID` request header resumes from a known event id; missed events are replayed before the live tail begins. Keep-alive heartbeats every 15s as `: heartbeat` comments.\n\nEach message carries the event id in `id:`, the event's `kind` in `event:` (`output` or `stage`), and a JSON object in `data:` with `kind`, `stream`, `data`, `stage`, `state`, `turn_id` and `ts`. The `data` field is a string: raw output for an `output` event, JSON-encoded metadata for a `stage` one.\n\n**This shape is an interface, not an implementation detail.** Two clients render from it — the web UI and `fountain acp`, the Agent Client Protocol adapter an editor spawns — so changing an event's shape or a stream's meaning breaks a surface outside this repo. See decisions/0015.","operationId":"FountainWeb.ConversationController.stream","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}},{"description":"Resume after this event id (integer as string). Missing or unparseable values are treated as 0.","in":"header","name":"Last-Event-ID","required":false,"schema":{"type":"string"}},{"description":"Comma-separated subset of `stdout`, `stderr`, `acp` and `stage`. Omitted or empty means everything.\n\n- `stdout` / `stderr` — the runtime process's own output, byte for byte.\n- `acp` — one Agent Client Protocol `session/update` notification per line, stored exactly as the runtime's adapter emitted it. This is what a protocol client forwards to an editor, so it is a compatibility surface (decisions/0015). Only conversations whose runtime speaks ACP have it — see the conversation's `acp` field.\n- `stage` — lifecycle events (`provision`, `setup`, `turn`, `reattach`, `sandbox`, `terminate`) with a `state` and JSON metadata. The terminal `turn`/`done` carries the turn's `stop_reason`.\n\nA name no event carries selects nothing; it is not an error.","in":"query","name":"streams","required":false,"schema":{"type":"string"}},{"description":"`false`/`0` drains the buffered events and closes immediately, rather than holding the connection open for the live tail. Defaults to true.","in":"query","name":"wait","required":false,"schema":{"type":"string"}},{"description":"Add `blocks` to each event payload — its `data` parsed server-side into the structured blocks a transcript renders, as on `/events?blocks=true`. Defaults to false.","in":"query","name":"blocks","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/StreamLogEvent"}}},"description":"SSE stream"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Stream log events (SSE)","tags":["Conversations"]}},"/api/claimable-users/{id}/claim":{"post":{"callbacks":{},"description":"Attaches the calling account as the principal's owner and returns a fresh credential for it. Nothing about the machine changes: the sandbox, agent, environment, vault, conversations and every id survive the claim. The credential the application held is revoked in the same transaction. A brand-new account and an account that already owns other principals claim the same way.","operationId":"FountainWeb.ClaimableUserController.claim","parameters":[{"description":"The grant's id.","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Replaying a successful claim with this key and the same account returns the same outcome with a fresh credential.","in":"header","name":"Idempotency-Key","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"claim_token":{"type":"string"}},"required":["claim_token"],"type":"object"}}},"description":"Claim","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimedPrincipalResponse"}}},"description":"Claimed"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Bad claim token, or an account that cannot claim"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such grant"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Already claimed"},"410":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Expired or released"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Claim a principal","tags":["Claimable principals"]}},"/api/environments":{"get":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentListResponse"}}},"description":"Environments"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List environments","tags":["Environments"]},"post":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentRequest"}}},"description":"Environment attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Create an environment","tags":["Environments"]}},"/api/account/chatgpt-subscriptions/attempts/{id}":{"delete":{"callbacks":{},"description":"Ends a pending attempt; a sign-in approved afterwards stores nothing. Already cancelled is a 200. An attempt that has completed, failed or expired is a 409 `chatgpt_link_attempt_not_pending` carrying its `state`.","operationId":"FountainWeb.ChatGPTSubscriptionController.cancel_attempt","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatGPTLinkAttemptResponse"}}},"description":"Attempt"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such attempt"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`chatgpt_link_attempt_not_pending`"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Cancel a ChatGPT sign-in","tags":["ChatGPT subscriptions"]},"get":{"callbacks":{},"description":"The endpoint to poll. It reads a row and contacts nobody. An attempt that ran out of time reads `expired`.","operationId":"FountainWeb.ChatGPTSubscriptionController.show_attempt","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatGPTLinkAttemptResponse"}}},"description":"Attempt"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such attempt"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Read a ChatGPT sign-in","tags":["ChatGPT subscriptions"]}},"/api/team/{agent_id}/schedules/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.TeamScheduleController.delete","parameters":[{"description":"","in":"path","name":"agent_id","required":true,"schema":{"type":"string"}},{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Delete a schedule","tags":["Team"]},"get":{"callbacks":{},"operationId":"FountainWeb.TeamScheduleController.show","parameters":[{"description":"","in":"path","name":"agent_id","required":true,"schema":{"type":"string"}},{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamScheduleResponse"}}},"description":"Schedule"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Show one schedule","tags":["Team"]},"patch":{"callbacks":{},"description":"Any of `name`, `cron`, `prompt`, `one_off`, `enabled`. A changed cron or a re-enable recomputes `next_run_at` and clears `last_error`. Audited as `team.schedule.updated` with the changed field names.","operationId":"FountainWeb.TeamScheduleController.update","parameters":[{"description":"","in":"path","name":"agent_id","required":true,"schema":{"type":"string"}},{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamScheduleUpdateRequest"}}},"description":"Schedule attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamScheduleResponse"}}},"description":"Schedule"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation errors"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Update a schedule","tags":["Team"]}},"/api/conversations/{conversation_id}/turns":{"get":{"callbacks":{},"operationId":"FountainWeb.ConversationController.turns","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TurnListResponse"}}},"description":"Turns"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List turns in a conversation","tags":["Conversations"]}},"/api/team/{agent_id}":{"delete":{"callbacks":{},"description":"Terminates the live conversation (its sandbox goes with it) and unbinds every conversation the agent had under the team channel; the rows stay in `GET /api/conversations`.","operationId":"FountainWeb.TeamController.delete","parameters":[{"description":"","in":"path","name":"agent_id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Removed"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not on the team"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Remove an agent from the team","tags":["Team"]},"get":{"callbacks":{},"operationId":"FountainWeb.TeamController.show","parameters":[{"description":"","in":"path","name":"agent_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeammateResponse"}}},"description":"Teammate"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not on the team"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Show one teammate","tags":["Team"]},"patch":{"callbacks":{},"description":"Sets what the teammate is called — its conversation's title (#831). `name` null or blank goes back to the agent's name. The name carries over to the fresh conversation opened when this one is past resuming. Audited as `team.renamed`; the stream sends `team` so clients re-list.","operationId":"FountainWeb.TeamController.update","parameters":[{"description":"","in":"path","name":"agent_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamRenameRequest"}}},"description":"Rename","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeammateResponse"}}},"description":"Teammate"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not on the team"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Name too long"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Rename a teammate","tags":["Team"]}},"/api/conversations/{conversation_id}/tree":{"get":{"callbacks":{},"description":"Every conversation in the same spawn tree — ancestors and descendants of this one — as flat `{id, source, status, parent_id}` entries. The API is how sub-conversations get created (`X-Fountain-Parent-Conversation-Id`), so this is how an agent that fanned out enumerates what it started without client-side bookkeeping.","operationId":"FountainWeb.ConversationController.tree","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationTreeResponse"}}},"description":"Conversation tree"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get the conversation's spawn tree","tags":["Conversations"]}},"/api/oauth/clients":{"get":{"callbacks":{},"description":"The OAuth clients this account registered. Each one is in development mode until an operator publishes it, which means it signs in its owner and refuses every other account.","operationId":"FountainWeb.OAuthClientController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthClientListResponse"}}},"description":"Clients"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List OAuth clients","tags":["OAuth clients"]},"post":{"callbacks":{},"description":"Register an app that can offer \"Sign in with Fountain\". The response carries the generated `client_id` to put in the app. Redirect URIs match exactly, must be https unless they are loopback, and a loopback URI matches on any port. The client starts in development mode, so it signs in nobody but you. A sandbox's public HTTPS URL is valid.","operationId":"FountainWeb.OAuthClientController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthClientRequest"}}},"description":"Client","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthClient"}}},"description":"Client"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Register an OAuth client","tags":["OAuth clients"]}},"/api/connection-providers/{id}":{"delete":{"callbacks":{},"description":"Deletes the provider and every connection on it, revoking each at the provider first (best effort). The platform provider cannot be deleted.","operationId":"FountainWeb.ConnectionProviderController.delete","parameters":[{"description":"Provider id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Delete a connection provider","tags":["Connections"]},"get":{"callbacks":{},"operationId":"FountainWeb.ConnectionProviderController.show","parameters":[{"description":"Provider id, or `google`","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionProvider"}}},"description":"Provider"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get a connection provider","tags":["Connections"]},"patch":{"callbacks":{},"description":"Any field but `kind`. A blank or absent `client_secret` keeps the stored one. The platform provider cannot be edited.","operationId":"FountainWeb.ConnectionProviderController.update","parameters":[{"description":"Provider id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionProviderRequest"}}},"description":"Provider","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionProvider"}}},"description":"Provider"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation failed"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Edit a connection provider","tags":["Connections"]}},"/api/sandboxes/{sandbox_id}/git-status":{"get":{"callbacks":{},"description":"`git status` of the repository containing `path` (default: the agent's working directory), one entry per changed path. This is the view that shows a file the agent created and never staged: `/diff` compares tracked content, so an untracked file is invisible to it whatever flags it is given. Entries cover the whole repository whatever `path` names inside it, and each entry's `path` is relative to `repo_root`. `index` and `worktree` are git's two porcelain columns read separately, so a file staged and then edited again reports a state in both; an untracked file reads `untracked` in both. `renamed_from` is set only where that side is a rename or a copy. `branch` is null on a detached HEAD. A directory outside any repository is `422 not_a_repository`. Full scope.","operationId":"FountainWeb.SandboxFilesController.git_status","parameters":[{"description":"","in":"path","name":"sandbox_id","required":true,"schema":{"type":"string"}},{"description":"In-sandbox path, absolute or relative to the agent's working directory (`/home/sprite` for claude and codex, `/tmp/gemini-workspace` and `/tmp/opencode-workspace` for the others). Confined to those directories: anything else is `422 path_outside_sandbox`.","in":"query","name":"path","required":false,"schema":{"type":"string"}},{"description":"What to do about untracked paths: collapse an untracked directory to one entry (`normal`), list every file under it (`all`), or leave them out (`no`).","in":"query","name":"untracked","required":false,"schema":{"default":"normal","enum":["normal","all","no"],"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxStatusResponse"}}},"description":"Status"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such sandbox or path"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox is not ready"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a repository, or outside the sandbox"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox provider unreachable, or the sandbox is being parked or destroyed (`sandbox_unavailable`, with `Retry-After`)"}},"summary":"git status on a sandbox","tags":["Sandboxes"]}},"/api/admin/users/{id}/sandbox-limit":{"post":{"callbacks":{},"description":"The only lever for a noisy or abusive tenant (ADR 0005). 0 stops the account from starting new conversations without suspending it.","operationId":"FountainWeb.AdminController.set_sandbox_limit","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSandboxLimitRequest"}}},"description":"Limit","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserResponse"}}},"description":"Account"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Admin required"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Refused"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Set an account's concurrent-sandbox cap","tags":["Admin"]}},"/api/oauth/revoke":{"post":{"callbacks":{},"description":"Revokes the API key in the `Authorization` header — what an app does on sign-out. Idempotent: an already-revoked key is 204 too (it would not have authenticated).","operationId":"FountainWeb.OAuthTokenController.revoke","parameters":[],"responses":{"204":{"description":"Revoked"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Revoke the presented token","tags":["Auth"]}},"/api/connections":{"get":{"callbacks":{},"description":"Every provider account the tenant has connected, active or revoked. Only on a deployment that runs the egress broker (ADR 0019); 404 otherwise.","operationId":"FountainWeb.ConnectionController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionListResponse"}}},"description":"Connections"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Connections are not enabled for this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List connections","tags":["Connections"]}},"/api/account/inference-credential-sets":{"get":{"callbacks":{},"description":"The default set first, then by name. Values are never returned.","operationId":"FountainWeb.InferenceCredentialSetController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InferenceCredentialSetListResponse"}}},"description":"Credential sets"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List inference credential sets","tags":["Inference credentials"]},"post":{"callbacks":{},"description":"Creates an empty set. The first set an account has is its default, whoever asked for it; every later one is not until it is promoted.","operationId":"FountainWeb.InferenceCredentialSetController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InferenceCredentialSetCreateRequest"}}},"description":"Credential set","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InferenceCredentialSetResponse"}}},"description":"Credential set"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or duplicate name"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Create an inference credential set","tags":["Inference credentials"]}},"/api/vaults/{vault_id}/secrets":{"get":{"callbacks":{},"operationId":"FountainWeb.VaultSecretController.index","parameters":[{"description":"","in":"path","name":"vault_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretListResponse"}}},"description":"Vault Secrets"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List secrets in a vault","tags":["Vault Secrets"]},"post":{"callbacks":{},"description":"Sets the value for `key`. If the key exists, the value is overwritten. Values are write-only — subsequent reads never return them.","operationId":"FountainWeb.VaultSecretController.create","parameters":[{"description":"","in":"path","name":"vault_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretRequest"}}},"description":"Vault Secret","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretResponse"}}},"description":"Vault Secret"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Upsert a vault secret","tags":["Vault Secrets"]}},"/api/admin/events":{"get":{"callbacks":{},"description":"Administrative actions taken against accounts — who did what to whom. Separate from the audit trail because these carry both an actor and a target.","operationId":"FountainWeb.AdminController.index_admin_events","parameters":[{"description":"1..500, default 100.","in":"query","name":"limit","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminEventListResponse"}}},"description":"Admin events"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Admin required"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"The privilege trail","tags":["Admin"]}},"/api/admin/sandboxes":{"get":{"callbacks":{},"operationId":"FountainWeb.AdminController.index_sandboxes","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSandboxListResponse"}}},"description":"Sandboxes"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Admin required"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List live sandboxes across all tenants","tags":["Admin"]}},"/api/webhooks":{"get":{"callbacks":{},"description":"Metadata only. The signing secret is returned once, at creation and at each rotation, and is not recoverable.","operationId":"FountainWeb.WebhookEndpointController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointListResponse"}}},"description":"Endpoints"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List webhook endpoints","tags":["Webhooks"]},"post":{"callbacks":{},"description":"The response is the only time the signing secret is available. A URL pointing at loopback, link-local (the cloud metadata address included) or RFC1918 space is refused here and again at every delivery.","operationId":"FountainWeb.WebhookEndpointController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointCreateRequest"}}},"description":"The endpoint","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointCreatedResponse"}}},"description":"The endpoint, with its secret"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid URL or event filter"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Create a webhook endpoint","tags":["Webhooks"]}},"/api/webhooks/{id}/deliveries":{"get":{"callbacks":{},"description":"Newest first, one row per HTTP attempt. Pruned on the `webhook_deliveries` retention window (30 days by default).","operationId":"FountainWeb.WebhookEndpointController.deliveries","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Default 50, max 200.","in":"query","name":"limit","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryListResponse"}}},"description":"Attempts"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such endpoint on this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Recent delivery attempts","tags":["Webhooks"]}},"/api/search":{"get":{"callbacks":{},"description":"Full-text search over the caller's conversation titles, turn prompts and assistant replies (`kind`: `title`, `prompt`, `reply`), ranked, newest first among equals. Postgres `websearch` syntax: `\"quoted phrase\"`, `-excluded`, `or`; matching is exact-token (no stemming), so identifiers and code fragments match as themselves. Each hit names the conversation, agent and turn to jump to, with a plain-text `snippet` (no markup) and the turn's (or conversation's) creation time as `ts`. A reply is searchable once its turn ends. Page with `limit` / `offset` while `meta.has_more`.","operationId":"FountainWeb.SearchController.index","parameters":[{"description":"The search text.","in":"query","name":"q","required":true,"schema":{"type":"string"}},{"description":"Page size; default 20, max 100.","in":"query","name":"limit","required":false,"schema":{"maximum":100,"minimum":1,"type":"integer"}},{"description":"Hits to skip; default 0.","in":"query","name":"offset","required":false,"schema":{"minimum":0,"type":"integer"}},{"description":"Only this agent's conversations.","in":"query","name":"agent_id","required":false,"schema":{"type":"string"}},{"description":"Only this conversation.","in":"query","name":"conversation_id","required":false,"schema":{"type":"string"}},{"description":"Only hits whose `ts` is at or after this instant (RFC 3339).","in":"query","name":"since","required":false,"schema":{"format":"date-time","type":"string"}},{"description":"Comma-separated subset of `title,prompt,reply`; default all.","in":"query","name":"kinds","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}},"description":"Hits"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Blank `q`"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing `q`, or a malformed parameter"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Search conversations, prompts and replies","tags":["Search"]}},"/api/team/{agent_id}/conversations":{"get":{"callbacks":{},"description":"Every conversation the agent has had on the team, newest first (#832): the current one flagged `current: true`, the retired ones — a previous computer's thread, read-only — behind it. Each is a full conversation object; read a retired thread with `GET /api/conversations/:id/events`. 404 when the agent is not on the team.","operationId":"FountainWeb.TeamController.conversations","parameters":[{"description":"","in":"path","name":"agent_id","required":true,"schema":{"type":"string"}},{"description":"Only conversations carrying these `key:value` labels (#1637). Repeatable and AND-combined, exactly as on `GET /api/conversations`. 400 `invalid_label_filter` on a value with no colon or an empty key.","explode":true,"in":"query","name":"label","required":false,"schema":{"items":{"type":"string"},"type":"array"},"style":"form"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeammateConversationListResponse"}}},"description":"Conversations"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid label filter"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not on the team"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List a teammate's conversations","tags":["Team"]},"post":{"callbacks":{},"description":"Retires the teammate's current conversation — it stays in its history, past resuming — and opens a new one on the **same sandbox**: the next message starts a fresh runtime session on the same disk, files and installed tools intact. Nothing is provisioned and nothing is interrupted: 400 `conversation_busy` while a turn is running (interrupt first), 503 `provisioning` while the computer is still starting. The computer's own door is asked before the current conversation is retired, so a refusal costs the teammate nothing and the same call can be repeated: 409 `sandbox_reset_pending` while the computer is being reset or deleted, 503 `sandbox_unavailable` while an operation holds it (retry after `Retry-After`), 422 `sandbox_identity_mismatch` or `sandbox_runtime_mismatch` when the agent's environment, vault or runtime no longer matches the computer it was built for — start a new conversation instead. When the computer is gone (sandbox terminated or failed, or the conversation already past resuming) a new sandbox is provisioned instead, as `POST /api/team` does. 201 with the teammate and its new conversation; the stream sends `team`. Audited as `team.conversation.rotated`.","operationId":"FountainWeb.TeamController.fresh_conversation","parameters":[{"description":"","in":"path","name":"agent_id","required":true,"schema":{"type":"string"}}],"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeammateResponse"}}},"description":"Teammate"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"A turn is still running"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not on the team"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The computer is being reset or deleted"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The computer no longer matches the agent"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The computer is still starting, or an operation holds it"}},"summary":"Open a fresh conversation on the teammate's computer","tags":["Team"]}},"/api/apply":{"post":{"callbacks":{},"description":"Applies all resources from a compiled fountain.yml manifest in one request. Resources are reconciled in a fixed order — environments, vaults, agents, teammates, schedules, webhooks — so a spec may name another document whatever the file's order: an agent's `environment`, a teammate's `agent`, `environment` and `vault`, and a schedule's `teammate`. Every kind is keyed by the document's `name`, except `Webhook`, which is keyed by `spec.url`. A `Webhook` created here returns its signing secret once, on that result row, and a manifest that holds one needs a full-scope credential. A `Teammate` is read as a whole declaration, so an absent `environment` or `vault` clears that binding, and moving either retires the computer the old binding named (refused with an error on that row while a turn is running on it). Two Teammate documents may not name the same agent. Apply is additive: a document dropped from the manifest leaves its record in place. Application is best-effort per resource: the response is 200 even when an individual resource fails validation, is refused by its context or raises, with per-resource errors in the result entries.","operationId":"FountainWeb.ApplyController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyRequest"}}},"description":"Compiled manifest","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyResponse"}}},"description":"Per-resource results"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Apply a compiled manifest (bulk upsert)","tags":["Apply"]}},"/api/webhooks/{id}/rotate-secret":{"post":{"callbacks":{},"description":"Returns a new secret and invalidates the old one immediately. A receiver that verifies signatures has to be updated in the same breath.","operationId":"FountainWeb.WebhookEndpointController.rotate","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointCreatedResponse"}}},"description":"The endpoint, with its new secret"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such endpoint on this account"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Rotate the signing secret","tags":["Webhooks"]}},"/api/team/stream":{"get":{"callbacks":{},"description":"One `text/event-stream` carrying the log events of every teammate's conversation, each payload the shape of `GET /api/conversations/:id/stream` plus `conversation_id` and `agent_id`. A `team` event (data `{reason: changed}`) is sent when the roster changes — a teammate added or removed, or a fresh conversation opened for one, or a self-hosted runner connecting or dropping (presence changes for the teammates on it) — and the stream follows the new conversation on its own; the client re-lists. A `schedule` event (same data) is sent when a team schedule is created, updated, deleted or fired (#825); the client re-lists `/api/team/schedules`. `Last-Event-ID` (a log event id) replays what was missed on each teammate's conversation. `?blocks=true` adds server-parsed ACP blocks per event. Other streams produce no blocks. Heartbeats every 15s; closes after 60s idle so the client reconnects.","operationId":"FountainWeb.TeamController.stream","parameters":[{"description":"Resume after this event id (integer as string). Missing or unparseable values are treated as 0.","in":"header","name":"Last-Event-ID","required":false,"schema":{"type":"string"}},{"description":"Comma-separated stream allow-list (`stdout,stderr,acp,stage,...`).","in":"query","name":"streams","required":false,"schema":{"type":"string"}},{"description":"Add `blocks` to each event payload — its `data` parsed server-side into the structured blocks a transcript renders, as on `/api/conversations/:id/stream?blocks=true`. Only ACP output events produce blocks. Defaults to false.","in":"query","name":"blocks","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"text/event-stream":{"schema":{"oneOf":[{"$ref":"#/components/schemas/StreamLogEvent"},{"$ref":"#/components/schemas/StreamSignal"}]}}},"description":"SSE stream"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Stream the whole team's events (SSE)","tags":["Team"]}},"/api/auth/device/token":{"post":{"callbacks":{},"description":"Polled by the CLI with the `device_code` from `POST /api/auth/device`. Until the user decides, 400 `authorization_pending` (or `slow_down` when polled faster than `interval`). A denial is 400 `access_denied`; a timed-out grant is 400 `expired_token`; an unknown or already-used code is 400 `invalid_grant`. On approval, 201 with a full-scope API key — the same shape `POST /api/auth/token` returns — and the grant is consumed.","operationId":"FountainWeb.DeviceAuthController.token","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenRequest"}}},"description":"Device token request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenResponse"}}},"description":"A new API key"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"authorization_pending / slow_down / access_denied / expired_token / invalid_grant"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"}},"security":[],"summary":"Poll a device grant for the API key","tags":["Auth"]}},"/api/auth/device":{"post":{"callbacks":{},"description":"The CLI login path for accounts without a password (\"Sign up with GitHub\"). Show the user `user_code` and send them to `verification_uri` (or open `verification_uri_complete`), then poll `POST /api/auth/device/token` with `device_code` every `interval` seconds until they approve. The grant expires after `expires_in` seconds. Rate-limited to 10 grants per IP per hour.","operationId":"FountainWeb.DeviceAuthController.create","parameters":[],"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceAuthResponse"}}},"description":"A new device grant"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"}},"security":[],"summary":"Start a device-authorization grant","tags":["Auth"]}},"/api/admin/users/{id}":{"delete":{"callbacks":{},"description":"The support path for a deletion request that cannot go through the account page — a locked-out user, or one who asked by email. Same teardown as self-serve; only the recorded actor differs.","operationId":"FountainWeb.AdminController.delete_user","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountDeletedResponse"}}},"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Admin required"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Refused"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"502":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Billing cancellation failed"}},"summary":"Delete an account (admin)","tags":["Admin"]},"get":{"callbacks":{},"operationId":"FountainWeb.AdminController.show_user","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserResponse"}}},"description":"Account"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Admin required"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get one account (admin)","tags":["Admin"]}},"/api/auth/email/confirm":{"post":{"callbacks":{},"description":"Public: the token arrives in an inbox, and the caller may hold no credential for the account at all. Applying it bumps `session_version`, so every session and every API key session for the account is dead afterwards — the response says so rather than letting the caller discover it as a mystery 401.","operationId":"FountainWeb.AccountSecurityController.api_confirm_email_change","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}},"description":"The emailed token","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailChangedResponse"}}},"description":"Email changed"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`email_taken`, `expired`, `invalid_token`, or a missing token"}},"security":[],"summary":"Complete a pending email change","tags":["Auth"]}},"/api/account/onboarding/complete":{"post":{"callbacks":{},"description":"Idempotent: completing an already-completed account keeps the original `completed_at` rather than moving it.","operationId":"FountainWeb.OnboardingController.complete","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingResponse"}}},"description":"Onboarding state"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Mark onboarding complete","tags":["Account"]}},"/api/account/chatgpt-subscriptions/attempts":{"get":{"callbacks":{},"description":"The account's pending attempts, oldest first, each with its code: what a client that lost its state reads to pick up where it was.","operationId":"FountainWeb.ChatGPTSubscriptionController.index_attempts","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatGPTLinkAttemptListResponse"}}},"description":"Attempts"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List open ChatGPT sign-ins","tags":["ChatGPT subscriptions"]},"post":{"callbacks":{},"description":"`name` links a new subscription; `grant_id` reconnects one, which keeps the old credential serving until the new sign-in commits. The answer carries the code to type and the page to type it on. It expires in fifteen minutes. Limited to ten an hour and three open at once, per account.","operationId":"FountainWeb.ChatGPTSubscriptionController.create_attempt","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatGPTLinkAttemptCreateRequest"}}},"description":"What to sign in for","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatGPTLinkAttemptResponse"}}},"description":"Attempt"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope, or `chatgpt_owner_ineligible`"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`chatgpt_subscriptions_not_enabled`, or no such subscription to reconnect"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`chatgpt_grant_limit_reached`, `chatgpt_link_attempts_exceeded` or `chatgpt_link_attempt_pending`"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or duplicate name"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`chatgpt_link_attempts_rate_limited`, with `Retry-After`"},"502":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`chatgpt_auth_unreachable`"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`chatgpt_tenant_key_unavailable`"}},"summary":"Start a ChatGPT sign-in","tags":["ChatGPT subscriptions"]}},"/api/auth/api-keys":{"get":{"callbacks":{},"description":"Metadata only — key material is returned once, at creation, and is not recoverable. Most of a busy tenant's list is auto-issued `sprite:<conversation_id>` tokens; `scopes` and `expires_at` are what tell those apart from a key a person minted.","operationId":"FountainWeb.ApiKeyController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyListResponse"}}},"description":"Active keys"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The presented key lacks key-management scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List active API keys","tags":["Auth"]},"post":{"callbacks":{},"description":"The response is the only time the plaintext key is available. A key minted here carries `full` scope, so it can do everything the presenting key can — including minting more.","operationId":"FountainWeb.ApiKeyController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyRequest"}}},"description":"Key name","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreatedResponse"}}},"description":"The new key, with plaintext"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The presented key lacks key-management scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid name"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Mint an API key","tags":["Auth"]}},"/api/sandboxes/{sandbox_id}/diff":{"get":{"callbacks":{},"description":"`git diff` of the repository containing `path` (default: the agent's working directory), redacted like a file read. `staged=true` compares the index (`--cached`); `ref` compares against a commit, branch or tag (`422 invalid_ref` for a malformed one, `404 ref_not_found` for an unknown one). A directory outside any repository is `422 not_a_repository`. Full scope.","operationId":"FountainWeb.SandboxFilesController.diff","parameters":[{"description":"","in":"path","name":"sandbox_id","required":true,"schema":{"type":"string"}},{"description":"In-sandbox path, absolute or relative to the agent's working directory (`/home/sprite` for claude and codex, `/tmp/gemini-workspace` and `/tmp/opencode-workspace` for the others). Confined to those directories: anything else is `422 path_outside_sandbox`.","in":"query","name":"path","required":false,"schema":{"type":"string"}},{"description":"Diff the index (`--cached`).","in":"query","name":"staged","required":false,"schema":{"type":"boolean"}},{"description":"A commit, branch or tag to diff against. Without it the comparison is the working tree against the index, or with `staged`, the index against HEAD.","in":"query","name":"ref","required":false,"schema":{"type":"string"}},{"description":"How many bytes to return, at most 4194304 (default 262144). `truncated` says whether the content stopped short.","in":"query","name":"max_bytes","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxDiffResponse"}}},"description":"Diff"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such sandbox, path or ref"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox is not ready"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a repository, bad ref, or outside the sandbox"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox provider unreachable, or the sandbox is being parked or destroyed (`sandbox_unavailable`, with `Retry-After`)"}},"summary":"git diff on a sandbox","tags":["Sandboxes"]}},"/api/account/onboarding":{"get":{"callbacks":{},"operationId":"FountainWeb.OnboardingController.show","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingResponse"}}},"description":"Onboarding state"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get onboarding state","tags":["Account"]}},"/api/account/chatgpt-subscriptions":{"get":{"callbacks":{},"description":"Every subscription the account holds, by name, with how many it may hold and whether it may link another now. Tokens are never returned.","operationId":"FountainWeb.ChatGPTSubscriptionController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatGPTSubscriptionListResponse"}}},"description":"Subscriptions"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List ChatGPT subscriptions","tags":["ChatGPT subscriptions"]}},"/api/buzz/agents":{"get":{"callbacks":{},"operationId":"FountainWeb.BuzzAgentController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuzzIdentityListResponse"}}},"description":"Buzz agents"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid API key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"List hosted Buzz agents","tags":["Buzz"]},"post":{"callbacks":{},"description":"Converges on the Nostr pubkey, so a provider may call it repeatedly. A deploy that would add a **new** hosted agent is gated by the credit balance and by `BUZZ_IDENTITY_CEILING`, both `402`; a converging deploy of an agent that already exists is not.","operationId":"FountainWeb.BuzzAgentController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuzzProvisionRequest"}}},"description":"Provision attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuzzIdentityResponse"}}},"description":"Buzz agent"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid API key"},"402":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Balance exhausted, or the hosted-agent ceiling is reached"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The named environment does not exist"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Provision (or converge on) a hosted Buzz agent","tags":["Buzz"]}},"/api/account/chatgpt-subscriptions/{id}":{"delete":{"callbacks":{},"description":"Deletes the row, which frees its place under the limit. Refused with 409 `chatgpt_grant_still_connected` until it is disconnected, and with 409 `chatgpt_grant_named_by_sets` while credential sets name it.","operationId":"FountainWeb.ChatGPTSubscriptionController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Removed"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such subscription"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Still connected, or named by sets"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Remove a disconnected ChatGPT subscription","tags":["ChatGPT subscriptions"]},"patch":{"callbacks":{},"description":"A name is a label. Renaming changes no credential, and conversations running on the subscription are not disturbed.","operationId":"FountainWeb.ChatGPTSubscriptionController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatGPTSubscriptionUpdateRequest"}}},"description":"New name","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatGPTSubscriptionResponse"}}},"description":"Subscription"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope, or `chatgpt_owner_ineligible`"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such subscription"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or duplicate name"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Rename a ChatGPT subscription","tags":["ChatGPT subscriptions"]}},"/api/admin/users/{id}/suspend":{"post":{"callbacks":{},"description":"The reversible lever between comp and delete (#287): sessions die, API keys refuse, sandboxes are reaped. Billing is deliberately untouched.","operationId":"FountainWeb.AdminController.set_suspended","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSuspendRequest"}}},"description":"Suspended","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserResponse"}}},"description":"Account"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Admin required"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Refused"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Suspend or unsuspend an account","tags":["Admin"]}},"/api/account/billing":{"get":{"callbacks":{},"description":"The credit balance, what of it expires and when, and the usage numbers the billing page shows, measured over the calendar month. On an instance with billing disabled this is a 404 carrying `billing: \"disabled\"`, mirroring the UI, which redirects away from the billing page entirely.","operationId":"FountainWeb.BillingApiController.show","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingResponse"}}},"description":"Billing"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Insufficient scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Billing disabled"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Credit balance and current-month usage","tags":["Billing"]}},"/api/conversations/{conversation_id}/reapply":{"post":{"callbacks":{},"description":"Applies a selection to the machine this conversation already runs on, so its files stay where the agent left them. Variables, the system prompt, skills and MCP configuration are rewritten, and the next prompt reads them. An omitted field keeps its current selection; null clears the Environment override, the Vault or the model override; an empty object reapplies what is already selected.\n\n`model` changes the model this conversation runs from its next turn (ADR 0061), continuing the same runtime session. It is refused with 422 `model_invalid` when the runtime cannot run it, and with 409 `inference_source_changed` when the conversation's credential does not serve it.\n\nRefused with 409 `conversation_busy` while a turn runs, 409 `rebuild_required` when the selection would need the machine built again (the `field` says which one forced it; for a conversation attached to another agent's sandbox with `sandbox_id`, `shared_sandbox` while another conversation is on it and `guest` once it is alone), 503 while the machine is still being built, and 410 once the conversation has ended.","operationId":"FountainWeb.ConversationController.reapply","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationReapplyRequest"}}},"description":"Configuration selection","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}},"description":"Reapplied conversation"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sprite keys may not reapply a conversation"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Conversation or selected resource not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The conversation has a running turn, or the selection needs the machine built again"},"410":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Conversation has ended"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Selection is not allowed"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The machine is still being built"}},"summary":"Reapply a conversation's Agent, Environment, Vault and model","tags":["Conversations"]}},"/api/vaults/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.VaultController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"An agent is mid-turn on a persistent sandbox built on this vault"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Delete a vault","tags":["Vaults"]},"get":{"callbacks":{},"operationId":"FountainWeb.VaultController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get a vault","tags":["Vaults"]},"patch":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.VaultController.update (2)","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultUpdate"}}},"description":"Partial vault attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Update a vault (partial)","tags":["Vaults"]},"put":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.VaultController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultUpdate"}}},"description":"Partial vault attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Update a vault (partial)","tags":["Vaults"]}},"/api/sandboxes/{sandbox_id}/file":{"get":{"callbacks":{},"description":"The bytes of one file, redacted: every value of the sandbox's environment and vault is replaced with `[REDACTED]`, as in the transcript. `content` is the text when it is valid UTF-8 (`encoding: utf-8`) and base64 otherwise (`encoding: base64`). `size` is the whole file; `truncated` says whether `content` is short of it, which happens when the file is longer than `max_bytes` and also when redaction grows what was read past that cap. Full scope.","operationId":"FountainWeb.SandboxFilesController.show","parameters":[{"description":"","in":"path","name":"sandbox_id","required":true,"schema":{"type":"string"}},{"description":"In-sandbox path, absolute or relative to the agent's working directory (`/home/sprite` for claude and codex, `/tmp/gemini-workspace` and `/tmp/opencode-workspace` for the others). Confined to those directories: anything else is `422 path_outside_sandbox`.","in":"query","name":"path","required":true,"schema":{"type":"string"}},{"description":"How many bytes to return, at most 4194304 (default 262144). `truncated` says whether the content stopped short.","in":"query","name":"max_bytes","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxFileResponse"}}},"description":"File"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No such sandbox or path"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox is not ready"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"A directory, unreadable, or outside the sandbox"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sandbox provider unreachable, or the sandbox is being parked or destroyed (`sandbox_unavailable`, with `Retry-After`)"}},"summary":"Read a file on a sandbox","tags":["Sandboxes"]}},"/api/auth/password":{"post":{"callbacks":{},"description":"Needs the current password on top of the bearer token, and `full` scope — a sandbox's per-conversation token must not be able to rotate the account password. Browser sessions are signed out; API keys are **not** revoked, which the response states outright (`api_keys_revoked: false`). If you are rotating because something leaked, revoke keys yourself at `DELETE /api/auth/api-keys/{id}`.","operationId":"FountainWeb.AccountSecurityController.api_change_password","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordChangeRequest"}}},"description":"Current and new password","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordChangeResponse"}}},"description":"Password changed"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`invalid_current_password`, or a key without full scope"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`no_password` (OAuth-only account), missing fields, or a password that fails validation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Change the account password","tags":["Auth"]}},"/api/connection-providers/{id}/discover":{"post":{"callbacks":{},"description":"For an `mcp` provider: fetch the server's metadata chain again and update the endpoints. The registered client is kept while the server names the same authorization server.","operationId":"FountainWeb.ConnectionProviderController.discover","parameters":[{"description":"Provider id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionProvider"}}},"description":"Provider"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found, or not an mcp provider"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Discovery failed"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Run MCP discovery again","tags":["Connections"]}},"/api/agents/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.AgentController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Delete an agent","tags":["Agents"]},"get":{"callbacks":{},"operationId":"FountainWeb.AgentController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Get an agent","tags":["Agents"]},"patch":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record. Moving `environment_id` retires the agent's persistent sandboxes built on the old environment, so the update is refused with `409 sandbox_mid_turn` while a conversation on one of them is running a turn.","operationId":"FountainWeb.AgentController.update (2)","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentUpdate"}}},"description":"Partial agent attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"An agent is mid-turn on a persistent sandbox the change retires"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Update an agent (partial)","tags":["Agents"]},"put":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record. Moving `environment_id` retires the agent's persistent sandboxes built on the old environment, so the update is refused with `409 sandbox_mid_turn` while a conversation on one of them is running a turn.","operationId":"FountainWeb.AgentController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentUpdate"}}},"description":"Partial agent attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"406":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NegotiationError"}}},"description":"No acceptable representation"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"An agent is mid-turn on a persistent sandbox the change retires"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Validation error"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too Many Requests"}},"summary":"Update an agent (partial)","tags":["Agents"]}}},"security":[{"bearer":[]}],"servers":[{"url":"https://scratch.t.managoat.com","variables":{}}],"tags":[]}