{"openapi":"3.0.3","info":{"title":"Worklyn API","version":"0.2.0","description":"The atom data API and the public endpoints. The app and the services read and write atoms through `/sync` and `/atoms`, files through `/files/{id}`, and make invite links through `/s`. `/message` (with `/c/{id}`) and `/errand` are the public endpoints. `/chat` proxies to a managed model provider for accounts on the pro plan. `/auth/me` says who a token belongs to. Atom shapes live in `std.openwork.africa/model/atoms/atoms.openapi.yml` (from `shared/protocol.ts`) and are merged into the document `/openapi.json` serves.\n\nSync model: an account has one feed of changes with a cursor. Read what is there with `GET /atoms` (pages of atoms, newest first), then keep up with `GET /sync?since=<cursor>` (a page of changes) or the same request with `Accept: text/event-stream` (the changes as they are written). If the feed no longer reaches back to your cursor the answer says `reload`: read `GET /atoms` again. Writes go through `POST /sync` as patches, each with an `opId` so that a retry writes nothing twice.\n\nFollowing changes, as an outside program: sign in with `me` (its link flow: `POST /auth/link`, then `POST /auth/exchange` for a session token), then 1. take the feed's head: `GET /sync?since=<a number past any head>` answers `reload` with the head as its `cursor` (there is no plainer call for the head yet); 2. read what you need with `GET /atoms` (for example `type=person&order=oldest`) while `more`; 3. follow `GET /sync?since=<the head>` while `more`, keeping the answer's `cursor`, or the same request with `Accept: text/event-stream`. Taking the head first means nothing written while you read is missed. A change is `{ cursor, atom }`, the atom as it is now with your `role`; a deleted atom arrives with `deleted: true` (it can be undone); `{ cursor, gone }` means you can no longer read that atom. A page of the feed holds at most 200 changes and names an atom once per page. Feed rows are kept 30 days by default (`FEED_RETENTION_MS`): a cursor older than that is answered `reload`, and you start again from 1. Private types (credentials) never come through the feed.\n\nEvery refusal from the atom, files, invite and message routes is `{ ok: false, code, error }` with the status of its code (`ErrorCode`); `busy` (503) is the one to try again. A size over a limit answers 413 `tooLarge`. `/chat` answers `{ ok, error }` and an unexpected errand fault is a 502 without a code."},"servers":[{"url":"http://localhost:8735","description":"Local (WORKLYN_PORT)"}],"security":[{"bearer":[],"appId":[]},{"sessionCookie":[]}],"tags":[{"name":"Sync","description":"Reading and writing atoms: /sync, /atoms"},{"name":"Files","description":"Signed links for an atom's file: /files/{id}"},{"name":"Sharing","description":"Invite links: /s"},{"name":"Session","description":"Who the caller is: /auth/me"},{"name":"Public","description":"/message, /c/{id}, /errand"},{"name":"Chat","description":"/chat: proxy to a managed model provider, pro plan required"}],"paths":{"/healthz":{"get":{"summary":"Health check","security":[],"responses":{"200":{"description":"Service is up.","content":{"text/plain":{"schema":{"type":"string","example":"ok"}}}}}}},"/auth/me":{"get":{"tags":["Session"],"summary":"Who a token belongs to","description":"The account the credentials name. A program that signs in (the companion) asks once, as its token names no account.","responses":{"200":{"description":"The account.","content":{"application/json":{"schema":{"type":"object","required":["userId"],"properties":{"userId":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/sync":{"post":{"tags":["Sync"],"summary":"Write atoms (push patches)","description":"Applies the patches in order, each in its own commit, so one refusal does not stop the rest. The answer holds one entry per patch, in order: `applied` (the atom as it is now, and the write's cursor in the caller's feed), `renamed` (the id was new but its `src` is already held: the atom is the existing one), or `refused` with a code. A write that more than 90 accounts can see is refused `tooLarge`; a fault in one patch answers that patch `busy` and the rest still apply. A patch needs `write` on the atom (or on its parent, to create one) and `share` on a container it joins or leaves; a writer who cannot read the atom is answered only its `id`, `type`, `created` and `updated`; the body of a private type is never answered, only its `id`, `type`, `ownerId`, `created`, `updated` and `role`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushRequest"}}}},"responses":{"200":{"description":"One answer per patch.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PushAnswer"}}}}},"400":{"$ref":"#/components/responses/Refused"},"401":{"$ref":"#/components/responses/Unauthorized"},"503":{"$ref":"#/components/responses/Refused"}}},"get":{"tags":["Sync"],"summary":"Changes after a cursor (pull), or follow them (stream)","description":"With `Accept: text/event-stream` the changes are streamed: every change after `since`, then each new one as it is written, with a `head` event now and then as the heartbeat. A reconnecting reader sends `Last-Event-ID`, which outranks `since`. If the cursor cannot be followed the stream sends `reload` and ends. Otherwise one page of changes is answered as JSON: call again from `cursor` while `more` is true.","parameters":[{"name":"since","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0},"description":"The cursor of the last change seen."},{"name":"device","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"The name this device pushes under (`device` of `POST /sync`). Its own writes are left out of the changes; `cursor` still moves past them. That an atom is `gone` is always sent."},{"name":"Last-Event-ID","in":"header","required":false,"schema":{"type":"integer","minimum":0},"description":"Stream only. The cursor of the last change the reader saw."}],"responses":{"200":{"description":"JSON `Pull`, or a stream of events: `change` (id is its cursor; data is a `Change`), `head` (data `{ \"head\": <cursor> }`) and `reload` (data `{}`; the stream ends).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pull"}},"text/event-stream":{"schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/Refused"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/atoms":{"get":{"tags":["Sync"],"summary":"Read atoms","description":"A page of atoms the caller can read, never a deleted one, one inside a deleted tree, or one of a private type. Each carries the caller's `role`. Which atoms: `id` or `src` (not enforced: `id` wins when both are sent), the children of `parent`, the members of the container `memberOf`, the atoms others gave the caller access to (`shared=true`; only `true` is accepted), or, with none of these, the caller's own atoms. Each listing has one order key: children by `created`, members by id, shared atoms by when access was given, your own atoms (all, or of one `type`) by `updated`. Pages go newest first by default (send the answer's `before` for the next page while `more`); with `order=oldest` they go oldest first (send its `after`). The last oldest-first page still answers `after`: keep it, and ask again later for what came since. A cursor works only on the listing and order that made it: any other is `invalid`. A page reads at most `limit` rows and one more, which tells `more`. With `type` or `updatedSince`, a page keeps only the atoms that fit out of the `limit` it read: it may hold fewer than `limit`, or none, and still say `more`; keep asking while `more` is true. `updated` is an atom's own last write: a message written under a room or a person does not move theirs.","parameters":[{"name":"id","in":"query","required":false,"schema":{"type":"string"},"description":"The device's form: a page of the one atom, or none. Another program reads one atom at `/atoms/{id}`."},{"name":"type","in":"query","required":false,"schema":{"type":"string"},"description":"Only atoms of this type, e.g. `person`. An unknown type is refused. Your own atoms are read by type, so a page of them is short only where atoms were deleted or expired; other listings keep the type out of what they read."},{"name":"updatedSince","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Only atoms whose `updated` is at or after this ISO time. Your own atoms are listed from there, so an unchanged page is cheap."},{"name":"src","in":"query","required":false,"schema":{"type":"string"},"description":"The caller's own atom with this src."},{"name":"parent","in":"query","required":false,"schema":{"type":"string"}},{"name":"memberOf","in":"query","required":false,"schema":{"type":"string"}},{"name":"shared","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","enum":["newest","oldest"],"default":"newest"},"description":"Newest first (go on with `before`) or oldest first (go on with `after`)."},{"name":"before","in":"query","required":false,"schema":{"type":"string"},"description":"The `before` of the previous page; newest first only."},{"name":"after","in":"query","required":false,"schema":{"type":"string"},"description":"The `after` of the previous page; with `order=oldest` only."},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated top-level fields and `metadata.<key>` to keep (at most 20); `id`, `type`, `created`, `updated` and `role` always come; a field the atom does not hold is left out."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"A page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page"}}}},"400":{"$ref":"#/components/responses/Refused"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/atoms/{id}":{"get":{"tags":["Sync"],"summary":"Read one atom","description":"The atom with the caller's `role`, as a page of it would answer. 404 `notFound` when there is no such atom, it is deleted or inside a deleted tree, past `expiresAt`, of a private type, or the caller may not read it: the answer is the same for each, so nobody learns that an atom they may not read exists. The answer carries an `ETag` (the atom's `updated` and the caller's role); send it as `If-None-Match` to be answered 304 with no body while nothing changed.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"fields","in":"query","required":false,"schema":{"type":"string"},"description":"As on `/atoms`: comma-separated fields and `metadata.<key>` to keep; `id`, `type`, `created`, `updated` and `role` always come."},{"name":"If-None-Match","in":"header","required":false,"schema":{"type":"string"},"description":"The ETag of the copy held."}],"responses":{"200":{"description":"The atom.","headers":{"ETag":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Atom"}}}},"304":{"description":"Not modified since the ETag sent; no body.","headers":{"ETag":{"schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/Refused"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/Refused"}}}},"/files/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"The file atom's id: one path segment."}],"post":{"tags":["Files"],"summary":"A link to put a file's bytes","description":"Bytes live in the bucket, never in an atom. Answers the bucket `key` (`<owner>/<id>`, each percent-encoded) and a signed `url` to PUT the bytes to, good for 15 minutes. The caller needs `write` on the atom. The atom is pushed after its file, so it may not exist yet: then `parent` (its nearest ancestor the server holds) says whose it will be, and without it the file is the caller's own. Refused `busy` when no bucket is set.","parameters":[{"name":"parent","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Where to put the bytes.","content":{"application/json":{"schema":{"type":"object","required":["key","url"],"properties":{"key":{"type":"string"},"url":{"type":"string","format":"uri"}}}}}},"400":{"$ref":"#/components/responses/Refused"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Refused"},"404":{"$ref":"#/components/responses/Refused"},"503":{"$ref":"#/components/responses/Refused"}}},"get":{"tags":["Files"],"summary":"A link to read a file's bytes","description":"A signed `url` good for 5 minutes, to whoever can read the atom. `notFound` when the atom is not visible, is a private type, or has no uploaded file (`metadata.stored` is not true).","responses":{"200":{"description":"Where to read the bytes.","content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"}}}}}},"400":{"$ref":"#/components/responses/Refused"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/Refused"},"503":{"$ref":"#/components/responses/Refused"}}}},"/s":{"post":{"tags":["Sharing"],"summary":"Make an invite link (copy link)","description":"A new single-use link to an atom for the viewer role, not tied to any address, valid for 30 days. One link per request. The caller needs `share` on the atom. Open the link at `/s/{token}`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["atom"],"properties":{"atom":{"type":"string"}}}}}},"responses":{"201":{"description":"The token for `/s/{token}`.","content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Refused"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Refused"},"404":{"$ref":"#/components/responses/Refused"}}}},"/s/{token}":{"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"get":{"tags":["Sharing"],"summary":"The invite page","description":"An HTML page a person opens in a browser, signed in: it offers to accept the invite, or, for the account that already accepted it, links to the item. An unknown, expired or used link (used by someone else) is a page with status 404.","responses":{"200":{"description":"The offer, or the link to the item for the account that accepted.","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"The invitation is unavailable.","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"tags":["Sharing"],"summary":"Accept an invite","description":"Anyone signed in who holds the link can accept it, once: the account is given access (the invite's role) and the link is used. The same account sending it again is redirected again. Redirects to the item in openwork.","responses":{"302":{"description":"To the item in openwork (`Location`)."},"400":{"$ref":"#/components/responses/Refused"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/Refused"},"503":{"$ref":"#/components/responses/Refused"}}}},"/message":{"post":{"tags":["Public"],"summary":"Send a message (learn, remember or errand)","description":"`from` is the signed-in account. `to` is a contact's address (an email or a number) or `operator://<name>` (one of the caller's operators, by its name or label without case, or by its id; no operator by that name: 404; two operators answering one name: 400); left out, the account's default operator. `subject` names a new task, or adds to an existing one as `task://<id>`; with no subject the message goes to the room with that person, the group with exactly those people, the room with that operator, or the main room. An operator together with people goes to the people's room or group (the operator is not a member). With no subject and no person, several operators are refused 400 (pending the owner's decision). Only a message to the default operator alone is classified; any other is saved as learn and needs no subject. A classifier sorts it: learn (saved, nothing else; any doubt is learn), remember (a search over what the caller can read: the results are answered and saved as a remember message under the task) or errand (an errand message under the task holds a run on this instance's runner; marking it stopped stops the run). remember and errand need a subject. Attachments are files of the task (or of the main room, when there is no subject and the message goes to the default operator alone), sent as base64 `data` or as the `url` of the caller's own upload (`POST /files/{id}`).\n\nA refused message writes nothing, with one exception: a run refused at its start (for example `sandboxUnavailable`) is answered its code after the task, the message and the failed errand message are written. Send an `Idempotency-Key` header: the same key again writes nothing twice and answers the same id, so a retry after any failure is safe.\n\nThe MCP `message` tool (at /mcp, with a Worklyn OAuth token) takes the same body and answers the same `{ id, link, results? }`, with the same refusals. /mcp is the MCP Streamable HTTP transport, stateless: no `Mcp-Session-Id` is issued or needed. Called without a valid token the tool answers 401 with `WWW-Authenticate: Bearer resource_metadata=...` (on a single JSON answer).","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Seeds the ids of what is written, for this account only.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageRequest"}}}},"responses":{"200":{"description":"The thread the message went to.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageAnswer"}}}},"400":{"$ref":"#/components/responses/MessageRefused"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/MessageRefused"},"404":{"$ref":"#/components/responses/MessageRefused"},"413":{"$ref":"#/components/responses/MessageRefused"},"422":{"$ref":"#/components/responses/MessageRefused"},"502":{"$ref":"#/components/responses/MessageRefused"},"503":{"$ref":"#/components/responses/MessageRefused"}}}},"/assistant":{"get":{"tags":["Public"],"summary":"Get the account's assistant","description":"The assistant the account has: its name, and its mail address once a POST has made one (`email` is null until then). Nothing is made; an account with no assistant is answered 404.","responses":{"200":{"description":"The assistant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Assistant"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/Refused"}}},"post":{"tags":["Public"],"summary":"Create the account's assistant and its mail address, or get the ones it has","description":"The assistant is the account's own AI (its main operator), reached by a mail address of its own, `<handle>@worklyn.me`. Whatever is missing is made and answered 201 with `created: true`: the assistant for an account that has none, and its mailbox. An account that has both is answered 200 with `created: false`, and nothing is changed: a `name` sent is then ignored. The body may be left out.\n\nA new assistant takes the name the account already keeps for its AI; else the `name` sent; else the usual name. An assistant the account already has keeps its name. The address is the name in lowercase letters, digits and hyphens (\"Kora Bloom\" is `korabloom@worklyn.me`). When the assistant has no name of its own, or another account or the mail server already holds that address, a generated address is made instead and the name stays. An existing mailbox is never taken over. One mailbox an account, ever.\n\nWhile the mail server cannot be reached the call is answered 503 `busy` and no assistant is written: send it again.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":24,"description":"The name to make it with."}}}}}},"responses":{"200":{"description":"The assistant and the address the account already had.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Assistant"}}}},"201":{"description":"The assistant, with what was just made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Assistant"}}}},"400":{"$ref":"#/components/responses/Refused"},"401":{"$ref":"#/components/responses/Unauthorized"},"503":{"$ref":"#/components/responses/Refused"}}}},"/inbox":{"get":{"tags":["Public"],"summary":"Read the messages in the room with the account's assistant","description":"What was said with the account's assistant, newest first: what the account wrote, what its assistant answered, and what arrived for it from outside, such as mail sent to the assistant's own address. It covers the room with the assistant and every task the assistant is in. A mail with no subject is in the room; a mail with a subject is in a task named by that subject, where the replies of the same conversation join it, and each message names its `task`. Nothing is made: an account with no assistant is answered an empty list. A message that shows a file names it in `file`; `file.url` answers a link to its bytes with the same credentials. Go on to older messages by sending the `before` answered, which is the `created` of the last message read.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"before","in":"query","required":false,"description":"The `before` of the page already read: only messages older than this time.","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"A page of messages.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Inbox"}}}},"400":{"$ref":"#/components/responses/Refused"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/c/{id}":{"get":{"tags":["Public"],"summary":"Open the conversation a message answered","description":"Redirects to that task or room in openwork, which checks who may open it. No session needed.","security":[],"parameters":[{"$ref":"#/components/parameters/AtomId"}],"responses":{"302":{"description":"To the conversation in openwork (`Location`)."}}}},"/chat":{"post":{"tags":["Chat"],"summary":"Chat completion via a managed model provider","description":"A thin, OpenAI-shaped proxy in front of the managed model providers worklyn holds keys for (Token Factory / Nebius, OpenRouter, Sail Research, Groq — see `provider`). Requires the caller's account to be on the pro plan; free accounts get `402`. Set `stream: true` for a `text/event-stream` response carrying the upstream's own OpenAI-style SSE chunks (`data: {...}\\n\\n`, terminated by `data: [DONE]\\n\\n`) relayed byte-for-byte; otherwise the full `ChatReply` is returned as JSON once complete.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatRequest"}}}},"responses":{"200":{"description":"The completed reply (`stream` omitted or `false`), or an SSE stream of upstream chunks (`stream: true`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatReply"}},"text/event-stream":{"schema":{"type":"string","description":"Raw upstream SSE, relayed as-is."}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"The caller's account is not on the pro plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"$ref":"#/components/responses/Internal"}}}},"/errand":{"post":{"tags":["Public"],"summary":"Hand a prompt to a coding agent (answers at once with the run)","description":"Starts a run: `prompt` is given to an agent, either in a cloud sandbox scoped to the caller (`where: cloud`) or on one of the caller's own connected computers (`where: local`, with `device` and `folder`). The work goes on after the answer; follow it with GET /errand/{id} and stop it with DELETE /errand/{id}. A local run is recorded as `waiting` and held for that computer, whose companion takes it, runs the agent in the folder, and sends back the final answer. The computer must be the caller's own (`notFound` otherwise) and live (`deviceUnreachable`), with the agent installed (`agentMissing`). A run and its output are kept for seven days. An errand sent as a message (POST /message, or the MCP `message` tool) runs in the cloud.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrandRequest"}}}},"responses":{"202":{"description":"The run, recorded; `running` in the cloud, `waiting` for the caller's computer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrandRun"}}}},"400":{"$ref":"#/components/responses/ErrandRefused"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/ErrandRefused"},"422":{"$ref":"#/components/responses/ErrandRefused"},"502":{"$ref":"#/components/responses/ErrandRefused"},"503":{"$ref":"#/components/responses/ErrandRefused"}}}},"/errand/agents":{"get":{"tags":["Public"],"summary":"The agents worklyn runs itself","responses":{"200":{"description":"The cloud's agents. The agents on a caller's own computer are listed with it (GET /errand/devices).","content":{"application/json":{"schema":{"type":"object","properties":{"agents":{"type":"array","items":{"$ref":"#/components/schemas/ErrandAgent"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/errand/devices":{"get":{"tags":["Public"],"summary":"The caller's own connected computers","description":"A computer is connected by running the companion on it and signing in. It is listed to its own account only, and is `live` while it has said it is there in the last 30 seconds. Its record is kept 30 days after it last did.","responses":{"200":{"description":"The computers, each with the agents it reported.","content":{"application/json":{"schema":{"type":"object","properties":{"devices":{"type":"array","items":{"$ref":"#/components/schemas/ErrandDevice"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/errand/devices/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer's id, chosen by its companion."}],"put":{"tags":["Public"],"summary":"A computer says it is there (the companion, every 10 seconds)","description":"Records the computer under the caller's account and marks it live. The first account to report an id holds it: to any other account it is `notFound`. Each run named in `running` that this computer has taken goes on counting as alive; one it stops naming ends as `interrupted`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrandDeviceReport"}}}},"responses":{"200":{"description":"The computer as its owner sees it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrandDevice"}}}},"400":{"$ref":"#/components/responses/ErrandRefused"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/ErrandRefused"}}},"delete":{"tags":["Public"],"summary":"A computer is gone (the companion signed out)","responses":{"200":{"description":"Removed; its id is free.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","example":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/ErrandRefused"}}}},"/errand/work/{id}":{"get":{"tags":["Public"],"summary":"The work held for a computer (SSE, the companion)","description":"Server-Sent Events, open for as long as the companion keeps it: `run` `{ type, run }` (a waiting run to take with POST /errand/start/{id}), `stop` `{ type, id }` (a run it took is to be stopped; end it with POST /errand/end/{id}) and `browse` `{ type, path }` (a folder to list with PUT /errand/directories/{id}) and `make` `{ type, id, parent, name }` (a folder to make, answered with the same PUT; `id` names the asking, so one handed over twice is made once). Each is sent once on a connection, and again on the next while it is still held. A comment line (`:`) says the stream is alive.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The events, as they come.","content":{"text/event-stream":{"schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/ErrandRefused"}}}},"/errand/directories/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"tags":["Public"],"summary":"The folders under a path on the caller's own computer","description":"Asks the computer and waits up to 8 seconds for its answer; an answer is served again for 15 seconds. With no `path`, the computer's home folder.","parameters":[{"name":"path","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"The listing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrandDirectoryListing"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/ErrandRefused"},"503":{"$ref":"#/components/responses/ErrandRefused"}}},"post":{"tags":["Public"],"summary":"Make a folder on the caller's own computer","description":"A new folder `name` inside the folder `parent`. The name is trimmed and must be one plain name: no `/ \\ : * ? \" < > |` or control characters, no leading dot, at most 255 characters (`invalid` otherwise, before the computer is asked). The computer is asked and waited for up to 8 seconds; it makes the folder only inside its home folder, never a path of folders, never over what is there. Its refusal is passed on with its reason in `error`: `nameTaken` (409), `forbidden` (403), `folderMissing` (422), `invalid` (400). `deviceUnreachable` (503) when it does not answer in time: the folder may then have been made or not.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["parent","name"],"properties":{"parent":{"type":"string","example":"/Users/me/Work"},"name":{"type":"string","example":"Notes"}}}}}},"responses":{"201":{"description":"The new folder.","content":{"application/json":{"schema":{"type":"object","required":["name","path"],"properties":{"name":{"type":"string","example":"Notes"},"path":{"type":"string","example":"/Users/me/Work/Notes"}}}}}},"400":{"$ref":"#/components/responses/ErrandRefused"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ErrandRefused"},"404":{"$ref":"#/components/responses/ErrandRefused"},"409":{"$ref":"#/components/responses/ErrandRefused"},"422":{"$ref":"#/components/responses/ErrandRefused"},"503":{"$ref":"#/components/responses/ErrandRefused"}}},"put":{"tags":["Public"],"summary":"A computer's answer to a `browse` or a `make` (the companion)","description":"A listing's `path` is the path as it was asked, character for character. The answer to a `make` names the asking (`make`) with the new folder's `path`, or a `code` and the reason in `error`; only the first answer to an asking still waited for counts.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrandDirectoryListing"},{"$ref":"#/components/schemas/ErrandDirectoryMade"}]}}}},"responses":{"200":{"description":"Kept.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","example":true}}}}}},"400":{"$ref":"#/components/responses/ErrandRefused"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/ErrandRefused"}}}},"/errand/start/{id}":{"post":{"tags":["Public"],"summary":"A computer takes a run held for it (the companion)","description":"The run goes from `waiting` to `running`, once. `notFound` when it is not the caller's, not this computer's, or no longer waiting.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["device"],"properties":{"device":{"type":"string"}}}}}},"responses":{"200":{"description":"The run, now running.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrandRun"}}}},"400":{"$ref":"#/components/responses/ErrandRefused"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/ErrandRefused"}}}},"/errand/end/{id}":{"post":{"tags":["Public"],"summary":"A computer ends a run held for it (the companion)","description":"`text` is the agent's final answer, or why it failed; the last 8000 characters are kept as the run's reply (none for `stopped`), and written onto the errand message when the run has one. A run that is already over is answered as it is.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["status","text"],"properties":{"status":{"type":"string","enum":["done","failed","stopped"]},"text":{"type":"string"}}}}}},"responses":{"200":{"description":"The run as it now is.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrandRun"}}}},"400":{"$ref":"#/components/responses/ErrandRefused"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/ErrandRefused"}}}},"/errand/{id}":{"get":{"tags":["Public"],"summary":"Follow a run (SSE)","description":"Streams the run as Server-Sent Events: every `output` so far, then new ones as they are written, then `reply` (the agent's last word: a statement of what it did, or a question when it is stuck) and `done`. An `output` event's id is its line number: reconnect with `Last-Event-ID`, or pass `?from=`, to read only what came after. Only the account that started a run can follow it; anyone else is answered as if it did not exist.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"from","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0},"description":"The first line to send."}],"responses":{"200":{"description":"Events `output` `{ type, line, text }`, `reply` `{ type, kind: \"statement\" | \"question\", text }` and `done` `{ type, status }`, in that order. A run that is over is replayed whole. A run on a computer has no `output`: while it is `waiting` or `running` the stream is open and silent.","content":{"text/event-stream":{"schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/ErrandRefused"}}},"delete":{"tags":["Public"],"summary":"Stop a run","description":"Ends the sandbox command (cloud), and the run ends as `stopped`. A run still waiting for a computer ends at once; one a live computer is working on is stopped by that computer (it is told through its work stream), so the answer may still say `running`. Stopping a run that is over changes nothing.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The run as it now is.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrandRun"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/ErrandRefused"}}}}},"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","description":"App token. Send it together with `X-App-Id`."},"appId":{"type":"apiKey","in":"header","name":"X-App-Id"},"sessionCookie":{"type":"apiKey","in":"cookie","name":"id","description":"Session cookie issued by `me`. In dev, `X-Owner-Id` can stand in."}},"parameters":{"AtomId":{"name":"id","in":"path","required":true,"schema":{"type":"string"}}},"responses":{"MessageRefused":{"description":"Refused, with a code: `invalid` (400), `forbidden` (403), `notFound` (404), `parentMissing` (409), `parentDeleted` (410), `tooLarge` (413, any size over a limit), `subjectRequired` or `agentMissing` (422), `sandboxUnavailable` (502), `busy` (503: try again with the same Idempotency-Key).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageRefusal"}}}},"ErrandRefused":{"description":"Refused, with a code: `invalid` (400), `notFound` (404), `agentMissing` or `folderMissing` (422), `sandboxUnavailable` (502), `deviceUnreachable` or `busy` (503).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrandRefusal"}}}},"BadRequest":{"description":"Invalid request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Missing or invalid credentials (code `unauthorised`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Refusal"}}}},"Internal":{"description":"Unexpected error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Refused":{"description":"Refused, with a code and the status of that code (`ErrorCode`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Refusal"}}}}},"schemas":{"Role":{"description":"What the caller may do with an atom. owner and editor read, write and share; viewer reads.","type":"string","enum":["owner","editor","viewer"]},"From":{"description":"What made a write. A write by `automation` does not count as an edit by a person.","type":"string","enum":["user","automation"]},"AtomType":{"description":"The kind of atom (`ATOM_TYPES` in shared/atomTypes.ts). Containers (members allowed): task, project, group, room, community, space. A `person` holds only `contact`s. `credential` is private: it never leaves the server.","type":"string","enum":["task","event","post","note","brief","contact","link","tag","operator","project","goal","skill","agentic_task","agentic_story","code","tool","personality","sourcemap","group","room","message","account","speed_dial","thread","person","image","video","file","story","onboarding","crib_note","community","space","activity","secret","search_artifact","folder","generation_job","transcript","call_event","column","mcp_server","credential"]},"Atom":{"description":"An atom as the server answers it. `role` is the caller's role on it. A writer who cannot read the atom is answered only `id`, `type`, `created` and `updated`. A private type's skeleton (answered to its writer) carries only `id`, `type`, `ownerId`, `created`, `updated` and `role`.","type":"object","additionalProperties":false,"required":["id","type","created","updated"],"properties":{"id":{"type":"string","minLength":1,"maxLength":200},"type":{"$ref":"#/components/schemas/AtomType"},"ownerId":{"type":"string"},"parent":{"description":"The atom it sits under; access follows this chain.","type":"string"},"parents":{"description":"The containers it is a member of; changed only through `join` and `leave`.","type":"array","items":{"type":"string"}},"refs":{"description":"Named references to other atoms (R1), for example `author`, `column`, `with`, `about`.","type":"object","additionalProperties":{"type":"string"}},"name":{"type":"string"},"content":{"type":"string"},"src":{"description":"Canonical dedupe key, unique per owner. Writing an id the server has not seen, with a `src` it already holds, answers `renamed` with the existing atom.","type":"string"},"status":{"type":"string"},"created":{"type":"string","format":"date-time"},"updated":{"type":"string","format":"date-time"},"deleted":{"type":"boolean"},"expiresAt":{"type":"string","format":"date-time"},"metadata":{"description":"Keys by type (`metadataOf(type)`); keys a type does not name are kept as written. No key holds another atom's id.","type":"object","additionalProperties":true},"role":{"$ref":"#/components/schemas/Role"}}},"DueDate":{"description":"When a task is due, on the person's calendar (`metadata.dueDate` of a task); the time is optional.","type":"object","required":["year","month","day"],"properties":{"year":{"type":"integer"},"month":{"type":"integer"},"day":{"type":"integer"},"hour":{"type":"integer"},"minute":{"type":"integer"}}},"Path":{"description":"A field of an atom: `parent`, `name`, `content`, `src`, `status`, `created`, `deleted`, `expiresAt`, or `metadata.<key>` and `refs.<key>` (at most 8 segments). The bare `metadata` and `refs` are not paths; `parents` changes only through `join` and `leave`; `id`, `type`, `ownerId` and `updated` are never set.","type":"string"},"Patch":{"description":"One change to one atom. A patch whose `id` the server has not seen creates the atom (its `type` is then required and fixed). `opId` makes a retry safe: the same opId again answers what it answered.","type":"object","additionalProperties":false,"required":["opId","id","type"],"properties":{"opId":{"type":"string","minLength":1,"maxLength":200},"id":{"type":"string","minLength":1,"maxLength":200},"type":{"$ref":"#/components/schemas/AtomType"},"set":{"type":"object","description":"Path to its new value.","additionalProperties":true},"unset":{"type":"array","items":{"$ref":"#/components/schemas/Path"}},"join":{"description":"Containers to become a member of.","type":"array","items":{"type":"string"}},"leave":{"type":"array","items":{"type":"string"}},"quiet":{"description":"Does not move `updated`.","type":"boolean"}}},"PushRequest":{"type":"object","additionalProperties":false,"required":["patches"],"properties":{"from":{"$ref":"#/components/schemas/From"},"device":{"description":"A name the writing device gives itself (any string it keeps for as long as it runs). Send the same name as `device` on `GET /sync` and the feed leaves this device's own writes out: the answer here is its copy of each, and when an answer's `cursor` is one past the cursor the device holds, it may go on from that cursor.","type":"string","minLength":1,"maxLength":200},"patches":{"type":"array","maxItems":100,"items":{"$ref":"#/components/schemas/Patch"}}}},"ErrorCode":{"description":"A refusal's code and the status it is answered with: invalid 400, unauthorised 401, forbidden 403, notFound 404, parentMissing 409, parentDeleted 410, tooLarge 413, busy 503 (the only code worth retrying as it is), nameTaken 409, agentMissing 422, folderMissing 422, subjectRequired 422, sandboxUnavailable 502, deviceUnreachable 503 (the caller's own connected computer is not answering).","type":"string","enum":["invalid","unauthorised","forbidden","notFound","parentMissing","parentDeleted","tooLarge","busy","agentMissing","sandboxUnavailable","folderMissing","subjectRequired","deviceUnreachable","nameTaken"]},"PushAnswer":{"description":"One answer per patch, in order, each with the opId of its patch.","oneOf":[{"type":"object","additionalProperties":false,"required":["opId","status","atom","cursor"],"properties":{"opId":{"type":"string"},"status":{"type":"string","enum":["applied"]},"atom":{"$ref":"#/components/schemas/Atom"},"cursor":{"type":"integer","minimum":0,"description":"The position of this write in the caller's own feed. One past the cursor you hold: nothing came between, go on from it."}}},{"type":"object","additionalProperties":false,"required":["opId","status","was","atom"],"properties":{"opId":{"type":"string"},"status":{"type":"string","enum":["renamed"]},"was":{"type":"string","description":"The id the patch used; the atom is the one already under that `src`."},"atom":{"$ref":"#/components/schemas/Atom"}}},{"type":"object","additionalProperties":false,"required":["opId","status","code","message"],"properties":{"opId":{"type":"string"},"status":{"type":"string","enum":["refused"]},"code":{"$ref":"#/components/schemas/ErrorCode"},"message":{"type":"string"}}}],"discriminator":{"propertyName":"status"}},"Change":{"description":"One entry of the caller's feed. `gone` names an atom (or the root of a tree) the caller can no longer see.","type":"object","additionalProperties":false,"required":["cursor"],"properties":{"cursor":{"type":"integer","minimum":0},"atom":{"$ref":"#/components/schemas/Atom"},"gone":{"type":"string"}}},"Pull":{"type":"object","additionalProperties":false,"required":["changes","cursor","more"],"properties":{"changes":{"type":"array","items":{"$ref":"#/components/schemas/Change"}},"cursor":{"type":"integer","minimum":0,"description":"Send it as `since` next time. It is past every row read, those left out for `device` among them."},"more":{"type":"boolean","description":"True while the feed holds more after `cursor`; pull again at once."},"reload":{"description":"The cursor is older than the feed reaches back; read everything again from `GET /atoms` and start from `cursor`.","type":"boolean","enum":[true]}}},"Page":{"type":"object","additionalProperties":false,"required":["atoms","more"],"properties":{"atoms":{"type":"array","items":{"$ref":"#/components/schemas/Atom"}},"before":{"type":"string","description":"Newest first: present when `more`; send it as `before` for the next page."},"after":{"type":"string","description":"Oldest first: where this page ended; send it as `after` for the next page, now while `more`, or later for what came since."},"more":{"type":"boolean","description":"True when rows remain past this page."}}},"Assistant":{"type":"object","required":["id","name","email","created"],"properties":{"id":{"type":"string","description":"The operator atom that is the assistant."},"name":{"type":"string"},"email":{"type":"string","nullable":true,"description":"Its own mail address; null until a POST has made the mailbox."},"created":{"type":"boolean","description":"Whether this call made the assistant, its mailbox, or both."}}},"Inbox":{"type":"object","required":["messages","more"],"properties":{"messages":{"type":"array","items":{"$ref":"#/components/schemas/InboxMessage"}},"before":{"type":"string","description":"Send it back as `before` for the next","older page.":null},"more":{"type":"boolean","description":"Whether older messages remain."}}},"InboxMessage":{"type":"object","required":["id","created","from","name","email","subject","task","text","htmlUrl","file"],"properties":{"id":{"type":"string"},"created":{"type":"string","format":"date-time","description":"When it was written or, for mail, sent."},"from":{"type":"string","enum":["assistant","you","other"],"description":"Who wrote it: the assistant, the account itself, or someone else."},"name":{"type":"string","nullable":true,"description":"The writer's name","when it is known.":null},"email":{"type":"string","nullable":true,"description":"The sender's address","for a message that came by mail.":null},"subject":{"type":"string","nullable":true,"description":"The mail's subject."},"task":{"type":"object","nullable":true,"description":"The task the message lives in; null in the room with the assistant.","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"text":{"type":"string","description":"The message's words. For a mail drawn in HTML, the words read out of it."},"htmlUrl":{"type":"string","nullable":true,"description":"For a mail drawn in HTML: answers a link to the mail as its sender drew it (GET, same credentials). The page is kept so that it runs no script and loads nothing from the network."},"file":{"type":"object","nullable":true,"required":["id","name","type","kind","url"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","nullable":true,"description":"Its media type."},"kind":{"type":"string","description":"image, video, document or voice."},"url":{"type":"string","description":"Answers a link to the file's bytes (GET, same credentials)."}}}}},"ErrandRefusal":{"type":"object","required":["ok","code","error"],"properties":{"ok":{"type":"boolean","example":false},"code":{"type":"string","enum":["invalid","unauthorised","forbidden","notFound","nameTaken","agentMissing","sandboxUnavailable","folderMissing","deviceUnreachable","busy"]},"error":{"type":"string"}}},"ErrandRequest":{"type":"object","additionalProperties":false,"required":["prompt","where"],"properties":{"prompt":{"type":"string","minLength":1,"maxLength":20000,"description":"What to do, as an instruction for the agent.","example":"Summarise the open pull requests"},"where":{"type":"string","enum":["local","cloud"]},"agent":{"type":"string","description":"Cloud: one of GET /errand/agents. Local: one the computer reported as installed. Defaults to the first.","example":"claude"},"model":{"type":"string","description":"A model by name. In the cloud, `provider/model` names its provider; it overrides `thinking`."},"thinking":{"type":"string","enum":["low","medium","high"],"description":"Cloud: picks the model when none is named and the account has saved no provider."},"folder":{"type":"string","description":"Local: required. The folder on the computer to work in. Ignored in the cloud."},"device":{"type":"string","description":"Local: required. One of the caller's own computers (GET /errand/devices)."}}},"ErrandRun":{"type":"object","required":["id","account","where","agent","prompt","status","startedAt"],"properties":{"id":{"type":"string","format":"uuid"},"account":{"type":"string"},"where":{"type":"string","enum":["local","cloud"]},"agent":{"type":"string"},"model":{"type":"string"},"prompt":{"type":"string"},"folder":{"type":"string"},"device":{"type":"string","description":"A local run: the computer it is held for."},"thread":{"type":"string","description":"A local run asked in a conversation: that conversation (an atom id)."},"status":{"type":"string","enum":["waiting","running","done","failed","stopped","interrupted"],"description":"`waiting`: held for a computer that has not taken it yet."},"startedAt":{"type":"string","format":"date-time"},"endedAt":{"type":"string","format":"date-time"},"reply":{"type":"object","properties":{"kind":{"type":"string","enum":["statement","question"]},"text":{"type":"string"}}},"message":{"type":"string","description":"The errand message (an atom id) that holds this run, when it was asked through a message."}}},"ErrandDeviceReport":{"type":"object","additionalProperties":false,"required":["name","home","agents","running"],"properties":{"name":{"type":"string","description":"What its user calls it.","example":"Alice's Mac"},"home":{"type":"string","description":"Its home folder: where browsing starts.","example":"/Users/alice"},"agents":{"type":"array","items":{"$ref":"#/components/schemas/ErrandAgent"}},"running":{"type":"array","items":{"type":"string"},"description":"The ids of the runs it is working on now."}}},"ErrandDevice":{"type":"object","required":["id","name","home","agents","live","seenAt"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"home":{"type":"string"},"agents":{"type":"array","items":{"$ref":"#/components/schemas/ErrandAgent"}},"live":{"type":"boolean","description":"It said it was there in the last 30 seconds."},"seenAt":{"type":"string","format":"date-time"}}},"ErrandDirectoryListing":{"type":"object","additionalProperties":false,"required":["path","directories"],"properties":{"path":{"type":"string"},"parent":{"type":"string","description":"Absent at the top."},"directories":{"type":"array","items":{"type":"object","required":["name","path"],"properties":{"name":{"type":"string"},"path":{"type":"string"}}}}}},"ErrandDirectoryMade":{"type":"object","additionalProperties":false,"required":["make"],"properties":{"make":{"type":"string","description":"The `id` of the `make` being answered."},"path":{"type":"string","description":"The new folder; absent when it was not made."},"code":{"type":"string","enum":["invalid","forbidden","folderMissing","nameTaken"]},"error":{"type":"string","description":"Why it was not made, in words."}}},"ErrandAgent":{"type":"object","required":["id","label","where"],"properties":{"id":{"type":"string","example":"claude"},"label":{"type":"string","example":"Claude Code"},"where":{"type":"array","items":{"type":"string","enum":["local","cloud"]}},"installed":{"type":"boolean","description":"A computer's agent only."},"install":{"type":"string","description":"A computer's agent only: the command that installs it there."},"providers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"models":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"}}}}}}}}},"Error":{"type":"object","required":["ok","error"],"properties":{"ok":{"type":"boolean","example":false},"error":{"type":"string"}}},"Refusal":{"type":"object","required":["ok","code","error"],"properties":{"ok":{"type":"boolean","example":false},"code":{"$ref":"#/components/schemas/ErrorCode"},"error":{"type":"string"}}},"MessageRefusal":{"type":"object","required":["ok","code","error"],"properties":{"ok":{"type":"boolean","example":false},"code":{"type":"string","enum":["invalid","unauthorised","forbidden","notFound","tooLarge","busy","subjectRequired","agentMissing","sandboxUnavailable","parentMissing","parentDeleted"]},"error":{"type":"string"}}},"MessageRequest":{"type":"object","additionalProperties":false,"required":["message"],"properties":{"to":{"description":"A contact's address (an email or a number) or operator://<name> (an operator's name or id); one or several (at most 50).","oneOf":[{"type":"string","minLength":1},{"type":"array","minItems":1,"maxItems":50,"items":{"type":"string","minLength":1}}]},"subject":{"type":"string","minLength":1,"maxLength":200,"description":"A new task's name, or task://<id>."},"message":{"type":"string","minLength":1,"maxLength":16000},"attachment":{"type":"array","maxItems":10,"items":{"type":"object","additionalProperties":false,"required":["name"],"description":"Exactly one of `data` (base64, at most 5 MiB decoded) or `url` (the object URL of your own upload).","properties":{"name":{"type":"string","minLength":1},"type":{"type":"string","description":"A media type; taken from the name when left out."},"data":{"type":"string","format":"byte"},"url":{"type":"string","format":"uri"}}}}}},"MessageAnswer":{"type":"object","additionalProperties":false,"required":["id","link"],"properties":{"id":{"type":"string","description":"The task, or the room when there is no subject."},"link":{"type":"string","format":"uri","description":"Opens that conversation in openwork (the same place GET /c/{id} redirects to)."},"results":{"type":"array","description":"remember only, best first.","items":{"type":"object","required":["id","snippet","score"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"snippet":{"type":"string"},"score":{"type":"number"}}}}}},"ChatMessage":{"type":"object","required":["role","content"],"properties":{"role":{"type":"string","enum":["user","assistant"]},"content":{"type":"string"}}},"ChatTool":{"type":"object","required":["name","description","schema"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"schema":{"type":"object","description":"JSON Schema for the tool's arguments."}}},"ChatRequest":{"type":"object","required":["provider","messages"],"properties":{"provider":{"type":"string","enum":["token-factory","openrouter","sail","groq","opencode"],"description":"Which managed upstream to proxy to. Token Factory (Nebius) and Sail speak the same OpenAI-chat-completions wire; OpenRouter and Groq too."},"model":{"type":"string","description":"Provider-specific model id. Defaults to that provider's standard model."},"system":{"type":"string","description":"System prompt. Optional."},"messages":{"type":"array","items":{"$ref":"#/components/schemas/ChatMessage"}},"tools":{"type":"array","items":{"$ref":"#/components/schemas/ChatTool"}},"stream":{"type":"boolean","default":false,"description":"If true, respond with `text/event-stream` instead of a single JSON reply."}}},"ChatReply":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["text","tool_call"]},"text":{"type":"string","description":"Present when type is text."},"tool":{"type":"string","description":"Present when type is tool_call."},"args":{"type":"object","description":"Present when type is tool_call."}}}}}}