Cotal
DocsAPI Reference
Build integrationsEmbedding reference

Embedded remote clients

Remote MCP gateway

@cotal-ai/connector-core exports registerCotalGatewayTools(server, host, source?, options?) for a service that serves Cotal to a hosted MCP client over HTTP. A hosted client can send many conversations through one bearer and one MCP session, so a request does not say which conversation sent it. Each conversation calls cotal_connect and passes the connection_id it returns on every other tool call. A missing, unknown or other-account handle returns CONNECTION_REQUIRED and runs nothing. No call falls back to another connection of the account.

Host contract

The host implements GatewayHost:

  • account(auth) returns the account the request authenticated as, read from the SDK's AuthInfo. It throws when the request has none, and the tool then runs nothing.
  • open(account, name, context?, selection?, approvalId?) starts a connection only after the person approves its permissions. selection is a proposal. Throw ApprovalRequired with the request ID, approval page, expiry and proposal while they decide. The tool returns a structured pending result and a readable link, with no connection handle. approvalId resumes one unchanged request when options.approvalResume is enabled. It conveys no authority. Bind it to the authenticated account, space, name and request generation. An issued approval cannot grant a replacement connection. Throw NameTaken only for an authoritative name collision. A new request tries name_2 and later variants up to GATEWAY_NAME_ATTEMPTS; a resumed approval never renames. The collision check and issue must be atomic. Other errors reach the caller without renaming and must not carry secrets.
  • find(account, id) and close(account, id) act only on a connection that account owns.

cotal_connect refuses a name assertValidName rejects before it calls open. Its reply names the channels the new connection reads, may join and may post to, from the configuration its grant produced, then carries the connection's orientation card and the shared agent briefing, because a conversation that joins here had no launch-time system prompt. GATEWAY_TOOLS names the mesh tools the gateway offers, cotal_docs included. A native embedder whose host issues each conversation its own grant may pass options.tools from NATIVE_GATEWAY_TOOLS, which adds spawn, despawn, persona, run and reconnect but not yield. Registering a tool offers it to no connection by itself: a connection calls only the tools its host lists in GatewayConnection.tools, default GATEWAY_TOOLS, and only those its credential's capabilities allow. With options.selection, cotal_connect takes channels, a role and capabilities, builds a canonical proposal with core's conversationSelection, and hands it to open. Its description tells the agent to ask in that first call for the channels, role and capabilities it already knows it needs, so the person approves one request. Only the person's decision in the authority's authenticated UI is consent. The approved selection may differ from the proposal and must stay within their current authority. The empty approved selection still includes direct-message send and receive and own activity publication; disclose these before approval. A model flag, API bearer or ordinary tool approval cannot approve the request. Unsupported approval paths refuse before issuing a grant. One tool list serves every connection, so no advertised text names one connection's channels. Each call runs against that connection's own configuration. Authentication, credential issuance and revocation and durable connection records stay with the host.

A remote MCP client gives the server no way to start a turn, so the gateway adds cotal_wait. It returns at once when a message that should wake the agent is buffered, and otherwise as soon as one arrives, with the same reply cotal_inbox gives. Wake-worthy follows the agent's attention: direct messages, role requests and @mentions always, other channel traffic only in open. With none, it returns after options.waitSeconds, which defaults to GATEWAY_WAIT_SECONDS (50). Keep it under every proxy and client timeout in front of the service. The reply is one JSON body, so nothing is sent until the wait ends: Claude Code gives a remote server 60 seconds to the first byte, Cloudflare ends an origin response after about 100 seconds and claude.ai ends a tool call after 240. A wait whose client closes the request stops listening, and a read whose client is gone by the time it would clear messages clears nothing, so neither takes a message it cannot deliver. A wait ends when its connection closes, also while the broker is unreachable; it listens for MeshAgent's stopping event. In focus an @mention is not buffered, so the gateway counts the ones since the last read, and a wait that starts after one returns it at once. Every other routed tool except cotal_inbox adds a line to its result when wake-worthy messages are waiting.

HTTP adapter

handleGatewayRequest(host, { req, res, body?, auth?, source?, instructions?, approvalResume?, selection?, waitSeconds? }) serves one HTTP request to the endpoint with the package's own MCP server, so the embedding service never imports an MCP SDK or a zod version of its own. The service authenticates the request, answers 401 itself when that fails, and passes the result as auth. A framework that already parsed the JSON body passes it as body. selection turns on the permission request described above for a hosted endpoint. The endpoint is stateless: each POST gets a fresh server and a JSON response, and every other method answers 405. A cotal_wait call holds its POST open for up to waitSeconds. GATEWAY_INSTRUCTIONS is the server instructions text sent when instructions is omitted.

Stdio adapter

