omnigraph/openapi.json
Ragnor Comerford f4c38bb75a
Scope the stored-query 404-hiding claim to non-invoke_query callers
Review found the deny==404 catalog-hiding was overstated as a contract: it
holds only at the outer invoke_query gate. A caller that HOLDS invoke_query
but lacks read/change gets the inner gate's 403 for an existing query vs 404
for an unknown one — so existence is visible to grant-holders by design (the
intended double-gate). The handler docstring, OpenAPI 404 description, and
server.md all claimed the 404 was airtight against any denied actor.

Correct the wording in all three (no behavior change) and add the missing
symmetric test (invoke_query but no read -> 403 for an existing query, 404
for unknown) so the actual contract is pinned. Also document that in
default-deny mode (tokens, no policy) every invocation 404s until an
invoke_query rule is configured.

Nits: the from_specs collision comment said "first declared wins" but it is
lexicographically-first by name (BTreeMap); the effective_tool_name docstring
overclaimed the CLI display routes through it (it resolves the rule on its
own output DTO).
2026-05-30 23:33:27 +02:00

2091 lines
59 KiB
JSON

{
"openapi": "3.1.0",
"info": {
"title": "Omnigraph API",
"description": "HTTP API for the Omnigraph graph database",
"license": {
"name": "MIT",
"identifier": "MIT"
},
"version": "0.6.0"
},
"paths": {
"/branches": {
"get": {
"tags": [
"branches"
],
"summary": "List all branches.",
"description": "Returns branch names sorted alphabetically. Read-only.",
"operationId": "listBranches",
"responses": {
"200": {
"description": "List of branches",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BranchListOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
},
"post": {
"tags": [
"branches"
],
"summary": "Create a new branch.",
"description": "Forks `name` off of `from` (defaults to `main`). The new branch shares\ntable data with its parent until it is mutated. Returns 409 if `name`\nalready exists.",
"operationId": "createBranch",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BranchCreateRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Branch created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BranchCreateOutput"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"409": {
"description": "Branch already exists",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"429": {
"description": "Per-actor admission cap exceeded; honor `Retry-After` header",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/branches/merge": {
"post": {
"tags": [
"branches"
],
"summary": "Merge one branch into another.",
"description": "Merges `source` into `target` (defaults to `main`). Outcome is one of\n`already_up_to_date`, `fast_forward`, or `merged`. Returns 409 with the\nlist of conflicts if the merge cannot be completed; the target is left\nunchanged in that case. **Destructive** to `target` on success.",
"operationId": "mergeBranches",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BranchMergeRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Branches merged",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BranchMergeOutput"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"409": {
"description": "Merge conflict",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"429": {
"description": "Per-actor admission cap exceeded; honor `Retry-After` header",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/branches/{branch}": {
"delete": {
"tags": [
"branches"
],
"summary": "Delete a branch.",
"description": "**Irreversible.** Removes the branch pointer; commits remain reachable\nonly if referenced by another branch. Returns 404 if the branch does not\nexist.",
"operationId": "deleteBranch",
"parameters": [
{
"name": "branch",
"in": "path",
"description": "Branch name to delete",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Branch deleted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BranchDeleteOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"404": {
"description": "Branch not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"429": {
"description": "Per-actor admission cap exceeded; honor `Retry-After` header",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/change": {
"post": {
"tags": [
"mutations"
],
"summary": "**Deprecated** — use [`POST /mutate`](#tag/mutations/operation/mutate) instead.",
"description": "Apply a GQ mutation to a branch. Behavior is unchanged; the route is\nkept indefinitely for back-compat. New integrations should target\n`POST /mutate`, which has identical semantics and a name that pairs\ncleanly with `POST /query`. Responses from this route include\n`Deprecation: true` and `Link: </mutate>; rel=\"successor-version\"`\nheaders per RFC 9745 / RFC 8288 so SDKs and proxies can surface the\nsignal.",
"operationId": "change",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChangeRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Mutation results (response includes `Deprecation: true` + `Link: </mutate>; rel=\"successor-version\"`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChangeOutput"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"409": {
"description": "Merge conflict",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"429": {
"description": "Per-actor admission cap exceeded; honor `Retry-After` header",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"deprecated": true,
"security": [
{
"bearer_token": []
}
]
}
},
"/commits": {
"get": {
"tags": [
"commits"
],
"summary": "List commits.",
"description": "Filter by `branch` to get the commits on a single branch (most recent\nfirst); omit to list across all branches. Read-only.",
"operationId": "listCommits",
"parameters": [
{
"name": "branch",
"in": "query",
"required": false,
"schema": {
"type": [
"string",
"null"
]
}
}
],
"responses": {
"200": {
"description": "List of commits",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CommitListOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/commits/{commit_id}": {
"get": {
"tags": [
"commits"
],
"summary": "Get a single commit.",
"description": "Returns the commit's manifest version, parent commit(s), and creation\nmetadata. Read-only.",
"operationId": "getCommit",
"parameters": [
{
"name": "commit_id",
"in": "path",
"description": "Commit identifier",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Commit details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CommitOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"404": {
"description": "Commit not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/export": {
"post": {
"tags": [
"queries"
],
"summary": "Stream the contents of a branch as NDJSON.",
"description": "Emits one JSON object per line (`application/x-ndjson`). Filter with\n`type_names` (node/edge type names) and/or `table_keys`; both empty\nstreams the entire branch. Suitable for large exports — the response is\nstreamed, not buffered. Read-only.",
"operationId": "export",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExportRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Exported data as NDJSON",
"content": {
"application/x-ndjson": {}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/graphs": {
"get": {
"tags": [
"management"
],
"summary": "List every graph currently registered with this server (MR-668).",
"description": "Multi-graph mode only. In single mode, the route returns 405 — there's\nno registry to enumerate. Cedar-gated by the server-level policy via\nthe `graph_list` action against `Omnigraph::Server::\"root\"`.\n\nOrder: alphabetical by `graph_id` (server-sorted so clients see\ndeterministic output across requests).",
"operationId": "listGraphs",
"responses": {
"200": {
"description": "List of registered graphs",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GraphListResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"405": {
"description": "Method not allowed (single-graph mode)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/healthz": {
"get": {
"tags": [
"health"
],
"summary": "Liveness probe.",
"description": "Returns server status and version. Unauthenticated; safe to call from any\ncaller. Use this to confirm the server is reachable before invoking other\nendpoints.",
"operationId": "health",
"responses": {
"200": {
"description": "Server is healthy",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HealthOutput"
}
}
}
}
}
}
},
"/ingest": {
"post": {
"tags": [
"mutations"
],
"summary": "Bulk-ingest NDJSON data into a branch.",
"description": "`data` is NDJSON with one record per line. `mode` controls behavior on\nexisting rows: `merge` upserts by id (default), `append` blindly inserts,\n`overwrite` replaces table contents. If `branch` does not exist it is\ncreated from `from` (defaults to `main`). **Destructive** when `mode` is\n`overwrite` or when ingest produces conflicting writes.",
"operationId": "ingest",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IngestRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Ingest results",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IngestOutput"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"429": {
"description": "Per-actor admission cap exceeded; honor `Retry-After` header",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/mutate": {
"post": {
"tags": [
"mutations"
],
"summary": "Apply a GQ mutation to a branch (canonical mutation endpoint).",
"description": "Writes to the named `branch` (defaults to `main`). Mutations are atomic\nper call and produce a new commit. Returns counts of nodes and edges\naffected. **Destructive**: on success the branch is updated; rejected\nmutations may still acquire locks briefly. Returns 409 on merge conflict.\n\nPairs with `POST /query` (read-only). The legacy `POST /change` route\nhas identical semantics and is kept as a deprecated alias.",
"operationId": "mutate",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChangeRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Mutation results",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChangeOutput"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"409": {
"description": "Merge conflict",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"429": {
"description": "Per-actor admission cap exceeded; honor `Retry-After` header",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/queries/{name}": {
"post": {
"tags": [
"queries"
],
"summary": "Invoke a curated, server-side stored query by name.",
"description": "The query source comes from the graph's `queries:` registry, not the\nrequest body — callers send only runtime inputs (`params`, `branch`,\n`snapshot`). Gated by the `invoke_query` Cedar action at the boundary;\na stored *mutation* additionally passes the engine's `change` gate\n(double-gated). An actor **without** `invoke_query` cannot tell a denied\nquery from a missing one — both return the same 404, so the catalog\ncan't be probed without the grant. Once `invoke_query` is held, the\ninner `read`/`change` gate may surface a 403 for an existing query the\nactor can't run (the intended double-gate signal).",
"operationId": "invoke_query",
"parameters": [
{
"name": "name",
"in": "path",
"description": "Stored query name (the registry key)",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvokeStoredQueryRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Read envelope (ReadOutput) or mutation envelope (ChangeOutput), serialized untagged",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvokeStoredQueryResponse"
}
}
}
},
"400": {
"description": "Bad request (param type error; snapshot on a stored mutation)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden (the inner `change` gate for a stored mutation)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"404": {
"description": "Unknown stored query, or `invoke_query` denied — indistinguishable to a caller without the grant",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"409": {
"description": "Merge conflict",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"429": {
"description": "Per-actor admission cap exceeded; honor `Retry-After` header",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/query": {
"post": {
"tags": [
"queries"
],
"summary": "Execute an inline read query (friendlier-named alternative to `POST /read`).",
"description": "Designed for ad-hoc exploration and AI-agent tool-use: short field\nnames (`query`, `name`) match the CLI `-e` flag and the GQ `query`\nkeyword. Mutations (`insert`/`update`/`delete`) are rejected with 400\n-- use `POST /mutate` (or its deprecated alias `POST /change`) for\nwrite queries. Otherwise behaves identically to `POST /read`: same\ntarget semantics (branch xor snapshot), same Cedar action (Read),\nsame response shape.",
"operationId": "query",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/QueryRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Query results",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ReadOutput"
}
}
}
},
"400": {
"description": "Bad request - also returned when the query body contains mutations; use POST /mutate (or its deprecated alias POST /change) for write queries",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/read": {
"post": {
"tags": [
"queries"
],
"summary": "**Deprecated** — use [`POST /query`](#tag/queries/operation/query) instead.",
"description": "Execute a GQ read query. Behavior is unchanged from prior releases; the\nroute is kept indefinitely for byte-stable back-compat. New integrations\nshould target `POST /query`, which has clean field names (`query` /\n`name`) and a 400-on-mutation guard. Responses from this route include\n`Deprecation: true` and `Link: </query>; rel=\"successor-version\"`\nheaders per RFC 9745 / RFC 8288 so SDKs and proxies can surface the\nsignal.",
"operationId": "read",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ReadRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Query results (response includes `Deprecation: true` + `Link: </query>; rel=\"successor-version\"`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ReadOutput"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"deprecated": true,
"security": [
{
"bearer_token": []
}
]
}
},
"/schema": {
"get": {
"tags": [
"schema"
],
"summary": "Read the current schema source.",
"description": "Returns the project's schema as a single string in `.pg` source form.\nUseful for clients that want to introspect available types and tables\nbefore constructing GQ queries. Read-only.",
"operationId": "getSchema",
"responses": {
"200": {
"description": "Current schema source",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SchemaOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/schema/apply": {
"post": {
"tags": [
"mutations"
],
"summary": "Apply a schema migration.",
"description": "Diffs `schema_source` against the current schema and applies the resulting\nmigration steps (add/drop type, add/drop column, etc.). **Destructive**:\nsome steps drop data. Returns the list of steps applied; if `applied` is\nfalse the diff was unsupported and no changes were made.",
"operationId": "applySchema",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SchemaApplyRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Schema apply results",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SchemaApplyOutput"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"429": {
"description": "Per-actor admission cap exceeded; honor `Retry-After` header",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/snapshot": {
"get": {
"tags": [
"snapshots"
],
"summary": "Read the current snapshot of a branch.",
"description": "Returns the manifest version plus per-table metadata (path, version, row\ncount) for every table on the branch. Defaults to `main` when `branch` is\nomitted. Read-only.",
"operationId": "getSnapshot",
"parameters": [
{
"name": "branch",
"in": "query",
"required": false,
"schema": {
"type": [
"string",
"null"
]
}
}
],
"responses": {
"200": {
"description": "Database snapshot",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SnapshotOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
}
},
"components": {
"schemas": {
"BranchCreateOutput": {
"type": "object",
"required": [
"uri",
"from",
"name"
],
"properties": {
"actor_id": {
"type": [
"string",
"null"
]
},
"from": {
"type": "string"
},
"name": {
"type": "string"
},
"uri": {
"type": "string"
}
}
},
"BranchCreateRequest": {
"type": "object",
"required": [
"name"
],
"properties": {
"from": {
"type": [
"string",
"null"
],
"description": "Parent branch to fork from. Defaults to `main`."
},
"name": {
"type": "string",
"description": "Name of the new branch. Must not already exist."
}
}
},
"BranchDeleteOutput": {
"type": "object",
"required": [
"uri",
"name"
],
"properties": {
"actor_id": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string"
},
"uri": {
"type": "string"
}
}
},
"BranchListOutput": {
"type": "object",
"required": [
"branches"
],
"properties": {
"branches": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"BranchMergeOutcome": {
"type": "string",
"enum": [
"already_up_to_date",
"fast_forward",
"merged"
]
},
"BranchMergeOutput": {
"type": "object",
"required": [
"source",
"target",
"outcome"
],
"properties": {
"actor_id": {
"type": [
"string",
"null"
]
},
"outcome": {
"$ref": "#/components/schemas/BranchMergeOutcome"
},
"source": {
"type": "string"
},
"target": {
"type": "string"
}
}
},
"BranchMergeRequest": {
"type": "object",
"required": [
"source"
],
"properties": {
"source": {
"type": "string",
"description": "Source branch whose commits will be merged."
},
"target": {
"type": [
"string",
"null"
],
"description": "Target branch that will receive the merge. Defaults to `main`."
}
}
},
"ChangeOutput": {
"type": "object",
"required": [
"branch",
"query_name",
"affected_nodes",
"affected_edges"
],
"properties": {
"actor_id": {
"type": [
"string",
"null"
]
},
"affected_edges": {
"type": "integer",
"minimum": 0
},
"affected_nodes": {
"type": "integer",
"minimum": 0
},
"branch": {
"type": "string"
},
"query_name": {
"type": "string"
}
}
},
"ChangeRequest": {
"type": "object",
"required": [
"query"
],
"properties": {
"branch": {
"type": [
"string",
"null"
],
"description": "Target branch. Defaults to `main`."
},
"name": {
"type": [
"string",
"null"
],
"description": "Name of the mutation to run when `query` declares multiple.\n\nAccepts the legacy field name `query_name` as a deserialization alias."
},
"params": {
"description": "JSON object whose keys match the mutation's declared parameters."
},
"query": {
"type": "string",
"description": "GQ mutation source containing `insert`, `update`, or `delete` statements.\nMay declare multiple named mutations; pick one with `name`.\n\nAccepts the legacy field name `query_source` as a deserialization alias.",
"example": "query insert_person($name: String, $age: I32) {\n insert Person { name: $name, age: $age }\n}"
}
}
},
"CommitListOutput": {
"type": "object",
"required": [
"commits"
],
"properties": {
"commits": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CommitOutput"
}
}
}
},
"CommitOutput": {
"type": "object",
"required": [
"graph_commit_id",
"manifest_version",
"created_at"
],
"properties": {
"actor_id": {
"type": [
"string",
"null"
]
},
"created_at": {
"type": "integer",
"format": "int64",
"description": "Commit creation time as Unix epoch microseconds.",
"example": 1714000000000000
},
"graph_commit_id": {
"type": "string"
},
"manifest_branch": {
"type": [
"string",
"null"
]
},
"manifest_version": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"merged_parent_commit_id": {
"type": [
"string",
"null"
]
},
"parent_commit_id": {
"type": [
"string",
"null"
]
}
}
},
"ErrorCode": {
"type": "string",
"enum": [
"unauthorized",
"forbidden",
"bad_request",
"not_found",
"method_not_allowed",
"conflict",
"too_many_requests",
"internal"
]
},
"ErrorOutput": {
"type": "object",
"required": [
"error"
],
"properties": {
"code": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ErrorCode"
}
]
},
"error": {
"type": "string"
},
"manifest_conflict": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ManifestConflictOutput",
"description": "Set when the conflict is a publisher CAS rejection\n(`ManifestConflictDetails::ExpectedVersionMismatch`). The caller's\npre-write view of `table_key` was at version `expected` but the\nmanifest is now at `actual`. Refresh and retry."
}
]
},
"merge_conflicts": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MergeConflictOutput"
}
}
}
},
"ExportRequest": {
"type": "object",
"properties": {
"branch": {
"type": [
"string",
"null"
],
"description": "Branch to export. Defaults to `main`."
},
"table_keys": {
"type": "array",
"items": {
"type": "string"
},
"description": "Restrict the export to these table keys. Empty exports all tables."
},
"type_names": {
"type": "array",
"items": {
"type": "string"
},
"description": "Restrict the export to these node/edge type names. Empty exports all types."
}
}
},
"GraphInfo": {
"type": "object",
"description": "One entry in the response from `GET /graphs`. Cluster operators\nconsume this list to discover which graphs the server is currently\nserving. The shape is intentionally minimal — `graph_id` and `uri`\nare the only fields a routing client needs.",
"required": [
"graph_id",
"uri"
],
"properties": {
"graph_id": {
"type": "string"
},
"uri": {
"type": "string"
}
}
},
"GraphListResponse": {
"type": "object",
"description": "Response from `GET /graphs`. Lists every graph registered with the\nserver in alphabetical order by `graph_id` (sorted server-side so\nclients get deterministic output across requests).",
"required": [
"graphs"
],
"properties": {
"graphs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GraphInfo"
}
}
}
},
"HealthOutput": {
"type": "object",
"required": [
"status",
"version"
],
"properties": {
"source_version": {
"type": [
"string",
"null"
]
},
"status": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
"IngestOutput": {
"type": "object",
"required": [
"uri",
"branch",
"base_branch",
"branch_created",
"mode",
"tables"
],
"properties": {
"actor_id": {
"type": [
"string",
"null"
]
},
"base_branch": {
"type": "string"
},
"branch": {
"type": "string"
},
"branch_created": {
"type": "boolean"
},
"mode": {
"$ref": "#/components/schemas/LoadMode"
},
"tables": {
"type": "array",
"items": {
"$ref": "#/components/schemas/IngestTableOutput"
}
},
"uri": {
"type": "string"
}
}
},
"IngestRequest": {
"type": "object",
"required": [
"data"
],
"properties": {
"branch": {
"type": [
"string",
"null"
],
"description": "Target branch. Created from `from` if it does not yet exist. Defaults to `main`."
},
"data": {
"type": "string",
"description": "NDJSON payload: one record per line, each shaped\n`{\"type\": \"<TypeName>\", \"data\": {...}}`.",
"example": "{\"type\": \"Person\", \"data\": {\"name\": \"Alice\", \"age\": 30}}\n{\"type\": \"Person\", \"data\": {\"name\": \"Bob\", \"age\": 25}}"
},
"from": {
"type": [
"string",
"null"
],
"description": "Parent branch used to create `branch` if it does not exist. Defaults to `main`."
},
"mode": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/LoadMode",
"description": "How existing rows are handled. Defaults to `merge`."
}
]
}
}
},
"IngestTableOutput": {
"type": "object",
"required": [
"table_key",
"rows_loaded"
],
"properties": {
"rows_loaded": {
"type": "integer",
"minimum": 0
},
"table_key": {
"type": "string"
}
}
},
"InvokeStoredQueryRequest": {
"type": "object",
"description": "Body for `POST /queries/{name}` — invokes the server-side stored query\nnamed in the path. The query source and name come from the registry,\nnever the body; only the runtime inputs are supplied here.",
"properties": {
"branch": {
"type": [
"string",
"null"
],
"description": "Branch to run against. Defaults to `main`; for a stored mutation the\nwrite targets this branch."
},
"params": {
"description": "JSON object whose keys match the stored query's declared parameters."
},
"snapshot": {
"type": [
"string",
"null"
],
"description": "Snapshot id to read from (read queries only — rejected for a stored\nmutation). Mutually exclusive with `branch`."
}
}
},
"InvokeStoredQueryResponse": {
"oneOf": [
{
"$ref": "#/components/schemas/ReadOutput"
},
{
"$ref": "#/components/schemas/ChangeOutput"
}
],
"description": "Response for `POST /queries/{name}`: the read envelope for a stored\nread, or the mutation envelope for a stored mutation. Serialized\n**untagged**, so the wire shape is exactly [`ReadOutput`] or\n[`ChangeOutput`] — classification follows the stored query, not a\nwrapper field."
},
"LoadMode": {
"type": "string",
"description": "Shadow enum for documenting [`LoadMode`] in the OpenAPI schema.",
"enum": [
"overwrite",
"append",
"merge"
]
},
"ManifestConflictOutput": {
"type": "object",
"description": "Structured details for a publisher-level OCC failure. Surfaces alongside\nHTTP 409 when a write was rejected because the caller's pre-write view of\none table's manifest version was stale relative to the current head. The\nexpected/actual fields tell the client which table to refresh.",
"required": [
"table_key",
"expected",
"actual"
],
"properties": {
"actual": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"expected": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"table_key": {
"type": "string"
}
}
},
"MergeConflictKindOutput": {
"type": "string",
"enum": [
"divergent_insert",
"divergent_update",
"delete_vs_update",
"orphan_edge",
"unique_violation",
"cardinality_violation",
"value_constraint_violation"
]
},
"MergeConflictOutput": {
"type": "object",
"required": [
"table_key",
"kind",
"message"
],
"properties": {
"kind": {
"$ref": "#/components/schemas/MergeConflictKindOutput"
},
"message": {
"type": "string"
},
"row_id": {
"type": [
"string",
"null"
]
},
"table_key": {
"type": "string"
}
}
},
"QueryRequest": {
"type": "object",
"description": "Inline read-query request for `POST /query`.\n\nFriendlier-named alternative to [`ReadRequest`] for ad-hoc reads and\nAI-agent integration. Mutations are rejected with 400 — use `POST\n/mutate` (or its deprecated alias `POST /change`) for write queries.\nField names are deliberately short (`query`, `name`) to match the GQ\nkeyword and the CLI `-e` flag.",
"required": [
"query"
],
"properties": {
"branch": {
"type": [
"string",
"null"
],
"description": "Branch to read from. Mutually exclusive with `snapshot`. Defaults to `main`."
},
"name": {
"type": [
"string",
"null"
],
"description": "Name of the query to run when `query` declares multiple. Optional when\nonly one query is declared."
},
"params": {
"description": "JSON object whose keys match the query's declared parameters."
},
"query": {
"type": "string",
"description": "GQ read-query source. May declare one or more named queries; pick one\nwith `name` when more than one is declared. Mutations\n(`insert`/`update`/`delete`) get 400 — use `POST /mutate` (or its\ndeprecated alias `POST /change`) instead.",
"example": "query get_person($name: String) {\n match {\n $p: Person { name: $name }\n }\n return { $p.name, $p.age }\n}"
},
"snapshot": {
"type": [
"string",
"null"
],
"description": "Snapshot id to read from. Mutually exclusive with `branch`."
}
}
},
"ReadOutput": {
"type": "object",
"required": [
"query_name",
"target",
"row_count",
"rows"
],
"properties": {
"columns": {
"type": "array",
"items": {
"type": "string"
}
},
"query_name": {
"type": "string"
},
"row_count": {
"type": "integer",
"minimum": 0
},
"rows": {},
"target": {
"$ref": "#/components/schemas/ReadTargetOutput"
}
}
},
"ReadRequest": {
"type": "object",
"required": [
"query_source"
],
"properties": {
"branch": {
"type": [
"string",
"null"
],
"description": "Branch to read from. Mutually exclusive with `snapshot`. Defaults to `main`."
},
"params": {
"description": "JSON object whose keys match the query's declared parameters."
},
"query_name": {
"type": [
"string",
"null"
],
"description": "Name of the query to run when `query_source` declares multiple. Optional\nwhen only one query is declared."
},
"query_source": {
"type": "string",
"description": "GQ query source. May declare one or more named queries; pick one with\n`query_name` if there is more than one.",
"example": "query get_person($name: String) {\n match {\n $p: Person { name: $name }\n }\n return { $p.name, $p.age }\n}"
},
"snapshot": {
"type": [
"string",
"null"
],
"description": "Snapshot id to read from. Mutually exclusive with `branch`."
}
}
},
"ReadTargetOutput": {
"type": "object",
"properties": {
"branch": {
"type": [
"string",
"null"
]
},
"snapshot": {
"type": [
"string",
"null"
]
}
}
},
"SchemaApplyOutput": {
"type": "object",
"required": [
"uri",
"supported",
"applied",
"step_count",
"manifest_version",
"steps"
],
"properties": {
"applied": {
"type": "boolean"
},
"manifest_version": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"step_count": {
"type": "integer",
"minimum": 0
},
"steps": {
"type": "array",
"items": {}
},
"supported": {
"type": "boolean"
},
"uri": {
"type": "string"
}
}
},
"SchemaApplyRequest": {
"type": "object",
"required": [
"schema_source"
],
"properties": {
"allow_data_loss": {
"type": "boolean",
"description": "When true, promote every `DropMode::Soft` step in the plan to\n`DropMode::Hard`, making the prior column data unreachable\nafter the apply. Matches the CLI's `--allow-data-loss` flag.\nDefaults to `false` (drops remain reversible via time travel)."
},
"schema_source": {
"type": "string",
"description": "Project schema in `.pg` source form. The diff against the current\nschema produces the migration steps that will be applied.",
"example": "node Person {\n name: String @key\n age: I32?\n}\n\nedge Knows: Person -> Person"
}
}
},
"SchemaOutput": {
"type": "object",
"required": [
"schema_source"
],
"properties": {
"schema_source": {
"type": "string"
}
}
},
"SnapshotOutput": {
"type": "object",
"required": [
"branch",
"manifest_version",
"tables"
],
"properties": {
"branch": {
"type": "string"
},
"manifest_version": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"tables": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SnapshotTableOutput"
}
}
}
},
"SnapshotTableOutput": {
"type": "object",
"required": [
"table_key",
"table_path",
"table_version",
"row_count"
],
"properties": {
"row_count": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"table_branch": {
"type": [
"string",
"null"
]
},
"table_key": {
"type": "string"
},
"table_path": {
"type": "string"
},
"table_version": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
}
},
"securitySchemes": {
"bearer_token": {
"type": "http",
"scheme": "bearer"
}
}
}
}