{"openapi":"3.1.0","info":{"title":"Memoshelf","version":"3.27.0","summary":"Tiny hosted toolbelt other agents call","description":"Token-cheap REST for a run log, notes, and public-URL → markdown. Hashed API keys (`Authorization: Bearer ar_…` or `X-Api-Key`). Demo Hobby CORS: any origin on `/health`, `/v1/*`, and `POST /mcp`; no cookie credentials. Errors for 400/401/404/413/429 are `{error, code}`. POST /v1/runs and /v1/notes reject JSON over ~64KB with 413; POST /v1/md rejects a URL string over ~2KB. GET /v1/what_happened_last is a keyed session handoff (last N runs + pinned notes as ≤300 tokens of markdown). Read routes accept max_chars (default 4000, max 80000) and return truncated plus a continuation next_cursor. Every REST and MCP response carries x-request-id (echo a safe incoming value, otherwise a UUID). This document contains no API secrets."},"servers":[{"url":"/","description":"This host"}],"tags":[{"name":"health","description":"Liveness. No auth. No secrets."},{"name":"runs","description":"Public list GET is unchanged (synthetic seed without a key). Owner list items and GET /v1/runs/{id} need a hashed key. Writes, patches, and deletes need a hashed key. Seed samples 404 on owner GET by id. Runs v2 adds status, started_at, finished_at, duration_ms, tags, small summary JSON, and optional expected_by. List filters: status, tag, since, until, q, stale=1."},{"name":"owner","description":"Per-owner settings. GET/PATCH retention_days (default 90) and optional digest webhook URL. The webhook secret is write-only and never returned. Hashed key required."},{"name":"cron","description":"Scheduled purge of expired owner runs and notes, and the optional daily digest of failed/stale runs. Protected by AGENTRACK_CRON_SECRET (or Vercel CRON_SECRET). Empty secret keeps the routes locked."},{"name":"notes","description":"Public list GET is unchanged (synthetic seed without a key). Owner list items and GET /v1/notes/{id} need a hashed key. Writes, patches, and deletes need a hashed key. Seed samples 404 on owner GET by id. Notes v2 adds optional title, 32KB body cap, list filters (tag, since, until), tsvector search on Supabase (substring on fallback), and cron purge with the same retention_days as runs. Notes may be pinned for session handoff."},{"name":"md","description":"Public http(s) only. Accept: text/markdown first; origin markdown or extractor. Boilerplate strip + heading repair. extraction origin|main|full plus tokens_in and tokens_out. Echoes x-markdown-tokens. DNS private-range refusal. robots.txt honored. Owner TTL cache. No Chrome. ~20 / 60s → 429 with rate-limit headers."},{"name":"keys","description":"Mint shows the `ar_…` secret once. Only SHA-256 is stored. Owner `/app` lists prefix and created time from the same gitignored store. MCP list_keys wraps this same owner list (active keys only: id/prefix/created; never plaintext). Revoke marks `revoked_at`; that secret then 401s like a missing key on REST and MCP. MCP keys action settings lists/creates/deletes outbound webhooks (same as /v1/webhooks)."},{"name":"webhooks","description":"Per-owner outbound webhooks for run.finished and note.stored. POST/GET/DELETE /v1/webhooks. Public https only (same refusal rules as fetch). HMAC-SHA256 in x-agentrack-signature. Signing secret shown once on create and never returned on GET. Three delivery attempts with backoff. Last 100 deliveries per owner. Auto-disabled after 10 consecutive failures. Owner /app reports enabled/disabled. No new services."},{"name":"handoff","description":"Keyed session handoff. GET /v1/what_happened_last returns the last N owner runs and pinned owner notes as ≤300 tokens of markdown. Optional project and routine filter by tag or goal/title. Seeds stay out."},{"name":"export","description":"Owner runs, notes, and usage rollups. JSON at /v1/export; CSV at /v1/export.csv."},{"name":"usage","description":"Workspace usage_daily rollups (runs, notes, fetches, fetch_bytes, tokens_out) plus per-key daily counters. GET /v1/usage returns this UTC month and last 30 days. MCP usage_summary matches. PATCH /v1/owner quotas. 429 quota_exceeded includes retry_after and reset_at. /health shape unchanged."},{"name":"mcp","description":"Streamable HTTP JSON-RPC at POST /mcp. initialize and notifications/initialized need no key. tools/call is keyed except read-only health (same payload as GET /health; no API key). Owner /app shows a read-only MCP tools cheat sheet (names + one-line purpose; no secrets) and copyable Cursor, Claude Code, and Grok mcp.json snippets (mcpServers url https://YOUR_AGENTRACK_URL/mcp, X-Api-Key or Authorization Bearer placeholder; Claude Code includes type http; never a real key). Conformance covers initialize, notifications/initialized, tools/list, and tools/call for every tool against spec revision 2025-03-26, including error mapping and REST/MCP parity and notes that POST /mcp is the endpoint (GET/DELETE 405). tools/list includes health, log_run, runs, notes, fetch_md, keys, what_happened_last, usage_summary. runs/notes/keys take action list/get/update/delete (notes also create; keys use list/mint/revoke). Old names stay as hidden aliases for one version: store_note, public_url_to_md, update_run, update_note, get_run, get_note, delete_run, delete_note, create_run, create_note, list_runs, list_notes, revoke_key, mint_key, list_keys, stale_runs. update_run / update_note wrap keyed REST PATCH (401 without a key; 404 if missing or a seed sample; success body matches REST PATCH). get_run / get_note: 401 without a key; 404 if missing or a seed sample; 200 body matches REST GET-by-id. delete_run / delete_note: 401 without a key; 404 if missing or a seed sample (same as REST DELETE); success body matches REST DELETE. create_run / create_note wrap keyed REST POST create (401 without a key; success body matches REST POST). list_runs / list_notes wrap keyed REST list/search (401 without a key; optional q= like owner ?q=; success body matches keyed GET /v1/runs and GET /v1/notes). revoke_key wraps owner API-key revoke (401 without a key; identify by id or prefix; success body matches DELETE /v1/keys/{id}; revoked secret then 401s like a missing key). mint_key wraps owner API-key mint (401 without a key; caller authenticates with an existing key; success body matches POST /v1/keys — one-time plaintext secret plus id/prefix; only the hash is persisted). list_keys wraps owner API-key list (401 without a key; active keys only: id/prefix/created; revoked omitted; never plaintext). health is a read-only ping matching GET /health ({ok, version}); no API key. fetch_md wraps keyed POST /v1/md (401 without a key; url required; honors robots.txt; same success body and robots/rate-limit errors as REST). what_happened_last wraps keyed GET /v1/what_happened_last (401 without a key; last N runs + pinned notes as ≤300 tokens of markdown)."}],"paths":{"/health":{"get":{"tags":["health"],"summary":"Liveness","description":"No auth. Returns `{ok:true, version, product, env, dataSource, degraded, migrations, remote, recent}`. product is memoshelf. env is production or sandbox (MEMOSHELF_ENV; default production). No secrets. remote.lastError.message never contains a URL, key, or token. recent is the last 20 degraded/undegraded transitions (memory-held, best effort). Response header x-request-id is always set.","operationId":"health","security":[],"responses":{"200":{"description":"Ok","headers":{"x-request-id":{"description":"Request id echoed from a safe incoming X-Request-Id, or a minted UUID.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}}}}},"/v1/runs":{"get":{"tags":["runs"],"summary":"List runs","description":"Public seed GET is unchanged: no key and empty/omitted `q` returns fictional seed samples. A hashed key is required for owner items. `q` searches owner goal/outcome/title/body only. Non-empty `q` without a key → 401. `limit` default 50, max 100. `cursor` is the last id from the previous page. Optional `status`, `tag`, `since`, `until`, and `stale=1` filter the page (invalid values → 400). Optional max_chars (default 4000) stops the page when serialized items would exceed the budget. Responses include truncated (true when more items remain) and next_cursor.","operationId":"listRuns","security":[{},{"bearerAuth":[]},{"apiKeyHeader":[]}],"parameters":[{"name":"limit","in":"query","required":false,"description":"Page size. Default 50, max 100.","schema":{"type":"integer","default":50,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Id of the last item from the previous page. Omit on the first page.","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Owner search. Case-insensitive substring match on goal, outcome, title, or body. Hashed key required. Empty or omitted lists as before (public seed without a key; owner items with a key). Non-empty q without a key → 401. Matches owner items only; seed samples are never mixed in.","schema":{"type":"string"}},{"name":"max_chars","in":"query","required":false,"description":"Character budget for this read. Default 4000, max 80000. Responses include truncated and a continuation next_cursor.","schema":{"type":"integer","default":4000,"minimum":1,"maximum":80000}},{"name":"status","in":"query","required":false,"description":"Filter runs by status: started, succeeded, or failed.","schema":{"type":"string","enum":["started","succeeded","failed"]}},{"name":"tag","in":"query","required":false,"description":"Filter runs that include this tag.","schema":{"type":"string"}},{"name":"since","in":"query","required":false,"description":"Inclusive lower bound on started_at (ISO datetime or YYYY-MM-DD).","schema":{"type":"string"}},{"name":"until","in":"query","required":false,"description":"Inclusive upper bound on started_at (ISO datetime or YYYY-MM-DD).","schema":{"type":"string"}},{"name":"stale","in":"query","required":false,"description":"When 1 or true, only started-but-unfinished runs whose expected_by is in the past. Runs without expected_by are omitted. Invalid values → 400 invalid_stale.","schema":{"type":"string","enum":["1","0","true","false"]}}],"responses":{"200":{"description":"Run page","content":{"application/json":{"schema":{"type":"object","required":["runs","next_cursor","truncated"],"properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/Run"}},"next_cursor":{"type":["string","null"]},"truncated":{"type":"boolean"}}}}}},"401":{"description":"Presented key is unknown, or non-empty q without a key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["runs"],"summary":"Append a run","description":"Hashed owner key required (401 without). Owner /app create form posts {goal, tools, outcome} here the same way other owner writes do. MCP create_run wraps this same keyed POST. Created records persist in gitignored `.data/` and then work with GET-by-id, PATCH, delete, CSV, MCP get_run, and ?q=. Seed samples cannot be created as seed ids.","operationId":"createRun","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Same key + same body within 10 minutes returns the original record (200) instead of inserting again. Persisted in gitignored `.data/`. Different body with the same key → 409.","schema":{"type":"string","maxLength":256}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["goal","outcome"],"properties":{"goal":{"type":"string"},"outcome":{"type":"string"},"tools":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["started","succeeded","failed"]},"started_at":{"type":"string","format":"date-time"},"finished_at":{"type":"string","format":"date-time"},"duration_ms":{"type":"integer","minimum":0},"tags":{"type":"array","items":{"type":"string"}},"summary":{"type":"object","additionalProperties":true},"expected_by":{"type":"string","format":"date-time"}},"description":"JSON body max ~64KB. Over the cap → 413 payload_too_large. Default status is succeeded."}}}},"responses":{"200":{"description":"Idempotent replay of the original record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Run"}}}},"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Run"}}}},"400":{"description":"goal and outcome required, or invalid Idempotency-Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Idempotency-Key reused with a different body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"JSON body over ~64KB (runs/notes) or md URL string over ~2KB. MCP tool args use the same caps.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/runs/{id}":{"get":{"tags":["runs"],"summary":"Get one owner run","description":"Hashed owner key required. Returns one persisted owner run plus truncated and next_cursor. Optional max_chars (default 4000) slices goal then outcome. cursor is field:offset or a concat offset. Seed samples and missing ids return 404. Public list GET is unchanged.","operationId":"getRun","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"max_chars","in":"query","required":false,"description":"Character budget for this read. Default 4000, max 80000. Responses include truncated and a continuation next_cursor.","schema":{"type":"integer","default":4000,"minimum":1,"maximum":80000}},{"name":"cursor","in":"query","required":false,"description":"Continuation cursor from the previous page (`field:offset` or a concat offset).","schema":{"type":"string"}}],"responses":{"200":{"description":"Owner run with char-budget fields","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Run"},{"type":"object","required":["truncated","next_cursor"],"properties":{"truncated":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}]}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Missing or seed sample","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["runs"],"summary":"Patch an owner run","description":"Hashed owner key required. Partial update of goal, tools, outcome, status, timing, tags, and/or summary. Seed samples cannot be edited and return 404. Change persists in gitignored `.data`. JSON body max ~64KB. MCP update_run wraps this same keyed PATCH.","operationId":"updateRun","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"goal":{"type":"string"},"outcome":{"type":"string"},"tools":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["started","succeeded","failed"]},"started_at":{"type":"string","format":"date-time"},"finished_at":{"type":"string","format":"date-time"},"duration_ms":{"type":"integer","minimum":0},"tags":{"type":"array","items":{"type":"string"}},"summary":{"type":"object","additionalProperties":true},"expected_by":{"type":"string","format":"date-time"}},"description":"At least one field. Empty goal/outcome → 400. Over ~64KB → 413."}}}},"responses":{"200":{"description":"Updated owner run","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Run"}}}},"400":{"description":"nothing_to_patch, invalid_goal, invalid_outcome, or invalid_tools","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Missing or seed sample","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"JSON body over ~64KB (runs/notes) or md URL string over ~2KB. MCP tool args use the same caps.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["runs"],"summary":"Delete an owner run","description":"Hashed owner key required (401 without). Owner /app detail Delete control calls this the same way other owner writes do. Seed samples cannot be deleted and return 404. Removal persists in gitignored `.data`. After delete, GET-by-id / PATCH / MCP get_run / MCP delete_run return 404 and the id is gone from ?q= / CSV / owner list.","operationId":"deleteRun","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","required":["deleted"],"properties":{"deleted":{"type":"string"}}}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Missing or seed sample","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/notes":{"get":{"tags":["notes"],"summary":"List notes","description":"Public seed GET is unchanged: no key and empty/omitted `q` returns fictional seed samples. A hashed key is required for owner items. `q` searches owner title/body (substring on file/memory; tsvector token-AND on Supabase). Non-empty `q` without a key → 401. `limit` default 50, max 100. `cursor` is the last id from the previous page. Optional `tag`, `since`, and `until` filter the page (invalid values → 400). Optional max_chars (default 4000) stops the page when serialized items would exceed the budget.","operationId":"listNotes","security":[{},{"bearerAuth":[]},{"apiKeyHeader":[]}],"parameters":[{"name":"limit","in":"query","required":false,"description":"Page size. Default 50, max 100.","schema":{"type":"integer","default":50,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Id of the last item from the previous page. Omit on the first page.","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Owner search. Case-insensitive substring match on goal, outcome, title, or body. Hashed key required. Empty or omitted lists as before (public seed without a key; owner items with a key). Non-empty q without a key → 401. Matches owner items only; seed samples are never mixed in.","schema":{"type":"string"}},{"name":"max_chars","in":"query","required":false,"description":"Character budget for this read. Default 4000, max 80000. Responses include truncated and a continuation next_cursor.","schema":{"type":"integer","default":4000,"minimum":1,"maximum":80000}},{"name":"tag","in":"query","required":false,"description":"Filter runs that include this tag.","schema":{"type":"string"}},{"name":"since","in":"query","required":false,"description":"Inclusive lower bound on started_at (ISO datetime or YYYY-MM-DD).","schema":{"type":"string"}},{"name":"until","in":"query","required":false,"description":"Inclusive upper bound on started_at (ISO datetime or YYYY-MM-DD).","schema":{"type":"string"}}],"responses":{"200":{"description":"Note page","content":{"application/json":{"schema":{"type":"object","required":["notes","next_cursor","truncated"],"properties":{"notes":{"type":"array","items":{"$ref":"#/components/schemas/Note"}},"next_cursor":{"type":["string","null"]},"truncated":{"type":"boolean"}}}}}},"401":{"description":"Presented key is unknown, or non-empty q without a key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["notes"],"summary":"Store a note","description":"Hashed owner key required (401 without). Owner /app create form posts {body, optional title, tags} here. MCP create_note wraps this same keyed POST. Body max 32KB (body_too_large). Created records persist in gitignored `.data/` and then work with GET-by-id, PATCH, delete, CSV, MCP get_note, and ?q=. Seed samples cannot be created as seed ids.","operationId":"createNote","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Same key + same body within 10 minutes returns the original record (200) instead of inserting again. Persisted in gitignored `.data/`. Different body with the same key → 409.","schema":{"type":"string","maxLength":256}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"title":{"type":"string"},"body":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"pinned":{"type":"boolean"}},"description":"JSON envelope max ~64KB. Note body max 32KB. Over the envelope → 413 payload_too_large."}}}},"responses":{"200":{"description":"Idempotent replay of the original record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Note"}}}},"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Note"}}}},"400":{"description":"body_required, body_too_large, invalid_title, invalid_tags, invalid_pinned, or invalid Idempotency-Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Idempotency-Key reused with a different body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"JSON body over ~64KB (runs/notes) or md URL string over ~2KB. MCP tool args use the same caps.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/notes/{id}":{"get":{"tags":["notes"],"summary":"Get one owner note","description":"Hashed owner key required. Returns one persisted owner note plus truncated and next_cursor. Optional max_chars (default 4000) slices body. cursor is field:offset or a concat offset. Seed samples and missing ids return 404. Public list GET is unchanged.","operationId":"getNote","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"max_chars","in":"query","required":false,"description":"Character budget for this read. Default 4000, max 80000. Responses include truncated and a continuation next_cursor.","schema":{"type":"integer","default":4000,"minimum":1,"maximum":80000}},{"name":"cursor","in":"query","required":false,"description":"Continuation cursor from the previous page (`field:offset` or a concat offset).","schema":{"type":"string"}}],"responses":{"200":{"description":"Owner note with char-budget fields","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Note"},{"type":"object","required":["truncated","next_cursor"],"properties":{"truncated":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}]}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Missing or seed sample","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["notes"],"summary":"Patch an owner note","description":"Hashed owner key required. Partial update of title, body, tags, and/or pinned. Seed samples cannot be edited and return 404. Change persists in gitignored `.data`. JSON envelope max ~64KB. Note body max 32KB. MCP update_note wraps this same keyed PATCH.","operationId":"updateNote","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"body":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"pinned":{"type":"boolean"}},"description":"At least one field. Empty body → 400. Body over 32KB → 400 body_too_large. Envelope over ~64KB → 413."}}}},"responses":{"200":{"description":"Updated owner note","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Note"}}}},"400":{"description":"nothing_to_patch, invalid_body, invalid_title, body_too_large, invalid_tags, or invalid_pinned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Missing or seed sample","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"JSON body over ~64KB (runs/notes) or md URL string over ~2KB. MCP tool args use the same caps.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["notes"],"summary":"Delete an owner note","description":"Hashed owner key required (401 without). Owner /app detail Delete control calls this the same way other owner writes do. Seed samples cannot be deleted and return 404. Removal persists in gitignored `.data`. After delete, GET-by-id / PATCH / MCP get_note / MCP delete_note return 404 and the id is gone from ?q= / CSV / owner list.","operationId":"deleteNote","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","required":["deleted"],"properties":{"deleted":{"type":"string"}}}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Missing or seed sample","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/md":{"post":{"tags":["md"],"summary":"Public URL → markdown","description":"Fetches public `http`/`https` only. Sends `Accept: text/markdown` first and uses the origin markdown when returned; otherwise falls back to the readability extractor (nav/footer/cookie/feedback strip, heading-hierarchy repair). Response includes `extraction` (origin | main | full), `tokens_in`, and `tokens_out` (whitespace tokens). Echoes `x-markdown-tokens` when the origin sent it. Refuses private/loopback/link-local addresses after DNS. Honors robots.txt. Owner-scoped TTL cache in fetch_cache. No headless Chrome. About 20 requests per 60 seconds per key → 429. Error codes: unsupported_scheme, private_address, robots_disallowed, login_wall, too_large, fetch_failed, timeout.","operationId":"publicUrlToMarkdown","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"Public http(s) URL. Max ~2KB. Over the cap → 413 payload_too_large."},"max_chars":{"type":"integer","minimum":1,"maximum":80000,"default":4000,"description":"Character budget for markdown. Default 4000."},"cursor":{"type":"string","description":"Character offset into markdown from a previous next_cursor."}}}}}},"responses":{"200":{"description":"Markdown","headers":{"X-RateLimit-Limit":{"description":"Max markdown fetches per 60s window (~20).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining markdown fetches in the current window.","schema":{"type":"integer"}},"x-markdown-tokens":{"description":"Echoed from the origin when present.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["url","markdown","extraction","tokens_in","tokens_out","truncated","next_cursor"],"properties":{"url":{"type":"string"},"title":{"type":"string"},"markdown":{"type":"string"},"extraction":{"type":"string","enum":["origin","main","full"]},"tokens_in":{"type":"integer","minimum":0,"description":"Whitespace token count of the fetched body before extraction."},"tokens_out":{"type":"integer","minimum":0,"description":"Whitespace token count of the returned markdown (before max_chars)."},"x-markdown-tokens":{"type":"string","description":"Echo of the origin x-markdown-tokens header when present."},"truncated":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}}}},"400":{"description":"url_required, unsupported_scheme, private_address, login_wall, or other rejected target","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"robots_disallowed — robots.txt disallows the URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"URL envelope payload_too_large or response too_large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. Includes X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After.","headers":{"X-RateLimit-Limit":{"description":"Max markdown fetches per 60s window (~20).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining markdown fetches in the current window.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying a 429.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"fetch_failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"timeout","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/keys":{"post":{"tags":["keys"],"summary":"Mint an API key","description":"Returns the `ar_…` secret once. Only a SHA-256 hash is stored. Mint is locked unless a signed-in owner, header AGENTRACK_BOOTSTRAP_TOKEN, or AGENTRACK_OPEN_RACK=1 (local/preview only). When Auth is configured, `/app` requires sign-in (same-origin, not a CORS cookie).","operationId":"mintKey","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"label":{"type":"string"},"scope":{"type":"string","enum":["read","write"]}}}}}},"responses":{"200":{"description":"Minted. Copy the key now; it is not shown again.","content":{"application/json":{"schema":{"type":"object","required":["id","key","prefix","created_at"],"properties":{"id":{"type":"string"},"key":{"type":"string","description":"Plaintext secret, shown once. Shape `ar_…`. Never stored."},"prefix":{"type":"string","description":"Non-secret prefix for later display."},"created_at":{"type":"string","format":"date-time"},"name":{"type":"string"}}}}}},"401":{"description":"Auth is configured and there is no owner session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/keys/{id}":{"delete":{"tags":["keys"],"summary":"Revoke an API key","description":"Marks one minted key revoked in the same gitignored `.data/` store. The hash stays on disk; `revoked_at` is set. Owner `/app` then drops it from the list. That secret immediately 401s on REST and MCP like a missing key. MCP revoke_key wraps this same store path (identify by id or prefix). Same mint-lock rule as POST /v1/keys: signed-in owner, AGENTRACK_BOOTSTRAP_TOKEN header, or AGENTRACK_OPEN_RACK=1.","operationId":"revokeKey","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Key id from mint (`key_…`)."}],"responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"type":"object","required":["revoked"],"properties":{"revoked":{"type":"string","description":"Id of the revoked key."}}}}}},"401":{"description":"Auth is configured and there is no owner session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown, already revoked, or not this owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks":{"get":{"tags":["webhooks"],"summary":"List outbound webhooks","description":"Hashed key required. Returns this owner's webhook endpoints and the last 100 deliveries. Never returns owner_id or signing secrets.","operationId":"listWebhooks","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"responses":{"200":{"description":"Owner webhooks and delivery log","content":{"application/json":{"schema":{"type":"object","required":["webhooks","deliveries"],"properties":{"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}},"deliveries":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}}}}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["webhooks"],"summary":"Create an outbound webhook","description":"Write-scoped hashed key required. Body: {url, events}. url must be public https (same refusal rules as fetch). events is a non-empty subset of run.finished and note.stored. Returns the endpoint plus secret once. MCP keys action settings with url/events is the same write.","operationId":"createWebhook","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["run.finished","note.stored"]}}}}}}},"responses":{"201":{"description":"Created. Copy the secret now; it is not shown again.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Webhook"},{"type":"object","required":["secret"],"properties":{"secret":{"type":"string","description":"HMAC signing secret, shown once. Never stored in API responses after this."}}}]}}}},"400":{"description":"invalid_webhook_url, invalid_webhook_events, webhook_limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-scoped key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["webhooks"],"summary":"Delete an outbound webhook","description":"Write-scoped hashed key required. Identify by ?id= or JSON {id}. Scoped to the caller's owner. MCP keys action settings with id + settings_op delete is the same write.","operationId":"deleteWebhook","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"parameters":[{"name":"id","in":"query","required":false,"description":"Webhook id (wh_…). Also accepted as JSON body {id}.","schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","required":["deleted"],"properties":{"deleted":{"type":"string"}}}}}},"400":{"description":"id_required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown id or another owner's webhook","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/owner":{"get":{"tags":["owner"],"summary":"Owner settings","description":"Hashed key required. Returns retention_days (default 90), digest_webhook_configured, and digest_webhook_url when set. Never returns owner_id or the webhook secret.","operationId":"getOwnerSettings","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"responses":{"200":{"description":"Owner settings","content":{"application/json":{"schema":{"type":"object","required":["retention_days","digest_webhook_configured","quotas"],"properties":{"retention_days":{"type":"integer","minimum":1,"maximum":3650},"digest_webhook_configured":{"type":"boolean"},"digest_webhook_url":{"type":"string","format":"uri"},"last_digest_at":{"type":"string","format":"date-time"},"quotas":{"$ref":"#/components/schemas/UsageQuotas"}}}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["owner"],"summary":"Delete my data","description":"Wipes this owner's runs, notes, keys, idempotency fingerprints, and fetch cache. Seeds stay. Signed-in owner session or a write-scoped hashed key. Never returns secrets or hashes.","operationId":"deleteOwnerData","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"responses":{"200":{"description":"Deleted counts","content":{"application/json":{"schema":{"type":"object","required":["deleted"],"properties":{"deleted":{"type":"object","required":["runs","notes","keys","idempotency","fetch_cache"],"properties":{"runs":{"type":"integer"},"notes":{"type":"integer"},"keys":{"type":"integer"},"idempotency":{"type":"integer"},"fetch_cache":{"type":"integer"},"usage":{"type":"integer"}}}}}}}},"401":{"description":"Missing session or write key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["owner"],"summary":"Set owner settings","description":"Write-scoped hashed key required. Optional retention_days (1–3650), digest_webhook_url plus digest_webhook_secret (16–256 chars, public http(s) only), and quotas (per-key daily soft caps). Null URL clears the webhook. The secret is write-only and never returned.","operationId":"patchOwnerSettings","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"retention_days":{"type":"integer","minimum":1,"maximum":3650},"digest_webhook_url":{"type":["string","null"],"format":"uri"},"digest_webhook_secret":{"type":"string","minLength":16,"maxLength":256},"quotas":{"$ref":"#/components/schemas/UsageQuotas"}}}}}},"responses":{"200":{"description":"Updated settings","content":{"application/json":{"schema":{"type":"object","required":["retention_days","digest_webhook_configured","quotas"],"properties":{"retention_days":{"type":"integer"},"digest_webhook_configured":{"type":"boolean"},"digest_webhook_url":{"type":"string","format":"uri"},"last_digest_at":{"type":"string","format":"date-time"},"quotas":{"$ref":"#/components/schemas/UsageQuotas"}}}}}},"400":{"description":"invalid_retention_days, invalid_digest_webhook_url, invalid_digest_webhook_secret, invalid_quotas, nothing_to_patch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/cron/purge-runs":{"get":{"tags":["cron"],"summary":"Purge expired owner runs","description":"Deletes owner runs older than each owner's retention_days (default 90). Protected by AGENTRACK_CRON_SECRET or Vercel CRON_SECRET (header x-agentrack-cron or Authorization: Bearer). Empty secret → 401. Seeds are never purged. Vercel Hobby cron hits GET daily.","operationId":"purgeExpiredRuns","security":[],"parameters":[{"name":"x-agentrack-cron","in":"header","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Purge count","content":{"application/json":{"schema":{"type":"object","required":["purged"],"properties":{"purged":{"type":"integer"}}}}}},"401":{"description":"Missing or unknown cron secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["cron"],"summary":"Purge expired owner runs","description":"Same as GET /v1/cron/purge-runs.","operationId":"purgeExpiredRunsPost","security":[],"responses":{"200":{"description":"Purge count","content":{"application/json":{"schema":{"type":"object","required":["purged"],"properties":{"purged":{"type":"integer"}}}}}},"401":{"description":"Missing or unknown cron secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/cron/purge-notes":{"get":{"tags":["cron"],"summary":"Purge expired owner notes","description":"Deletes owner notes older than each owner's retention_days (default 90). Same lock as purge-runs: AGENTRACK_CRON_SECRET or Vercel CRON_SECRET. Empty secret → 401. Seeds are never purged.","operationId":"purgeExpiredNotes","security":[],"parameters":[{"name":"x-agentrack-cron","in":"header","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Purge count","content":{"application/json":{"schema":{"type":"object","required":["purged"],"properties":{"purged":{"type":"integer"}}}}}},"401":{"description":"Missing or unknown cron secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["cron"],"summary":"Purge expired owner notes","description":"Same as GET /v1/cron/purge-notes.","operationId":"purgeExpiredNotesPost","security":[],"responses":{"200":{"description":"Purge count","content":{"application/json":{"schema":{"type":"object","required":["purged"],"properties":{"purged":{"type":"integer"}}}}}},"401":{"description":"Missing or unknown cron secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/cron/digest-runs":{"get":{"tags":["cron"],"summary":"Daily digest of failed and stale runs","description":"POSTs a signed JSON digest of each owner's failed (since last send or 24h) and currently stale runs to that owner's optional digest_webhook_url. HMAC-SHA256 in x-agentrack-signature. At most once per UTC day per owner. Protected by AGENTRACK_CRON_SECRET or Vercel CRON_SECRET. Empty secret → 401. Owners without a webhook are skipped. Private/loopback webhook hosts are refused. Never returns secrets.","operationId":"digestFailedAndStaleRuns","security":[],"parameters":[{"name":"x-agentrack-cron","in":"header","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Delivery counts","content":{"application/json":{"schema":{"type":"object","required":["delivered","skipped","failed"],"properties":{"delivered":{"type":"integer"},"skipped":{"type":"integer"},"failed":{"type":"integer"}}}}}},"401":{"description":"Missing or unknown cron secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["cron"],"summary":"Daily digest of failed and stale runs","description":"Same as GET /v1/cron/digest-runs.","operationId":"digestFailedAndStaleRunsPost","security":[],"responses":{"200":{"description":"Delivery counts","content":{"application/json":{"schema":{"type":"object","required":["delivered","skipped","failed"],"properties":{"delivered":{"type":"integer"},"skipped":{"type":"integer"},"failed":{"type":"integer"}}}}}},"401":{"description":"Missing or unknown cron secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/usage":{"get":{"tags":["usage"],"summary":"Workspace usage meter","description":"Hashed owner key required. Returns this UTC month (`period`) plus last 30 days of workspace usage_daily (runs, notes, fetches, fetch_bytes, tokens_out), plan quotas, and per-key daily rollups. Never returns owner_id, hashes, or plaintext secrets. Allowed when over quota so owners can inspect usage.","operationId":"getOwnerUsage","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"responses":{"200":{"description":"Usage rollups","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["plan","quotas","period","last_30_days","days"],"properties":{"plan":{"type":"string"},"quotas":{"$ref":"#/components/schemas/UsageQuotas"},"period":{"$ref":"#/components/schemas/UsagePeriod"},"last_30_days":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceUsageDay"}},"days":{"type":"array","items":{"$ref":"#/components/schemas/UsageDay"}}}}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/export":{"get":{"tags":["export"],"summary":"Export owner runs and notes","description":"Hashed owner key required. JSON of persisted owner runs, notes, usage rollups, and quotas — no seed samples, no raw API secrets, no hashes. Operator may be a key prefix. Run rows include status, started_at, finished_at, duration_ms, tags, and summary. Note rows include optional title and pinned.","operationId":"exportOwner","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"responses":{"200":{"description":"Owner bundle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OwnerExport"}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/what_happened_last":{"get":{"tags":["handoff"],"summary":"Session handoff","description":"Hashed owner key required. Returns the last N owner runs and pinned owner notes as markdown, each page capped at 300 whitespace tokens and max_chars. Optional `project` and `routine` match a tag or a substring of run goal / note title. `n` is 1–20 (default 5). Optional max_chars (default 4000) and cursor (char offset). Seeds stay out. MCP `what_happened_last` is the same payload.","operationId":"whatHappenedLast","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"parameters":[{"name":"project","in":"query","required":false,"description":"Filter by tag or run goal / note title (case-insensitive).","schema":{"type":"string","maxLength":80}},{"name":"routine","in":"query","required":false,"description":"Filter by tag or run goal / note title (case-insensitive).","schema":{"type":"string","maxLength":80}},{"name":"n","in":"query","required":false,"description":"How many latest matching runs to include. Default 5, max 20.","schema":{"type":"integer","default":5,"minimum":1,"maximum":20}},{"name":"max_chars","in":"query","required":false,"description":"Character budget for this read. Default 4000, max 80000. Responses include truncated and a continuation next_cursor.","schema":{"type":"integer","default":4000,"minimum":1,"maximum":80000}},{"name":"cursor","in":"query","required":false,"description":"Character offset into the handoff markdown from a previous next_cursor.","schema":{"type":"string"}}],"responses":{"200":{"description":"Handoff markdown","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["markdown","tokens","n","truncated","next_cursor"],"properties":{"markdown":{"type":"string"},"tokens":{"type":"integer","minimum":0,"maximum":300},"n":{"type":"integer","minimum":1,"maximum":20},"truncated":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}}}},"400":{"description":"invalid_n, invalid_project, invalid_routine, invalid_max_chars, or invalid_cursor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/mcp":{"post":{"tags":["mcp"],"summary":"Streamable HTTP MCP (JSON-RPC)","description":"JSON-RPC 2.0. `initialize` returns protocolVersion, capabilities.tools, and serverInfo name memoshelf (hidden alias agentrack for one version). No key required for initialize, notifications/initialized, ping, or tools/list. `tools/call` requires a hashed key (`X-Api-Key` or Bearer) except read-only `health`. `tools/list` is health, log_run, runs, notes, fetch_md, keys, what_happened_last. `runs` / `notes` / `keys` take `action` list/get/update/delete (notes also create; keys use list/mint/revoke). Hidden aliases for one version: create_run, get_run, update_run, delete_run, list_runs, stale_runs, store_note, create_note, get_note, update_note, delete_note, list_notes, mint_key, revoke_key, list_keys, public_url_to_md. `health` is a read-only ping matching GET /health (`{ok, version}`); no API key. `update_run` / `update_note` wrap keyed REST PATCH (same body as PATCH /v1/runs/{id} and PATCH /v1/notes/{id}). `get_run` / `get_note` fetch one persisted owner record (same shape as GET /v1/runs/{id} and GET /v1/notes/{id}). `delete_run` / `delete_note` wrap keyed REST DELETE (`{deleted}`). `create_run` / `create_note` wrap keyed REST POST create (same body as POST /v1/runs and POST /v1/notes). `list_runs` / `list_notes` wrap keyed owner list/search (same `{runs|notes, next_cursor, truncated}` as GET /v1/runs and GET /v1/notes). Optional `q` and `max_chars` filter like owner list. `revoke_key` wraps owner API-key revoke (same `{revoked}` as DELETE /v1/keys/{id}). Identify by id or prefix from the same hashed store. `mint_key` wraps owner API-key mint (same `{id, key, prefix, created_at}` as POST /v1/keys). Authenticate with an existing hashed key. Optional label is echoed like owner mint name and is not stored. `list_keys` wraps owner API-key list (same id/prefix/created fields as owner `/app`; active keys only; revoked omitted; never plaintext). `health` matches GET /health (`{ok:true, version}`); no API key. `what_happened_last` wraps keyed GET /v1/what_happened_last (`{markdown, tokens, n, truncated, next_cursor}`). `stale_runs` wraps keyed GET /v1/runs?stale=1 (`{runs, next_cursor, truncated}`). Missing or seed ids → 404 `{error, code:\"not_found\"}`. notifications/initialized → 202 empty ack. Optional Mcp-Session-Id on initialize.","operationId":"mcpJsonRpc","security":[{},{"bearerAuth":[]},{"apiKeyHeader":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"description":"Omit on notifications.","oneOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"method":{"type":"string","examples":["initialize","notifications/initialized","tools/list","tools/call"]},"params":{"type":"object"}}}}}},"responses":{"200":{"description":"JSON-RPC result (initialize, tools/list, tools/call, ping)","content":{"application/json":{"schema":{"type":"object"}}}},"202":{"description":"Notification ack (notifications/initialized). Empty body. No JSON-RPC error."},"401":{"description":"tools/call missing or unknown key (except health)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"tools/call get_run or get_note or delete_run or delete_note or update_run or update_note or revoke_key: missing id or a seed sample or unknown/already-revoked key (same as REST GET-by-id / DELETE / PATCH / key revoke)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"JSON body over ~64KB (runs/notes) or md URL string over ~2KB. MCP tool args use the same caps.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"public_url_to_md or fetch_md over the cap","headers":{"X-RateLimit-Limit":{"description":"Max markdown fetches per 60s window (~20).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining markdown fetches in the current window.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying a 429.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["mcp"],"summary":"Method not allowed","description":"GET /mcp is 405. Handshake is POST-only. Not an auth miss.","operationId":"mcpGetNotAllowed","security":[],"responses":{"405":{"description":"Use POST","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["mcp"],"summary":"Method not allowed","description":"DELETE /mcp is 405. Handshake is POST-only. Not an auth miss.","operationId":"mcpDeleteNotAllowed","security":[],"responses":{"405":{"description":"Use POST","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/export.csv":{"get":{"tags":["export"],"summary":"Export owner runs and notes as CSV","description":"Hashed owner key required. CSV of persisted owner runs and notes only — no seed samples, no raw API secrets, no hashes. Operator may be a key prefix. Columns: kind,id,goal,tools,outcome,body,tags,operator,created_at,status,started_at,finished_at,duration_ms,summary,expected_by,title,pinned.","operationId":"exportOwnerCsv","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"responses":{"200":{"description":"Owner CSV","content":{"text/csv":{"schema":{"type":"string"}}}},"401":{"description":"Missing or unknown key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Hashed API key. Send the plaintext `ar_…` secret shown once at mint."},"apiKeyHeader":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"Same secret as Bearer. Hashed at rest."}},"schemas":{"Health":{"type":"object","additionalProperties":false,"required":["ok","version","product","env","dataSource","degraded","migrations","remote","recent"],"properties":{"ok":{"type":"boolean","const":true},"version":{"type":"string","examples":["3.27.0"]},"product":{"type":"string","const":"memoshelf","examples":["memoshelf"]},"env":{"type":"string","enum":["production","sandbox"],"examples":["production"]},"dataSource":{"type":"string","enum":["supabase","file","memory"]},"degraded":{"type":"boolean"},"recent":{"type":"array","description":"Last 20 degraded/undegraded transitions on this isolate. Memory-held, best effort.","maxItems":20,"items":{"type":"object","additionalProperties":false,"required":["at","degraded","dataSource"],"properties":{"at":{"type":"string","format":"date-time"},"degraded":{"type":"boolean"},"dataSource":{"type":"string","enum":["supabase","file","memory"]}}}},"migrations":{"type":"object","additionalProperties":false,"required":["applied","expected"],"properties":{"applied":{"type":"integer","minimum":0},"expected":{"type":"integer","minimum":0}}},"remote":{"type":"object","additionalProperties":false,"required":["ok","lastError"],"properties":{"ok":{"type":"boolean"},"lastError":{"type":["object","null"],"additionalProperties":false,"required":["code","message","at"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"at":{"type":"string","format":"date-time"}}}}}}},"Run":{"type":"object","additionalProperties":false,"required":["id","goal","tools","outcome","operator","created_at","status","started_at","tags"],"properties":{"id":{"type":"string","examples":["run_0001"]},"goal":{"type":"string"},"tools":{"type":"array","items":{"type":"string"}},"outcome":{"type":"string"},"operator":{"type":"string","description":"Key prefix or seed operator name. Not a secret."},"created_at":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["started","succeeded","failed"]},"started_at":{"type":"string","format":"date-time"},"finished_at":{"type":"string","format":"date-time"},"duration_ms":{"type":"integer","minimum":0},"tags":{"type":"array","items":{"type":"string"}},"summary":{"type":"object","additionalProperties":true,"description":"Small JSON, max ~2KB."},"expected_by":{"type":"string","format":"date-time","description":"Optional ISO-8601 deadline. Started-but-unfinished runs past this are stale."},"seed":{"type":"boolean","description":"Present on public demo samples only."}}},"Note":{"type":"object","additionalProperties":false,"required":["id","body","tags","operator","created_at"],"properties":{"id":{"type":"string","examples":["note_0001"]},"title":{"type":"string","description":"Optional. Max 200 characters."},"body":{"type":"string","description":"Required on create. Max 32KB."},"tags":{"type":"array","items":{"type":"string"}},"operator":{"type":"string","description":"Key prefix or seed operator name. Not a secret."},"created_at":{"type":"string","format":"date-time"},"pinned":{"type":"boolean","description":"When true, the note is included in what_happened_last."},"seed":{"type":"boolean","description":"Present on public demo samples only."}}},"Webhook":{"type":"object","additionalProperties":false,"required":["id","url","events","enabled","consecutive_failures","created_at"],"properties":{"id":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["run.finished","note.stored"]}},"enabled":{"type":"boolean"},"consecutive_failures":{"type":"integer","minimum":0},"created_at":{"type":"string","format":"date-time"},"disabled_at":{"type":"string","format":"date-time"},"last_delivered_at":{"type":"string","format":"date-time"}}},"WebhookDelivery":{"type":"object","additionalProperties":false,"required":["id","webhook_id","event","status","attempt","created_at"],"properties":{"id":{"type":"string"},"webhook_id":{"type":"string"},"event":{"type":"string","enum":["run.finished","note.stored"]},"status":{"type":"string","enum":["delivered","failed"]},"attempt":{"type":"integer","minimum":1},"created_at":{"type":"string","format":"date-time"},"status_code":{"type":"integer"},"error":{"type":"string"}}},"Error":{"type":"object","additionalProperties":false,"required":["error","code"],"properties":{"error":{"type":"string","description":"Same snake_case token as code."},"code":{"type":"string","description":"Stable machine code. 400/401/404/413/429 always include this.","examples":["unauthorized","not_found","payload_too_large","rate_limited","goal_and_outcome_required"]},"retry_after":{"type":"integer"}}},"UsageQuotas":{"type":"object","additionalProperties":false,"required":["requests","runs","notes","fetches","bytes"],"properties":{"requests":{"type":"integer","minimum":0},"runs":{"type":"integer","minimum":0},"notes":{"type":"integer","minimum":0},"fetches":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0}}},"UsageDay":{"type":"object","additionalProperties":false,"required":["key_id","day","requests","runs","notes","fetches","bytes"],"properties":{"key_id":{"type":"string"},"prefix":{"type":"string"},"label":{"type":"string"},"day":{"type":"string","format":"date"},"requests":{"type":"integer","minimum":0},"runs":{"type":"integer","minimum":0},"notes":{"type":"integer","minimum":0},"fetches":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0}}},"WorkspaceUsageDay":{"type":"object","additionalProperties":false,"required":["day","runs","notes","fetches","fetch_bytes","tokens_out"],"properties":{"day":{"type":"string","format":"date"},"runs":{"type":"integer","minimum":0},"notes":{"type":"integer","minimum":0},"fetches":{"type":"integer","minimum":0},"fetch_bytes":{"type":"integer","minimum":0},"tokens_out":{"type":"integer","minimum":0}}},"UsagePeriod":{"type":"object","additionalProperties":false,"required":["month","from","until","runs","notes","fetches","fetch_bytes","tokens_out"],"properties":{"month":{"type":"string"},"from":{"type":"string","format":"date"},"until":{"type":"string","format":"date"},"runs":{"type":"integer","minimum":0},"notes":{"type":"integer","minimum":0},"fetches":{"type":"integer","minimum":0},"fetch_bytes":{"type":"integer","minimum":0},"tokens_out":{"type":"integer","minimum":0}}},"OwnerExport":{"type":"object","additionalProperties":false,"required":["product","version","exported_at","runs","notes","usage","quotas"],"properties":{"product":{"type":"string","examples":["Memoshelf"]},"version":{"type":"string","examples":["3.27.0"]},"exported_at":{"type":"string","format":"date-time"},"runs":{"type":"array","items":{"$ref":"#/components/schemas/Run"}},"notes":{"type":"array","items":{"$ref":"#/components/schemas/Note"}},"usage":{"type":"array","items":{"$ref":"#/components/schemas/UsageDay"}},"quotas":{"$ref":"#/components/schemas/UsageQuotas"}}}}}}