serveGatewayStdio(host, { input?, output?, source?, instructions?, waitSeconds?, wake? }) serves the same surface over stdio, for a local client such as Claude Desktop that launches one process for all of its conversations. A stdio request carries no authentication, so host.account receives undefined and names the local user. A client that cancels a call aborts its signal, so a cancelled wait stops listening. The streams default to the process's own, and stdout then carries only MCP.

Client wake integration

An embedder whose client can be woken passes wake, to serveGatewayStdio or in the options of registerCotalGatewayTools. The gateway calls wake.signal(connection, preview) when a connection gets something that would end a cotal_wait while no wait holds it. A true result means a reader heard the signal, not that it received complete message content. The preview never consumes mail. cotal_connect adds wake.listening(connection) to its reply. An embedder delivering full content can return false from wake.inboxNotice(connection) to omit competing inbox-read advice from other tool results. Notification-only clients retain that advice.

wake.attach receives a rearm callback. Call it without arguments when a replacement reader starts listening. Call it with one connection after confirmed inline delivery to reoffer that connection's remaining work. Pass false as its second argument to clear the latch without immediately retrying a refused handoff. Ordinary inbox reads and waits also clear the latch. An adapter offering complete inline messages can use prepareInlineDelivery: hold whole automatic messages, validate the final transport size, recheck the batch before admission, then settle it with the native result. A failed or unknown admission releases the hold without committing. Never settle successfully on a preview, socket write or queued native call. GATEWAY_INSTRUCTIONS combines GATEWAY_CONNECTING and GATEWAY_LISTENING, so an embedder can supply its own listening instructions.

Private tool results

SPEC §13.19 accepts a private, consent-bound read path for managed-agent tool results. It is not served or enabled. Core exports the closed ToolResultConsent, ToolResultRevocation and ToolResultServingMaterial contracts, their parsers, and the frozen TOOL_RESULTS_CLUSTER declaration. These parse structure and static bindings only. They do not verify a consent signature, establish human proof or read current revocation and lifecycle state. The serving material's grant coordinates are not a branded EpServeGrant and confer no authority. parseToolResultAnswer also checks the UTF-8 byte count; a schema's character limit alone is not the byte bound.

Human proof

@cotal-ai/auth exports verifyToolResultHumanProof for the consent prerequisite. It verifies an EdDSA IdP JWT against the existing issuer, audience and key pins, then requires integer-second auth_time no later than the decision and at most 300 seconds old, plus a signed amr array containing pwd. The host compares elapsed milliseconds without rounding its clock to seconds. JWT clock tolerance does not widen this window. Ordinary bearer exchange does not require these claims or turn them into consent authority.

The loopback-only POST /tool-result-human-proof/verify takes only { idpToken, subject } with the host's current capability and no Origin header. The host supplies its authenticated session subject. Success returns { authorized: true, sub, owner, authTime, exp }; the owner derives from the verified subject, which must match the session. The door verifies proof only. It does not sign consent, issue credentials, register a service or serve private content. A host must still refuse platform keys and check current membership and lifecycle at consent admission.

A host may opt into consent issuance with startAuthService({ ..., toolResultConsents: { keyId, resolveAnchor } }). Provision its Ed25519 nkey seed under toolResultConsentKey(space) in the injected private store as { ver: 1, keyId, seed }. The resolver reads that space's current consents anchor with scope tool-results. Missing, corrupt, revoked or mismatched signing material refuses. Neither the private store nor the resolver belongs in a worker or client.

The loopback-only POST /tool-result-consent/issue requires the current host capability and no Origin header. Its closed input is { id, idpToken, subject, accountPublicKey, holder, target, ttlMs }. The host resolves those coordinates from its current authenticated membership and account assignment. holder carries { actor, lifecycleUid }; target carries { owner, actor, lifecycleUid }. Auth verifies the signed password proof and derives the owner itself, requires a current same-owner interactive holder and managed target, and caps the window at 24 hours. It returns only { consent }, after a create-only consent.<id> auth-store write. Duplicate ids refuse. A failed or lost acknowledgement returns no consent and never retries. The host must not label an unknown outcome completed or resubmit it automatically. Only the auth mint writer holds consent-KV publish authority. The environment admission writer and remote manager issuer do not inherit that grant. Create-only enforcement remains in the recorder.

This door is absent from the public face. Without the opt-in signer it answers unavailable. It installs no serving material, revocation store, caller view, capture or result reader.

Public tool arguments and results remain withheld. Auth and host issuance, Manager material, connector capture and the browser caller must each be landed and qualified in the design's owner order, then carried by a stock release before activation. Without a complete authoritative revocation snapshot and an established, enforced clock-skew bound, the serving commands must answer unavailable.

Browser control

