{"openapi":"3.1.0","info":{"title":"Grain External Agent API","version":"1.0.0","description":"Supported external protocol for managing workspace API tokens, running workspace SQLite SQL, discovering or executing enabled workspace commands, and—when composed with the Front Door extension—accessing a bounded workspace runtime. Artifact synchronization and control-plane routes remain CLI/MCP-owned or internal. Front Door endpoints let external agents run argv shell commands through HTTPS with mfd_ tokens, without exposing E2B URLs or traffic tokens."},"servers":[{"url":"https://api.rungrain.com","description":"RunGrain Cloud API"}],"tags":[{"name":"API tokens","description":"Workspace-bound mat_ token lifecycle."},{"name":"SQL","description":"Scoped SQLite access inside a workspace runtime."},{"name":"Commands","description":"Discovery and execution of enabled workspace commands."},{"name":"Front Door","description":"Bounded shell and file transfer for a workspace runtime."}],"paths":{"/api/workspaces/{workspaceId}/api-tokens":{"post":{"operationId":"createWorkspaceApiToken","tags":["API tokens"],"description":"Creates a workspace-bound Grain API token. The raw mat_ token is returned once and only token metadata is stored afterward.","security":[{"FirebaseBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"}],"requestBody":{"description":"Token name, role preset, optional custom scopes, and optional expiration. Custom role requires explicit scopes.","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiTokenRequest"},"examples":{"createSchemaWriter":{"summary":"Create a schema writer token","value":{"name":"Hosted agent","role":"schema_writer","expiresAt":"2026-06-09T00:00:00.000Z"}},"createCommandRunner":{"summary":"Create a command runner token","value":{"name":"Command agent","role":"command_runner","expiresAt":"2026-06-09T00:00:00.000Z"}}}}}},"responses":{"200":{"description":"Created token metadata plus the raw token value. Store the raw token immediately because it is never returned again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiTokenResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"get":{"operationId":"listWorkspaceApiTokens","tags":["API tokens"],"description":"Lists metadata for workspace API tokens. Raw token values and token hashes are never included.","security":[{"FirebaseBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"}],"responses":{"200":{"description":"Token metadata for the workspace, including revoked and expired state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenList"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/workspaces/{workspaceId}/api-tokens/{tokenId}":{"delete":{"operationId":"revokeWorkspaceApiToken","tags":["API tokens"],"description":"Revokes a workspace API token. Revocation is idempotent for existing tokens and immediately prevents future SQL calls.","security":[{"FirebaseBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"},{"$ref":"#/components/parameters/TokenId"}],"responses":{"204":{"description":"The token exists in this workspace and is now revoked."},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/workspaces/{workspaceId}/commands":{"get":{"operationId":"listWorkspaceCommands","tags":["Commands"],"description":"Lists enabled workspace commands from .grain/grain.json inside the E2B workspace. Only commands with top-level enabled: true are exposed. The response includes agent-facing metadata only and never returns adapter internals or E2B session material.","security":[{"FirebaseBearer":[]},{"GrainApiTokenBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"}],"responses":{"200":{"description":"Available command summaries for the workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandList"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/CommandManifestInvalid"},"504":{"$ref":"#/components/responses/CommandTimeout"}}}},"/api/workspaces/{workspaceId}/commands/{commandId}/schema":{"get":{"operationId":"getWorkspaceCommandSchema","tags":["Commands"],"description":"Returns one enabled command with its generated JSON Schema input contract. The schema is derived from .grain/grain.json version 2 params metadata.","security":[{"FirebaseBearer":[]},{"GrainApiTokenBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"},{"$ref":"#/components/parameters/CommandId"}],"responses":{"200":{"description":"Command details and generated input schema.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandSchema"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/CommandNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/CommandManifestInvalid"},"504":{"$ref":"#/components/responses/CommandTimeout"}}}},"/api/workspaces/{workspaceId}/commands/execute":{"post":{"operationId":"executeWorkspaceCommands","tags":["Commands"],"description":"Executes one to ten enabled workspace commands sequentially inside the E2B workspace. Per-command results are returned in request order.","security":[{"FirebaseBearer":[]},{"GrainApiTokenBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"}],"requestBody":{"description":"Batch of command calls. arguments defaults to an empty object when omitted.","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecuteCommandsRequest"},"examples":{"createAndListTodos":{"summary":"Create then list todos","value":{"commands":[{"commandId":"todo.create","arguments":{"title":"Ship command API"}},{"commandId":"todo.list","arguments":{}}]}}}}}},"responses":{"200":{"description":"Batch id and one result envelope per requested command call.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecuteCommandsResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/CommandManifestInvalid"},"504":{"$ref":"#/components/responses/CommandTimeout"}}}},"/api/workspaces/{workspaceId}/sql":{"post":{"operationId":"runWorkspaceSql","tags":["SQL"],"description":"Runs raw SQL against /project/workspace/data.db inside the E2B workspace. V1 scopes authorize the requested accessMode lane; SQL text is not parsed or blocked.","security":[{"FirebaseBearer":[]},{"GrainApiTokenBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"}],"requestBody":{"description":"Raw SQL execution request with an access lane, mode, SQL text, optional scalar params, and optional row cap for all mode.","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunWorkspaceSqlRequest"},"examples":{"query":{"summary":"Read rows","value":{"accessMode":"data:read","mode":"all","sql":"SELECT * FROM todos LIMIT ?","params":[100],"maxRows":100}},"insert":{"summary":"Insert row","value":{"accessMode":"data:write","mode":"run","sql":"INSERT INTO todos (title) VALUES (?)","params":["Ship API"]}},"schema":{"summary":"Change schema","value":{"accessMode":"schema:write","mode":"exec","sql":"CREATE TABLE IF NOT EXISTS agent_notes (id INTEGER PRIMARY KEY, body TEXT)"}}}}}},"responses":{"200":{"description":"SQL result rows, a single row, mutation metadata, or exec metadata depending on the selected mode.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunWorkspaceSqlResponse"}}}},"400":{"$ref":"#/components/responses/SqlFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/SqlResultTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"},"504":{"$ref":"#/components/responses/SqlTimeout"}}}},"/api/workspaces/{workspaceId}/frontdoor-tokens":{"post":{"operationId":"createWorkspaceFrontDoorToken","tags":["Front Door"],"description":"Creates a workspace-bound Front Door agent shell token. The raw mfd_ token is returned once; only an HMAC hash is stored.","security":[{"FirebaseBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFrontDoorTokenRequest"},"examples":{"sevenDays":{"summary":"Short-lived build token","value":{"name":"build-agent"}}}}}},"responses":{"200":{"description":"Created Front Door token metadata, one-time raw token, and env block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFrontDoorTokenResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"get":{"operationId":"listWorkspaceFrontDoorTokens","tags":["Front Door"],"description":"Lists Front Door token metadata. Raw token values and token hashes are never returned.","security":[{"FirebaseBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"}],"responses":{"200":{"description":"Front Door token metadata list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrontDoorTokenList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/workspaces/{workspaceId}/frontdoor-tokens/{tokenId}":{"delete":{"operationId":"revokeWorkspaceFrontDoorToken","tags":["Front Door"],"description":"Revokes a Front Door token immediately.","security":[{"FirebaseBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"},{"$ref":"#/components/parameters/FrontDoorTokenId"}],"responses":{"204":{"description":"Token revoked."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/workspaces/{workspaceId}/frontdoor/capabilities":{"get":{"operationId":"getWorkspaceFrontDoorCapabilities","tags":["Front Door"],"description":"Returns the Front Door connection, execution, and file transfer contract. Workspace Settings may use Firebase auth; token-only agents may use an mfd_ Front Door token.","security":[{"FirebaseBearer":[]},{"FrontDoorTokenBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"}],"responses":{"200":{"description":"Front Door capability summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrontDoorCapabilities"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/workspaces/{workspaceId}/frontdoor/exec":{"post":{"operationId":"execWorkspaceFrontDoorShell","tags":["Front Door"],"description":"Runs argv as the workspace node user in the real workspace sandbox. This endpoint accepts only mfd_ bearer tokens. User command nonzero exits still return HTTP 200 with exitCode.","security":[{"FrontDoorTokenBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrontDoorExecRequest"},"examples":{"printWorkingDirectory":{"summary":"Inspect the runtime folder","value":{"argv":["pwd"],"cwd":"/project/workspace"}}}}}},"responses":{"200":{"description":"Structured command result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrontDoorExecResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/FrontDoorRateLimited"},"502":{"$ref":"#/components/responses/FrontDoorInfrastructureFailed"}}}},"/api/workspaces/{workspaceId}/frontdoor/files":{"put":{"operationId":"uploadWorkspaceFrontDoorFile","tags":["Front Door"],"description":"Uploads binary bytes directly to a user-controlled relative path under /project/workspace. Rejects absolute paths, .. segments, Grain-owned paths, excluded directories, and symlink parent escapes. V1 uploads are not resumable.","security":[{"FrontDoorTokenBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"},{"$ref":"#/components/parameters/FrontDoorFilePath"},{"name":"X-Grain-Content-SHA256","in":"header","required":false,"schema":{"type":"string","pattern":"^[A-Fa-f0-9]{64}$"},"description":"Optional hexadecimal SHA-256 digest for the uploaded bytes; the service normalizes it to lowercase."}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"},"examples":{"binaryFile":{"summary":"Raw file bytes","value":"<binary bytes>"}}}}},"responses":{"201":{"description":"Uploaded file metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrontDoorFileUploadResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/FrontDoorRateLimited"},"502":{"$ref":"#/components/responses/FrontDoorInfrastructureFailed"}}},"get":{"operationId":"downloadWorkspaceFrontDoorFile","tags":["Front Door"],"description":"Downloads a binary workspace file from a user-controlled relative path under /project/workspace. V1 does not support HTTP Range requests.","security":[{"FrontDoorTokenBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceId"},{"$ref":"#/components/parameters/FrontDoorFilePath"}],"responses":{"200":{"description":"Raw file bytes.","headers":{"Content-Length":{"schema":{"type":"integer"},"description":"File size in bytes."},"X-Grain-Workspace-Path":{"schema":{"type":"string"},"description":"Absolute path inside the workspace sandbox."}},"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/FrontDoorRateLimited"},"502":{"$ref":"#/components/responses/FrontDoorInfrastructureFailed"}}}}},"components":{"securitySchemes":{"FirebaseBearer":{"type":"http","scheme":"bearer","bearerFormat":"Firebase ID token","description":"Firebase user bearer token used for first-party token management and user-authorized SQL calls."},"GrainApiTokenBearer":{"type":"http","scheme":"bearer","bearerFormat":"mat_ opaque token","description":"Workspace-bound Grain API token accepted by scoped SQL and command endpoints."},"FrontDoorTokenBearer":{"type":"http","scheme":"bearer","bearerFormat":"mfd_ opaque token","description":"Workspace-bound Front Door token accepted by /frontdoor/capabilities, /frontdoor/exec, and /frontdoor/files."}},"parameters":{"WorkspaceId":{"name":"workspaceId","in":"path","required":true,"description":"Workspace identifier from Grain. API tokens can only access the workspace they were created for.","schema":{"type":"string","minLength":1,"maxLength":128}},"TokenId":{"name":"tokenId","in":"path","required":true,"description":"Opaque token metadata id returned by create/list token operations. This is not the raw mat_ token.","schema":{"type":"string","minLength":1,"maxLength":128}},"CommandId":{"name":"commandId","in":"path","required":true,"description":"Workspace command id registered in .grain/grain.json, such as todo.create.","schema":{"type":"string","minLength":1,"maxLength":120,"pattern":"^[A-Za-z0-9_][A-Za-z0-9_.-]{0,119}$"}},"FrontDoorFilePath":{"name":"path","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":2048,"pattern":"^[^\\u0000]*$"},"description":"Relative path under /project/workspace, for example uploads/archive.zip. Absolute paths, .. segments, NUL bytes, Grain-owned paths, and excluded directories are rejected."},"FrontDoorTokenId":{"name":"tokenId","in":"path","required":true,"description":"Front Door token metadata id returned by create/list operations; this is not the raw mfd_ token.","schema":{"type":"string","pattern":"^fdt_[A-Za-z0-9_-]{16,80}$"}}},"schemas":{"ApiTokenScope":{"type":"string","enum":["data:read","data:write","schema:write","commands:full_access"],"description":"Coarse execution permission authorized for an API token. SQL scopes authorize requested SQL lanes; commands:full_access authorizes command discovery, schema reads, and execution."},"ApiTokenRole":{"type":"string","enum":["reader","writer","schema_writer","command_runner","custom"],"description":"Display preset used when creating a token. Authorization is based on the stored scopes."},"CreateApiTokenRequest":{"type":"object","description":"Request to create a workspace API token through Firebase-authenticated token management.","properties":{"name":{"type":"string","minLength":1,"maxLength":120,"pattern":"^[\\s\\S]*\\S[\\s\\S]*$","description":"Human-readable non-blank name shown in token lists and audit context."},"role":{"$ref":"#/components/schemas/ApiTokenRole","default":"reader","description":"Preset role to expand into scopes unless custom is selected."},"scopes":{"type":"array","minItems":1,"description":"Explicit scopes required only when role is custom; custom tokens must include at least one scope.","items":{"$ref":"#/components/schemas/ApiTokenScope"}},"expiresAt":{"type":["string","null"],"format":"date-time","description":"Optional expiration timestamp; omitted uses a 30-day default, null means no expiration, and a timestamp cannot be more than 90 days ahead."}},"required":["name"],"additionalProperties":false},"ApiTokenMetadata":{"type":"object","description":"Token metadata safe to list. It never contains raw token values or token hashes.","properties":{"id":{"type":"string","description":"Metadata identifier used for revoke calls."},"workspaceId":{"type":"string","description":"Workspace this token is bound to."},"createdByUserId":{"type":["string","null"],"description":"Firebase user id that created the token, retained for attribution."},"name":{"type":"string","description":"Human-readable token name."},"role":{"$ref":"#/components/schemas/ApiTokenRole","description":"Display preset stored for the token."},"scopes":{"type":"array","minItems":1,"description":"Access modes this token may request.","items":{"$ref":"#/components/schemas/ApiTokenScope"}},"expiresAt":{"type":["string","null"],"format":"date-time","description":"When the token stops authenticating, or null when it has no expiry."},"lastUsedAt":{"type":["string","null"],"format":"date-time","description":"Most recent successful authentication time for SQL usage."},"revokedAt":{"type":["string","null"],"format":"date-time","description":"Revocation timestamp, or null when active."},"createdAt":{"type":"string","format":"date-time","description":"Creation timestamp."},"updatedAt":{"type":"string","format":"date-time","description":"Last metadata update timestamp."}},"required":["id","workspaceId","createdByUserId","name","role","scopes","expiresAt","lastUsedAt","revokedAt","createdAt","updatedAt"]},"CreateApiTokenResponse":{"type":"object","description":"Create-token response that includes the one-time raw mat_ token.","properties":{"token":{"$ref":"#/components/schemas/ApiTokenMetadata","description":"Stored token metadata."},"rawToken":{"type":"string","pattern":"^mat_[A-Za-z0-9_-]+$","description":"One-time raw token. Store it immediately; it is not recoverable later."}},"required":["token","rawToken"]},"ApiTokenList":{"type":"object","description":"List response containing token metadata only.","properties":{"tokens":{"type":"array","description":"Workspace token metadata entries.","items":{"$ref":"#/components/schemas/ApiTokenMetadata"}}},"required":["tokens"]},"CommandSafety":{"type":"object","description":"Agent-facing safety summary from .grain/grain.json.","properties":{"sideEffects":{"type":"string","enum":["read","write","destructive"],"description":"Declared side-effect class for the command."},"description":{"type":"string","minLength":1,"description":"Human-readable safety detail for the command."}},"required":["sideEffects","description"],"additionalProperties":false},"CommandSummary":{"type":"object","description":"Workspace command metadata safe for external agents.","properties":{"id":{"type":"string","description":"Command id registered in .grain/grain.json."},"title":{"type":"string","description":"Short command title."},"description":{"type":"string","description":"Detailed natural-language command description."},"safety":{"$ref":"#/components/schemas/CommandSafety","description":"Side-effect and safety metadata."}},"required":["id","title","description","safety"],"additionalProperties":false},"CommandList":{"type":"object","description":"Enabled command discovery response. Input schemas are intentionally omitted from this summary list.","properties":{"commands":{"type":"array","description":"Available enabled workspace commands.","items":{"$ref":"#/components/schemas/CommandSummary"}}},"required":["commands"],"additionalProperties":false},"JsonSchemaObject":{"type":"object","description":"Generated JSON Schema object for a command argument bag.","additionalProperties":true},"CommandSchema":{"type":"object","description":"Detailed command metadata plus generated input schema.","properties":{"commandId":{"type":"string","description":"Command id this schema describes."},"title":{"type":"string","description":"Short command title."},"description":{"type":"string","description":"Detailed natural-language command description."},"inputSchema":{"$ref":"#/components/schemas/JsonSchemaObject","description":"Generated JSON Schema for command arguments."},"safety":{"$ref":"#/components/schemas/CommandSafety","description":"Side-effect and safety metadata."}},"required":["commandId","title","description","inputSchema","safety"],"additionalProperties":false},"CommandCall":{"type":"object","description":"One command invocation in a batch.","properties":{"commandId":{"type":"string","minLength":1,"maxLength":120,"pattern":"^[A-Za-z0-9_][A-Za-z0-9_.-]{0,119}$","description":"Command id to execute."},"arguments":{"type":"object","description":"Object argument bag matching the command input schema. Defaults to {}.","default":{},"additionalProperties":true}},"required":["commandId"],"additionalProperties":false},"ExecuteCommandsRequest":{"type":"object","description":"Sequential command execution batch.","properties":{"commands":{"type":"array","minItems":1,"maxItems":10,"description":"Command calls to run in request order.","items":{"$ref":"#/components/schemas/CommandCall"}}},"required":["commands"],"additionalProperties":false},"CommandResultSuccess":{"type":"object","properties":{"runId":{"type":"string","description":"Audit run id for the known command."},"commandId":{"type":"string","description":"Command id that was requested."},"ok":{"const":true},"result":{"description":"Command result returned by the workspace command."},"startedAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time"}},"required":["runId","commandId","ok","result","startedAt","finishedAt"],"additionalProperties":false},"CommandResultFailure":{"type":"object","properties":{"runId":{"type":["string","null"],"description":"Audit run id, or null when the command id was unknown and not audited."},"commandId":{"type":"string","description":"Command id that was requested."},"ok":{"const":false},"error":{"type":"object","properties":{"code":{"type":"string","description":"Stable command error code, such as command_not_found or command_failed."},"message":{"type":"string","description":"Human-readable error message safe for clients."}},"required":["code","message"],"additionalProperties":false},"startedAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time"}},"required":["runId","commandId","ok","error","startedAt","finishedAt"],"additionalProperties":false},"CommandResult":{"oneOf":[{"$ref":"#/components/schemas/CommandResultSuccess"},{"$ref":"#/components/schemas/CommandResultFailure"}]},"ExecuteCommandsResponse":{"type":"object","description":"Command batch result.","properties":{"batchId":{"type":"string","description":"Batch id shared by audited known command calls."},"results":{"type":"array","description":"Per-command results in request order.","items":{"$ref":"#/components/schemas/CommandResult"}}},"required":["batchId","results"],"additionalProperties":false},"RunWorkspaceSqlRequest":{"type":"object","description":"Raw SQLite execution request. V1 intentionally passes SQL through without statement parsing.","properties":{"accessMode":{"$ref":"#/components/schemas/WorkspaceSqlAccessMode","default":"data:read","description":"Requested execution lane for this SQL run."},"mode":{"$ref":"#/components/schemas/WorkspaceSqlMode","default":"all","description":"SQL execution mode."},"sql":{"type":"string","minLength":1,"maxLength":262144,"description":"Raw SQL text to execute against /project/workspace/data.db."},"params":{"description":"Optional scalar positional array or named object params.","oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceSqlValue"}},{"type":"object","additionalProperties":{"$ref":"#/components/schemas/WorkspaceSqlValue"}}]},"maxRows":{"type":"integer","minimum":1,"maximum":100000,"description":"Optional all-mode row cap; rejected for get, run and exec. The response sets truncated when capped."}},"required":["sql"],"additionalProperties":false},"WorkspaceSqlValue":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}],"description":"Scalar SQLite parameter value. Arrays are positional; objects are named parameters."},"WorkspaceSqlCell":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"null"},{"type":"object","properties":{"kind":{"const":"bigint"},"value":{"type":"string"}},"required":["kind","value"],"additionalProperties":false},{"type":"object","properties":{"kind":{"const":"unsafeInteger"},"value":{"type":"string"}},"required":["kind","value"],"additionalProperties":false},{"type":"object","properties":{"kind":{"const":"blob"},"bytes":{"type":"integer","minimum":0}},"required":["kind","bytes"],"additionalProperties":false},{"type":"object","properties":{"kind":{"const":"truncatedText"},"value":{"type":"string"},"bytes":{"type":"integer","minimum":0}},"required":["kind","value","bytes"],"additionalProperties":false}],"description":"A SQLite cell. Large integers, blobs and truncated text use tagged objects."},"WorkspaceSqlRow":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/WorkspaceSqlCell"}},"WorkspaceSqlAccessMode":{"type":"string","enum":["data:read","data:write","schema:write"],"description":"Execution lane. data:read opens SQLite readonly; write lanes open read/write and trust caller SQL."},"WorkspaceSqlMode":{"type":"string","enum":["all","get","run","exec"],"description":"SQL execution mode: all returns rows, get returns one row, run returns mutation metadata, exec runs SQL text directly."},"RunWorkspaceSqlResponse":{"type":"object","description":"SQL execution response. Shape depends on mode and may be truncated for bulk reads.","properties":{"accessMode":{"$ref":"#/components/schemas/WorkspaceSqlAccessMode","description":"Access mode used for this run."},"mode":{"$ref":"#/components/schemas/WorkspaceSqlMode","description":"Execution mode used for this run."},"columns":{"type":"array","description":"Column names returned by all mode.","items":{"type":"string"}},"rows":{"type":"array","description":"Rows returned by all mode.","items":{"$ref":"#/components/schemas/WorkspaceSqlRow"}},"row":{"oneOf":[{"$ref":"#/components/schemas/WorkspaceSqlRow"},{"type":"null"}],"description":"Single row returned by get mode, or null."},"changes":{"type":"integer","minimum":0,"description":"Rows changed by run mode when SQLite reports it."},"lastInsertRowid":{"oneOf":[{"type":"integer"},{"type":"string"}],"description":"Last inserted row id from run mode. Large ids are strings."},"truncated":{"type":"boolean","description":"True when row or byte caps stopped result accumulation."}},"required":["accessMode","mode"]},"ApiError":{"type":"object","description":"Shared Grain API error envelope with a stable machine code and human-readable message.","properties":{"error":{"type":"object","description":"Error details.","properties":{"code":{"type":"string","description":"Stable error code such as unauthorized, forbidden, sql_failed, sql_timeout, or sql_result_too_large."},"message":{"type":"string","description":"Human-readable error message safe for clients."}},"required":["code","message"],"additionalProperties":true}},"required":["error"],"additionalProperties":false},"CreateFrontDoorTokenRequest":{"type":"object","description":"Create a workspace-bound Front Door token. Omit expiresAt for the default seven-day lifetime; use null for no expiry.","properties":{"name":{"type":"string","minLength":1,"maxLength":120,"pattern":"^[\\s\\S]*\\S[\\s\\S]*$","description":"Human-readable non-blank token name."},"expiresAt":{"type":["string","null"],"format":"date-time","description":"Optional future expiry. The maximum lifetime is 90 days."}},"required":["name"],"additionalProperties":false},"FrontDoorTokenMetadata":{"type":"object","description":"Safe token metadata. Raw token values and hashes are never included.","properties":{"id":{"type":"string","pattern":"^fdt_[A-Za-z0-9_-]{16,80}$"},"workspaceId":{"type":"string","minLength":1},"createdByUserId":{"type":["string","null"]},"name":{"type":"string","minLength":1},"scratchId":{"type":"string","pattern":"^fds_[A-Za-z0-9_-]{16,80}$"},"expiresAt":{"type":["string","null"],"format":"date-time"},"lastUsedAt":{"type":["string","null"],"format":"date-time"},"revokedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","workspaceId","createdByUserId","name","scratchId","expiresAt","lastUsedAt","revokedAt","createdAt","updatedAt"],"additionalProperties":false},"CreateFrontDoorTokenResponse":{"type":"object","description":"Created token metadata, one-time raw mfd_ token, and a ready-to-use environment block.","properties":{"token":{"$ref":"#/components/schemas/FrontDoorTokenMetadata"},"rawToken":{"type":"string","pattern":"^mfd_[A-Za-z0-9_-]{32,}$","description":"One-time raw token. Store it immediately."},"env":{"type":"object","properties":{"GRAIN_FRONTDOOR_URL":{"type":"string","format":"uri"},"GRAIN_FRONTDOOR_TOKEN":{"type":"string","pattern":"^mfd_[A-Za-z0-9_-]{32,}$"}},"required":["GRAIN_FRONTDOOR_URL","GRAIN_FRONTDOOR_TOKEN"],"additionalProperties":false}},"required":["token","rawToken","env"],"additionalProperties":false},"FrontDoorTokenList":{"type":"object","description":"Front Door token metadata list; raw values are never returned.","properties":{"tokens":{"type":"array","items":{"$ref":"#/components/schemas/FrontDoorTokenMetadata"}}},"required":["tokens"],"additionalProperties":false},"FrontDoorCapabilities":{"type":"object","description":"Workspace Front Door support, execution, filesystem and transfer contract.","properties":{"workspaceId":{"type":"string","minLength":1},"supported":{"type":"boolean"},"unsupportedReason":{"type":["string","null"]},"lastCheckedAt":{"type":["string","null"],"format":"date-time"},"connection":{"type":"object","properties":{"frontDoorUrl":{"type":"string","format":"uri"},"execEndpoint":{"type":"string"},"authHeader":{"const":"Authorization: Bearer $GRAIN_FRONTDOOR_TOKEN"}},"required":["frontDoorUrl","execEndpoint","authHeader"],"additionalProperties":false},"execution":{"type":"object","properties":{"requestShape":{"const":"argv"},"maxArgs":{"type":"integer","minimum":1},"maxArgBytes":{"type":"integer","minimum":1},"defaultTimeoutMs":{"type":"integer","minimum":1},"maxTimeoutMs":{"type":"integer","minimum":1},"maxOutputBytes":{"type":"integer","minimum":1},"nonzeroExitHttpStatus":{"const":200}},"required":["requestShape","maxArgs","maxArgBytes","defaultTimeoutMs","maxTimeoutMs","maxOutputBytes","nonzeroExitHttpStatus"],"additionalProperties":false},"filesystem":{"type":"object","properties":{"workspace":{"type":"object","properties":{"path":{"const":"/project/workspace"},"mode":{"const":"read-write"}},"required":["path","mode"],"additionalProperties":false},"tmp":{"type":"object","properties":{"path":{"const":"/tmp"},"mode":{"const":"read-write"},"lifecycle":{"const":"per-exec"}},"required":["path","mode","lifecycle"],"additionalProperties":false}},"required":["workspace","tmp"],"additionalProperties":false},"fileTransfer":{"type":"object","properties":{"directUpload":{"type":"object","properties":{"available":{"type":"boolean"},"endpoint":{"type":"string"},"maxBytes":{"type":"integer","minimum":1},"contentTypes":{"type":"array","items":{"const":"application/octet-stream"}}},"required":["available","endpoint","maxBytes","contentTypes"],"additionalProperties":false},"download":{"type":"object","properties":{"available":{"type":"boolean"},"endpoint":{"type":"string"},"rangeRequests":{"const":false}},"required":["available","endpoint","rangeRequests"],"additionalProperties":false},"resumableUpload":{"type":"object","properties":{"available":{"const":false}},"required":["available"],"additionalProperties":false}},"required":["directUpload","download","resumableUpload"],"additionalProperties":false},"enabled":{"type":"array","items":{"$ref":"#/components/schemas/FrontDoorCapabilityEntry"}},"blocked":{"type":"array","items":{"$ref":"#/components/schemas/FrontDoorCapabilityEntry"}},"examples":{"type":"array","items":{"$ref":"#/components/schemas/FrontDoorCapabilityExample"}}},"required":["workspaceId","supported","unsupportedReason","lastCheckedAt","connection","execution","filesystem","fileTransfer","enabled","blocked","examples"],"additionalProperties":false},"FrontDoorCapabilityEntry":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"}},"required":["id","label","description"],"additionalProperties":false},"FrontDoorCapabilityExample":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"curl":{"type":"string"},"enabled":{"type":"boolean"}},"required":["id","label","curl","enabled"],"additionalProperties":false},"FrontDoorFileUploadResponse":{"type":"object","properties":{"path":{"type":"string","minLength":1,"example":"uploads/file.bin"},"workspacePath":{"type":"string","minLength":1,"example":"/project/workspace/uploads/file.bin"},"bytesWritten":{"type":"integer","minimum":0},"sha256":{"type":["string","null"],"pattern":"^[a-f0-9]{64}$"}},"required":["path","workspacePath","bytesWritten","sha256"],"additionalProperties":false},"FrontDoorExecRequest":{"type":"object","properties":{"argv":{"type":"array","items":{"type":"string","maxLength":4096,"pattern":"^[^\\u0000]*$"},"minItems":1,"maxItems":64},"cwd":{"type":"string","maxLength":1024,"pattern":"^[^\\u0000]*$","default":"/project/workspace"},"timeoutMs":{"type":"integer","minimum":1000,"maximum":60000,"default":60000}},"required":["argv"],"additionalProperties":false},"FrontDoorExecResponse":{"type":"object","properties":{"runId":{"type":"string","minLength":1},"exitCode":{"type":["integer","null"]},"stdout":{"type":"string"},"stderr":{"type":"string"},"timedOut":{"type":"boolean"},"stdoutBytes":{"type":"integer"},"stderrBytes":{"type":"integer"},"truncated":{"type":"boolean"}},"required":["runId","exitCode","stdout","stderr","timedOut","stdoutBytes","stderrBytes","truncated"],"additionalProperties":false}},"responses":{"InvalidRequest":{"description":"The request body, path parameter or query selection failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"invalid_request","message":"Invalid request"}}}}},"RateLimited":{"description":"The route-specific request rate limit was exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"rate_limited","message":"Too many requests"}}}}},"Unauthorized":{"description":"Bearer token is missing, expired, revoked, malformed, or not the required auth type for this route.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"unauthorized","message":"Unauthorized"}}}}},"Forbidden":{"description":"Authenticated principal exists but lacks the required workspace role or API token scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"forbidden","message":"You do not have access to this workspace"}}}}},"NotFound":{"description":"Workspace, token, or workspace database was not found, including workspace-isolation failures.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"workspace_not_found","message":"Workspace not found"}}}}},"SqlFailed":{"description":"SQLite rejected the caller-provided SQL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"sql_failed","message":"SQLite execution failed"}}}}},"SqlTimeout":{"description":"SQL execution exceeded the 120 second timeout.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"sql_timeout","message":"SQL execution timed out"}}}}},"SqlResultTooLarge":{"description":"The SQL result exceeded response byte caps before it could be returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"sql_result_too_large","message":"SQL result exceeded the response size limit"}}}}},"CommandManifestInvalid":{"description":".grain/grain.json command metadata is invalid or unsupported for API exposure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"command_manifest_invalid","message":"Command manifest is invalid"}}}}},"CommandNotFound":{"description":"Workspace or enabled command was not found. Disabled or missing-enabled commands are treated as not found. Workspace-isolation failures also use not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"command_not_found","message":"Command not found"}}}}},"CommandTimeout":{"description":"Command discovery or execution exceeded the sandbox timeout.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"command_timeout","message":"Command execution timed out"}}}}},"FrontDoorUnsupported":{"description":"The workspace sandbox cannot run Front Door commands.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"frontdoor_unsupported","message":"Front Door is unavailable"}}}}},"FrontDoorRateLimited":{"description":"Front Door rate or concurrency limits rejected the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"rate_limited","message":"Too many Front Door requests"}}}}},"FrontDoorInfrastructureFailed":{"description":"Sandbox boot, transport, or command execution infrastructure failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"frontdoor_infrastructure_failed","message":"Workspace command failed"}}}}},"BadRequest":{"description":"Invalid request shape, headers, path, checksum, or content type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"invalid_request","message":"Invalid request"}}}}},"Conflict":{"description":"The requested workspace path conflicts with an existing file, directory, or symlink boundary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"file_path_conflict","message":"Target path conflicts with an existing workspace path"}}}}},"PayloadTooLarge":{"description":"The uploaded file is larger than the direct upload limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"code":"upload_too_large","message":"Upload is larger than the direct upload limit"}}}}}}}}