| POST | /api/start/spaces | GitHub human session, same-origin browser | Create one private Mainmind-managed space with {name, purpose?, template?, idempotency_key}. template is blank (the default), business, job-hunt or project; it adds a few example pages the space then owns. The same key and details resume the original creation; changed details conflict. The signed-in person becomes its Founder. No GitHub repository or App installation is created. Storage and serving readiness are separate; a retry never silently replaces saved data. | {ok, state, workspace, name, purpose, template, storage, replayed, message?, retryable?, commit?, open_url?}. 200 ready; 202 creating or reading; 409 conflict or recovery required. |
| GET | /api/start/spaces | GitHub human session | Read your original space creation with query creation_key. Rechecks current Founder membership and serving evidence. A creation key is a retry reference, not an access credential. | The bounded creation status for the current person only, or a refusal. Cache disabled. |
| POST | /api/machine-enrollment | sponsor invitation | Claim a pending machine member in the explicit workspace query using an Authorization Bearer enrollment token and JSON {credential}. The runner creates its credential locally; the server stores only digests, rechecks the live sponsor and access policy, and activates the same member atomically. A client registration alone grants no membership. | Credential-free machine activation receipt, or a refusal. An exact retry with the same credential is recoverable only during the original invitation window. |
| GET | /api/tool-gateway/v1/connections | Mainmind lease with connections.use | List only the active Connection aliases, read/write modes and kind granted to this exact lease. api-key and oauth2-refresh Connections also include the non-secret credential_generation used for write-only replacement. The list exposes no origin, token endpoint, descriptor internals or credential values. | {connections:[{alias,modes[],kind,credential_generation?,slots?}]} for the lease's workspace and generation-bound capabilities. |
| GET | HEAD | /api/tool-gateway/v1/connections/:alias/* | Mainmind lease with the exact Connection generation's read capability | Provider-neutral read transport for an installed Connection. The local tool chooses an alias and relative path only. Mainmind selects the immutable server-installed API origin. oauth2-refresh/v1 uses the installed HTTPS token endpoint and injects the sealed OAuth credential and optional tenant binding; api-key/v1 injects the sealed static key into the installed authentication header and does not call a token endpoint. Then it strips caller credentials and forwarding headers, refuses redirects and traversal, bounds both bodies, and blocks exact credential reflection. | The provider status and installed safe response headers, with no lease, provider token, refresh token, client secret, API key or redirect location. |
| POST | PUT | PATCH | DELETE | /api/tool-gateway/v1/connections/:alias/* | Mainmind lease with the exact Connection generation's write capability | Provider-neutral mutation transport for the same installed Connection. Every non-read method requires the generation-specific write grant and a durable run-ledger attempt before network egress. Installation is technical access only; the space's Process and Authority still govern the business act. | The bounded safe provider response. A lost response after egress is recorded as partial and requires provider-state reconciliation. |
| GET | HEAD | /api/tool-gateway/zoho-inventory/* | Mainmind lease with zoho.inventory.read | The read half of the full Zoho Inventory adapter used by the unchanged local CLI. Replaces the lease and every caller organization selector with server-held credentials and the configured Zoho organization, and can reach only its configured Zoho data-centre Inventory API host. | The streamed Zoho response with status and safe response headers. No configured refresh token, access token or organization selector is returned by the gateway. |
| POST | PUT | PATCH | DELETE | /api/tool-gateway/zoho-inventory/* | Mainmind lease with zoho.inventory.write | The mutation half of the same full Zoho Inventory adapter. Forwards JSON or multipart bodies up to 16 MiB plus safe conditional and idempotency headers; requires a durable run-ledger attempt before sending. A write capability is transport access, not business Authority. | The streamed Zoho response with safe headers. The run observes method, path and transport outcome only; resulting provider state requires a fresh read. |
| GET | HEAD | /api/tool-gateway/zoho-desk/* | Mainmind lease with zoho.desk.read | The read half of the full Zoho Desk adapter used by the unchanged local CLI. The CLI points ZOHO_DESK_BASE_URL here and presents the lease as its access token; the gateway swaps in the server-held OAuth token, injects the configured orgId header, and can reach only its configured Zoho data-centre Desk API host. | The streamed Zoho Desk response with status and safe response headers. No configured refresh token, access token or space id is returned by the gateway. |
| POST | PUT | PATCH | DELETE | /api/tool-gateway/zoho-desk/* | Mainmind lease with zoho.desk.write | The mutation half of the same full Zoho Desk adapter — the transport that lets daily-ticket-triage save a verified draft. Forwards JSON or multipart bodies up to 16 MiB plus safe conditional and idempotency headers; requires a durable run-ledger attempt before sending. A write capability is transport access, not business Authority. | The streamed Zoho Desk response with safe headers. The run observes method, path and transport outcome only; resulting provider state requires a fresh read. |
| GET | HEAD | /api/tool-gateway/shopify-admin/* | Mainmind lease with shopify.admin.read | The read half of the full Shopify Admin adapter used by unchanged local tools. The tool points SHOPIFY_STORE_URL here and sends the lease as x-shopify-access-token; the gateway swaps in the server-held static admin token and can only ever reach the configured *.myshopify.com store. No OAuth machinery: nothing is minted, cached or replayed. | The streamed Shopify response with status and safe response headers. The configured admin token and store selection never leave the gateway. |
| POST | /api/tool-gateway/shopify-admin/admin/api/:version/graphql.json | Mainmind lease with shopify.admin.read or shopify.admin.write, chosen by the document | The Admin GraphQL endpoint rides POST, so the gateway classifies the parsed document instead of the method: a query with no mutation or subscription keyword outside strings and comments uses the read capability; anything else — including an unparseable or oversized body — requires write. Over-asking on an ambiguous read refuses safely; the reverse would let a mutation ride the read grant. POST /graphql.json and POST /graphql rewrite to /admin/api/2025-01/graphql.json before that classification, so a documented relative path is not a REST write 404. | As above. |
| POST | /api/tool-gateway/shopify-admin/graphql.json | Mainmind lease with shopify.admin.read or shopify.admin.write, chosen by the document | Short form of the Admin GraphQL endpoint. Rewritten to /admin/api/2025-01/graphql.json, then classified by the document as the versioned route is. | As above. |
| POST | PUT | PATCH | DELETE | /api/tool-gateway/shopify-admin/* | Mainmind lease with shopify.admin.write | The mutation half of the same full Shopify Admin adapter (REST writes, and GraphQL documents the classifier refuses to call reads). Requires a durable run-ledger attempt before sending. A write capability is transport access, not business Authority. | The streamed Shopify response with safe headers. The run observes method, path and transport outcome only; resulting provider state requires a fresh read. |
| GET | HEAD | /api/tool-gateway/amazon-sp/* | Mainmind lease with amazon.sp.read | The read half of the full Amazon Selling Partner API business/data-plane adapter. Replaces the lease with a server-held LWA token or an exact-resource server-held RDT and can reach only the configured regional SP-API host. | The SP-API response with safe status and rate-limit headers. Restricted tokens and provider-signed artifact URLs stay sealed; model-named document fields become narrow Mainmind artifact locators. |
| POST | /api/tool-gateway/amazon-sp/batches/* | Mainmind lease with amazon.sp.read | The batch-read exception: only the exact batch paths named in AMAZON_SP_BATCH_READS use the read capability even though Amazon expresses them as POST. The body is bounded at 128 KiB. | As above. |
| POST | PUT | PATCH | DELETE | /api/tool-gateway/amazon-sp/* | Mainmind lease with amazon.sp.write | The mutation half of the full SP-API business/data-plane adapter, except the named POST batch reads and the provider credential-control plane. Forwards bodies up to 16 MiB and provider-modeled conditional, idempotency and signature headers to the fixed regional host; requires a durable run-ledger attempt before sending. LWA, restricted tokens, signed destinations and Services encryption material stay sealed server-side. Client-secret rotation is refused until Mainmind owns its queue-to-sealed-credential lifecycle. | The safe SP-API response, a managed-token alias, or an opaque Mainmind artifact locator. The run observes method, path and transport outcome only; resulting provider state requires a fresh read. |
| GET | HEAD | PUT | /api/tool-gateway/amazon-artifacts/:handle | one opaque run-bound artifact locator | Transfers exactly one provider-issued Amazon document or upload destination without revealing its signed URL, headers or encryption material. A download accepts GET/HEAD; an upload accepts one raw PUT. The locator rechecks its owning lease, live grant, open run and capability; upstream redirects, private hosts, caller credentials and arbitrary methods are refused. Services uploads are encrypted at the trusted edge. | The streamed artifact response with safe media, range and disposition headers. The opaque locator itself grants no provider API access and expires no later than the provider destination. |
| GET | /api/tool-gateway/knowledge/records | Mainmind tool lease | The named knowledge.records.read adapter. Query: prefix= repeated, each naming one Record Kind such as records/products. Returns the space's own Records as one bundle, so a tool that needs them can run on a lease whose checkout carries no knowledge. GET only, records/<kind> only with no traversal, and filtered by the live knowledge scope of the member the lease belongs to — the same gate read_node uses, recompiled per request rather than read from the lease. It grants no access that member did not already have; only the transport differs. | {version, source, commit, prefixes[], records{path: raw Markdown}} at the serving projection commit. Bounded at 8 prefixes, 2000 Records and 8 MB, and a node projected without content is a 503 — every limit refuses rather than truncating, because a scan handed half a catalogue reports a clean business. |
| POST | /api/tool-credentials | bearer, owner or co-founder session | Place one vendor credential for a workspace. Body: {workspace?, capability, credential}. The provider credential is create-only under its read capability and backs separately leased read/write transport; D1 first reserves the unique workspace/capability pair, then a fresh AES-256 data key encrypts the normalized credential under AES-GCM and the root key wraps that data key with AES-KW before the ciphertext reaches KV. A second placement returns 409; change a placed credential with PUT. There is deliberately NO GET for the value: once placed, a credential is reachable by its adapter and by no surface a person or agent can call. An owner session placing from Setup passes the same create-only gate as the machine bearer. | {ok, workspace, capability, fields[]} naming the fields stored and never their values; {error} when the capability is unknown, the credential malformed, storage unavailable, or a credential is already configured. |
| PUT | /api/tool-credentials | bearer, owner or co-founder session | Replace some of a placed vendor credential's values in place, such as a rotated Amazon client secret. Body: {workspace?, capability, credential, expected_generation}. credential holds only the fields that change; every other stored value is kept, because sealed values cannot be read back to type again. expected_generation is the slot's generation from /api/tool-credentials/status; the generation moves by one, so of two concurrent replacements one wins and the other gets 409. The account fields (Amazon region, Zoho dc and organization_id, Shopify store_domain) must stay the same, so a replacement never points the slot at another account. Access tokens minted from the old values are forgotten, and the next call mints from the new ones. A slot whose first placement never sealed takes the whole credential, and so does a second replacement within five minutes of the last, because KV may still return the values it replaced (409 otherwise). | {ok, workspace, capability, generation, fields[]} naming the fields replaced and never their values; {error} when a field is unknown, an account field differs, the generation moved, or storage is unavailable. |
| POST | /api/tool-credentials/amazon/sign-in | owner or co-founder session | Start Sign in with Amazon for the built-in Amazon Selling Partner account, through Amazon's SP-API website authorization workflow. Body: {workspace?, store, app_id, client_id?, client_secret?}. store is one of the Seller Central stores (us, ca, mx, br, uk, de, fr, it, es, nl, se, pl, be, tr, ae, sa, eg, in, jp, au, sg) and sets the region; app_id is the app's amzn1.sp.solution. ID. client_id and client_secret are required when no Amazon account is stored; when one is, a missing one is taken from it server-side and never returned, and a store in another region is refused. Parks a single-use state for 15 minutes, bound to this browser session, with the app's values sealed and the account's current generation recorded; expired states are deleted. A stored account that cannot be opened refuses with 503. A browser session only: Amazon returns to this browser to finish. | {authorize_url, redirect_uri, login_uri}: the Seller Central consent address to send the browser to, and the two addresses the app in Seller Central must list as its Redirect URI and Login URI. {error} in plain words otherwise; never a value. |
| GET | /api/tool-credentials/status | bearer, owner or co-founder session | Which provider-credential slots exist for a workspace and what state each is in — presence only, so Setup can show the vault without weakening it. placed is the registry reservation; sealed says whether the ciphertext is actually present (true), missing after a lost placement (false), or unverifiable because KV was unreachable (null) — a reservation is never presented as a credential. registered_at is when the registry first recorded the slot, which for credentials placed before the registry existed is the backfill date, and placed_by names the actor a browser or bearer placement recorded (null for backfilled rows). options carries the closed server-side enums a form may offer, account_fields the fields a replacement must keep, and generation, replaced_at and replaced_by the last in-place replacement (generation 0 and nulls when never replaced). The Amazon slot also carries client_secret_due_at: Amazon expires an app's client secret 180 days after it is made, so this is 180 days from when the secret was saved here, the latest it could be due; a replacement that changes client_secret restarts it. Also lists the workspace's active installed Connections by alias. The sealed values themselves remain unreadable on every surface; this route returns no credential material, no field values and no provider hosts — the sealed check reads KV key names only. | {workspace, registry_complete, credentials:[{capability, write_capability, provider, fields[], secret_fields[], account_fields[], options{}, placed, sealed, registered_at, placed_by, generation, replaced_at, replaced_by, client_secret_due_at?}], connections:[{alias, created_at}]}. |
| POST | /api/tool-connections | bearer, owner or co-founder session | Install one create-only provider-neutral Connection for a workspace. Body: {workspace?, alias, profile, credential?}. Omit credential to install the Connection pending: the response carries a single-use placement_url (seven days) at which a Founder or co-founder of the space pastes the values in a browser after seeing the exact destination; GET /place/<key> shows that page and POST /place/<key> seals the values, flips the Connection active and burns the key. alias is kebab-case (google-ads); underscores are refused. profile must name schema mainmind.connection-profile/v1, a lowercase id, and driver api-key/v1, sign-in/v1 or oauth2-refresh/v1. oauth2-refresh/v1 may declare slots, which without lease_env must each fill a request header through header_slots; secret-slots/v1 and inject_access_token are refused, because no tool lease carries a Connection's values. A 400 names the failing field and the accepted shape, points at https://mainmind.app/docs/mcp-tools#install-provider-connection, and never echoes credential values. This trusted control-plane act binds api-key/v1 (a fixed HTTPS API origin and authentication header, with a server-held api_key), sign-in/v1 (a login URL on the API origin and the sealed login values) or oauth2-refresh/v1 (HTTPS token URL, API origin, relative API root, header policy, body bounds, optional header_slots and lease_env) together with the sealed credential into immutable generations. A checked-out tool and its Git-authored code cannot alter these trust anchors, and there is deliberately no credential GET. | {ok, workspace, alias, profile_id, profile_generation, connection_generation, credential_generation, modes, fields, kind} with field names only and no credential values; without credential {ok, status: pending, fields, destination, placement_url, placement_expires_at}; 409 when an active alias, or an unexpired pending one, already exists; 400 names the failing profile or credential field and the accepted shape. |
| POST | /api/tool-connections/:alias/place | bearer, owner or co-founder session | Place the values on a pending Connection by alias, for a signed-in Founder or co-founder who does not need the placement link. Body: {workspace?, credential}. The same refusal and sealing path as the placement page; the Connection becomes active and its placement link stops working. | {ok, alias, status: active, credential_generation, fields, modes}; 404 when the alias is not pending; 410 when it is pending but its placement week has passed, naming that reason (install again under the same alias for a fresh link); 409 when an active Connection took the alias meanwhile; 400 names the failing credential field. |
| DELETE | /api/tool-connections/:alias | bearer, owner or co-founder session | Revoke one active or pending Connection. The sealed values are never read; the row stays as the audit record and the alias is free for a fresh install. | {ok, alias, previous_status, revoked_at}; 404 when no active or pending Connection carries the alias. |
| PUT | /api/tool-connections | bearer, owner or co-founder session | Replace an api-key/v1 or oauth2-refresh/v1 Connection credential write-only. Body: {workspace?, alias, expected_credential_generation, credential}; credential is {api_key} for api-key/v1 and the full install credential (client_id, client_secret, refresh_token, tenant_id when bound, every declared slot) for oauth2-refresh/v1. alias is kebab-case. The installed destination and profile stay immutable. A 400 names the failing field and the accepted shape and points at https://mainmind.app/docs/mcp-tools#replace-provider-connection-credential. A stale expected_credential_generation returns 409. oauth2-refresh replacement reaches this route only through the deployment bearer, a Founder owner session or a co-founder session; a bound tenant_id must be the installed one. Old Connection permits cannot use the replacement. No credential GET exists. | {ok, alias, kind, credential_generation, fields, ...} with generation locators only; no old or new values. |
| GET | /api/access | bearer | The access requests waiting on you, newest first. The founder's own surface. | {requests[]} with created_at, email, note, source and status. |
| GET | /api/oracle/voice | member browser session | Same-origin WebSocket upgrade for one live voice call. Query: workspace, random session_id, optional conversation_id and previous_session_id. The edge resolves the linked GitHub member and immutable epoch, then derives an internal session object name that browser input cannot retarget. Microphone PCM streams continuously, including pauses, to Cloudflare Flux while the call is enabled. Flux detects turns and interruptions. Each transcript uses the same bounded conversational agent and OrganizationRuntime ledger as typed Oracle; the complete answer is committed, reread and source-validated before sentence and Aura PCM streaming begins. One active call per member epoch, thirty call admissions per rolling hour and a ten-minute call limit. Mainmind saves no raw or generated audio and disables the SDK transcript store. | Voice protocol JSON, interim and final transcripts, private conversation snapshots and raw 16 kHz PCM audio on the initiating WebSocket. No cross-connection broadcast. Interrupting cancels active answer and speech work. |
| POST | /api/oracle/voice | member browser session | Legacy same-origin JSON speech route. operation=transcribe takes request_id, base64 audio (at most 750000 decoded bytes), mime_type and workspace; operation=speak takes request_id, conversation_id and turn_id. The browser uses speak only for an explicit Read aloud replay, which never opens the microphone. Cloudflare Whisper transcribes; Cloudflare Aura speaks a bounded excerpt of a current owned sourced answer. No arbitrary client text is synthesized. Admission is limited per live membership to thirty recordings and sixty playbacks per rolling hour. Mainmind does not persist raw or generated audio. No automatic audio retry; access/source/deletion checks run before return. | Transcription {text}, private audio/mpeg, or an explicit error. A transcription does not itself submit a durable question or authorize work. |
| GET | /api/oracle | member browser session | Read private durable conversations for the selected space and live membership. Query: conversation_id?. Source-bearing history is withheld when current access or source revisions change. | {conversations,conversation,capabilities,notice,retention_days}. An active typed turn survives browser reconnection. Live microphone capture uses the WebSocket voice route; explicit Read aloud replay uses its bounded POST compatibility route. |
| POST | /api/oracle | member browser session | Same-origin JSON. Operations: create, send, cancel, delete, action, feedback, metrics. Stable request_id for create/send/action. Workers AI can answer conversationally or choose scoped search, read_node, find_process, waiting_questions and read_question tools, using bounded private history. Its one write is record_answer: the Founder's own answer to a waiting ask, through decide, where the words are the Founder's message in that turn and the server records only a yes, no or settled that message plainly says (or a changes note), one per turn, for an ask looked up that turn. Explicit source-bound comments/requests use existing working state; no canonical policy or human ruling is executed. Conversation-only retention: 30 days. Feedback follows the existing private issue lifecycle. Metrics are content-free and Founder-only. | Conversation, accepted turn, verified working-state result or feedback receipt. No caller-supplied actor, role or workspace retargeting. |
| POST | /api/knowledge-answer | bearer or session | Explicitly answer a question using bounded authorized source excerpts through Workers AI. Body: query, context?, workspace?. Existing search and guide do not send source bytes for inference. Does not save, queue or execute work. | {hits,guide,answer,serving}. Answer labels source summaries/inferences, citations, gaps and conflicts; no invented source links. Invalid/unavailable inference retains sources. Final membership and source revision are rechecked. |
| GET | /api/boot | member credential | Start as this member. Query: workspace?, session_kind?, seat?, full?. A seat that resolves returns the same summary MCP boot does (the seat first, AUTHORITY.md inline up to 6 KB and the other entry documents named, each space-wide list at its count and first three, and summary); full=true returns it whole. The same organizationBoot as MCP boot: identity, entry documents, attention, Process count, recorded team, your_seat, colleagues, and bounded restore (inbox, open_asks with open_asks_complete beside it, open_runs locators; never a control_key), with open_asks_total and open_runs_total naming how many of each are open for this caller, so a truncated list says what it was cut from; each is null, and restore_error true, when the restore read failed, since a zero there would be a count nothing took, while a queue that was read and is empty is still 0. An open ask carries changes_note and changes_at for the one member its name resolves to, or changes_notice when the caller's own display name is shared and the note is withheld, exactly as MCP boot discloses them. A Founder, and a co-founder who can already open that ask at /d/, receives becomes on each open_asks item; other roles keep the locator fields only. An open ask a Founder reopened with decide changes also carries changes_note and changes_at for the member who raised it, whatever their role, so a proposer who cannot open /d/ still reads the note; in boot restore the note is disclosed only where the ask's asked_by name resolves, across every member this workspace has ever had — revoked and not-yet-redeemed rows included — to exactly one member who is this live caller and who holds the ask's knowledge scope when it has one. A name two members share, and a name the server itself defaulted, disclose nothing to anyone, including the member who really raised the ask. A missing team, or an index with no frontmatter team declaration, is a restore gap under Needs attention, with the schema at https://mainmind.app/docs/agent-team.md. An index with agent-team: [] is a complete, deliberately empty team and returns declared_empty: true; an undeclared empty index names the frontmatter Role-path dialect rather than implying seats from a body table. feedback_notices lists this member's receipts that moved to shipped, closed, or answered since they last saw that change, once, and, with change replied and new_replies, up to five reports this member filed, answered or opened that have unread messages from the builders or other agents, until the conversation is opened. The deployment bearer is refused. A malformed workspace slug is rejected, never defaulted onto another space. | {text,organization_state,reasons,process_paths,process_commands,team,your_seat,colleagues,summary?,inbox,open_asks,open_asks_more,open_asks_complete,open_asks_total,open_runs,open_runs_more,open_runs_total,restore_error,to_recover?,feedback_notices,feedback_notices_more,identity{member,name,role,workspace}}. to_recover is present only when work addressed to this member has a lapsed hold, as MCP boot returns it. Ready spaces also receive ORG.md and the entry documents ORG.md declares always-read, in text. process_commands names the caller's in-scope seat Processes and each portable CLI, or explicit absence. colleagues is the ordinary directory of non-revoked live members without invite codes, credentials, credential presence or knowledge-scope grants; optional live harness from a recorded start is omitted when unknown. Open asks come back oldest filed first, so what the openAsks bound drops is the newest. open_asks locators are key, ask, url, created_at, asked_by; a Founder and a co-founder who can already see that ask at /d/ also receive becomes. An open ask a Founder reopened with decide changes also carries changes_note and changes_at for the member who raised it, whatever their role, so a proposer who cannot open /d/ still reads the note; in boot restore the note is disclosed only where the ask's asked_by name resolves, across every member this workspace has ever had — revoked and not-yet-redeemed rows included — to exactly one member who is this live caller and who holds the ask's knowledge scope when it has one. A name two members share, and a name the server itself defaulted, disclose nothing to anyone, including the member who really raised the ask; a caller whose own display name is that shared name gets changes_notice on the item instead, with no byte of the note. |
| GET | /api/page-work | member credential | Read bounded queued work or reported outcomes on accessible pages. Query: status?,kind?,limit?,cursor?,inbox?,for_agent?,workspace?. Same listPageWork as MCP page_work. The deployment bearer is refused. | {items,next_cursor,total,notice}; summaries and stable identities only, no full pages. total is the size of the inbox this page was cut from, or null wherever no list is held whole to measure it: a discovery listing without inbox and a work Kind gap. Exactly as the MCP tool reports it. |
| GET | /api/decisions | member credential | Where this member's questions to the person stand. Query: key?, workspace?. Same read as MCP decisions: without key, every question this member asked that still waits on the person and every answer from the last 7 days, each with the next step; with key, that one decision (its own, or one the person behind the credential may open on its decision page; a bundle part answers for its bundle). The deployment bearer is refused. | Without key: {waiting:{total,items}, answered:{total,since,kept,items}, person, text}. With key: {decision, text}; decision.state is waiting, changes, answered or went_ahead. 400 for a malformed key, 404 for a decision this credential may not see, 503 when it could not be read; nothing changes either way. |
| GET | /api/work-context | member credential | Read one existing page request, its execution attempt, portable checkpoint and linked discussion. Query: path, id (canonical asg-... or legacy positive integer), cursor?, workspace?. Same workContext as MCP work_context. The deployment bearer is refused. | {item,execution,context_refs,current_source_commit,source_access_scope} with saved progress, current-access reference reconciliation and discussion. source_access_scope is the already-authorized current page's compartment, or null when unclassified. |
| POST | /api/work-session | member credential (teammate+) | Claim, checkpoint or hand off an addressed request. Body: {path,id (canonical asg-... or legacy positive integer),action,version,source_commit,idempotency_key,run_id?,control_key?,attempt_id?,checkpoint?,outcome?,reconciliation?,workspace?}. Same workSession as MCP work_session; execution actions need an owned run_id and control_key from run_start. The deployment bearer is refused and is not a start identity. | {item,execution,context_refs,current_source_commit,replayed?}. |
| GET | /api/search | bearer or session | Search the space in plain language. Query: workspace?, q, kind?, guided=1?, context? (bounded trail of previously submitted user questions). Questions and context are limited to 300 characters each. Ordinary search uses hybrid retrieval. Guided search uses Workers AI only to refine the submitted question once; document ranking and exact excerpts stay inside Mainmind. Supports title/topic and recent-document exploration, with current workspace, projection and live member access checked before returning results. Read-only; it cannot change knowledge or start agent work. | {hits[{path, kind, title, status, snippet, updated_at?, document_date?}], serving, degraded?, guide?}. guided=1 adds guide:{status:ready|unavailable|no_match, summary, sources:[{path,title,quote}], suggestions:[string], query}. Quotes are literal passages from accessible documents; the summary is an orientation, not model-written business advice. No document content or metadata is supplied to the guide model. Unavailable refinement preserves original-query retrieval and authorized literal passages, with guide.refinement=unavailable when a source guide is available. Incidental matches and absent pages are not represented as an answer. Ordinary degraded lexical search remains identified. |
| GET | /api/node | bearer or session | Read one projected knowledge item for the owner app. Query: workspace?, path. Freshness is proved before content is returned. A declared decision shape is rendered by the same canonical renderer used on the ruling page, and the same derived plain-language view the explain_node tool serves rides along so the app leads with it. | One node with path, kind, title, status, excerpt, frontmatter, content, source_commit, access_scope, write_class, optional inert diagram_html and optional human_view; 404 with suggestions when absent. |
| GET | /api/page-collaboration | member or owner session | Read ordinary page collaboration under live page access and freshness. Query: workspace?, path, cursor?. Working state, never authority. | {items,next_cursor,source_commit,can_write,notice} |
| POST | /api/page-collaboration | teammate+ session | Create comment/task/request. Body: {path,kind,body,idempotency_key,source_commit?,block_id?,origin_id?}. Identity is session supplied; workspace cannot differ from the page. No automatic execution. | {item,replayed?,notice} |
| POST | /api/page-collaboration/:id/update | teammate+ session | Update task/request with optimistic concurrency. Body: {path,version,status,outcome?}. Completion needs an outcome; requests are explicitly claimed first. Cross-page ids refuse. | {item,notice} |
| GET | /api/notes | bearer or session | Open notes against the standing knowledge. Query: workspace?, path?; the machine bearer may additionally filter status for operational review, while browser sessions remain open-only. Never served by any read path an agent uses. Proves projection freshness first, as list_notes does; a stale or building projection is a named refusal. | {notes[]} with kind, claim, quote, who left it and when; a browser session also receives mine for its own notes. |
| POST | /api/notes | bearer or teammate+ session | Record a note. Browser body: {workspace?, path, kind, claim, quote?}; session identity supplies the author and live knowledge access. Bearer callers retain {run_id?, by_member?, by_label?}. Changes no document. | {ok, path, kind}, or {error} naming the six permitted kinds. |
| POST | /api/notes/:id/retract | bearer or teammate+ session | Withdraw a note. Browser body: {workspace?}; session identity supplies the author and may withdraw only its own attributable note. Bearer body may carry member for the existing machine contract. A ruling settles the rest. | {ok, id}, or {error} when the note belongs to somebody else, has no attributable author, or is already settled. |
| GET | /api/feedback | bearer | Product feedback left by connected agents. Query: workspace?, status?, limit?. Read at the start of every improvement pass; never served by any knowledge read path. | {workspace, feedback[]} with kind, tool, message, who left it and when. |
| POST | /api/feedback | bearer | Record product feedback from a runner and automatically open or link a private issue in Mainmind's product repository. Body: {workspace?, kind, message, tool?, run_id?}. The D1 receipt commits first; GitHub delivery has a six-second deadline and failure cannot discard it. Bounded at 12 reports per connected member and 50 per workspace in a rolling hour. | {ok, id, kind, github{status, attempted_at, issue_number?, issue_url?, reason?}}, or {error} naming the four permitted kinds, refusing credential-like text, or naming the rate limit. |
| POST | /api/feedback/:id/close | bearer | Mark one feedback item handled. Body: {workspace?, reason?}. The reason survives on the row, so a later pass can see what was already answered. | {ok, id}, or {error} when it is already closed or not found. |
| POST | /api/feedback/:id/sync | bearer | Retry private GitHub issue delivery for one already-durable feedback receipt. Body: {workspace?}. Exact repeated reports link to the first still-open issue rather than opening another. | {status, issue_number?, issue_url?, reason?}; missing when the receipt is not in that workspace. |
| GET | /api/strain | bearer or session | What the knowledge is struggling with: contested documents, Lessons waiting on review, documents with a decision pending, and ledger reach over 28 days on those paths. Query: workspace?. Proves projection freshness first, as under_strain does; a stale or building projection is a named refusal. | {workspace, contested[{path, notes, kinds, latest, oldest, reach{runs,asks,notes}}], waiting{receivers[{path, lessons, oldest, titles, reach}], unresolved[], total}, proposed[{path, key, ask, reach}], reach{window_days, since, exposure, areas[{path,runs,asks,notes}], by_path}}. |
| GET | /api/why | bearer or session | Why is this the way it is. Query: path= or key=, plus workspace?. Walks the recorded chain and never guesses. | {workspace, subject, chain[], complete, note}. Each chain step carries when, who, what and a citation. note says plainly what is missing when the chain is incomplete. |
| GET | /api/links | bearer or session | What else in this space's knowledge points at one document. Query: path=, plus workspace?. Derived at read time from the serving projection, never a stored index, so it cannot go stale against the document it describes. | {workspace, path, links[{path, kind, title, excerpt, doc_class, how}], truncated}. how is the relation: targets, applies-to or source-process when the frontmatter names this document; process, skill or source-process when this is a skill and a page names it by its name; links when the prose does. truncated says the bounded candidate scan stopped before the end rather than presenting a clipped list as the whole. |
| GET | /api/page-history | bearer or session | A page's earlier versions, as page_history reads them. Query: path=, version? (one listed version), plus workspace?. Readable by whoever can read the page; an earlier version only when it declares a part of the space the reader can open. | Without version: {path, exists, versions[{commit, change, at, author, start_unknown, summary?, decision?, member?}], more}, newest first. With version: {path, exists, version, content, change, before_unknown, same_as_now}; content is the whole page at that version and change its line view. 503 {error, still_reading} while the space's history is being read for the first time. |
| POST | /api/page-history | teammate+ session, same-origin browser | Put an earlier version of a page back. Body: {workspace?, path, version}. Never writes the page: it opens an ordinary proposal carrying that version's whole text, on a run of the signed-in person's own, ruled through the usual yes. | {ok, proposed, decision_key, decision_url} when the proposal waits for a yes; {ok, same} when the page already reads as that version and nothing was proposed; otherwise {error, detail?} with 403 or 422 for a refusal a retry will not change, 503 with still_reading, or 409 for one worth retrying. |
| GET | /api/skills | bearer or session | Each skill the reader may open, as a person reads it: what it does, how often work follows it, how fresh it is, and its tags. An old processes/ page with a skills/<name>/SKILL.md of the same name is listed once, as the skill, and its runs, changes and lessons count for it. Query: path? for one skill and its timeline, plus workspace?. Skills, Decisions and Lessons pass the knowledge gate; runs pass the list_runs gate. | {workspace, weeks, skills[{path, name, title, description, state, origin: yours|starter (starter while a "Start with" example, its state example, in metadata or at the top of the page), tags[] (the skill's own metadata.tags, lower case), last_used{at, by}|null, weekly_runs[8, oldest first], runs_in_weeks, last_changed{at, title, by: decision|file}, lessons{waiting (pending Lessons naming it), absorbed}, runs_awaiting_decision, agents[{name, runs}] (who ran it in the weeks, most first, at most 5), author (who asked for the approved change that created it, a person or an agent; null when it came with the space)}], tags[{tag, count}] most used first, truncated, runs_truncated, last_used_truncated, decisions_truncated, lessons_truncated, authors_truncated}. With path=: {workspace, weeks, skill, waiting{lessons[{title, path, since}], runs[{who, since}]} (at most 10 each), decisions{items[{at, title, path}] (the 5 newest that changed it), total}, timeline[{type: run|change|lesson, at, who?, status?, title?, path?}] newest first and at most 30, timeline_truncated and the same five flags}; 404 when the reader cannot open that skill. Each *_truncated flag says a bounded read stopped early: counts are a floor, last_used may be unknown, and a change or lesson may be missing. |
| GET | /api/home-activity | bearer or session; agents Founder-only | Latest saved objects on Home. Query: workspace?, section=knowledge|agents, limit? (1–12), after? (knowledge only). Knowledge uses ordinary current knowledge access and complete projection freshness. Both publication-observation scope and current object scope are checked before pagination. Skills and legacy Process aliases appear once. First complete baseline and changed source bindings produce no backfilled dates. Agents reads active authorized machine membership and profile owner availability without Git, and says registration, never running. No new read authority. | {workspace, items[] {id, kind: skill|lesson|decision|agent, action: added|updated|recorded, title, occurred_at, timestamp_basis: publication_observed|registration, source: {path?, member?, version?, blob_sha?}, state: saved|pending|proposed|recorded}, checked_at, next_cursor, partial: false, has_saved_content? (knowledge), has_agents? (agents), coverage?}. has_saved_content checks current accessible eligible objects even when their initial baseline dates are unknown; has_agents checks active authorized machines even when their registration date is unavailable. An empty recent feed alone never proves a new space. Verified explicit titles only. Pending Lessons are saved awaiting incorporation; Decisions are recorded, never claimed applied. Knowledge dates are verified content changes observed at publication, never authored dates. A stale cursor returns 409 with cursor_stale; unavailable freshness or changed authorization returns no items. Cache disabled. |
| GET | /api/done | GitHub human session | Done for you: the schedule changes agents made on their own in this space, for its owner or a co-owner. Query: workspace? (needed when the person holds more than one). | {on, default_on, always_on, can_change, today, items[] {id, agent, action (add, pause, remove, default or always), line, at, undone}, standing?[] {id, line, at}}. standing is the owner's live standing yeses, sent to the owner only. An id of ask-<key> is an ask the 7-day default or a standing yes approved. The last day, plus anything not undone from the last week, newest first, at most 20. today counts the last day's changes not undone. 401 with no session, 403 for anyone but the owner or a co-owner. |
| POST | /api/done/:id/undo | GitHub human session, same-origin browser | Undo one schedule change an agent made on its own. Writes the inverse with the same compare-and-swap, attributed to the signed-in owner; the owner only. | {ok, id, line}. 401 with no session, 403 from another origin or for a co-owner, 404 when this person may not see it, 409 when it was already undone, a later change to the same schedule is still in place (undo that first), or the file changed since. |
| POST | /api/done/setting | GitHub human session, same-origin browser | Turn one owner switch on or off for this space; the owner only. Each is off by default. Body: {on: true|false, setting?: just_done|default|always}. No setting means Just done; default is "If I don't answer in 7 days, go with the recommendation" and always offers Always after two yeses to the same change. Query: workspace?. | {ok, on, setting}. 401 with no session, 403 from another origin or for anyone but the owner, 400 without a true or false or for any other setting. |
| POST | /api/done/ask-:key/undo | GitHub human session, same-origin browser | Undo an ask the 7-day default or a standing yes approved: a reverting ask with the inverse change, ruled yes by the signed-in owner and landed with the same per-file compare-and-swap. The owner only. | {ok, id, line}. 401 with no session, 403 from another origin or for a co-owner, 404 when this person may not see it, 409 when it was already undone, is still being carried out, or a file changed since. |
| POST | /api/done/standing/:id/revoke | GitHub human session, same-origin browser | Take back a standing yes. The owner only; it stops matching at once. Query: workspace?. | {ok, id}. 401 with no session, 403 from another origin or for anyone but the owner, 404 when there is no live standing yes with that id. |
| GET | /api/space/export | owner session | Take everything with you: the whole space as one ZIP of plain files. Query: workspace?. Read through the same freshness fence and owner check as an agent's home; a member session gets 403 and a machine bearer is refused with 403. Every row read is bound to this space. | A streamed application/zip attachment named <space>-mainmind.zip: knowledge/<path> for every page (agents/ included), files/<upload_id>/<filename> for each saved original, work.json with open and recent work (path, kind, status, title, body, asked_by, assignee, outcome, timestamps) and a README.md. Never a password, connected-account key, token or idempotency key. |
| GET | /api/space/github-copy | owner session | Copy to GitHub: whether this space keeps a copy in a GitHub repository, where, and how the last copy went. Query: workspace?, choices?: 1 also lists the private repositories the signed-in owner could pick. A member session or machine bearer gets 403. | {workspace, storage: mainmind|github, copy: {status: off|waiting|current|behind|connected, repository?, branch?, github_head?, copied_at?, error?, retry_at?}, choices?[] {repository, installation_id, repository_id}, install_url?, choices_error?}. No token. |
| POST | /api/space/github-copy | owner session | Turn Copy to GitHub on or off for a space that lives in Mainmind. Query: workspace?. Body: {repository: "owner/name"} to start, or {off: true} to stop. The repository must be private, pushable by the owner through the Mainmind GitHub App, and unused by any other space. The copy is one-way: Mainmind pushes its main as a fast-forward after every change, in the background, and never pulls from or forces GitHub. | {ok, copy} on start (the first copy runs in the background), or {ok, copy: {status: off}, left_on_github?} on stop; GitHub is left as it was. 403 for anyone but the owner, 409 for a refused repository or a space that started from GitHub. |
| GET | /api/team | bearer or session | Who is in one space. Query: workspace?, view?: roster. The roster view reads live membership and authenticated agent contact without waiting for canonical knowledge; context_pending is true and knowledge permissions are unverified. The full view adds freshness-checked Role and work context. Both reads have an eight-second deadline. A browser session is locked to spaces owned by its signed-in Founder. | {workspace, members[], available_scopes, scope_enforced, scope_source, scope_policy, policy_error}. scope_policy is the exact review snapshot for a browser access change; members carry slug, name, role, charter, knowledge scopes, status and activity timestamps; directory carries authenticated presence. The roster view omits knowledge scopes and sets policy_error until the full view verifies context. No credentials. |
| GET | /api/team/:slug/home | bearer or session | One agent's home for the Team screen: what it remembers, where it stopped, the apps it has used and its schedule. Founders only; anyone else gets 403. 404 for an unknown slug or a member that is not an agent. | {agent, name, home: present|missing|not_projected, instructions_summary, boundaries[], remembers[] {name, description, memory_kind, updated, from}, stopped {date, app, summary, next[], open_questions[], unresolved_effects[]} or null, journal_count, apps[] {harness, first_seen, last_contact}, description, avatar {style, seed} or null, skills[] {name, description}, plugins[] {id}, routines[] {id, when, timezone, do, why, state, source: host|approved, last_run {slot, harness, state} or null}, source_commit}; a built-in agent adds builtin (librarian|toolsmith) and outcomes {versions[] {version, since, proposed, approved, rejected, waiting, undone} newest version first, truncated}, how its proposals went under each version of its instructions. source host was recorded from an app and is not an approval. No credentials. |
| GET | /api/team/:slug/work | owner session | One agent's work for its page: what it needs from the person, its checklist with who asked, and its routines. Founders only; anyone else gets 403. 404 for an unknown slug or a member that is not an active agent. | The agent's open questions, checklist and routines, with source_commit. No credentials. |
| POST | /api/team/:slug/answer | owner session | Answer what an agent is waiting on. Body: {question, answer, idempotency_key}. The answer is an ordinary comment on the agent's page, marked with the question. Founders only. | The recorded comment receipt, or a refusal. |
| POST | /api/team/invite | bearer or owner session | Invite a co-founder, teammate, or viewer from the owner app. Body: {name, role, charter?, knowledge_scopes?, scope_policy}; workspace is locked to the space on screen. Co-founder primary focus is optional and knowledge access is every live non-Founder scope; teammate and viewer carry a required charter and selected knowledge. An owner-session co-founder grant must match the exact policy snapshot the authenticated Founder reviewed. A machine bearer may invite only a teammate or viewer; its co-founder request is refused before mutation. | The member and a browser Team URL, or HTTP 403 with confirmation_required, next_action and team_url for a machine-bearer co-founder request. No GitHub credential crosses this response. |
| GET | /api/team/requests | owner session | People who reach this space's repository on GitHub and have asked to be recorded here. Repository access is evidence the asker is a real collaborator, never authority: nothing is granted until the Founder answers. | {workspace, requests[]} with each request's GitHub login, the repository that proved the ask was real, an optional note, when it was asked, and member_status (null, or the roster's status for an account this space already recorded or revoked — such a request cannot be accepted, only declined). No codes and no credentials. |
| POST | /api/team/requests/:id/approve | owner session | Answer a waiting access request with an ordinary invitation. Body is the same as /api/team/invite — {role, charter?, knowledge_scopes?, scope_policy} — so the role, knowledge boundary and reviewed policy snapshot all come from the Founder's Team form, never from the request. The request records which invitation answered it. | The new member, bound to the requester's GitHub login (bound_login, status active) so they connect with GitHub at once and no code changes hands; when that binding cannot complete, bound_login is null, bind_error says why and the invitation stays for its link. Plus request_settled. The answer is taken before the invitation is minted, so a second answer to the same request is refused with 409 and creates nothing. A request from an account this space already recorded or revoked is refused with 409 and the reason, whatever the queue showed. |
| POST | /api/team/requests/:id/decline | owner session | Close a waiting access request without inviting anybody. Nothing about their repository access on GitHub changes. | {ok, id, status}. A second answer to the same request changes nothing. |
| GET | /api/team/actions/:key | owner session | Review an unexpired team-access proposal queued by MCP. The signed-in Founder and space must match the proposal. | The proposed invite or revocation, requester and expiry. No credential. |
| POST | /api/team/actions/:key/confirm | owner session | Confirm an unexpired MCP team-access proposal. The access change, audit receipt and one-time action consumption commit atomically. | For an invite, the new member, seven-day invite expiry, and the one-time invite code the Team screen turns into a /join link, which grants nothing until the invited person signs in with GitHub; for a revocation, {ok, slug}. No MCP or machine bearer may call this route. |
| GET | /api/team/:slug/invite-code | owner session | Reveal one unused and unexpired invitation in the authenticated Team screen. Machine bearers and MCP cannot use this route. | {slug, name, invite_code, invite_expires_at}, only while status is invited and before the seven-day expiry; unavailable after use, cancellation, or expiry. |
| POST | /api/team/:slug/update | bearer or owner session | Update a non-Founder member's role, charter and knowledge access inside the space on screen, with the change and audit committed atomically. Body: {role: cofounder|teammate|viewer, charter?, knowledge_scopes?}. Co-founder focus is optional and access automatically follows every live non-Founder scope; teammate and viewer require both responsibility and selected knowledge. An authenticated owner session may submit the exact co-founder grant shown in Team; a machine-bearer co-founder request is refused before mutation. | The current credential-free member record, or HTTP 403 with confirmation_required, next_action and team_url for a machine-bearer co-founder request. Current connections use the live role, charter and scopes on their next operation; a host may need to refresh or reconnect before newly available tools appear. |
| POST | /api/team/:slug/revoke | bearer or owner session | Revoke a non-Founder teammate inside the space on screen and audit the access change. | {ok, slug}; future authorization and current tool calls are denied. The repository and other spaces are untouched. |
| GET | /oauth/amazon/login | the session that started Sign in with Amazon | The Login URI of the owner's app in Seller Central. Amazon opens it with amazon_callback_uri, amazon_state, selling_partner_id and state. The state must be unused, unexpired and started by this browser's session, and amazon_callback_uri must be https on a Seller Central host with no login or port. | 302 to amazon_callback_uri with redirect_uri, amazon_state and state; otherwise a plain page that echoes nothing. |
| GET | /oauth/amazon/callback | the session that started Sign in with Amazon | The Redirect URI of the owner's app in Seller Central. Amazon opens it with state, selling_partner_id and spapi_oauth_code (or error). The state is spent exactly once and its sealed values emptied in the same write; the code is exchanged (10-second timeout) at Amazon's token endpoint with the sealed client ID and secret, and the refresh token is stored as the built-in Amazon account, placed when none was stored at the start and otherwise replaced from the generation recorded at the start (an account changed meanwhile is left as it is, with a page saying to start again); a replacement forgets cached access and restricted-data tokens. | An HTML page: Amazon is connected, or what went wrong and what to do. Never the code, a token or a secret. |
| GET | /place/:key | GitHub human session | The placement page for a Connection an agent installed without its values. Authentication happens before the key is looked up; the key resolves only for a live Founder of the pending Connection's space, or a co-founder there, and only while unexpired and unused. The page shows who asked when an actor name was recorded, the alias, the driver and the exact origin and header the values will be sent to, then one write-only input per field. Signed out: the same sign-in page the decision link shows. Anyone else, and any used, expired or invented key: one 404 that discloses nothing. | An HTML page with a nonce-bound progress script and form-action 'self'. No value is ever rendered. |
| POST | /place/:key | GitHub human session | Place the values. Accepted only from a form this origin served (Origin must equal the page origin), for the same person the GET admits. The values pass the same refusal and sealing path as an install with values; one conditional update flips the Connection active, mints its credential generation, records who placed and burns the key. A refusal re-renders the form without echoing anything typed. | An HTML page naming the alias as active, with no form. A spent or expired key answers 404. |
| GET | /api/source-context | owner/member session or source-only capability | Read bounded extracted context for record, with optional query and cursor. export=1 returns the exact authorized manifest and original fingerprint for offline export. | Source identity, extraction coverage, located excerpts and next_cursor; pending/failed extraction never claims completion. |
| GET | /api/source-files | Founder session or source-only capability | Read the bounded raw-source upload contract for the space on screen: accepted document/image extensions, maximum bytes, source-document types, and the live access-scope vocabulary. Returns no file bytes. | {max_bytes, accepted_extensions[], doc_types[], available_scopes[]}. |
| POST | /api/source-files | Founder session or source-only capability | Save a multipart original or JSON begin/append/finalize operation. Folder copies use import_begin with a reviewed plan, import_file with import_id/path, import_status and import_finish. Plans bind up to 100 files/100 MiB to the current space, member instance and scope. Existing uploads retain originals; a saved inventory maps relative paths to their source records. No synchronization or instruction execution occurs. | Saved multipart uploads return HTTP 201; JSON operations return HTTP 200. File receipts include upload_id, saved, path, resource_uri, sha256, size, commit, storage and extraction. Import status includes plan, files, originals_saved, total, extraction_ready and inventory. An uncertain save remains distinct; retries retain its operation. |
| GET | /api/source-file | owner/member session or source-only capability | Read one retained original by its source-doc register path. Query: workspace?, record. The register is read through the normal freshness and live knowledge-scope gate, then the private R2 receipt is matched to that exact manifest and original bytes are fingerprint-checked. Legacy in-Git originals retain their exact-commit GitHub reader. | The original bytes with registered MIME type, safe inline/attachment disposition, no-store caching, nosniff, sandbox, and a SHA-256 ETag; 304 only for that exact fingerprint. |