@cotal-ai/core/browser-control describes and invokes an endpoint from a strict-CSP page. The page compiles no code: the host passes validators it generated at build time with generateBrowserContracts from @cotal-ai/core/browser-validators-build (Node only). The generator takes each cluster document with its recorded digest and schema roots, recomputes every digest and refuses a mismatch or a missing root before writing anything. It supports single-document closures only, so a command whose closure spans several documents refuses. The generated module imports Ajv's runtime helpers (ajv/dist/runtime/*.js), so the host's bundler must resolve ajv from where the host writes that module.

Selected Manager caller

openBrowserManagerCaller opens a Manager caller for ONE instance the person selected; there is no implicit selection. It takes the broker's websocket URL, the space, the principal and lifecycle the bearer is bound to, the instance, and the Manager's contracts generated from managerBrowserContractSource() (@cotal-ai/manager). Authority comes from an acquire callback that returns the person's own manager-caller bearer and the space's sentinel credential; an embedding's backend obtains the bearer from the stock exchange with the person's IdP proof. The bearer's space, principal, lifecycle and instance are checked before anything is dialled, and the describe is pinned to that instance. The view carries only the commands the person's ledger scope grants. spawn, for example, carries spawn, despawn, inspect, the persona commands (define-persona, remove-persona, list-personas, show-persona) and goal-result, plus the launch reads status, models and resolve-cwd on that one instance, while operator reads such as ps and slots come only with admin; the broker refuses a command the view does not carry. The connection never reconnects on its own; reopen acquires again and re-checks every pin.

Goal outcomes

followGoal submits a goal command once under an envelope id the host persisted before calling it, and follows the goal to its terminal with the stock goal follow. Live progress settles the goal only when it comes from the instance and epoch that accepted it; any other terminal settles nothing and prompts a read of the goal's own record. reconcileGoal reads an accepted goal back through the Manager's goal-result command, and verifyGoalResultFact checks the returned fact, including its outcome digest, through Web Crypto. Nothing here retries. Three outcomes stay distinct: an error reply is a refusal; a goal accepted but not seen to finish answers outcome unknown and is read back, never submitted again; a request no instance answered throws the stock unanswered error (unansweredRequest).

Manager instance list

POST /manager-instances on the auth service lists the signed-in person's own Manager instances. It takes { idpToken }, verifies it as the human exchange does and answers { v: 1, owner, complete: true, instances: [{ instanceId, placement, state }] }. placement is remote or local; state is the verdict the manager-caller exchange selects on (candidate, not open, not registered, not this owner's, unknown). A candidate is registration evidence, not proof that the instance answers; a describe pinned to it is. The route refuses browser-origin requests like every exchange route, so a page reaches it through its own backend, and a failed gate read answers 503, never an empty list.

Remote Manager client composition

@cotal-ai/manager exports the remoteManagerClient namespace, containing the stock remote request builders and response validators, and registerRemoteManagerAuthority for registration with a host-issued prepare credential. It returns the process epoch and registration revision its own registration committed. When a later start of the same instance registers before this start authorizes its serve grant, this start is refused with expired. RemoteManagerIdentityState describes the five private manager identities stored under an explicit account-local root. Use these public exports when composing ManagerOptions.remoteAuthority; do not copy CLI validators or import private modules. managerClusterArtifacts() returns the canonical document, manifest and their digests used by registration. Pass its [document, manifest] pair as contractArtifacts to both remoteManagerRegistrationProof(owner, state, contractArtifacts) from @cotal-ai/core and remoteManagerClient.remoteManagerAuthorityRequest(state, actor, "activate", { registrationProof, contractArtifacts }). The request builder takes each operation's coordinates as named fields of its last argument. The host still validates the artifact closure and current registration before activation. managerBrowserContractSource() returns the same document with its closure digest and each distinct schema root, the input generateBrowserContracts from @cotal-ai/core/browser-validators-build takes at a host's build. Its closure manifests are left out because they are not schemas; the generator recomputes every digest and refuses a mismatch. The namespace includes closed standing/run renewal, admission, maintenance, enrollment, retirement and goal-index scan helpers. publicIdentities(state) returns the identities record every request carries: the five public ids, in the order the host echoes them. remoteManagerClient.remoteRunHosting builds six callbacks: submitRun, revokeRun, admitRun, issueAttempt, issueOperator, and renewRun. Submission and revocation refuse when their respective optional request transports are absent. The Manager sends the same requests as the stock composition. The namespace provides no signer or new grant. The host still owns authenticated issuance, current registration and activated-run observations, and any guarded foreign-holder repair. An embedding must preserve those checks and supply a supported runtime; the client exports alone do not provide a pooled runtime, an authority service or a complete hosted context.

Waiting for messages

cotal_wait reports buffered messages and bounds waiting by its timeout. See the detailed contract.

On this page