All docs

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 from GET /embed/mindlink-widget.min.js (backend/internal/embedsdk); token API POST /api/v1/user-agents/{id}/embed-token. Integration notes: Website integration in 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 (default mindlink-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 in embed_access_tokens (includes app_id, revoke jwt_jti). The same user_agent_id can be reused by multiple third-party app_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 row jwt_jti), exp. The JWT does not contain app_id / user_agent_id; those are constrained by init params and the registration row, and must match issue time.
  • Protected APIs: Authorization: Bearer. When emb=1, middleware also checks embed_access_tokens not revoked and not expired; failure code embed_token_revoked.
  • Expiry / signature errors: generic unauthorized (copy “Token is invalid or expired”). embed_token_expired / embed_token_invalid are not returned separately yet.
  • Optional header X-Workspace-Id: recommended when it matches init.workspace_id (embed-sdk already sends it).
  • Header X-Host-External-User-Id (embed-sdk already sends it): path B is the bound host_actor.external_user_id from 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_json at issue time. Request-body host_actor cannot impersonate someone else. Path A with no binding does not trust client identity for data queries.
  • POST /api/v1/host/agent-runs rejects 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 set http.allowed_origins (or env HTTP_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_id is required; empty at init throws on the frontend and cannot send.
  • client_context: client already sends it, server already parses it. When channel=embed_widget (or JWT emb=1), conversation source is embed; app_id is written to the conversation’s embed_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 by host_actor.external_user_id.
  • History and conversations: GET /api/v1/chat/sessions?kind=agent&user_agent_id=... (embed may pass host_external_user_id or header X-Host-External-User-Id), GET /api/v1/chat/history?session_id=... (embed-sdk already uses these).
  • Optional attachments: attachment_ids (first POST /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 config max_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_url should check a domain allowlist.
  • open_module may only open host-preregistered module IDs.
  • emit_event should 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 action event, action.type === "emit_event", payload.kind === "mindlink_action", payload.action is the path (for example page.orders), payload.params is the query-param object (including filter fields and optional label).
  • The host page handles it in widget.on("action", …) with a whitelist; it does not auto-jump business routes.
  • entry.auto_execute_navigation (default true): when the user message matches “open / jump / go to…” intent (also matches sentence-start “打开|去|进入…”, English open|navigate|go to, and similar — see mindlinkAction.ts), after the streamed reply ends the widget automatically runs the first mindlink://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.workspace are not clickable on the embed surface (hint “Open this in Cadau”); host-custom page.* / host.* / non-main-site module.* 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 (also bottom-left, middle-right right-edge center, center page center); a corner or chosen-position launcher, click to expand the panel. When entry.greeting is 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: true skips the corner button; a legacy-host top-bar / toolbar button calls open() / close(); the conversation panel still opens per position. The HR example keeps the float by default and can switch to “pin to the top bar”.
  • Sidebar mode: not implemented (no standalone sidebar position).
  • 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 if html or body has class dark, treat as dark; last, browser prefers-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)

CapabilityStatus
Shadow DOM + window.MindLinkWidget.init / <mindlink-widget> (including Host API: on, etc.)Delivered
chat/stream streaming conversation, conversation list and historyDelivered
ready / message / action / error / close / updateAuth / updateHostActor / sendMessageDelivered
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_idDelivered
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 copyDelivered (zh / en, aliases zh-CN / en-US)
Action cards, mobile full-screen drawerNot 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 codeNot delivered (depends on host updateAuth)
SSE directly sending structured action cardsNot delivered

7. Sequence (end to end)

  1. Issue an embed token in Cadau (embed-token API, register app_id + user_agent_id); path A generates on Website embed; path B may be called by the host backend.
  2. Host frontend loads the widget and init (A: base_url + user_agent_id + token; B must pass host_actor, updateHostActor on user switch).
  3. Widget fires ready.
  4. User sends a message; widget calls /api/v1/chat/stream.
  5. Streamed assistant body (may include mindlink://action/ links).
  6. (Optional) user clicks a navigation link, or auto-navigation → host whitelist runs it.
  7. (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.
  8. Token failure: unauthorized / embed_token_revoked; path B may updateAuth / 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.token makes init fail or cannot send (currently throws at init).
  • [ ] chat/stream can send and receive; request_id is 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_id is 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 fire action (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: false is set); main-site-only actions are not auto-run.
  • [ ] (Legacy host) host_actor is passed; updateHostActor works after user switch.
  • [ ] After revoke, requests return embed_token_revoked.
  • [ ] After token refresh, updateAuth can continue the conversation.
  • [ ] (Production) http.allowed_origins is set; unlisted origins return embed_origin_not_allowed.
  • [ ] User-visible errors are natural language, not bare codes.

9. Error codes

CodeStatusMeaning
embed_token_revokedImplementedEmbed registration revoked or expired
unauthorizedImplementedMissing token, invalid, or JWT expired
embed_generation_limitImplementedConcurrent in-flight replies at cap (HTTP 429); copy “Too many people talking right now. Try again in a moment.”
embed_token_expiredPlannedCurrently folded into unauthorized
embed_token_invalidPlannedCurrently folded into unauthorized
embed_origin_not_allowedImplementedOrigin allowlist is configured and request Origin is not listed
forbidden_embed_tokenImplementedAn embed token was used on an integration-account-only API (for example Host Agent Run)
embed_agent_forbiddenPlanned
embed_action_not_allowedPlanned
embed_workspace_mismatchPlanned

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). Without data-position it 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 and embed_origin_not_allowed after http.allowed_origins; Host Agent Run rejects embed tokens.
  • V1.5.10 (2026-08-17): Docs aligned with implementation — max_embed_streams parse (capped by server; agent 0 falls back when server default > 0), reply “next step” chips delivered, data-app-id / data-theme, mint response adds token_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_available counts 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 — HostActor fields, human-support/status breakdown, conversation attachments/stop/edit-resend, embed concurrent-reply cap (max_embed_streams).
  • V1.5.7 (2026-08-13): Host Agent Run supports fresh for 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_url minimal init, widget live support and /embed/cs-tickets; static example examples/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_context server parse and conversation isolation, Web Component Host API, auto-navigation consistent with click filter, SSE extra events; marked locale and 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 notes Website integration §5.6.
  • V1.3 (2026-05-19): Aligned with embed-sdk / backend: chat/stream, embed-token issue, mindlink_action navigation, Web Component auth/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.