All posts
Updated

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 whatPoint
Legacy systemSign-in, roles, business permissions, binding employee files, which assistant template each person gets
CadauTemplate agents, conversation and retrieval, embed tokens, Host Agent Run, row/field policy enforcement on data connections
Every requestThe 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"| DB

2.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.

IdentityWhere it livesScaleRole
Cadau integration accountCadauOne 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 userLegacy business DBReal headcountAdmins, managers, employee self-service; sign in on legacy pages
Cadau end user0Not 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 sideCadau side
One host system (may include several business tenants)One workspace (this phase; MINDLINK_WORKSPACE_ID)
Admin / manager / employee self-service rolesA few template agents (can share one; data scope differs by policy)
Legacy user IDhost_actor.external_user_id
Employee file ID (self-service)host_actor.employee_id (required)
Tenant IDhost_actor.tenant_external_id
App integration idapp_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…>"]
}
FieldNotes
actor_kindbusiness (manages others) or employee (self only)
external_user_idLegacy sign-in user primary key; one of the conversation isolation keys
employee_idRequired 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):

TableMeaning
hr_mindlink_agents (rename as you like)Register Cadau template agent IDs, workspace ID, app_id, role-template flags
hr_user_agent_assignmentsLegacy 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 on init passes api_base_url (prefer a same-origin proxy /mindlink-api) and host_actor (same as embed-session issued)
  • BFF API: GET /tenants/{id}/me/embed-session; if unassigned, return 200 + { "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

  1. Create a workspace (this phase: the whole host shares one Cadau workspace)
  2. In that workspace, create or pick template agents, and attach knowledge documents (business wording, which pages to open)
  3. Agree a globally unique app_id
  4. Create a Cadau account used only for integration, invite it into that workspace (it need not own an agent)
  5. 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
  6. Write access_policy_json (the legacy system can compile a draft first; a person confirms)

Phase B: Legacy side

  1. 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> ``

  1. Tables: register template agents and user assignments (see §3.3)
  2. BFF:

- Integration-account login → POST /workspaces/{id}/switchPOST /user-agents/{id}/embed-token - Response includes host_actor

  1. Frontend: business page fetches embed-session → loads the widget → same-origin proxy to Cadau API
  2. 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:

CommandWhat it checksCommon failure causes
npm run smoke:embedWidget script reachable → HR sign-in → embed-session → Cadau conversation with embed JWTMINDLINK_* unset, integration account not in the workspace, wrong agent ID
npm run smoke:host-agentPolicy compile non-empty → admin/manager/employee host_actor correct → agent-run conversation isolationMissing 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:

  1. 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
  2. access policy: admin pulls a policy draft from the legacy system and writes it onto the Cadau data connection
  3. Employee self-service (e.g. 13800138010): ask “what is my employee number”; result is self only
  4. Manager (e.g. 13800138001): can look up reports; sensitive columns such as salary are stripped
  5. Admin: wider scope, still keyed by tenant
  6. Conversation isolation: two accounts agent-run in turn; different session_id, histories do not mix
  7. 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

TopicSuggestion
Integration accountProduction-only, strong password, rotatable; rights only “enter workspace + mint tokens”
CredentialsEmbed JWT short TTL; issue only over HTTPS; never write into frontend static config repos
NetworkBrowser reaches Cadau API through a legacy same-origin proxy; avoid CORS and mixing localhost / 127.0.0.1
AuditThe agent owner in Cadau run log → Embed can read conversations (not in the integration account’s personal Messages)
PolicyFetches follow data-connection policy; host API keys and embed assignments are different dimensions
SQLite pathThe 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

SymptomWhere to look
embed-session 502Integration account not in the workspace; MINDLINK_WORKSPACE_ID does not match the agent’s actual workspace; app_id mismatch
Assistant appears but fetches overreachaccess_policy_json not written or no require_host_actor; host_actor missing employee_id
Employee self-service unavailableLegacy system did not bind hr_user_employee_links; actor_kind is not employee
Two people share one conversationBFF omitted or wrong external_user_id; static path A used one token for everyone
link:mindlink-agent says the agent does not existCadau 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)

CapabilityPlain embed SDKLegacy host extras
End users enter CadauNot neededNot needed
Isolate conversations by legacy userDepends on embed registration bindinghost_actor.external_user_id forced isolation
Read the legacy business DBOptional, coarse policyaccess_policy_json row/field enforcement by role
Server-side LLM runYou wire a model yourselfHost Agent Run always through a work agent
Typical docssdk/host-embed/README.mdThis 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

ResourceNotes
sdk/host-embed/README.mdThird-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.mdLegacy mapping source of truth (in-repo)
examples/hr-multi-tenant/docs/ACCEPTANCE_HOST_LEGACY.mdManual acceptance list
examples/hr-multi-tenant/web/scripts/smoke-embed.mjsEmbed smoke script
examples/hr-multi-tenant/web/scripts/smoke-host-agent.mjsHost-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:

  1. Whole host → one Cadau workspace (this phase)
  2. A few template agents + host_actor → everyone covered
  3. Integration account mints tokens on the server only; end users never enter Cadau
  4. Business-DB permissions are forced in data-connection policy, not by hoping the prompt behaves
  5. 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.