Plugin API contract
The API contract between a plugin app and Cadau, for the integrating team.
Source docs/en/site/sdk-platform-api.md
Platform plugin REST API contract.
Base URL: {Cadau site}/api/v1 Version: 0.1 (aligned with Cadau backend platform_plugin_*.go)
Scope: the REST below and
platform_plugins[]registration are only for apps-desktop iframe plugins. To let a work assistant call a business API from conversation (face recognition, drawing, and similar), use skills + internet capability pack. See [../../agent-capability/README.md](/docs/sdk-agent-capability) and [../README.md](/docs/sdk-platform-plugin).
0. Registration and plugin gateway
Register in Cadau mindlink.json → platform_plugins[] (example registration.example.json):
{
"module_id": "my-plugin",
"display_name": "我的插件 (My plugin)",
"entry_base_url": "/p/my-plugin/",
"upstream_url": "http://127.0.0.1:3010",
"enabled": true
}
| Field | Meaning |
|---|---|
module_id | Unique id; gateway path is /p/{module_id}/ |
entry_base_url | Launch appends launch_token then gives it to the iframe; prefer same-origin /p/{module_id}/ |
upstream_url | Optional; Cadau reverse-proxies /p/{module_id}/* to this internal URL |
enabled | When false, the desktop does not show it |
Gateway request path (browser → Cadau Web → backend → upstream):
GET /p/my-plugin/api/health
→ reverse-proxied to {upstream_url}/api/health (prefix /p/my-plugin is stripped).
Without upstream: only entry_base_url points at your public/standalone URL; Cadau does not proxy static pages or plugin APIs.
Implementation: backend/internal/platformplugin/proxy.go; Web Nginx location /p/ → backend.
1. Cadau side (user signed in)
1.1 List registered plugins
GET /platform-plugins
Authorization: Bearer {user access_token}
Response items[]: module_id, display_name, suite, icon, entry_base_url, enabled (internal upstream_url is not returned to the client)
1.2 Launch a plugin
POST /workspaces/{workspace_id}/platform-plugins/{module_id}/launch
Authorization: Bearer {user access_token}
Response:
| Field | Meaning |
|---|---|
module_id | Plugin id |
display_name | Human-readable app name. Chinese then English in parentheses, for example “邮件 (Email)”, so people who cannot read Chinese can still recognize it |
launch_token | JWT for the plugin, about 15 minutes; the Cadau shell silently renews before expiry and pushes to the iframe |
entry_url | iframe entry, already with launch_token and workspace_id query params |
expires_at | ISO 8601 |
api_base_url | API prefix relative to the site, usually /api/v1 |
workspace_id | Current workspace |
member_role | owner / admin / member |
data_namespace | Workspace plugin data isolation (same shape as session) |
1.3 Skills shipped with the plugin (install into the workspace skill center)
An app-style plugin may ship skills/{slug}/ in the repo (see platform-plugin §7.5). Installing into the workspace skill center uses user sign-in auth, not launch_token.
Product voice: write into the skill center idempotently by slug; agents in the workspace can recall by default; do not rewrite each agent’s available-skills narrow list at seed time.
GET /workspaces/{workspace_id}/platform-plugins/{module_id}/skills
Authorization: Bearer {user access_token}
Response:
| Field | Meaning |
|---|---|
module_id | Plugin id |
skills[] | slug, installed, up_to_date, needs_update, skill_id, name, bundle_fingerprint, installed_fingerprint |
any_missing | Whether any item is not installed |
any_needs_update | Whether any installed item’s fingerprint is behind |
can_manage_members | Whether the current user can seed |
With no registered shipped skills, returns skills: [] (still 200).
POST /workspaces/{workspace_id}/platform-plugins/{module_id}/skills/seed
Authorization: Bearer {user access_token}
Content-Type: application/json
{ "slug": "practice-checklist-to-pack" }
- Requires a member who can manage.
slugis optional; omit to install/upgrade all registered shipped skills for that module.- Response:
{ "ok": true, "module_id", "results": [ { slug, created, upgraded, skipped, skill_id, name, fingerprint } ] }. - Unknown
module_id/slug(no registered pack) →404.
Implementation: backend/internal/skillfromchat/plugin_skill_seed.go (RegisterPluginSkillBundle); compliance registration in the same package compliance_practice_seed.go; author mirror: plugins/compliance/skills/.
2. Plugin side (Bearer launch_token)
The following APIs do not need a Cadau user cookie, only:
Authorization: Bearer {launch_token}
2.1 Verify session
GET /platform-plugins/session
Response:
{
"module_id": "my-plugin",
"module_display_name": "我的插件 (My plugin)",
"user_id": "uuid",
"user_display_name": "Zhang San",
"workspace_id": "uuid",
"workspace_name": "R&D team",
"member_role": "admin",
"expires_at": "2026-07-02T10:00:00Z",
"data_namespace": {
"kind": "postgres_schema",
"location": "ws_…",
"schema": "ws_…"
}
}
data_namespace: workspace plugin data isolation (Postgres schema or SQLite file path/DSN). Launch response includes the same field.
Agents calling the plugin: tool plugin_invoke → Cadau issues a short-lived JWT (knd=plugin_invoke) → plugin POST /api/agent/invoke. Operation list: plugin operations.json (or configure operations_url).
Error codes: plugin_launch_invalid (expired or invalid), plugin_disabled, forbidden
2.1.1 Workspace members (for binding accounts)
GET /platform-plugins/workspace-members
Bearer launch_token. Returns a short list of current workspace members (user_id, display_name, email, phone, member_role) so the plugin can bind employee files and appoint in-plugin roles. Any workspace member can read.
2.1.2 Send SMS / email on behalf
GET /platform-plugins/notify
POST /platform-plugins/notify
Bearer is launch_token, or a short-lived JWT the plugin issues with the same jwt_secret as Cadau (knd=plugin_notify, wid / mod). Used for camera callbacks and similar alerts when there is no user session.
Query channels GET: same checks as send-on-behalf (local channel or router already enabled). Do not guess in the plugin.
{ "sms": { "configured": true }, "email": { "configured": false }, "wecom": { "configured": false, "note": "WeCom messages are not enabled yet" } }
Send POST:
{ "channel": "sms", "to": "13800138000", "text": "201811" }
{ "channel": "email", "to": "ops@example.com", "subject": "Security alert", "text": "body", "html": "<p>body</p><p><img src=\"cid:snap1\" alt=\"capture\"></p>", "images": [{ "cid": "snap1", "content_type": "image/jpeg", "filename": "capture.jpg", "data_base64": "…" }] }
channel:smsoremail(WeCom messages not enabled yet)- SMS uses the site SMS channel (first template item is the alert summary; prefer ≤ 60 characters)
- Email uses the site mail channel; custom subject and body; security alerts may include
htmland up to twoimages(JPEG, cid-embedded and as attachments, so webmail can show the capture) - At most 40 messages per minute per workspace
Error codes: sms_not_configured, smtp_not_configured, invalid_phone, invalid_email, rate_limited
2.2 Knowledge documents
Paths are relative to the plugin theme directory plugin-{module_id}/ (Cadau maintains the index).
List
GET /platform-plugins/knowledge/items
Read
GET /platform-plugins/knowledge/content?path=guide.md
Write (member_role owner/admin)
PUT /platform-plugins/knowledge/content
Content-Type: application/json
{
"path": "guide.md",
"content": "# Title\n\nBody…",
"title": "Getting started",
"summary": "One-line summary for retrieval matching",
"tags": ["tag1", "tag2"]
}
Delete (owner/admin)
DELETE /platform-plugins/knowledge/content?path=guide.md
Batch sync (owner/admin)
POST /platform-plugins/knowledge/sync
Content-Type: application/json
{
"documents": [
{ "path": "a.md", "content": "…", "title": "A" },
{ "path": "b.md", "content": "…", "title": "B" }
]
}
Response: { "ok": true, "synced": ["a.md","b.md"], "count": 2, "theme_dir": "plugin-my-plugin" }
Scope: plugin knowledge sync is Markdown body (
path+content). Images/short video and other rich media in workspace knowledge are managed on the Cadau product side and are not uploaded via this sync API.
2.2.1 Standards library read-only (workspace regulations and management standards)
Source of truth and publish flow: product §3.2.2 / docs/core-mechanisms/规范库.md. The plugin may read only published and in-effect entries.
GET /platform-plugins/standards
GET /platform-plugins/standards?tag=hr
GET /platform-plugins/standards/{standardId}
GET /platform-plugins/standards/{standardId}/content
Auth: Authorization: Bearer <launch_token> (same as knowledge APIs).
2.2.2 Asset library read-only (workspace templates and tables)
Source of truth and publish flow: product §3.2.3 / docs/core-mechanisms/资料库.md. The plugin may read only published entries;
/filereturns a binary file stream.
GET /platform-plugins/assets
GET /platform-plugins/assets?tag=hr
GET /platform-plugins/assets/{assetId}
GET /platform-plugins/assets/{assetId}/file
Auth: Authorization: Bearer <launch_token> (same as knowledge APIs).
2.2.3 Workspace model (structured JSON)
Plugins must not bring their own model API key. For short-lived structured results such as “generate a survey from this body / recommend staffing”, use this API with the large model already configured for this workspace.
Unlike “in-plugin embed assistant conversation” (§2.3 embed): this API is a one-shot server
system+user→ JSON text, with no conversation state.
POST /platform-plugins/ai/json
Authorization: Bearer <launch_token>
Content-Type: application/json
{
"system": "You are……output one JSON object only: {...}",
"user": "{ \"context\": \"…\" }"
}
Response:
{ "text": "{ \"…\": \"JSON string the model returned (may still include prose; the caller should parse/clean)\" }" }
| Constraint | Meaning |
|---|---|
| Auth | launch_token (same as knowledge / standards library) |
system / user | Both required; length counted in Unicode characters: system ≤ 32000, user ≤ 400000 (fits large-context models; business should still split by task) |
| Model not configured | HTTP 503, code ≈ llm_disabled, copy says the workspace has no large model yet |
| Call failed | HTTP 502, code ≈ llm_error |
| SDK | Go Client.AIJSON · JS client.aiJSON(system, user) |
| Optional | attachment_ids attach images/PDF; pdf_pages=true attaches page screenshots even when the PDF has a text layer (used to extract ID photos) |
Official usage: plugins/hr staffing recommendations, resume photo and certificate extract, plugins/compliance generate surveys from standards.
2.2.4 Plugin upload extract / crop
Resume analysis and similar extract embedded images from an already uploaded PDF / Word (.docx) / web resume / image, or crop ID photos and certificate scans by a normalized box. Results are written to the current user’s upload library and return a new
file_id.
POST /platform-plugins/uploads/{id}/extract-images
Authorization: Bearer <launch_token>
POST /platform-plugins/uploads/{id}/crop
Authorization: Bearer <launch_token>
Content-Type: application/json
{ "items": [{ "page": 1, "x": 0.7, "y": 0.05, "width": 0.25, "height": 0.28, "label": "Applicant photo" }] }
x/y/width/height are 0–1 relative to the page (or the whole image). Both responses are { "images": [{ "file_id", "filename", "mime_type", "width", "height", "page" }] }.
2.3 In-plugin assistant token
POST /platform-plugins/embed-token
Content-Type: application/json
{
"user_agent_id": "uuid",
"app_id": "my-plugin",
"ttl_seconds": 3600
}
Response is similar to POST /user-agents/{id}/embed-token: access_token, token_type, expires_in, expires_at, workspace_id, user_agent_id, app_id, permanent, record_id
Then the frontend loads {Cadau}/embed/mindlink-widget.min.js and init (see the embed SDK contract).
3. Plugin-owned HTTP conventions (recommended)
| Path | Meaning |
|---|---|
POST /api/bootstrap | body { "launch_token" } → verify session, optionally sync knowledge, return user context (including data_namespace) |
GET /api/session | Refresh session with the launch token in Bearer or cookie |
GET /api/health | Health check |
GET /operations.json | Operations an agent can call; if there is a fill dialog, must include fill_open_form (see §3.2); may declare workflow_steps in the same file; or set operations_url at registration |
POST /api/agent/invoke | (Optional) agent plugin_invoke and workflow auto-nodes; verify JWT (knd=plugin_invoke), not launch_token |
iframe session: frontend stores launch_token in sessionStorage; later requests send Authorization: Bearer …. Plugin-owned API paths should use a relative path or resolvePluginAPI(), so they work with the Cadau same-origin gateway /p/{module_id}/.
3.1 Host ↔ plugin postMessage
| Direction | type | Meaning | |
|---|---|---|---|
| Host → plugin | mindlink:launch_token | Silent renew; must write sessionStorage (JS: listenHostLaunchToken) | |
| Host → plugin | mindlink:theme | theme: light \ | dark (JS: listenHostTheme) |
| Host → plugin | mindlink:locale | locale: zh \ | en (JS: listenHostLocale) |
| Host → plugin | mindlink:plugin_tab | Help deep-links and similar switch in-plugin tab (JS: listenHostPluginTab) | |
| Host → plugin | mindlink:plugin_data_changed | Business data changed; suggest refresh (JS: listenHostPluginDataChanged) | |
| Host → plugin | mindlink:plugin_form_fill | Write fields the assistant parsed back into the current draft (§3.2; JS: listenHostPluginFormFill) | |
| Plugin → host | mindlink:request_launch_token | source must be mindlink-plugin; ask for a token on hot reload/401 (JS: requestHostLaunchToken) | |
| Plugin → host | mindlink:request_locale | source must be mindlink-plugin; ask for current language on first screen or hot reload (JS: requestHostLocale) | |
| Plugin → host | mindlink:plugin_ui_changed | Report the currently open fill dialog (§3.2; JS: postPluginUiChanged) |
Host messages have source mindlink; plugin messages have source mindlink-plugin.
3.2 Recognize the open form (host protocol)
Plugins with a fillable dialog must implement this. Product voice: [../README.md §6.1](/docs/sdk-platform-plugin); spec §4.1.5.
When the user opens a fill dialog, the app assistant must recognize this form, write natural language back into the draft, and show it immediately; until the user says “save / create / submit / complete”, do not write the DB.
#### 3.2.1 Plugin → host: mindlink:plugin_ui_changed
window.parent.postMessage, source must be mindlink-plugin. JS: postPluginUiChanged.
{
"source": "mindlink-plugin",
"type": "mindlink:plugin_ui_changed",
"module_id": "my-plugin",
"tab": "home",
"dialog": "draft",
"dialog_title": "Example draft",
"form": { "name": "Zhang San" },
"field_hints": "Name,notes",
"readonly": false
}
| Field | Meaning |
|---|---|
module_id | Same as registration |
tab | Current tab (optional) |
dialog | Current dialog id; on close pass an empty string, and the host clears “the form being filled” |
dialog_title | User-visible title (used when asked “what is on screen”) |
form | Already-filled non-empty fields (strings); an empty draft may report only dialog + field_hints |
field_hints | User-language names of writable fields, comma-separated, for example Name,notes |
readonly | true: view dialog; the assistant only names the title and does not call fill_open_form |
Cadau puts dialog / dialog_title / form / field_hints / readonly into conversation client_context.
#### 3.2.2 Operation fill_open_form
In operations.json, name must be fill_open_form, mutates: false. Cadau POST /api/agent/invoke body:
{
"operation": "fill_open_form",
"args": {
"form": "draft",
"fields": { "name": "Zhang San", "备注": "urgent" }
}
}
args | Meaning |
|---|---|
form | Same as current dialog; may also accept dialog / case_type |
fields | Key-values to write; Chinese or English field names; may nest; host/SDK flatten |
Plugin success response (may be wrapped in { "ok": true, "result": { … } }; the host looks down for ui_action):
{
"ui_action": "fill_form",
"form": "draft",
"fields": { "name": "Zhang San", "note": "urgent" },
"unresolved": [],
"note": "Written into the currently open form; not saved to the system yet"
}
| Field | Meaning |
|---|---|
ui_action | Must be fill_form, or the host will not write back to the UI |
form | Target draft id |
fields | Normalized strings that can go straight into inputs (names already resolved to ids belong here) |
unresolved | Optional; { "field", "value", "reason" }[], tell the assistant when it does not match an existing record |
note | Optional; one sentence the assistant can read |
Do not INSERT/UPDATE business tables in this operation. When you need to resolve “R&D” → department id, only change fields; still do not write the DB.
Go: FillOpenFormPassthrough(body.Args); TS: fillOpenFormPassthrough(args). Skeleton: templates/starter/cmd/plugin/main.go.
#### 3.2.3 Host → plugin: mindlink:plugin_form_fill
After the Cadau shell parses ui_action=fill_form, it postMessages the iframe (source=mindlink). JS: listenHostPluginFormFill.
{
"source": "mindlink",
"type": "mindlink:plugin_form_fill",
"module_id": "my-plugin",
"form": "draft",
"fields": { "name": "Zhang San", "note": "urgent" }
}
The plugin merges fields into the currently open draft whose dialog matches, and refreshes inputs immediately. mutates: false; the host does not refresh lists because of this.
3.3 Extra workflow steps and list contract (workflow_steps / rubric)
A plugin can declare workflow_steps in the same operations.json, hanging domain auto-nodes on the process designer (they appear in the catalog only when that plugin is enabled). When the engine reaches that step it calls POST /api/agent/invoke (same entry as plugin_invoke).
{
"module_id": "my-plugin",
"operations": [
{ "name": "workflow_load_checklist", "mutates": false, "min_role": "member" }
],
"workflow_steps": [
{
"type": "my-plugin.load_checklist",
"label": "Load material list",
"description": "Write instance rubric for upload evidence / auto score",
"color": "#0891b2",
"kind": "auto",
"operation": "workflow_load_checklist",
"config_hint": "Optional phases"
}
]
}
workflow_steps[] field | Meaning |
|---|---|
type | Step type (prefer {module_id}.{action}) |
kind | Currently only auto: on arrival Invoke, then advance on always after success |
operation | Matching operation name in operations |
label / description / color | Designer display |
Success response (load-style nodes return rubric; write-back nodes may return only variables):
{ "ok": true, "rubric": [ /* RubricItem[] */ ], "approval_doc": { }, "variables": { } }
rubric: written onto the instance for core nodes upload evidence (human.upload) and auto score (llm.score); must matchRubricItembelow. Required for load-style nodes.approval_doc: optional. Business-document snapshot shown on the approval step (same idea as check items). Can also be sent invariables.approval_docat start.variables: merged into instance variables (flat keys). Write-back nodes (confirm staffing request, and similar) use this forstaffing_req_statusetc.- Failure:
{ "ok": false, "error": "user-visible explanation", "rubric": [] }. - Put
okat the HTTP body top level (do not nest it only underresult). Seeplugins/complianceload standards andplugins/hrconfirm/return staffing request.
#### RubricItem (list item · implementation source of truth)
Aligned with backend backend/internal/standards.RubricItem and SDK type RubricItem (Go / TS). Other domain load nodes (onboarding materials, training confirmation, and similar) must output the same shape; do not invent field names.
| Field | Type | Required | Meaning | ||
|---|---|---|---|---|---|
id | string | Yes | Stable primary key; upload item_photos, comments, and score results all attach by this id | ||
requirement_text | string | Yes | Requirement copy shown on the handling page | ||
scoring_text | string | Upload can be loose; recommended when connecting auto score | Pass/score criteria | ||
code | string | No | Display number | ||
phase | string | No | Phase key (compared lowercase); for upload-node config.phases filter | ||
tier | string | No | red \ | baseline \ | excellence |
photo_standard_text | string | No | Photo/evidence hint | ||
reference_photo_upload_ids | string[] | No | Passing sample image upload_ids (at most 3); handling page shows them for comparison | ||
must_pass | bool | No | Red-line item; default false | ||
iway_ref | string | No | Domain reference; omit if not generally needed |
Reference: plugins/compliance workflow_load_check_items → toRubric. Product notes: docs/core-mechanisms/workflow.json-v1.md §check-item list contract; mechanism guide: [../README.md](/docs/sdk-platform-plugin) §6.2.
3.4 Cookie / storage names (do not mix)
| Name | Who writes | Use |
|---|---|---|
ml_plugin_gw | Cadau gateway | Path=/p/{module_id}/; gateway auth when the browser hits the reverse-proxy path |
ml_plugin_launch | Plugin process (Go bootstrap) | Helper cookie on the plugin origin (optional) |
ml_plugin_launch_token | Plugin frontend sessionStorage | Bearer source of truth; must update after renew |
4. Security response headers (plugin service)
Content-Security-Policy: frame-ancestors 'self' http://localhost:8080 http://127.0.0.1:8080
In production, replace with the customer Cadau Web origin. Go SDK: mindlinkplugin.FrameAncestorsCSP(...).
5. Error body
{
"error": "user-visible explanation",
"code": "machine_code",
"request_id": "…"
}
Common code: unauthorized, plugin_launch_invalid, forbidden, validation_error, not_found