Imported from mainsequence-sdk/command-center-sdk (
command-center-ai/agent_scaffold/skills/backend/connect-command-center-ai-to-the-platform/SKILL.md). Install upstream withnpx skills add mainsequence-sdk/command-center-sdk --skill connect-command-center-ai-to-the-platform. Copyright stays with the author.
Connect Command Center AI To The Platform
The package talks to two backends: the platform API, for sessions, history, runtime access, model
providers, and agent icons; and the Agent runtime, for the message request, its check, and cancel. The
human guide is docs/connect-to-the-platform.md in the installed package.
Build The Connection
import { createChatBackendConnection } from "@dev-mainsequence/command-center-ai";
const connection = createChatBackendConnection({
apiBaseUrl: "https://platform.example.com",
rewriteRequestUrl: (url, target) =>
target === "platform" ? `/__platform__${url.pathname}${url.search}` : url.toString(),
});
apiBaseUrlis the platform API's absolutehttp(s)URL; anything else throws.- The package reads no environment variable and no configuration file. The application decides the
URL; reading
import.meta.envfor it belongs in the application. - Build one connection and pass it to
ChatEngineProviderandModelProviderSettings.
Know Where Each Request Goes
- The platform API: every route under
apiBaseUrl, sent by the application'ssendPlatformRequeston the connection, which adds the person's credential. Session collection reads are scoped by the Organization Environment, and user-scoped lists by the person. - The Agent runtime:
POST {rpc_url}/api/chat(a message; the answer streams back),GET {rpc_url}/api/chat(the check that the Agent answers), andPOST {rpc_url}/api/chat/session/cancel(stop the run).rpc_urland the runtime token come from the platform's runtime access for the session; they are never configured. - A custom provider's own endpoint: only the direct test conversation of the model provider settings calls it, with the provider's key and never the platform token. It never goes through the rewrite or a proxy.
Reach Them From The Application's Origin
The platform API and the Agent runtime answer browsers only from the origins on their allow-lists.
An application on another origin has two ways in: the platform adds its origin, or the application
forwards the calls through an address on its own origin. The package serves the second through
rewriteRequestUrl(url, target): it receives the full URL the package is about to request and
where it is going ("platform" or "agent-runtime"), and returns the address the browser requests
instead. Every request passes through it, including the icon URLs the platform returns, except the
direct test turn above. Whether and when to rewrite is the application's decision; the package
detects no development build and knows no proxy.
In development, the forwarder is the dev server's proxy. The standalone application in
node_modules/@dev-mainsequence/command-center-ai/standalone/ does exactly this: its
connection.ts rewrites platform requests to /__platform__, and its development server forwards
that prefix to the platform. In Vite:
server: {
proxy: {
"/__platform__": {
target: "https://platform.example.com",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/__platform__/, ""),
},
},
},
In production, the forwarder is the site's own server on the same origin, for example its FastAPI
application, which passes platform requests on. Who the person is stays a server-side decision;
follow $integrate-static-site-iframe and its references/local-vite-fastapi.md for server-side
identity, and never accept a browser-supplied user uid in its place.
The Agent runtime is at rpc_url. The standalone application calls it directly, which works when
the runtime's trusted origins include the application's origin; otherwise forward it too, from the
"agent-runtime" target.
Own The Authentication
- The application owns sign-in, the credential, and its renewal. Give the connection
sendPlatformRequest(request): it receives every platform request as a standardRequestwith no credential, adds the person's credential, and on a401renews it and sends the request again. Keeprequest.clone()for the retry, because a body can be sent once:
const connection = createChatBackendConnection({
apiBaseUrl: "https://platform.example.com",
async sendPlatformRequest(request) {
// A request's body can be sent once, so the retry needs its own copy.
const retry = request.clone();
const response = await fetch(withCredential(request));
if (response.status !== 401 || !(await renewCredential())) {
return response;
}
return fetch(withCredential(retry));
},
});
authis then{ userUid }; the package never needs the credential.- Inside an application embedded in Command Center, the sender is the SDK's static-site client,
(request) => client.sendPlatformRequest(request). Command Center sends the request as the person; the application holds no platform credential. ItsapiBaseUrlonly builds request paths, so the page's own origin works. This needs SDK^0.5.5, the first release withclient.sendPlatformRequest; upgrade an older one. - In local development, a top-level page under
vite servehas no host. AddplatformRequestProxy()from@dev-mainsequence/command-center-sdk/vite(SDK^0.5.6) to the dev server; the developer exportsMAINSEQUENCE_ENDPOINTandMAINSEQUENCE_ACCESS_TOKENbeforenpm run dev. Only whenimport.meta.env.DEVis true andwindow.parent === window, the sender fetches/__mainsequence__plus the request's path and query; otherwise it staysclient.sendPlatformRequest.auth.userUidcomes from/__mainsequence__/api/v1/users/me/. Never read the token in page code or give it aVITE_name. - Never put a credential in a build variable (
VITE_*), an environment file, the bundle, or browser storage. The standalone application keeps its token in memory only. - The runtime token: the package resolves runtime access for the session, through the sender, and
sends that token only to the runtime. On a
401or403from the runtime it resolves access again and retries once. - Without a sender, clients send
auth.tokenthemselves and nothing can renew it; use a sender.
An Agent On This Machine
An Agent the developer runs with ms-tau in local mode has no platform session, so the platform
connection does not apply. Use the local source instead of writing a second chat (ADR 099; the
human guide is docs/local-agents.md in the installed package):
// vite.config.ts: forward the chat's routes to the runtime on 127.0.0.1:8787, dev server only.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { localAgentProxy } from "@dev-mainsequence/command-center-sdk/vite";
export default defineConfig({ plugins: [react(), localAgentProxy()] });
import { ChatEngineProvider, ChatThread, createLocalAgentSource } from "@dev-mainsequence/command-center-ai";
const localAgent = createLocalAgentSource({ baseUrl: "/__agent__", displayName: "CRM assistant" });
<ChatEngineProvider source={localAgent} isVisible={visible} notify={notify} viewContext={context}>
<ChatThread surface="page" />
</ChatEngineProvider>
localAgentProxy()needs Command Center SDK^0.5.7; the local source needs Command Center AI^0.0.7. Upgrade rather than proxying/tauby hand.- Never call the runtime from the browser directly or across origins: it has no authentication and
answers every caller as the developer.
createLocalAgentSourcerefuses another origin. - Do not speak A2A (
message:send) for the chat: it carries text only, so reasoning, tool calls, and streaming markdown are lost. - Choose the source in the application (for example from its bootstrap, in development only); the package never falls back from one source to the other. A production build uses the platform.
- Until the runtime serves
ms-tau-sdk#47, a reload starts a new conversation and the thread says so. Archive, server search, insights, and provider settings are hidden for a local source (useChatEngine().capabilities).
Read The Failure States
401or403from the platform: the credential is missing, expired, or not allowed. Renew it in the application'ssendPlatformRequestand send the request again. A403that persists is the platform's authorization decision; the package does not work around it.- An unreachable platform: requests fail with a network error and the thread shows its error states; nothing is retried behind the person's back.
- A CORS rejection: the browser reports it exactly like an unreachable host, as a network error. Check the browser's network panel: a request to the platform's own origin from another origin means the rewrite or the forwarder is missing, or the origin is not on the allow-list.
- An Agent that does not answer yet: the engine checks
GET {rpc_url}/api/chatbefore it sends. While the Agent starts, wakes, or updates, the composer is locked, a draft is kept, and nothing is sent on the person's behalf. When the platform's runtime interaction says the session cannot take messages, its notice is shown as it is.
Read The Errors
- Platform and Agent runtime failures reach the application as
MainSequenceAiError, with thesourceof the failure, the HTTPstatus, and the platform'scodeanddetailwhen it sent them.toMainSequenceAiError()turns anything caught into one. Showmessageto the person and keeprawMessagefor logs. - A model provider's failure arrives in the answer's stream as the
errorframe'serrorTextand shows as the turn's error, with "Send again";docs/main-sequence-ai-provider-errors.mdin the installed package describes it. The package maps no provider or status to messages of its own. - The engine reports what the person must know through
notify; show it the application's way.
Verify
- On the scripted stand-in (
$mount-agent-conversation), connect and send a message with no platform. - Against the platform, through the forwarder: in the browser's network panel, every platform
request goes to the application's own origin, carries the credential the sender added, and
succeeds; the runtime requests go to
rpc_urlor through the forwarder. - Expire or revoke the credential and confirm the sender renews it and the request succeeds.
