{
  "openapi": "3.1.0",
  "info": {
    "title": "openbash",
    "version": "1.0.0",
    "description": "Bash programs over data, reachable by HTTP, SSH and MCP.\n\nThere is ONE execution endpoint, not one per program. Programs are commands on a path inside a shell — \"customs | jq | sort\" is a single request — so they compose without any of them knowing about the others. Which programs exist is decided per ACCOUNT, not per call: once enabled, every session that account opens has them, and help lists exactly those.\n\nStateless by default; pass a session_id to carry a working directory, environment and /tmp across calls. A session id names the shell, not the connection — the same id is resumable from HTTP, MCP and SSH, and survives a restart of this gateway. On MCP, an Mcp-Session-Id sent with initialize NAMES the session: it is resumed if it exists and started under that name if it does not, so a client can choose an id once and keep it. A client that sends no header still keeps one session: an OAuth connector gets a DEFAULT session bound to its client_id, so continuity does not depend on header plumbing. API keys bind nothing, because a key is shared and each caller under it must get its own session. From inside the shell, session list, session use <id> and session new move that default."
  },
  "servers": [{ "url": "https://openbash.ai" }],
  "security": [{ "apiKey": [] }],
  "paths": {
    "/api/v1/changelog": {
      "get": {
        "operationId": "getChangelog",
        "summary": "What has been added, newest first.",
        "description": "Public. The thing to poll. Programs are dated by when they first appeared in the catalog, posts by their publication date — so an agent can tell an addition from a routine catalog refresh without holding the previous response and diffing it. The same content is served as Atom at /feed.xml.",
        "security": [{}, { "apiKey": [] }],
        "responses": {
          "200": {
            "description": "Dated entries, newest first.",
            "content": { "application/json": { "schema": {
              "type": "object",
              "properties": {
                "origin": { "type": "string" },
                "feed": { "type": "string", "description": "The Atom copy of this list." },
                "entries": { "type": "array", "items": {
                  "type": "object",
                  "properties": {
                    "kind": { "type": "string", "enum": ["program", "post"] },
                    "slug": { "type": "string" },
                    "title": { "type": "string" },
                    "summary": { "type": "string" },
                    "path": { "type": "string" },
                    "date": { "type": "string", "format": "date",
                      "description": "Absent when the store has no record of when — never the epoch." }
                  }
                } }
              }
            } } }
          }
        }
      }
    },
    "/api/v1/programs": {
      "get": {
        "operationId": "listPrograms",
        "summary": "List every program, with this key's enablement marked.",
        "description": "Public. Programs a key cannot run are still listed, with enabled=false.",
        "security": [{}, { "apiKey": [] }],
        "parameters": [
          { "name": "domain", "in": "query", "schema": { "type": "string" },
            "description": "Filter to one domain, e.g. housing." }
        ],
        "responses": {
          "200": {
            "description": "The catalog.",
            "content": { "application/json": { "schema": {
              "type": "object",
              "properties": {
                "data": { "type": "array", "items": { "$ref": "#/components/schemas/ProgramSummary" } },
                "domains": { "type": "array", "items": { "type": "string" } },
                "authenticated": { "type": "boolean" }
              }
            } } }
          }
        }
      }
    },
    "/api/v1/programs/{slug}": {
      "get": {
        "operationId": "getProgram",
        "summary": "One program, help text included.",
        "description": "The help text is the definition of the program: what it returns, in what units, and what it does not cover.",
        "security": [{}, { "apiKey": [] }],
        "parameters": [
          { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "The program.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Program" } } } },
          "404": { "description": "No such program.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/exec": {
      "post": {
        "operationId": "exec",
        "summary": "Run one command.",
        "description": "Run one shell command. This is the ONLY execution endpoint — programs are commands, not routes, and a pipeline of several is one request.\n\nStateless by default. Set stream=true (or send Accept: text/event-stream) for Server-Sent Events: one 'chunk' event per piece of output, then a terminal 'exit' or 'error'.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExecRequest" } } }
        },
        "responses": {
          "200": { "description": "The command ran. Check exit_code — a non-zero exit is a result, not an HTTP failure.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ExecResponse" } },
              "text/event-stream": { "schema": { "type": "string" } }
            } },
          "400": { "description": "Malformed request.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Missing or refused key.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limited for this account.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "504": { "description": "The command did not finish in time.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/sessions": {
      "post": {
        "operationId": "createSession",
        "summary": "Open a session, or resume one, for state across calls.",
        "description": "Shell state (cwd, environment, /tmp) lives in the execution backend, and the returned id names the SHELL rather than this connection. The same id is therefore resumable from anywhere — here, an MCP Mcp-Session-Id, or SSH — and it survives a restart of the gateway. Pass resume to pick a session up again; an id that no longer names a live session is a 404 and never a new shell wearing its name.",
        "requestBody": { "required": false,
          "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "resume": { "type": "string", "description": "An id from a previous session, to continue it." }
            }
          } } } },
        "responses": {
          "201": { "description": "Opened.", "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "session_id": { "type": "string" },
              "resumed": { "type": "boolean" },
              "idle_timeout_ms": { "type": "integer" },
              "note": { "type": "string" }
            }
          } } } },
          "404": { "description": "The session named by resume does not exist, was closed, or belongs to another account.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/sessions/{id}/exec": {
      "post": {
        "operationId": "execInSession",
        "summary": "Run one command inside an open session.",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "requestBody": { "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExecRequest" } } } },
        "responses": {
          "200": { "description": "The command ran.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExecResponse" } } } },
          "404": { "description": "No such session.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/sessions/{id}/tmp/{path}": {
      "get": {
        "operationId": "readSessionFile",
        "summary": "Read a file from a session's scratch.",
        "description": "The draft a session is composing lives in its own /tmp. This reads one back — the same bytes the shell sees, with a content type. Paths are relative to /tmp and admit letters, digits, dot, dash, underscore and slash only.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "path", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "The file.", "content": { "text/plain": { "schema": { "type": "string" } } } },
          "404": { "description": "No such session, or no such file in it.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/runs/{session}": {
      "get": {
        "operationId": "listRun",
        "summary": "What a submission froze.",
        "description": "submit writes the document, the compiler's report, the run's trace and every cited query result into a bundle outside every mount, so that a document outlives the things it was checked against. This lists that bundle.",
        "parameters": [ { "name": "session", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "The bundle.", "content": { "application/json": { "schema": {
          "type": "object",
          "properties": {
            "session": { "type": "string" },
            "files": { "type": "array", "items": { "type": "string" } }
          }
        } } } } }
      }
    },
    "/api/v1/runs/{session}/{file}": {
      "get": {
        "operationId": "readRunFile",
        "summary": "Read one file from a submission.",
        "parameters": [
          { "name": "session", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "file", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "The file." },
          "404": { "description": "No such submission, or no such file in it.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/sessions/{id}": {
      "delete": {
        "operationId": "closeSession",
        "summary": "End a session.",
        "description": "Ends the shell rather than hanging up on it: the id stops resolving and cannot be resumed. Simply disconnecting is a detach, and a detached session is still there under its id.",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "204": { "description": "Closed." }, "404": { "description": "No such session." } }
      }
    },
    "/api/v1/key": {
      "get": {
        "operationId": "whoami",
        "summary": "What this key resolves to, and which programs the catalog records as enabled for it.",
        "responses": { "200": { "description": "The key's account.", "content": { "application/json": { "schema": {
          "type": "object",
          "properties": {
            "account_id": { "type": "string" },
            "scope": { "type": "string" },
            "programs_enabled": { "type": "array", "items": { "type": "string" } }
          }
        } } } } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": { "type": "http", "scheme": "bearer", "description": "Your openbash key, as Authorization: Bearer <key>." }
    },
    "schemas": {
      "ProgramSummary": {
        "type": "object",
        "properties": {
          "slug": { "type": "string", "description": "The command name, as typed in a shell. Not a route: there is no endpoint for this program." },
          "domain": { "type": "string" },
          "title": { "type": "string" },
          "summary": { "type": "string" },
          "cost_class": { "type": "string" },
          "tier": { "type": "string", "enum": ["free", "metered", "contract"] },
          "status": { "type": "string", "enum": ["available", "preview"] },
          "enabled": { "type": "boolean", "description": "Whether this key's account has it. False for an anonymous read." }
        }
      },
      "Program": {
        "allOf": [
          { "$ref": "#/components/schemas/ProgramSummary" },
          { "type": "object", "properties": {
              "help": { "type": "string", "description": "The --help text, which is the definition of the program." },
              "example_command": { "type": "string" },
              "example_output": { "type": "string" }
          } }
        ]
      },
      "ExecRequest": {
        "type": "object",
        "required": ["command"],
        "properties": {
          "command": { "type": "string", "description": "One shell command line. Pipes and redirection work." },
          "stream": { "type": "boolean", "default": false },
          "session_id": { "type": "string", "description": "Continue an open session." }
        }
      },
      "ExecResponse": {
        "type": "object",
        "properties": {
          "output": { "type": "string", "description": "stdout and stderr, interleaved as the program produced them." },
          "exit_code": { "type": "integer", "description": "0 results, 1 usage error, 2 not entitled, 3 no results found, 4 upstream data failure." },
          "truncated": { "type": "boolean", "description": "Output hit the 1 MiB non-streaming cap. Stream instead." },
          "session_id": { "type": "string" },
          "duration_ms": { "type": "integer" }
        }
      },
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "object", "properties": {
          "code": { "type": "string" }, "message": { "type": "string" }
        } } }
      }
    }
  }
}
