Embedding Cadau in a legacy system
Embed the assistant in an existing HR or OA system. People stay signed in where they already work — no extra account for every employee.
Source docs/en/site/host-embed-tutorial.md
In user language: This article is for architects and backend/frontend developers of a legacy business system. It explains how to embed a Cadau work agent in ERP, OA, HR, and similar systems: people still sign in and get work done in the original system; the assistant shows up as a built-in capability. Employees do not each need a Cadau account.
User-facing product write-up: [docs/产品介绍.md](/docs/product-intro); third-party delivery pack:
sdk/host-embed/; runnable example:examples/hr-multi-tenant/in this repo.
Date: 2026-08-17 Related code: backend/internal/api/handlers/host_agent_run.go, examples/hr-multi-tenant/server/internal/mindlinkclient/, examples/hr-multi-tenant/server/internal/store/host_actor.go
Takeaway (read this first)
Embedding Cadau in a legacy system is not merely “put Cadau on the page”. It is a separation of duties:
| Who owns what | Point |
|---|---|
| Legacy system | Sign-in, roles, business permissions, binding employee files, which assistant template each person gets |
| Cadau | Template agents, conversation and retrieval, embed tokens, Host Agent Run, row/field policy enforcement on data connections |
| Every request | The legacy BFF carries host_actor (who is doing the work). Cadau isolates conversations and limits which rows can be queried from that |
Do not clone a Cadau agent per end user. This phase: the whole host (which may include several business tenants) shares one Cadau workspace, with a few template agents plus runtime identity covering everyone. People are isolated by host_actor. “One tenant, one workspace” is in [宿主增强-AgentRun与数据权限.md](/docs/sdk-host-agent-run) §0.3 and is not implemented yet.
You can run the full chain locally: examples/hr-multi-tenant + npm run setup:mindlink + smoke scripts + a manual acceptance list.
1. Typical scene: why a legacy system needs Cadau
Many companies already run a legacy system (in-house HR, ERP, industry SaaS) with mature:
- Sign-in and org structure
- Business databases and permission models
- Pages and workflows
If the legacy system calls a large-model API directly, common problems are:
- Keys and prompts scattered across services; hard to govern and audit
- A prompt / conversation per user; high ops cost
- When the assistant reads the business database, it is hard to enforce at the connection layer “employees only see themselves, managers only see reports”
- Conversation is hard to line up with product capabilities such as knowledge documents, skills, and memory
Cadau’s place is an agent platform: the legacy system keeps sovereignty over “who may sign in and what data they manage”; Cadau provides configurable work agents, an embed widget, and — with host extras — Host Agent Run and data-connection policy. What users feel: “I still sign in to the old system; it just gained a reliable assistant.”
2. Architecture overview
2.1 Logical layers
flowchart TB
subgraph Legacy["Legacy system (host)"]
U["End user signs in"]
ADM["Admin: register templates, assign assistants"]
BFF["BFF: embed-session / agent-run"]
DB[(Business DB + assignment tables)]
UI["Business page + embed widget"]
U --> UI
ADM --> DB
BFF --> DB
UI --> BFF
end
subgraph ML["Cadau"]
WS["Workspace"]
UA["Template agent"]
ET["embed-token / host agent-runs"]
DS["Data connection + access_policy"]
CHAT["Conversation / tools / retrieval"]
WS --> UA
ET --> UA
UA --> CHAT
CHAT --> DS
end
BFF -->|"Integration account"| ET
UI -->|"Short-lived embed JWT"| CHAT
BFF -->|"host_actor"| ET
DS -->|"Read legacy business DB"| DB2.2 Three kinds of account (keep them distinct)
The easiest mix-up during integration is “how many users do we create in Cadau?” — the answer is only an integration account, not end users.
| Identity | Where it lives | Scale | Role |
|---|---|---|---|
| Cadau integration account | Cadau | One set per legacy deployment (or per customer) | Server-side only: sign in to Cadau, mint embed-tokens, call Host Agent Run; password in MINDLINK_* env vars |
| Legacy sign-in user | Legacy business DB | Real headcount | Admins, managers, employee self-service; sign in on legacy pages |
| Cadau end user | — | 0 | Not needed; identity is expressed as host_actor |
The integration account is not the “demo admin” in the legacy system. The former is a machine minting identity; the latter is a real business user whose UUID appears in host_actor.external_user_id.
2.3 Concept mapping
| Legacy side | Cadau side |
|---|---|
| One host system (may include several business tenants) | One workspace (this phase; MINDLINK_WORKSPACE_ID) |
| Admin / manager / employee self-service roles | A few template agents (can share one; data scope differs by policy) |
| Legacy user ID | host_actor.external_user_id |
| Employee file ID (self-service) | host_actor.employee_id (required) |
| Tenant ID | host_actor.tenant_external_id |
| App integration id | app_id (e.g. mindlink-embed-hr) |
3. Core mechanics
3.1 host_actor: the legacy user’s “passport”
On every embed conversation or server-side run, the legacy BFF builds a host_actor JSON so Cadau knows who is doing the work, what role they have, and whom they can manage.
{
"external_user_id": "<legacy-system user UUID>",
"actor_kind": "business",
"display_name": "Zhang Ming",
"tenant_external_id": "<tenant UUID>",
"employee_id": "",
"roles": ["manager"],
"managed_org_unit_ids": ["<department UUID>"],
"managed_employee_ids": ["<report employee UUID…>"]
}
| Field | Notes |
|---|---|
actor_kind | business (manages others) or employee (self only) |
external_user_id | Legacy sign-in user primary key; one of the conversation isolation keys |
employee_id | Required for employee self-service; must already be bound to an employee file in the legacy system |
managed_* | Manager scenes: managed departments / report list, for policy match |
Cadau writes this into embed registration and conversation context. When a data-connection tool runs a query, the policy engine reads the same object for forced filters.
3.2 Conversation isolation: same template, different people, no crossed history
Under host extras, the conversation key is:
(workspace_id, user_agent_id, app_id, host_actor.external_user_id)
So:
- The whole company can share one “employee self-service assistant” template agent
- Zhang and Li have different
session_ids; histories are not visible to each other - You do not create a Cadau agent per person, and you do not click “generate token” per person
3.3 Template agents + assignment by role
The legacy database usually has two logical tables (HR example names for contrast):
| Table | Meaning |
|---|---|
hr_mindlink_agents (rename as you like) | Register Cadau template agent IDs, workspace ID, app_id, role-template flags |
hr_user_agent_assignments | Legacy sign-in user → which template + a note on behavior scope |
Silent provisioning: when a legacy user is created, write an assignment from admin / manager / employee (and similar) roles. If they open the assistant the first time with no assignment yet, the BFF can fill a default (HR example EnsureDefaultAgentAssignment).
End users never pick an agent in the Cadau UI; a legacy admin configures it under “user management”.
3.4 Data connections and row/field policy
The assistant reads the legacy business database (SQLite / MySQL / Postgres, …) through a workspace data connection. Skill write-ups are not enough — you must configure access_policy_json on the connection so the tool-execution layer enforces:
- Row level: append
force_params/row_filter_params(the model cannot strip them) - Field level:
allowed_columns/ deny sensitive columns
Policy rules match on host_actor, for example:
{
"require_host_actor": true,
"rules": [
{
"when": { "actor_kind": "employee" },
"tables": {
"hr_employees": {
"row_filter_params": {
"id": "{{host_actor.employee_id}}",
"tenant_id": "{{host_actor.tenant_external_id}}"
},
"allowed_columns": ["id", "emp_no", "full_name", "org_unit_id"]
}
}
}
]
}
The HR example provides GET …/mindlink-access-policy, which compiles a policy draft from this tenant’s permissions and org data; an admin confirms, then PUTs it onto the Cadau data connection. Skill docs can describe the same bounds; they cannot replace enforcement on the connection.
4. Two integration paths
4.1 Page embed (users see the assistant)
Fits: a conversation widget in the corner or side of a legacy Web page.
sequenceDiagram participant U as Legacy user browser participant H as Legacy BFF participant M as Cadau U->>H: Signed in, opens a business page H->>H: Read user assignment + build host_actor H->>M: Integration-account login + switch workspace H->>M: POST embed-token (includes app_id) M-->>H: Short-lived access_token H-->>U: embed-session (token + user_agent_id + host_actor) U->>M: Widget chat/stream (embed JWT) M->>M: Isolate conversation / policy fetch by host_actor
Points:
- The browser only gets a short-lived embed JWT; no Cadau password
- The legacy frontend loads
mindlink-widget.min.js, and oninitpassesapi_base_url(prefer a same-origin proxy/mindlink-api) andhost_actor(same as embed-session issued) - BFF API:
GET /tenants/{id}/me/embed-session; if unassigned, return200+{ "available": false }
4.2 Host Agent Run (server-side run; users never see Cadau)
Fits: batch jobs, approval summaries, chatbot APIs, and similar that used to call an LLM directly from the legacy backend — now they go through a work agent.
POST /api/v1/host/agent-runs
Authorization: Bearer <integration-account access_token>
Content-Type: application/json
{
"user_agent_id": "<template-agent UUID>",
"app_id": "mindlink-embed-hr",
"message": "Summarize this month’s hires and leavers for my department",
"stream": false,
"fresh": false,
"host_actor": { "...": "same as embed" }
}
The legacy side can wrap POST …/me/agent-run and forward internally to Cadau. Page embed and Agent Run share the same host_actor and policy, so you do not get “one permission model on the page, another on the API”.
5. Implementation steps (from zero to testable)
Recommended order. The HR multi-tenant example (examples/hr-multi-tenant) and sdk/host-embed/ are a reference implementation.
Phase A: Cadau side
- Create a workspace (this phase: the whole host shares one Cadau workspace)
- In that workspace, create or pick template agents, and attach knowledge documents (business wording, which pages to open)
- Agree a globally unique
app_id - Create a Cadau account used only for integration, invite it into that workspace (it need not own an agent)
- Configure a data connection pointing at the legacy business database; predefined queries should be generated as JSON from the legacy codebase and imported (see Generating predefined queries from legacy code) — do not hand-write a second SQL set in Cadau that will drift
- Write
access_policy_json(the legacy system can compile a draft first; a person confirms)
Phase B: Legacy side
- Environment variables (server only; not in the frontend repo):
``env MINDLINK_API_BASE=https://your-mindlink/api/v1 MINDLINK_INTEGRATION_EMAIL=integration@your-corp.com MINDLINK_INTEGRATION_PASSWORD=*** MINDLINK_APP_ID=your-corp-hr MINDLINK_WORKSPACE_ID=<workspace UUID> HRMS_SEED_MINDLINK_USER_AGENT_ID=<template-agent UUID> ``
- Tables: register template agents and user assignments (see §3.3)
- BFF:
- Integration-account login → POST /workspaces/{id}/switch → POST /user-agents/{id}/embed-token - Response includes host_actor
- Frontend: business page fetches embed-session → loads the widget → same-origin proxy to Cadau API
- Provisioning: create users as usual in the legacy system; write assignments by role; employee self-service must bind an employee file
Phase C: Local HR example, one-command probe
If you use the in-repo example (Cadau already up on :8080):
cd examples/hr-multi-tenant/web
npm run setup:mindlink
The script registers (or signs in) the demo integration account hr-embed-demo@mindlink.local and writes MINDLINK_* into ../server/.env. Do not use that email in production; create your own integration account.
In another terminal, start the HR backend and frontend:
cd examples/hr-multi-tenant/server
go run ./cmd/hrms-server
cd examples/hr-multi-tenant/web
npm run dev
# Browser http://localhost:5180
# Demo account 13800138000 / Demo-HR-2026
6. How to test
Two layers: automated smoke (fast) and business acceptance (full).
6.1 Automated smoke
In examples/hr-multi-tenant/web, with both Cadau and the HR backend running:
| Command | What it checks | Common failure causes |
|---|---|---|
npm run smoke:embed | Widget script reachable → HR sign-in → embed-session → Cadau conversation with embed JWT | MINDLINK_* unset, integration account not in the workspace, wrong agent ID |
npm run smoke:host-agent | Policy compile non-empty → admin/manager/employee host_actor correct → agent-run conversation isolation | Missing employee_id, policy API not implemented, BFF omitted host_actor |
Smoke does not replace hand verification of data connections and row/field policy, but it quickly answers: “is the chain up?”
6.2 Manual acceptance (required for host extras)
See examples/hr-multi-tenant/docs/ACCEPTANCE_HOST_LEGACY.md:
- Data connection: in the Cadau workspace, create a SQLite (or other) connection whose path is the legacy business DB (e.g.
…/server/data/hrms.db); test connection succeeds - access policy: admin pulls a policy draft from the legacy system and writes it onto the Cadau data connection
- Employee self-service (e.g.
13800138010): ask “what is my employee number”; result is self only - Manager (e.g.
13800138001): can look up reports; sensitive columns such as salary are stripped - Admin: wider scope, still keyed by tenant
- Conversation isolation: two accounts
agent-runin turn; differentsession_id, histories do not mix - Experience: Cadau sign-in / register does not appear in the UI
6.3 Recommended integration order
Data connection + import predefined queries generated by the legacy system
→ write access_policy
→ smoke:embed
→ smoke:host-agent
→ ask numbers by role, by hand
→ page embed and navigation (optional)
7. Security and ops
| Topic | Suggestion |
|---|---|
| Integration account | Production-only, strong password, rotatable; rights only “enter workspace + mint tokens” |
| Credentials | Embed JWT short TTL; issue only over HTTPS; never write into frontend static config repos |
| Network | Browser reaches Cadau API through a legacy same-origin proxy; avoid CORS and mixing localhost / 127.0.0.1 |
| Audit | The agent owner in Cadau run log → Embed can read conversations (not in the integration account’s personal Messages) |
| Policy | Fetches follow data-connection policy; host API keys and embed assignments are different dimensions |
| SQLite path | The Cadau process must be able to read the file; if DATA_SOURCE_ALLOWED_HOSTS is set, the path must be under an allowed prefix |
8. FAQ
| Symptom | Where to look |
|---|---|
| embed-session 502 | Integration account not in the workspace; MINDLINK_WORKSPACE_ID does not match the agent’s actual workspace; app_id mismatch |
| Assistant appears but fetches overreach | access_policy_json not written or no require_host_actor; host_actor missing employee_id |
| Employee self-service unavailable | Legacy system did not bind hr_user_employee_links; actor_kind is not employee |
| Two people share one conversation | BFF omitted or wrong external_user_id; static path A used one token for everyone |
link:mindlink-agent says the agent does not exist | Cadau primary DB is Postgres but the script read SQLite mindlink.db; invite the integration account in the Cadau UI and re-run setup:mindlink |
| Does each person need a Cadau account? | No; integration account + host_actor only |
9. Difference from plain embed (no host extras)
| Capability | Plain embed SDK | Legacy host extras |
|---|---|---|
| End users enter Cadau | Not needed | Not needed |
| Isolate conversations by legacy user | Depends on embed registration binding | host_actor.external_user_id forced isolation |
| Read the legacy business DB | Optional, coarse policy | access_policy_json row/field enforcement by role |
| Server-side LLM run | You wire a model yourself | Host Agent Run always through a work agent |
| Typical docs | sdk/host-embed/README.md | This article + [宿主增强-AgentRun与数据权限.md](/docs/sdk-host-agent-run) |
If the legacy system only needs “one assistant identical for everyone, no business-DB fetches”, start with host-embed path B. Once you need per-person fetches, server-side LLM runs, and conversation isolation, turn on the full host extras.
10. Docs and code index
| Resource | Notes |
|---|---|
sdk/host-embed/README.md | Third-party embed overview, smoke notes |
| [sdk/host-embed/宿主增强-AgentRun与数据权限.md](/docs/sdk-host-agent-run) | User model, checklist, API and policy JSON |
| [sdk/host-embed/参考范例-HR接入指南.md](/docs/sdk-host-hr-guide) | HR example APIs, tables, config |
examples/hr-multi-tenant/docs/HOST_LEGACY_INTEGRATION.md | Legacy mapping source of truth (in-repo) |
examples/hr-multi-tenant/docs/ACCEPTANCE_HOST_LEGACY.md | Manual acceptance list |
examples/hr-multi-tenant/web/scripts/smoke-embed.mjs | Embed smoke script |
examples/hr-multi-tenant/web/scripts/smoke-host-agent.mjs | Host-extras smoke script |
11. Wrap-up
Embedding Cadau in a legacy system means keeping “who is using the assistant” and “which rows and columns the assistant may read” under legacy sovereignty, while reusing Cadau’s agents, knowledge, conversation, and audit. Technically, remember:
- Whole host → one Cadau workspace (this phase)
- A few template agents +
host_actor→ everyone covered - Integration account mints tokens on the server only; end users never enter Cadau
- Business-DB permissions are forced in data-connection policy, not by hoping the prompt behaves
- Predefined queries are generated as JSON from the legacy codebase and imported — see Generating predefined queries from legacy code
Implement §5, test §6, and use the HR example plus the host-embed pack to go from architecture to a testable integration environment in a short time.