{
"openapi": "3.1.0",
"info": {
"title": "Pathbase API",
"description": "HTTP API for Pathbase — repositories, agent trace paths, computation graphs, and anonymous share links.\n\n**Stability.** v1 endpoints are stable in shape; additive changes (new fields, new endpoints, broader query params) ship without a version bump. Breaking changes get a new prefix (`/api/v2/...`) and a deprecation window.\n\n**Spec format.** OpenAPI 3.1, served live at `/api/v1/openapi.json`.\n\n**Versioning header.** Every response carries an `X-Pathbase-Version` header with the running build's identifier (`<semver>+<git-sha>`). The spec does not enumerate it per operation; consumers may read it from any response to pin or correlate.\n\n**Auth.** Most endpoints require a `pat_…` bearer token (see `bearerAuth`). Obtain one through the CLI grant flow (`POST /api/v1/auth/cli/request-grant` then `POST /api/v1/auth/cli/redeem`). Unauthenticated endpoints — the `Anon` namespace, public profile reads — are clearly tagged. Per-operation `security` annotations reflect the actual gate.",
"license": {
"name": ""
},
"version": "1.1.0"
},
"servers": [
{
"url": "https://pathbase.dev",
"description": "Production"
}
],
"paths": {
"/api/v1/auth/cli/redeem": {
"post": {
"tags": [
"Auth: CLI"
],
"summary": "Redeem a CLI pairing grant for a long-lived bearer token.",
"description": "Second leg of the CLI pairing flow (see\n`/api/v1/auth/cli/request-grant` for the full sequence). The CLI\nposts the code the user typed; the server normalizes case and\nwhitespace, consumes the grant, and — on a still-valid grant —\nreturns the bearer token plus user. Single-use; subsequent\nredemptions of the same code return 401.\n\nNo authentication is required to call this endpoint; the grant code\nIS the authentication. A code that is unknown, expired, or already\nredeemed all collapse to `401 Unauthorized` so the response cannot\nbe used to probe for valid codes — the three failure modes are\ndeliberately indistinguishable.",
"operationId": "cli_redeem",
"requestBody": {
"description": "The grant code the user pasted into the CLI. Case and surrounding whitespace are normalized server-side.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RedeemBody"
},
"example": {
"code": "AB12CD34"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Grant redeemed. Store `token` securely and send it as `Authorization: Bearer <token>` on subsequent API calls; it is shown only once.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RedeemResponse"
}
}
}
},
"400": {
"description": "Code is the wrong length after normalization (must be exactly 8 alphanumeric characters).",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"401": {
"description": "Code is unknown, expired, or has already been redeemed. The three failure modes are deliberately indistinguishable.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
}
}
},
"/api/v1/auth/cli/request-grant": {
"post": {
"tags": [
"Auth: CLI"
],
"summary": "Mint a short-lived grant code for pairing the pathbase CLI to this\nbrowser session.",
"description": "**Pairing flow.**\n1. Browser (already authenticated) calls this endpoint and displays\n the returned `code` to the user.\n2. User pastes the code into a CLI prompt (`pathbase login`).\n3. CLI POSTs the code to `/api/v1/auth/cli/redeem`.\n4. Server consumes the grant and returns `{ token, user }` to the\n CLI.\n5. CLI stores `token` and sends it as `Authorization: Bearer\n <token>` on every subsequent API call.\n\nThe grant is single-use and expires after a short window. Multiple\ngrants may be active concurrently; redeeming one has no effect on\nexisting tokens.",
"operationId": "cli_request_grant",
"responses": {
"200": {
"description": "Grant issued. Display `code` to the user so they can paste it into the CLI before `expires_in` seconds elapse.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CliGrantResponse"
}
}
}
},
"401": {
"description": "Caller is not authenticated. Grants can only be minted by an already-logged-in browser session.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/api/v1/auth/sessions": {
"get": {
"tags": [
"Auth: Sessions"
],
"summary": "List the authenticated user's active sessions.",
"description": "Returns one entry per session owned by the caller — both browser\nlogins and CLI tokens. Includes an `is_current` flag so the UI can\nmark which entry is the caller's own session.\n\nUsed by the settings UI to render \"active devices/CLIs\" with a\nper-entry revoke button (which calls\n`DELETE /api/v1/auth/sessions/{id}`).",
"operationId": "list_sessions",
"responses": {
"200": {
"description": "All active sessions for the current user, including the one that issued this request (marked with `is_current: true`).",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SessionSummary"
}
}
}
}
},
"401": {
"description": "Not authenticated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/api/v1/auth/sessions/{id}": {
"delete": {
"tags": [
"Auth: Sessions"
],
"summary": "Revoke one of the caller's sessions.",
"description": "Ends the session identified by `{id}`, immediately invalidating\nits token. The caller can revoke any of their own sessions,\nincluding the one that issued this request — doing so will end\nthe current login. Sessions belonging to other users are reported\nas `404 Not Found` (no leakage of \"exists but isn't yours\").",
"operationId": "revoke_session",
"parameters": [
{
"name": "id",
"in": "path",
"description": "ID of the session to revoke. Obtain from `GET /api/v1/auth/sessions`. Revoking your current session ends the request's own login.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"204": {
"description": "Session revoked. The associated token is no longer accepted."
},
"401": {
"description": "Not authenticated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"404": {
"description": "No session with this ID exists, or it belongs to another user. The two cases are deliberately conflated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/api/v1/u/anon/repos/pathstash/graphs": {
"post": {
"tags": [
"Anon"
],
"summary": "Upload a graph anonymously as the `anon` user. No authentication\nrequired; the resulting graph is always `Unlisted` and reachable\nonly via the returned share URL.",
"operationId": "create_anon_graph",
"requestBody": {
"description": "Toolpath Graph document. The body's `visibility` field is ignored; anon uploads are always `Unlisted`.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UploadGraphBody"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "Graph stored; the `url` in the response is the share link.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GraphDocumentResponse"
}
}
}
},
"400": {
"description": "Document failed to parse as a toolpath Graph.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"413": {
"description": "Request body too large.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"429": {
"description": "Rate limit exceeded; retry later.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
}
}
},
"/api/v1/u/me": {
"get": {
"tags": [
"Users"
],
"summary": "Return the authenticated user's full profile.",
"description": "Canonical \"who am I?\" endpoint. Returns the user matching the\ncaller's bearer token. The returned shape matches the public\n`GET /api/v1/u/{username}` response with one difference: `email`\nis included here because the caller is reading their own profile.",
"operationId": "get_me",
"responses": {
"200": {
"description": "The authenticated user, including the private `email` field.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"401": {
"description": "Not authenticated. Send `Authorization: Bearer <token>`.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
},
"put": {
"tags": [
"Users"
],
"summary": "Replace the authenticated user's editable profile fields.",
"description": "PUT semantics: the body is the new state. Fields not present\n(or `null`) are cleared. Username and email are not editable\nhere. Returns the updated user.",
"operationId": "update_me",
"requestBody": {
"description": "Complete new state of the editable profile fields. Missing or `null` clears the field — send the current value to preserve it.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateProfileBody"
},
"example": {
"bio": "Wonderland correspondent.",
"display_name": "Alice Liddell"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Profile updated; returns the post-update user.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"401": {
"description": "Not authenticated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/api/v1/u/{owner}/repos": {
"get": {
"tags": [
"Repos"
],
"summary": "List repositories owned by `{owner}`. The DB layer applies the\nvisibility filter in SQL using the resolved `Viewer`: owners get\nall three states (Public + Unlisted + Private); non-owners get\n`Public` only. There is no caller-side branching, and no helper\nthat returns \"everything\" without a viewer.",
"operationId": "list_repos",
"parameters": [
{
"name": "owner",
"in": "path",
"description": "Owner username",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
"description": "Optional. Server-bounded; out-of-range values are clamped silently.",
"required": false,
"schema": {
"type": "integer",
"minimum": 0
}
}
],
"responses": {
"200": {
"description": "Owner's repositories. Owners see all repos; other viewers see only public repos.",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Repo"
}
}
}
}
},
"404": {
"description": "Owner not found"
}
},
"security": [
{
"bearerAuth": []
},
{}
]
},
"post": {
"tags": [
"Repos"
],
"summary": "Create a new repository owned by the authenticated user.",
"description": "The new repo's owner is taken from the session — there is no way\nto create a repo on behalf of someone else through this endpoint.\n(Owner usernames in URLs are read-only metadata; ownership is set\nonce at creation.) The repo defaults to public visibility.\n\nReturns the created `Repo` with its server-assigned UUID and\ntimestamps.",
"operationId": "create_repo",
"parameters": [
{
"name": "owner",
"in": "path",
"description": "Owner username",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Name and optional description for the new repo. Owner is implicit (the authenticated user).",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateRepoBody"
},
"example": {
"description": "Captured runs of my coding agent.",
"name": "agent-traces"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "Repository created. Body is the new `Repo`.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Repo"
}
}
}
},
"401": {
"description": "Not authorized to create under this owner.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"409": {
"description": "An owner already has a repo with this `name`. Names are unique per owner.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/api/v1/u/{owner}/repos/{repo}": {
"get": {
"tags": [
"Repos"
],
"summary": "Fetch a repository by `(owner, name)`.",
"description": "Public read; no authentication required. Returns the `Repo`\nincluding description, README, visibility flag, and timestamps. To\nlist a repo's paths or graphs, follow up with the corresponding\n`/paths` and `/graphs` collection endpoints.\n\n**Owner aliases.** The literal `me` resolves to the authenticated\nviewer (401 if unauthenticated) — `/u/me/repos/{repo}` is the\nsame as `/u/{your-username}/repos/{repo}` without needing your\nown username. `anon` is the seeded system namespace; its\n`pathstash` repo is reachable here like any other `Unlisted`\nrepo.\n\n**Visibility.** `can_read` is enforced: `Public` returns to anyone,\n`Unlisted` (system-managed; URL-addressable) returns to anyone,\n`Private` returns only to the owner — others get `404`. Repos are\naddressed by guessable name, so there's no share-by-link at this\nlevel for `Private` repos. (Per-graph/per-path share-by-link inside\n`Public` repos is unaffected — those still gate on their own\nvisibility.)",
"operationId": "get_repo",
"parameters": [
{
"name": "owner",
"in": "path",
"description": "Owner username. The literal `me` resolves to the authenticated viewer (401 if unauthenticated).",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"description": "Repository name (unique per owner).",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Repository.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Repo"
}
}
}
},
"404": {
"description": "Owner unknown, repo unknown, repo is secret and the viewer is not the owner, or `anon` (no profile).",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
},
{}
]
},
"put": {
"tags": [
"Repos"
],
"summary": "Replace a repository's editable fields (name, description, README).",
"description": "Owner-only: the authenticated user must match `{owner}`. The\nliteral `me` resolves to the authenticated viewer's own\nnamespace.\n\nPUT semantics: the body is the new state. To preserve any field,\nsend its current value; missing or `null` clears the nullable\nfields. Visibility is changed through the separate `/visibility`\nsub-resource. Renaming changes the canonical URL of the repo (and\nevery nested resource), so callers should expect old links to 404\nafterward.",
"operationId": "update_repo",
"parameters": [
{
"name": "owner",
"in": "path",
"description": "Owner username; must match the authenticated user. The literal `me` resolves to the authenticated viewer.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"description": "Repository name (current — pre-rename).",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Complete new state of the editable repo fields. `name` is required; missing/`null` clears `description` and `readme`. Send back the current values to preserve them.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateRepoBody"
},
"example": {
"description": "Captured runs of my coding agent (now with vendor labels).",
"name": "agent-traces",
"readme": "# agent-traces\n\nUploaded via `pathbase push`."
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Repository updated; returns the post-update `Repo`.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Repo"
}
}
}
},
"401": {
"description": "Not authenticated, or the caller is not the owner.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"404": {
"description": "Owner unknown, repo unknown, or `\u0007non` (no profile).",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"409": {
"description": "Renaming to a name already used by another of this owner's repos.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
},
"delete": {
"tags": [
"Repos"
],
"summary": "Delete a repository and all its contents.",
"description": "Owner-only. Removes the repo and all paths and graphs in it.\nIrreversible. The literal `me` resolves to the authenticated\nviewer's own namespace.",
"operationId": "delete_repo",
"parameters": [
{
"name": "owner",
"in": "path",
"description": "Owner username; must match the authenticated user. The literal `me` resolves to the authenticated viewer.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"description": "Repository name.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": "Repository (and all paths and graphs in it) deleted."
},
"401": {
"description": "Not authenticated, or the caller is not the owner.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"404": {
"description": "Owner unknown, repo unknown, or `\u0007non` (no profile).",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/api/v1/u/{owner}/repos/{repo}/graphs": {
"get": {
"tags": [
"Graphs"
],
"summary": "List graphs in a repository.",
"description": "Returns up to `?limit=` graphs under `{owner}/{repo}`, ordered by\ncreation time. The DB layer applies the visibility filter in SQL\nusing the resolved `Viewer`: the owner sees all three states\n(`Public` + `Unlisted` + `Private`) so the UI can render the\n`unlisted` pill and offer a show/hide toggle; everyone else sees\nonly `Public`. Non-public graphs remain reachable by direct UUID\nshare-link regardless of listing visibility.",
"operationId": "list_graphs",
"parameters": [
{
"name": "owner",
"in": "path",
"description": "Username of the repository's owner.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"description": "Repository name within the owner's namespace.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
"description": "Optional. Server-bounded; out-of-range values are clamped silently.",
"required": false,
"schema": {
"type": "integer",
"minimum": 0
}
}
],
"responses": {
"200": {
"description": "Page of graphs in the repository, capped by `?limit=`. Each entry carries the graph row plus `path_count` and the `url` clients should link to. Owners see all graphs; other viewers see only public graphs.",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GraphSummaryResponse"
}
}
}
}
},
"404": {
"description": "Owner unknown, repo unknown for that owner, or `\u0007non` (no profile).",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
},
{}
]
},
"post": {
"tags": [
"Graphs"
],
"summary": "Create a graph from a multi-path toolpath document.",
"description": "Inline path entries (full path objects) are admitted into the same\nworkspace as the graph (deduped by toolpath ID; an existing match\nis linked rather than re-created). Refs to existing paths\n(`{ \"$ref\": \"...\" }` entries) are kept as ID references and not\nre-stored.\n\nCaller must own `{owner}/{repo}`. The `document` field must parse\nas a toolpath v1 `Graph`.",
"operationId": "create_graph",
"parameters": [
{
"name": "owner",
"in": "path",
"description": "Username of the repository's owner. Must match the authenticated caller.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"description": "Repository name within the owner's namespace.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Multi-path toolpath Graph document, optional display name, and visibility (`public`/`unlisted`/`private`, default `unlisted`). Inline paths are admitted into the workspace; refs are kept as ID references.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UploadGraphBody"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "The newly created graph with its server-assigned UUID, the reconstructed multi-path `document`, and the canonical `url`.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GraphDocumentResponse"
}
}
}
},
"400": {
"description": "Document failed to parse as a toolpath Graph.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"401": {
"description": "Missing/invalid bearer token, or the authenticated user does not own the target repository.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/api/v1/u/{owner}/repos/{repo}/graphs/{id}": {
"get": {
"tags": [
"Graphs"
],
"summary": "Fetch a single graph with its full reconstructed multi-path\ndocument. `Public` and `Unlisted` graphs are readable by anyone\n(possession of the UUID is the share token for `Unlisted`);\n`Private` graphs require the caller to authenticate as the owner.\nNon-owner reads of a `Private` graph return `404` — same response as\n\"doesn't exist\" so the gate doesn't leak existence.",
"description": "Responses carry an `ETag`; conditional requests using\n`If-None-Match` return `304 Not Modified` when the representation\nis unchanged.",
"operationId": "get_graph",
"parameters": [
{
"name": "owner",
"in": "path",
"description": "Username of the repository's owner.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"description": "Repository name within the owner's namespace.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "id",
"in": "path",
"description": "Graph UUID. For `Unlisted` graphs the UUID is the share token; `Private` graphs additionally require owner authentication.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "The graph row, its `path_count`, the reconstructed multi-path `document`, and the canonical `url`.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GraphDocumentResponse"
}
}
}
},
"304": {
"description": "If-None-Match matched the current ETag."
},
"404": {
"description": "Owner, repository, or graph not found — or graph is `Private` and the caller isn't the owner.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
}
},
"delete": {
"tags": [
"Graphs"
],
"summary": "Delete a graph. Owner only — knowing the UUID is enough to *read*\n(it's the share token) but not enough to mutate.",
"operationId": "delete_graph",
"parameters": [
{
"name": "owner",
"in": "path",
"description": "Owner username — must match the authenticated caller.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"description": "Repository name.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "id",
"in": "path",
"description": "Graph UUID.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"204": {
"description": "Graph deleted; constituent paths remain in the workspace."
},
"401": {
"description": "Not authorized.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"404": {
"description": "Not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/api/v1/u/{owner}/repos/{repo}/graphs/{id}/download": {
"get": {
"tags": [
"Graphs"
],
"summary": "Stream the graph's reconstructed Graph document as raw JSON — the\ninverse of `POST .../graphs`. `Public` and `Unlisted` graphs\ndownload for anyone with the UUID; `Private` graphs require owner\nauthentication and 404 for everyone else (mirrors \"doesn't exist\").",
"operationId": "download_graph",
"parameters": [
{
"name": "owner",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Toolpath Graph JSON file download. Send the returned `ETag` back as `If-None-Match` on a subsequent request to receive 304 when unchanged.",
"headers": {
"Content-Disposition": {
"schema": {
"type": "string"
},
"description": "`attachment; filename=\"…\"`. Filename derives from the graph's display `name` when set, otherwise the graph UUID."
},
"ETag": {
"schema": {
"type": "string"
},
"description": "Strong ETag for conditional GETs. Echo as `If-None-Match` on subsequent requests."
}
},
"content": {
"application/json": {}
}
},
"304": {
"description": "If-None-Match matched the current ETag."
},
"404": {
"description": "Owner, repository, or graph not found — or graph is `Private` and the caller isn't the owner.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
}
}
},
"/api/v1/u/{owner}/repos/{repo}/graphs/{id}/paths": {
"get": {
"tags": [
"Graphs"
],
"summary": "List the paths that belong to a graph, in graph-defined order.\nIf the parent graph is readable to the caller (`Public`/`Unlisted`,\nor `Private` for the owner), all of its constituent paths are\nreturned; the parent's UUID is the share token. A `Private` graph\n404s for non-owners — same as the per-graph GET.",
"operationId": "list_graph_paths",
"parameters": [
{
"name": "owner",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "limit",
"in": "query",
"description": "Optional. Server-bounded; out-of-range values are clamped silently.",
"required": false,
"schema": {
"type": "integer",
"minimum": 0
}
}
],
"responses": {
"200": {
"description": "Page of constituent paths in graph-defined order. Each entry carries the path row, `step_count`, and the `url` clients should link to.",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TracePathSummaryResponse"
}
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
}
}
},
"/api/v1/u/{owner}/repos/{repo}/graphs/{id}/paths/{path_id}": {
"get": {
"tags": [
"Graphs"
],
"summary": "Fetch a constituent path with its full reconstructed document.\nConditional via `If-None-Match`.",
"operationId": "get_graph_path",
"parameters": [
{
"name": "owner",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "path_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Path row plus `step_count`, the reconstructed Graph `document`, and the canonical `url`.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TracePathDocumentResponse"
}
}
}
},
"304": {
"description": "If-None-Match matched the current ETag."
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
}
},
"delete": {
"tags": [
"Graphs"
],
"summary": "Delete a constituent path. Owner only. Cascades to its step rows\nAND removes the junction row from this graph.",
"operationId": "delete_graph_path",
"parameters": [
{
"name": "owner",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "path_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"204": {
"description": ""
},
"401": {
"description": ""
},
"404": {
"description": ""
}
},
"security": [
{
"bearerAuth": []
}
]
},
"patch": {
"tags": [
"Graphs"
],
"summary": "Patch a constituent path's mutable fields — currently just\n`visibility`. Owner only.",
"operationId": "update_graph_path",
"parameters": [
{
"name": "owner",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "path_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateGraphPathBody"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Path updated; returns the path row plus its canonical `url`. Re-fetch the path's full document via the GET endpoint when needed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TracePathResponse"
}
}
}
},
"401": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/api/v1/u/{owner}/repos/{repo}/graphs/{id}/paths/{path_id}/chat": {
"get": {
"tags": [
"Graphs"
],
"summary": "Render the path's HEAD-ancestor chain as a chat-projection — a\ndensely-indexed, pre-classified, optionally pre-rendered\ntranscript.",
"operationId": "get_graph_path_chat",
"parameters": [
{
"name": "owner",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "path_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "include_html",
"in": "query",
"description": "Render `text` and `thinking` fields to sanitized HTML\nserver-side. Defaults to true; set false for lighter payloads\nwhen the client renders markdown lazily.",
"required": false,
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChatProjection"
}
}
}
},
"404": {
"description": ""
}
}
}
},
"/api/v1/u/{owner}/repos/{repo}/graphs/{id}/visibility": {
"patch": {
"tags": [
"Graphs"
],
"summary": "Toggle a graph's public/secret visibility. Owner only.",
"operationId": "update_graph_visibility",
"parameters": [
{
"name": "owner",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateGraphVisibilityBody"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Visibility updated; returns the graph row plus its canonical `url`.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GraphResponse"
}
}
}
},
"401": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/api/v1/u/{owner}/repos/{repo}/visibility": {
"patch": {
"tags": [
"Repos"
],
"summary": "Flip a repo's visibility between `public` and `private`. The\n`unlisted` state is system-only — owner cannot set it, and any\nrepo currently `unlisted` (e.g., the seeded `pathstash`) is locked.",
"operationId": "update_repo_visibility",
"parameters": [
{
"name": "owner",
"in": "path",
"description": "Owner username; must match the authenticated user. The literal `me` resolves to the authenticated viewer.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "repo",
"in": "path",
"description": "Repository name.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "New visibility. Repos accept `public` or `private` only — `unlisted` is system-only.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepoVisibilityBody"
},
"example": {
"visibility": "public"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Visibility updated; returns the post-update `Repo`.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Repo"
}
}
}
},
"400": {
"description": "Body contained `unlisted`, which is not accepted on repos.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"401": {
"description": "Not authenticated, or the caller is not the owner.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"403": {
"description": "Repo's current visibility is `Unlisted` (system-pinned); user cannot modify.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"404": {
"description": "Owner unknown, repo unknown, or owner is reserved.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/api/v1/u/{username}": {
"get": {
"tags": [
"Users"
],
"summary": "Look up a user's public profile by username.",
"description": "No authentication required. Returns the same `User` shape as\n`/u/me` with one guarantee: `email` is always `null` here — that\nfield is only ever populated for the caller's own profile via\n`GET /api/v1/u/me`.\n\n**Reserved namespaces.** The literal usernames `me` and `anon` are\nreserved and always return 404 from this endpoint. Use\n`GET /api/v1/u/me` for the calling user and `/api/v1/u/anon/...`\nfor anonymous content.",
"operationId": "get_user",
"parameters": [
{
"name": "username",
"in": "path",
"description": "Public username. Lowercase ASCII alphanumerics with `-`/`_`. The literal `me` resolves to the authenticated viewer (401 if unauthenticated). `anon` is the seeded system namespace; its profile is not browseable.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Public user profile. `email` is always null here; use `/api/v1/u/me` to read your own email.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"404": {
"description": "No user with that username, or the username is the `anon` system namespace (which has no profile).",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"ActorView": {
"type": "object",
"description": "Resolved actor identity for a turn. Lookup happens once on the\nserver against the document's `meta.actors` block; turns reference\nthe actor by `u32` index into the projection's `actors` array.",
"required": [
"id",
"kind",
"display"
],
"properties": {
"display": {
"type": "string",
"description": "Display name resolved against `meta.actors` if present, else the\nsuffix after `:`."
},
"id": {
"type": "string",
"description": "Canonical actor string (e.g. `\"claude:opus-4-7\"`)."
},
"kind": {
"type": "string",
"description": "Prefix before `:` — `\"claude\"`, `\"human\"`, etc."
},
"model": {
"type": "string",
"nullable": true
}
}
},
"ApiErrorCode": {
"type": "string",
"description": "Closed set of error codes the API can emit. Renders as a snake_case\nstring on the wire (`not_found`, `unauthorized`, …) and is declared\nas an OpenAPI enum so generated clients can switch on it\nexhaustively. Add a variant here, never a free-form string in\nthe `code` field of `ApiErrorResponse`.",
"enum": [
"not_found",
"unauthorized",
"bad_request",
"conflict",
"visibility_locked",
"internal_error"
]
},
"ApiErrorResponse": {
"type": "object",
"description": "Standard error response envelope for all 4xx/5xx responses.\n\nEvery non-2xx response from the API has this shape. The `code`\nis a short, stable token from a closed enum suitable for\nprogrammatic dispatch; the `error` is a human-readable message\nsuitable for log/UI display.",
"required": [
"code",
"error"
],
"properties": {
"code": {
"$ref": "#/components/schemas/ApiErrorCode",
"description": "Stable token identifying the error class. Clients should\nbranch on this rather than parsing `error`."
},
"error": {
"type": "string",
"description": "Human-readable error message. Suitable for log/UI display; do\nnot parse for branching."
}
}
},
"AppendStepsResponse": {
"type": "object",
"required": [
"inserted",
"path"
],
"properties": {
"inserted": {
"type": "integer",
"description": "Number of steps newly inserted (existing `step_id`s are skipped).",
"minimum": 0
},
"path": {
"$ref": "#/components/schemas/TracePathDocumentResponse",
"description": "The path's full HATEOAS-decorated document after the append."
}
}
},
"ChatCursor": {
"type": "object",
"description": "Pagination cursor for fetching older turns. Reserved for future\n`?before=<step_id>` requests; currently every projection returns the\nfull chain in one shot.",
"properties": {
"before": {
"type": "string",
"description": "Canonical step ID one step older than `turns[0]`. `None` when the\nchain reaches a root step in this projection.",
"nullable": true
}
}
},
"ChatProjection": {
"type": "object",
"description": "Top-level chat-projection response. Densely-indexed (`u32` IDs into\n`turns` / `actors`) so JSON parsing is cheap and the wire shape\navoids duplicating actor strings on every turn.",
"required": [
"path_id",
"actors",
"turns",
"step_ids",
"cursor"
],
"properties": {
"actors": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ActorView"
}
},
"cursor": {
"$ref": "#/components/schemas/ChatCursor"
},
"head": {
"type": "integer",
"format": "int32",
"description": "Index into `turns` — the latest turn along the HEAD chain. `None`\nwhen the document has no head.",
"minimum": 0,
"nullable": true
},
"path_id": {
"type": "string"
},
"step_ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "Canonical step IDs parallel to `turns`, for deep-links and\n`/step/{id}` fetches."
},
"title": {
"type": "string",
"nullable": true
},
"turns": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ChatTurn"
}
}
}
},
"ChatTurn": {
"type": "object",
"description": "One linearized turn along the HEAD-ancestor chain. Already\nclassified, optionally pre-rendered to HTML, with tool invocations\ninlined — the renderer drops in `text_html` and renders no further.",
"required": [
"actor_id",
"is_head",
"kind",
"text_html",
"thinking_html",
"tool_uses",
"invocations"
],
"properties": {
"actor_id": {
"type": "integer",
"format": "int32",
"description": "Index into `actors`.",
"minimum": 0
},
"intent": {
"type": "string",
"nullable": true
},
"invocations": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ToolInvocation"
},
"description": "`tool.invoke` siblings of an assistant step, spliced inline."
},
"is_head": {
"type": "boolean"
},
"kind": {
"$ref": "#/components/schemas/ChatTurnKind"
},
"model": {
"type": "string",
"nullable": true
},
"parent_id": {
"type": "integer",
"format": "int32",
"description": "Index into `turns` — `i - 1` along the HEAD chain, `None` at the root.",
"minimum": 0,
"nullable": true
},
"text": {
"type": "string",
"nullable": true
},
"text_html": {
"type": "string",
"description": "Sanitized HTML for `text`. Empty when `include_html` is false or\n`text` is empty."
},
"thinking": {
"type": "string",
"nullable": true
},
"thinking_html": {
"type": "string"
},
"timestamp": {
"type": "string",
"nullable": true
},
"tool_diff": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/ToolDiff"
}
],
"description": "For `kind = \"tool\"` only: the first non-empty `change[k].raw`,\npre-split into lines."
},
"tool_name": {
"type": "string",
"description": "For `kind = \"tool\"` only: the tool name (`extra.name`).",
"nullable": true
},
"tool_uses": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tool names from `extra.tool_uses` (string list)."
}
}
},
"ChatTurnKind": {
"type": "string",
"description": "Pre-classified turn kind. Server-side classification follows the\nprecedence rules in `packages/frontend/src/classify.ts`; the client\nrenders verbatim without re-deriving.",
"enum": [
"user",
"assistant",
"tool",
"system"
]
},
"CliGrantResponse": {
"type": "object",
"description": "Response body for `POST /api/v1/auth/cli/request-grant`.\n\nCarries the human-typeable grant `code` that the user pastes into\nthe pathbase CLI, plus its lifetime. Display the code prominently\nin the UI; do not store it in the URL or log files.",
"required": [
"code",
"expires_in"
],
"properties": {
"code": {
"type": "string",
"description": "Short, visually unambiguous alphanumeric code. Single-use;\nconsumed by the next call to `/api/v1/auth/cli/redeem`.\nComparison is case-insensitive on redeem."
},
"expires_in": {
"type": "integer",
"format": "int64",
"description": "Lifetime of the grant in seconds, set by the server."
}
}
},
"CreateRepoBody": {
"type": "object",
"description": "Request body for `POST /api/v1/u/{owner}/repos`.\n\nCreates a repository owned by the authenticated user. The (owner,\nname) pair must be unique; collisions return 409.",
"required": [
"name"
],
"properties": {
"description": {
"type": "string",
"description": "Optional short description shown next to the repo name in\nlistings. Free-form; no length cap.",
"nullable": true
},
"name": {
"type": "string",
"description": "Repository name. Must be unique among the authenticated user's\nexisting repos. Used as the URL slug at\n`/api/v1/u/{owner}/repos/{repo}`."
},
"visibility": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/RepoVisibilityInput"
}
],
"description": "Initial visibility. Defaults to `public` if omitted. The\n`unlisted` state is not accepted on repos — it's reserved for\nthe system-managed pathstash repo and seeded directly in the DB."
}
},
"additionalProperties": false
},
"Graph": {
"type": "object",
"description": "A computation graph — a named ordered collection of paths within\na repo. Pure storage shape — the columns on `graphs`. Derived\nproperties (path count, reconstructed document) live on the response\nwrappers that need them (`GraphSummary`, `GraphDocument`).",
"required": [
"id",
"repo_id",
"toolpath_id",
"visibility",
"created_at",
"updated_at"
],
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"header": {
"type": "object",
"description": "Graph metadata: `{graph: GraphIdentity, meta?: GraphMeta}` —\nthe toolpath `Graph` shape minus its `paths`. Absent when not set.",
"nullable": true
},
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string",
"description": "Optional human-readable label for display. Free-form; no URL\nimplications — graphs are addressed by `id` (UUID) at the wire\nboundary. Defaults from the uploader (filename or caller-\nprovided) and can be edited.",
"nullable": true
},
"repo_id": {
"type": "string",
"format": "uuid"
},
"title": {
"type": "string",
"nullable": true
},
"toolpath_id": {
"type": "string"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"visibility": {
"$ref": "#/components/schemas/Visibility",
"description": "Visibility — `Public` is listed and world-readable; `Unlisted`\nis link-share (UUID grants read); `Private` is owner-only."
}
}
},
"GraphDocumentResponse": {
"allOf": [
{
"$ref": "#/components/schemas/Graph"
},
{
"type": "object",
"required": [
"path_count",
"document",
"url"
],
"properties": {
"document": {
"type": "object",
"description": "Reconstructed `{ graph, paths: [{path, steps, meta?}, …] }`."
},
"path_count": {
"type": "integer",
"format": "int32"
},
"url": {
"type": "string"
}
}
}
],
"description": "Wraps a `Graph` with its path count, reconstructed multi-path\ndocument, and the URL. Used by endpoints that promise the full\ndocument on the wire."
},
"GraphResponse": {
"allOf": [
{
"$ref": "#/components/schemas/Graph"
},
{
"type": "object",
"required": [
"url"
],
"properties": {
"url": {
"type": "string"
}
}
}
],
"description": "`Graph` plus the absolute URL clients should link to."
},
"GraphSummaryResponse": {
"allOf": [
{
"$ref": "#/components/schemas/Graph"
},
{
"type": "object",
"required": [
"path_count",
"url"
],
"properties": {
"path_count": {
"type": "integer",
"format": "int32",
"description": "Number of constituent paths in this graph."
},
"url": {
"type": "string"
}
}
}
],
"description": "Wraps a `Graph` with its path count plus the URL. Used by listing\nendpoints."
},
"RedeemBody": {
"type": "object",
"description": "Request body for `POST /api/v1/auth/cli/redeem`.\n\nCarries the grant code that the user pasted from the browser into\nthe CLI. Whitespace and case-folding are tolerated server-side.",
"required": [
"code"
],
"properties": {
"code": {
"type": "string",
"description": "The grant code displayed by the browser. Whitespace, dashes,\nand case differences are normalized away (server uppercases and\nstrips non-alphanumerics) before lookup; submit it however the\nuser typed it."
}
},
"additionalProperties": false
},
"RedeemResponse": {
"type": "object",
"description": "Response body for `POST /api/v1/auth/cli/redeem`.\n\nReturns the new bearer token plus the authenticated user. The token\nis shown to the API ONCE; pathbase cannot recover the plaintext if\nit is lost.",
"required": [
"token",
"user"
],
"properties": {
"token": {
"type": "string",
"description": "Opaque session bearer token, prefixed `pat_…`. Send as\n`Authorization: Bearer <token>` on subsequent API calls. Treat\nas a secret; only its hash is retained server-side, so it\ncannot be re-shown if lost — the user must re-pair the CLI."
},
"user": {
"$ref": "#/components/schemas/User",
"description": "The user the CLI is now authenticated as."
}
}
},
"Repo": {
"type": "object",
"description": "A repository — a named workspace of paths and graphs owned by a\nsingle user. `name` is unique per owner and serves as the URL\nsegment in `/{owner}/{name}` and nested resources.",
"required": [
"id",
"owner_id",
"name",
"visibility",
"created_at",
"updated_at"
],
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"description": {
"type": "string",
"nullable": true
},
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string"
},
"owner_id": {
"type": "string",
"format": "uuid"
},
"readme": {
"type": "string",
"nullable": true
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"visibility": {
"$ref": "#/components/schemas/Visibility",
"description": "Repository visibility. See `Visibility` for the state semantics.\nRepos are user-controllable as `Public`/`Private`; `Unlisted`\nis system-set (e.g., the seeded `pathstash` repo)."
}
}
},
"RepoVisibilityBody": {
"type": "object",
"description": "Body for `PATCH /api/v1/u/{owner}/repos/{repo}/visibility`.",
"required": [
"visibility"
],
"properties": {
"visibility": {
"$ref": "#/components/schemas/RepoVisibilityInput"
}
},
"additionalProperties": false
},
"RepoVisibilityInput": {
"type": "string",
"description": "Write-side narrowing of `Visibility` for repo\nbodies. The repo *read* shape (`Repo.visibility`) still uses the\nfull three-state enum — a `GET` on the seeded `pathstash` repo\nreturns `\"unlisted\"` like always. This type only constrains what\nthe API will *accept* from a client: `Unlisted` is system-pinned,\nusers can't enter or leave it through the API, so it doesn't\nappear in `POST /repos` or `PATCH .../visibility` bodies.\nGenerated clients see only `public | private` on inputs and the\n`Unlisted` write is unreachable by construction. Graph and path\nbodies use the full `Visibility` enum; all three values are legal\nthere.",
"enum": [
"public",
"private"
]
},
"SessionSummary": {
"type": "object",
"description": "One entry of the session list returned by `GET /api/v1/auth/sessions`.\n\nA session is the durable side of an authentication: one entry\nexists for every browser login (`kind = \"web\"`) and every redeemed\nCLI grant (`kind = \"cli\"`). Sessions can be inspected and revoked\nfrom the user's settings UI.",
"required": [
"id",
"kind",
"created_at",
"expires_at",
"is_current"
],
"properties": {
"created_at": {
"type": "string",
"description": "RFC 3339 timestamp of session creation."
},
"expires_at": {
"type": "string",
"description": "RFC 3339 timestamp at which the session token stops being\naccepted."
},
"id": {
"type": "string",
"description": "Stable session ID. Use this with\n`DELETE /api/v1/auth/sessions/{id}` to revoke."
},
"is_current": {
"type": "boolean",
"description": "`true` if this entry corresponds to the session that issued\nthe current request — useful for the UI to mark the \"this\ndevice\" entry and warn before revoking it."
},
"kind": {
"type": "string",
"description": "`\"web\"` for browser logins (password, GitHub OAuth, dev login),\n`\"cli\"` for CLI tokens minted via `/api/v1/auth/cli/redeem`."
},
"user_agent": {
"type": "string",
"description": "`User-Agent` header captured at session creation. Useful for\n\"is that you on Firefox?\" recognition. May be absent for\nclients that omit the header.",
"nullable": true
}
}
},
"ToolDiff": {
"type": "object",
"description": "Pre-split diff payload — picked from the first non-empty\n`change[k].raw` on a structural step. Lines are pre-split so the\nrenderer doesn't repeat the work.",
"required": [
"path",
"lines"
],
"properties": {
"lines": {
"type": "array",
"items": {
"type": "string"
}
},
"path": {
"type": "string"
}
}
},
"ToolInvocation": {
"type": "object",
"description": "A `tool.invoke` step spliced inline next to its parent assistant\nturn. Saves the client a second pass over the path's step graph.",
"required": [
"step_id",
"actor_id",
"text_html"
],
"properties": {
"actor_id": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"input": {
"type": "string",
"description": "Tool input args from the structural payload (`extra.input`),\nJSON-stringified when not already a string.",
"nullable": true
},
"result": {
"type": "string",
"description": "Tool output from the structural payload (`extra.result`),\nJSON-stringified when not already a string.",
"nullable": true
},
"step_id": {
"type": "string",
"description": "Canonical step ID of the tool.invoke step."
},
"text": {
"type": "string",
"nullable": true
},
"text_html": {
"type": "string"
},
"timestamp": {
"type": "string",
"nullable": true
},
"tool_diff": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/ToolDiff"
}
]
},
"tool_name": {
"type": "string",
"nullable": true
}
}
},
"ToolpathDocument": {
"type": "object",
"description": "A toolpath v1 Graph document. See <https://github.com/empathic/toolpath>\nfor the full schema; the wire format is JSON with top-level fields\n`graph`, `paths`, and optional `meta`.\n\nThis schema is a permissive object placeholder — generated clients\nsee free-form JSON for `graph`, each entry of `paths`, and `meta`,\nand should defer real validation to the toolpath project.",
"required": [
"graph",
"paths"
],
"properties": {
"graph": {
"type": "object",
"description": "Graph identity and head pointer."
},
"meta": {
"type": "object",
"description": "Optional document metadata (title, description). Free-form\nobject; clients should defer real validation to the toolpath\nproject.",
"nullable": true
},
"paths": {
"type": "array",
"items": {},
"description": "One or more entries — each is either a full inline path object\nor a `{ \"$ref\": \"...\" }` reference to an external path."
}
}
},
"TracePath": {
"type": "object",
"description": "A single agent trace path within a repo. Pure storage shape — the\ncolumns on `paths`. Derived properties (step count, reconstructed\ndocument) live on the response wrappers that need them\n(`TracePathSummary`, `TracePathDocument`).",
"required": [
"id",
"repo_id",
"toolpath_id",
"visibility",
"created_at",
"updated_at"
],
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"header": {
"type": "object",
"description": "Path metadata: `{path: PathIdentity, meta?: PathMeta}` — the\ntoolpath `Path` shape minus its `steps`. Absent when not set.",
"nullable": true
},
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string",
"description": "Optional human-readable label for display. Free-form; no URL\nimplications — paths are addressed by `id` (UUID) at the wire\nboundary. Defaults from the uploader (filename or caller-\nprovided) and can be edited.",
"nullable": true
},
"repo_id": {
"type": "string",
"format": "uuid"
},
"title": {
"type": "string",
"nullable": true
},
"toolpath_id": {
"type": "string"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"visibility": {
"$ref": "#/components/schemas/Visibility",
"description": "Visibility — `Public` is listed and world-readable; `Unlisted`\nis link-share (UUID grants read); `Private` is owner-only."
}
}
},
"TracePathDocumentResponse": {
"allOf": [
{
"$ref": "#/components/schemas/TracePath"
},
{
"type": "object",
"required": [
"step_count",
"document",
"url"
],
"properties": {
"document": {
"type": "object",
"description": "Reconstructed `{ graph, paths: [{path, steps, meta?}] }`."
},
"step_count": {
"type": "integer",
"format": "int32"
},
"url": {
"type": "string"
}
}
}
],
"description": "Wraps a `TracePath` with its step count, reconstructed Graph\ndocument, and the URL. Used by endpoints that promise the full\ndocument (single-path GET, append-steps response)."
},
"TracePathResponse": {
"allOf": [
{
"$ref": "#/components/schemas/TracePath"
},
{
"type": "object",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"description": "Absolute URL for this path's web page. UUID-addressed: works\nregardless of visibility (knowing the UUID is the share\ntoken for secret resources)."
}
}
}
],
"description": "`TracePath` plus the absolute URL clients should link to."
},
"TracePathSummaryResponse": {
"allOf": [
{
"$ref": "#/components/schemas/TracePath"
},
{
"type": "object",
"required": [
"step_count",
"url"
],
"properties": {
"step_count": {
"type": "integer",
"format": "int32",
"description": "Number of steps in this path."
},
"url": {
"type": "string"
}
}
}
],
"description": "Wraps a `TracePath` with its step count plus the URL. Used by\nlisting endpoints and any single-row reads that surface the count."
},
"UpdateGraphPathBody": {
"type": "object",
"properties": {
"visibility": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/Visibility"
}
]
}
},
"additionalProperties": false
},
"UpdateGraphVisibilityBody": {
"type": "object",
"description": "Body for `PATCH /api/v1/u/{owner}/repos/{repo}/graphs/{id}/visibility`.",
"required": [
"visibility"
],
"properties": {
"visibility": {
"$ref": "#/components/schemas/Visibility",
"description": "New graph visibility. `public` lists in feeds and is anonymously\nreadable; `unlisted` hides from listings but stays reachable via\nthe UUID share link; `private` restricts read to the owner."
}
},
"additionalProperties": false
},
"UpdateProfileBody": {
"type": "object",
"description": "Body for `PUT /api/v1/u/me`.\n\nCarries the complete replacement state of the editable profile\nfields. PUT semantics: the body IS the new value — fields not\npresent (or set to `null`) are cleared. To preserve an existing\nvalue, send it back. Read the current values from `GET /u/me`.\n\nUsername and email are NOT editable here — username is immutable\nand email is bound to the auth provider.",
"properties": {
"bio": {
"type": "string",
"description": "Free-form short biography rendered on the user's public\nprofile. Missing or `null` clears the field. No length cap is\nenforced at the API layer; expect Markdown but no rendering\nguarantees.",
"nullable": true
},
"display_name": {
"type": "string",
"description": "Human-readable display name shown next to the username on\nprofile pages and trace headers. Missing or `null` clears the\nfield (the UI then falls back to the bare username).",
"nullable": true
}
},
"additionalProperties": false
},
"UpdateRepoBody": {
"type": "object",
"description": "Request body for `PUT /api/v1/u/{owner}/repos/{repo}`.\n\nCarries the complete replacement state of the editable repo\nfields. PUT semantics: every field is written. To preserve an\nexisting value, send it back; missing or `null` for the nullable\nfields clears them. Read the current state from `GET /repos/{repo}`\nfirst if the form doesn't already have it.\n\nVisibility is NOT editable here — it lives at the sub-resource\n`/repos/{repo}/visibility` for two reasons: (1) it's a single\nwell-defined op clients flip independently of content edits, and\n(2) `Unlisted` is system-only and that gate is cleaner to enforce\non its own endpoint.",
"required": [
"name"
],
"properties": {
"description": {
"type": "string",
"description": "New description. Missing or `null` clears the field.",
"nullable": true
},
"name": {
"type": "string",
"description": "New repository name. Required (the column is non-nullable —\nthere is no \"no name\" state). Renames must be unique among\nthis owner's repos (409 on collision). Renaming changes the\nURL of the repo and every nested resource — clients holding\nold URLs will 404 afterward."
},
"readme": {
"type": "string",
"description": "New README body (free-form text/Markdown). Missing or `null`\nclears the field. No length cap is enforced at the API layer.",
"nullable": true
}
},
"additionalProperties": false
},
"UploadGraphBody": {
"type": "object",
"description": "Request body for `POST /api/v1/u/{owner}/repos/{repo}/graphs`.\n\nThe `document` field carries a multi-path toolpath Graph; inline\npath entries are admitted into the workspace and deduped by their\ntoolpath ID, refs are kept as ID references.",
"required": [
"document"
],
"properties": {
"document": {
"$ref": "#/components/schemas/ToolpathDocument",
"description": "The toolpath v1 Graph document with one or more `PathOrRef`\nentries. Inline paths are admitted into the same workspace as\nthe graph; refs are kept as ID references to existing paths."
},
"name": {
"type": "string",
"description": "Optional human-readable display label. Free-form; no URL or\nuniqueness implications — the server addresses the created\ngraph by its UUID `id` in the response. Defaults to whatever\nthe uploader (web/CLI) chose to send, typically the filename.",
"nullable": true
},
"visibility": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/Visibility"
}
],
"description": "Graph visibility. Omitted defaults to `unlisted` —\nreachable only via the graph's UUID share link or to the\nauthenticated owner. Flip later via `PATCH\n/graphs/{id}/visibility`."
}
},
"additionalProperties": false
},
"User": {
"type": "object",
"description": "A registered Pathbase account. `email` is optional because GitHub\nOAuth users can have a private email; CLI / API consumers see the\ncaller's own email via `/u/me` but never another user's.",
"required": [
"id",
"username",
"created_at",
"updated_at"
],
"properties": {
"bio": {
"type": "string",
"nullable": true
},
"created_at": {
"type": "string",
"format": "date-time"
},
"display_name": {
"type": "string",
"nullable": true
},
"email": {
"type": "string",
"description": "Caller's own email. Populated only by `GET /api/v1/u/me` (the\nauthenticated self-read). The public profile endpoint\n`GET /api/v1/u/{username}` always returns `null` here; emails\nare never exposed across users.",
"nullable": true
},
"id": {
"type": "string",
"format": "uuid"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"username": {
"type": "string"
}
}
},
"Visibility": {
"type": "string",
"description": "The visibility state of a `Repo`, `TracePath`, or `Graph`. Used uniformly\nacross the three entities at the storage and wire layers.\n\n- `Public`: appears in public and owner listings, readable by anyone.\n- `Unlisted`: hidden from default listings (even from the owner), but\n readable by anyone in possession of the resource's URL/ID. The\n resource ID is the access gate — for paths/graphs that's the UUID;\n for repos it's a system-pinned state used by `pathstash` only.\n- `Private`: hidden from public listings, listed only to the owner,\n readable only by the owner.",
"enum": [
"public",
"unlisted",
"private"
]
}
},
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"description": "Pathbase personal access token (`pat_…`) obtained via the CLI grant flow (`POST /auth/cli/request-grant` then `POST /auth/cli/redeem`). Send as `Authorization: Bearer <token>`."
}
}
},
"tags": [
{
"name": "Auth: CLI",
"description": "CLI pairing — request a short-lived grant code and redeem it for a `pat_…` bearer token."
},
{
"name": "Auth: Sessions",
"description": "Inspect and revoke the caller's active sessions (browser logins and CLI tokens)."
},
{
"name": "Users",
"description": "User profiles and ownership listings."
},
{
"name": "Repos",
"description": "Repositories: create, list, update, delete."
},
{
"name": "Graphs",
"description": "Computation graphs and their constituent trace paths."
},
{
"name": "Anon",
"description": "Unauthenticated share-by-link uploads under the `anon` user. Always `Unlisted`."
}
]
}