Embed contract
Cadau embed SDK contract V1.5.13.
Source docs/en/site/sdk-host-contract.md
Cadau embed SDK contract V1.5.13.
Scope: an external system embeds a “My agent” with a JS snippet, so the host page gets AI conversation and controlled page actions.
Aligned with: docs/产品规格.md §4.2.3, §7.5.
Implementation: embed frontend in
sdk/host-embed/widget/; static script fromGET /embed/mindlink-widget.min.js(backend/internal/embedsdk); token APIPOST /api/v1/user-agents/{id}/embed-token. Integration notes:Website integrationin this directory.
1. Goals and bounds
- Goal: let a host system add “My agent” conversation at low cost, plus limited, auditable page actions.
- Bounds: the embed client does not hold long-lived secrets; arbitrary script execution is not allowed; all conversation capability goes through Cadau backend REST.
2. How to connect
2.1 Script + JS init
Official deploy: the Cadau service serves the static script (same origin as the API). The path is fixed: /embed/mindlink-widget.min.js. After you upgrade Cadau you do not replace a self-hosted JS file; keep pointing at that URL.
Path A (public support; recommended minimum config):
<script src="https://mindlink.example.com/embed/mindlink-widget.min.js"></script>
<script>
window.MindLinkWidget.init({
base_url: "https://mindlink.example.com",
user_agent_id: "ua_123",
auth: { token: "eyJ..." }
});
</script>
You can also auto-mount with data-base-url / data-agent-id / data-token on the script tag (optional data-app-id, data-theme, data-position, data-entry, data-title, data-greeting, data-welcome). Website embed copy-script can pick the entry shape. Static walkthrough: examples/cadau-embed-site/.
Path B (business system) must pass host_actor (the signed-in user), and may pass api_base_url, app_id, workspace_id, auth.expires_at, and similar (see the host-enhancement docs).
2.2 Web Component init
HTML attributes can override app-id, api-base-url, base-url, user-agent-id, workspace-id, theme, position, locale; auth / host_actor must be set in JS (no HTML attributes). Without auth, nothing mounts.
The element exposes the same methods as WidgetHostApi: on / open / close / destroy / updateAuth / updateHostActor / sendMessage. container is always the element itself (for inline mount, put the element in the business container).
<mindlink-widget
base-url="https://mindlink.example.com"
user-agent-id="ua_123"
theme="auto"
position="bottom-right"
></mindlink-widget>
<script>
const el = document.querySelector("mindlink-widget");
el.auth = { token: "eyJ..." };
// el.host_actor = { external_user_id: "...", actor_kind: "employee", ... };
el.on("action", (ev) => { /* same as Script init */ });
</script>
Script inline mount can pass container (selector or HTMLElement) together with position: "inline" (see WidgetInitOptions).
3. TypeScript contract (recommended)
export type WidgetTheme = "light" | "dark" | "auto";
export type WidgetPosition = "bottom-right" | "bottom-left" | "middle-right" | "center" | "inline";
export interface EmbedAuth {
token: string;
/** Optional; if omitted the widget does not locally predict expiry */
expires_at?: string; // ISO 8601
}
/**
* Current host signed-in user (required on path B). Cadau does not create an account from this.
* Validation: `external_user_id` required; when `actor_kind` is `employee`, `employee_id` is required.
* If `actor_kind` is omitted: treat as `employee` when `employee_id` is present, otherwise `business`.
*/
export interface HostActor {
external_user_id: string;
actor_kind?: "business" | "employee";
display_name?: string;
tenant_external_id?: string;
employee_id?: string;
emp_no?: string;
roles?: string[];
managed_org_unit_ids?: string[];
managed_employee_ids?: string[];
org_unit_id?: string;
org_unit_name?: string;
}
export interface WidgetInitOptions {
/** Cadau site root; if api_base_url is omitted, derived as `{base_url}/api/v1` */
base_url?: string;
/** Optional; default `mindlink-embed` */
app_id?: string;
/** Optional; may be derived from base_url */
api_base_url?: string;
user_agent_id: string;
auth: EmbedAuth;
/** Optional; default relies on JWT wid */
workspace_id?: string;
theme?: WidgetTheme;
position?: WidgetPosition;
/**
* UI language (default `"zh-CN"`, normalized to `zh` / `en`). Widget buttons, input hints, and API errors follow this.
*/
locale?: string;
entry?: {
auto_open?: boolean;
/** When the user clearly asks to open a UI, auto-run the first host-executable mindlink://action/ in the reply (default true) */
auto_execute_navigation?: boolean;
/** Empty-state hint; empty string uses the built-in default sentence */
welcome_text?: string;
/** Panel title; empty uses “Action assistant” */
title?: string;
/** Subtitle under the title */
subtitle?: string;
/** First visit: a greeting next to the corner button (gone after open or dismiss) */
greeting?: boolean;
greeting_text?: string;
/** Hide the corner launcher; a host button calls `open()` / `close()` */
hide_launcher?: boolean;
};
/** Mount parent; default `document.body`. For inline, pass a page content container. */
container?: HTMLElement | string;
/**
* Signed-in user identity (required on path B). Written to chat `client_context.host_actor` and top-level `host_actor`;
* history / live support / tickets are isolated by this person; host data queries use it too. On user switch call `updateHostActor`.
*/
host_actor?: HostActor;
}
export interface WidgetHostApi {
open(): void;
close(): void;
destroy(): void;
updateAuth(auth: EmbedAuth): void;
/** Update identity when the signed-in user changes (same shape as init.host_actor). */
updateHostActor(hostActor: HostActor | null | undefined): void;
sendMessage(message: string): Promise<void>;
on(event: WidgetEventName, handler: (event: WidgetEvent) => void): () => void;
}
export type WidgetEventName = "ready" | "message" | "action" | "error" | "close";
export interface WidgetBaseEvent {
type: WidgetEventName;
request_id?: string;
ts: string; // ISO 8601
}
export interface WidgetReadyEvent extends WidgetBaseEvent {
type: "ready";
app_id: string;
user_agent_id: string;
}
export interface WidgetMessageEvent extends WidgetBaseEvent {
type: "message";
session_id: string;
message_id: string;
role: "user" | "assistant" | "system";
content: string;
}
export type WidgetActionType = "open_url" | "open_module" | "emit_event";
export interface WidgetActionEvent extends WidgetBaseEvent {
type: "action";
action: {
type: WidgetActionType;
payload: Record<string, unknown>;
};
}
export interface WidgetErrorEvent extends WidgetBaseEvent {
type: "error";
code: string;
message: string;
}
export interface WidgetCloseEvent extends WidgetBaseEvent {
type: "close";
reason?: string;
}
export type WidgetEvent =
| WidgetReadyEvent
| WidgetMessageEvent
| WidgetActionEvent
| WidgetErrorEvent
| WidgetCloseEvent;
4. Auth and security
4.1 Token issue (current implementation)
- Issued by a Cadau main-site signed-in user calling
POST /api/v1/user-agents/{id}/embed-token(that assistant “Manage → Website embed”). Optional body:app_id(defaultmindlink-embed),ttl_seconds,permanent,host_actor(path B mint writes it into the registration; conversation and data policy follow the registration, so the browser cannot impersonate someone else). - Response includes
access_token,token_type(Bearer),expires_in(seconds),expires_at,user_agent_id,workspace_id,app_id,permanent,record_id. Registration is stored inembed_access_tokens(includesapp_id, revokejwt_jti). The sameuser_agent_idcan be reused by multiple third-partyapp_ids; host-side users and permissions stay in the third-party system. - Host-backend minting is the recommended production shape, but you must wire the API above (or an equivalent issuer) yourself. The contract does not assume the host builds its own JWT.
4.2 JWT and validation (current implementation)
- Embed JWT claims (
authx.Claims):sub(user),wid(workspace),emb=1,jti(= registration rowjwt_jti),exp. The JWT does not containapp_id/user_agent_id; those are constrained byinitparams and the registration row, and must match issue time. - Protected APIs:
Authorization: Bearer. Whenemb=1, middleware also checksembed_access_tokensnot revoked and not expired; failure codeembed_token_revoked. - Expiry / signature errors: generic
unauthorized(copy “Token is invalid or expired”).embed_token_expired/embed_token_invalidare not returned separately yet. - Optional header
X-Workspace-Id: recommended when it matchesinit.workspace_id(embed-sdk already sends it). - Header
X-Host-External-User-Id(embed-sdk already sends it): path B is the boundhost_actor.external_user_idfrom issue time; path A is a browser visitor id. Used to isolate history per person / visitor. Without this header and with no identity bound on the token, the embed conversation list is empty (avoids cross-talk). - Data-permission identity: only the registration written into
embed_access_tokens.host_actor_jsonat issue time. Request-bodyhost_actorcannot impersonate someone else. Path A with no binding does not trust client identity for data queries. POST /api/v1/host/agent-runsrejects embed tokens (forbidden_embed_token); call it from the server with the integration account.
4.3 Other
- Short-lived and long-lived tokens are both supported; long-lived can still be revoked with
DELETE /api/v1/embed-access-tokens/{id}. embed_origin_not_allowed/ origin allowlist: takes effect after you sethttp.allowed_origins(or envHTTP_ALLOWED_ORIGINS); if unset, CORS still reflects the request Origin (local testing only).- Default Shadow DOM isolation; the host should set CSP and block untrusted script injection.
5. Conversation and action protocol
5.1 Chat request (embed mode, current implementation)
The embed widget uses POST /api/v1/chat/stream (SSE), not synchronous POST /api/v1/chat.
SSE events the widget already handles: start / token / done / error, plus tool-round round_start / tool_call / tool_output (progress UI; hosts usually need not listen).
{
"message": "Give me today’s follow-up suggestions",
"session_id": "optional",
"user_agent_id": "ua_123",
"request_id": "req_embed_001",
"workspace_id": "ws_001",
"client_context": {
"channel": "embed_widget",
"app_id": "crm-prod",
"page_url": "https://crm.example.com/home",
"host_actor": { "external_user_id": "…", "actor_kind": "employee" }
},
"host_actor": { "external_user_id": "…", "actor_kind": "employee" },
"attachment_ids": []
}
user_agent_idis required; empty atinitthrows on the frontend and cannot send.client_context: client already sends it, server already parses it. Whenchannel=embed_widget(or JWTemb=1), conversation source is embed;app_idis written to the conversation’sembed_app_id;host_actor(or the same top-level field) is used for conversation isolation and data-connection policy. On path B, lists / history / live support / tickets return only that signed-in user’s content, keyed byhost_actor.external_user_id.- History and conversations:
GET /api/v1/chat/sessions?kind=agent&user_agent_id=...(embed may passhost_external_user_idor headerX-Host-External-User-Id),GET /api/v1/chat/history?session_id=...(embed-sdk already uses these). - Optional attachments:
attachment_ids(firstPOST /api/v1/uploads); edit-and-resend:POST /api/v1/chat/truncate; stop generation:GET /api/v1/chat/sessions/{id}/generation,POST …/generation/stop(widget already wired). - When concurrent in-flight replies hit the cap, this API returns HTTP 429, code
embed_generation_limit. The cap prefers the agent configmax_embed_streams(Cadau: My agents → Embed assistant concurrent replies). If unset, the server default applies (usually 20). Agent config cannot exceed the server cap; when the server cap is > 0, agent value 0 falls back to the server default and does not mean unlimited. Only when the server default itself is 0 does 0 mean unlimited. Hard ceiling 256.
5.2 Action allowlist
- Allowed:
open_url,open_module,emit_event - Forbidden:
eval, dynamic script injection, undeclared cross-origin proxy requests
5.3 Action gating suggestions
open_urlshould check a domain allowlist.open_modulemay only open host-preregistered module IDs.emit_eventshould limit allowed fields; do not pass through sensitive data.
5.4 In-reply navigation links (current implementation)
The agent may output this in a Markdown reply (knowledge documents must teach the model):
`Open pending orders`
- After a click, the widget fires an
actionevent,action.type === "emit_event",payload.kind === "mindlink_action",payload.actionis the path (for examplepage.orders),payload.paramsis the query-param object (including filter fields and optionallabel). - The host page handles it in
widget.on("action", …)with a whitelist; it does not auto-jump business routes. entry.auto_execute_navigation(defaulttrue): when the user message matches “open / jump / go to…” intent (also matches sentence-start “打开|去|进入…”, Englishopen|navigate|go to, and similar — seemindlinkAction.ts), after the streamed reply ends the widget automatically runs the firstmindlink://action/link that is clickable on the embed surface (skips main-site-only actions, same as click filter; still goes through the host whitelist). Implementation:sdk/host-embed/widget/src/mindlinkAction.ts,EmbedApp.tsx.- Main-site help actions such as
mindlink://action/module.workspaceare not clickable on the embed surface (hint “Open this in Cadau”); host-custompage.*/host.*/ non-main-sitemodule.*are executable. - Details:
Website integration§5, §5.6; host knowledge writing:Host knowledge writing§6.2.
Not implemented yet: the backend sending structured open_url / open_module / emit_event action cards on the SSE stream. Host-side handleHostNavigation may leave handlers for those action.types, but the current embed path only fires via Markdown mindlink://action/ → emit_event + mindlink_action.
6. Embed UI (currently delivered)
6.1 Presentation
- Floating panel (default):
position: bottom-right(alsobottom-left,middle-rightright-edge center,centerpage center); a corner or chosen-position launcher, click to expand the panel. Whenentry.greetingis true, that browser’s first visit shows a greeting next to the button (gone after open or dismiss).data-entry:corner(default) /greeting/open(expand on load). - Host launcher:
entry.hide_launcher: trueskips the corner button; a legacy-host top-bar / toolbar button callsopen()/close(); the conversation panel still opens perposition. The HR example keeps the float by default and can switch to “pin to the top bar”. - Sidebar mode: not implemented (no standalone
sidebarposition). - Inline mode:
position: inline+container; fits a help center or settings page.
6.2 theme and the host page
light/dark: fixed light or dark panel.auto: first read host<html data-theme="dark|light">(if present, that wins); else ifhtmlorbodyhas classdark, treat as dark; last, browserprefers-color-scheme. When the host switches theme, the embed listens and syncs light/dark so the panel does not stay light when “system is light, the app is dark”.
6.3 Currently delivered (embed-sdk, for integration)
| Capability | Status |
|---|---|
Shadow DOM + window.MindLinkWidget.init / <mindlink-widget> (including Host API: on, etc.) | Delivered |
chat/stream streaming conversation, conversation list and history | Delivered |
ready / message / action / error / close / updateAuth / updateHostActor / sendMessage | Delivered |
init.host_actor → chat client_context.host_actor (conversation isolation / data policy) | Delivered |
Path B: history / live support / tickets show only self, by host_actor.external_user_id | Delivered |
| Path A: history / live support / tickets show only self, by browser visitor id (same isolation as tickets) | Delivered |
mindlink://action/ click → action (mindlink_action) | Delivered |
Auto-run first host-executable navigation when the user clearly asks to open (entry.auto_execute_navigation, default true) | Delivered |
Launcher/panel drag and size memory (keyed by app_id; path B conversation key also includes the host user) | Delivered |
Path A minimal init: base_url + user_agent_id + auth.token (workspace_id / app_id / expires_at optional) | Delivered |
Widget “Live support”: GET …/human-support/status (enabled / live_available / tickets_available) + /embed/cs-tickets* (queue, end, rate, convert to ticket; A: browser visitor id; B: host signed-in user) | Delivered |
Widget “Submit a ticket / My tickets”: /embed/support-cases* (async; can convert from live; same isolation) | Delivered |
Several people talking at once; overflow embed_generation_limit (agent can set concurrent-reply cap) | Delivered |
Host LLM service: POST /api/v1/host/agent-runs (integration account; chosen agent answers; host_actor required; task-style may fresh a new conversation) | Delivered |
Context-budget ring (context_budget) | Delivered |
| “Next step” suggestion chips at the end of a reply (same parse as the main site) | Delivered |
locale switches UI copy | Delivered (zh / en, aliases zh-CN / en-US) |
| Action cards, mobile full-screen drawer | Not delivered |
Panel title / first-visit greeting (entry.title / entry.greeting; script data-title / data-entry / data-greeting) | Delivered |
Dedicated token-expiry UI / embed_token_expired error code | Not delivered (depends on host updateAuth) |
| SSE directly sending structured action cards | Not delivered |
7. Sequence (end to end)
- Issue an embed token in Cadau (
embed-tokenAPI, registerapp_id+user_agent_id); path A generates on Website embed; path B may be called by the host backend. - Host frontend loads the widget and
init(A:base_url+user_agent_id+token; B must passhost_actor,updateHostActoron user switch). - Widget fires
ready. - User sends a message; widget calls
/api/v1/chat/stream. - Streamed assistant body (may include
mindlink://action/links). - (Optional) user clicks a navigation link, or auto-navigation → host whitelist runs it.
- (Optional) with live support on, the widget may submit
/api/v1/embed/cs-tickets(live) or/api/v1/embed/support-cases(tickets); support works them in the Cadau desk (seats in this workspace or an authorized support team); lists refresh live. The team desk does not change the top-bar current workspace. The host need not call support-team management APIs. - Token failure:
unauthorized/embed_token_revoked; path B mayupdateAuth/updateHostActor.
8. Integration checklist
- [ ] Widget loads (including Shadow DOM).
- [ ] Path A can converse with only
base_url+user_agent_id+auth.token. - [ ] Missing
user_agent_id/auth.tokenmakesinitfail or cannot send (currently throws at init). - [ ]
chat/streamcan send and receive;request_idis traceable. - [ ] With live support on, the widget can “Live support” (someone must be on duty) and “Submit a ticket”; if off, no entry. The support desk sees new items without a full page refresh. Seats in this workspace or an authorized support team can take work.
- [ ] (B) two signed-in users cannot see each other’s history, in-progress live support, or tickets;
host_actor.external_user_idis passed. - [ ] (A) two website visitors cannot see each other’s history, in-progress live support, or tickets (by browser visitor id).
- [ ] Two visitors can ask at once and each get a reply; when overloaded the widget says “Too many people talking right now. Try again in a moment.”
- [ ]
mindlink://action/links are clickable and fireaction(kind: mindlink_action); the host has a whitelist. - [ ] When the user says “open xx for me”, auto-navigation after the reply matches expectations (or
auto_execute_navigation: falseis set); main-site-only actions are not auto-run. - [ ] (Legacy host)
host_actoris passed;updateHostActorworks after user switch. - [ ] After revoke, requests return
embed_token_revoked. - [ ] After token refresh,
updateAuthcan continue the conversation. - [ ] (Production)
http.allowed_originsis set; unlisted origins returnembed_origin_not_allowed. - [ ] User-visible errors are natural language, not bare codes.
9. Error codes
| Code | Status | Meaning |
|---|---|---|
embed_token_revoked | Implemented | Embed registration revoked or expired |
unauthorized | Implemented | Missing token, invalid, or JWT expired |
embed_generation_limit | Implemented | Concurrent in-flight replies at cap (HTTP 429); copy “Too many people talking right now. Try again in a moment.” |
embed_token_expired | Planned | Currently folded into unauthorized |
embed_token_invalid | Planned | Currently folded into unauthorized |
embed_origin_not_allowed | Implemented | Origin allowlist is configured and request Origin is not listed |
forbidden_embed_token | Implemented | An embed token was used on an integration-account-only API (for example Host Agent Run) |
embed_agent_forbidden | Planned | |
embed_action_not_allowed | Planned | |
embed_workspace_mismatch | Planned |
10. Version history
- V1.5.15 (2026-08-26):
entry.hide_launcher: hide the corner launcher; a host button opens/closes the panel. - V1.5.14 (2026-08-18): Website embed “which side” adds right (
middle-right) and center (center). Withoutdata-positionit is still bottom-right. - V1.5.13 (2026-08-18): Website embed copy-script can pick entry shape — corner button / first-visit greeting / open expanded; panel name and greeting sentence (
data-entry/data-title/data-greeting/data-position). Without these attributes, behavior matches V1.5.12. - V1.5.12 (2026-08-17): Widget script from ~8MB down to ~500KB (gzip ~150KB) — no longer bundles the main-site file preview library and the full API client; diagrams load Mermaid on demand from the Cadau site; HTML fences preview in a new window.
- V1.5.11 (2026-08-17): Issue can bind
host_actor(conversation/query follow the registration); origin allowlist andembed_origin_not_allowedafterhttp.allowed_origins; Host Agent Run rejects embed tokens. - V1.5.10 (2026-08-17): Docs aligned with implementation —
max_embed_streamsparse (capped by server; agent 0 falls back when server default > 0), reply “next step” chips delivered,data-app-id/data-theme, mint response addstoken_type/expires_in/permanent. - V1.5.9 (2026-08-17): Live-support ops aligned — seats in this workspace or a support team can take embed inbound;
live_availablecounts on-duty support covering that workspace; team desk does not switch the top-bar workspace. Widget APIs unchanged. - V1.5.8 (2026-08-13): Docs aligned with implementation —
HostActorfields,human-support/statusbreakdown, conversation attachments/stop/edit-resend, embed concurrent-reply cap (max_embed_streams). - V1.5.7 (2026-08-13): Host Agent Run supports
freshfor a new conversation each time; HR example “suggest jobs by department” uses the analyze agent from.env/ host settings (preview only, not written to the job catalog). - V1.5.6 (2026-08-13): Embed assistant allows several visitors to talk at once; overflow returns
embed_generation_limit. - V1.5.5 (2026-08-13): Path A history isolated by browser visitor id (same as tickets); embed conversation list is empty if the visitor is not identified, to avoid cross-talk.
- V1.5.4 (2026-08-13): Path B history / live support / tickets show only the current signed-in user’s content, by
host_actor.external_user_id. - V1.5.3 (2026-08-13): Path B aligned with static embed — widget live support + async tickets (
/embed/support-cases); support desk live refresh. - V1.5.2 (2026-08-12): Path A public support —
base_urlminimal init, widget live support and/embed/cs-tickets; static exampleexamples/cadau-embed-site/. - V1.5.1 (2026-08-11): Docs trimmed — removed undelivered UI planning long-form and wireframe lists; in-reply navigation folded into
Website integration§5. - V1.5 (2026-08-11): Aligned with implementation:
host_actor/updateHostActor,client_contextserver parse and conversation isolation, Web Component Host API, auto-navigation consistent with click filter, SSE extra events; markedlocaleand structured action cards as not delivered. - V1.4 (2026-05-26): Added
entry.auto_execute_navigation(default true), auto-navigation behavior and integration items; integration notesWebsite integration§5.6. - V1.3 (2026-05-19): Aligned with
embed-sdk/ backend:chat/stream,embed-tokenissue,mindlink_actionnavigation, Web Componentauth/container; distinguished delivered vs planned UI/error codes. - V1.2 (2026-04-27): Added wireframe-level component list, page skeleton sketch, and frontend state-machine suggestions (removed in V1.5.1).
- V1.1 (2026-04-27): Added embed UI design notes (planning section trimmed in V1.5.1).
- V1 (2026-04-27): First version covering init params, TS contract, auth, security, action allowlist, and integration checklist.