COLLAB PLATFORM MODULE DEVELOPMENT GUIDE Personal API Keys (v0.6): signed-in users self-serve POST /api/account/api-keys {name,expires_days:30}; GET the same URL lists only their key metadata; POST /api/account/api-keys//revoke {} revokes. Browser session+CSRF required for key lifecycle. The api_key secret appears once; only SHA256 hashes are stored. Use Authorization: Bearer for existing business/admin APIs without cookies or CSRF. Default scopes follow the owner's live permissions; optional permissions:[] narrows to explicit grants (empty array grants none). Account disabling, expiry and revocation take effect immediately. Keys cannot manage keys or login authentication. All registered users can submit applications and query their own submission IDs; simulation/approval and module lifecycle still require modules.manage. One personal key works across modules, with no per-module submission token requirement. Use scripts/agent_publish_module.py with OA_API_KEY in environment and optional --publish. Existing scoped submission tokens remain compatible. Protocol: 1. Platform: Windows amd64. Runtime: trusted local EXE via HashiCorp go-plugin net/rpc. This guide, /docs/api.json and /devkit.zip are embedded in platform.exe. The EXE does not need a source checkout to serve them. 1. Download SDK GET /devkit.zip. Extract into your NEW module source directory. Archive has platform-sdk/go.mod and platform-sdk/modulekit/module.go. The SDK is complete; do not import platform internals. Create go.mod: module example.local/workorder go 1.24.0 require collab.local/platform v0.0.0 replace collab.local/platform => ./platform-sdk Run go mod tidy before building. First build requires Go and dependency download access. EXEs require neither Go nor internet at runtime. 2. Implement Go interface import "collab.local/platform/modulekit" Business has: Describe() modulekit.Descriptor Execute(modulekit.Request, modulekit.Host) (modulekit.Response, error) main must call modulekit.Serve(&YourBusiness{}). Descriptor fields: ID string, Name string, Version string, Protocol int (1), Description string, Fields []modulekit.Field. Field fields: Name string, Label string, Type string (text|textarea|select), Required bool, Options []string, MaxLength int. Module ID and field names match ^[a-z][a-z0-9_-]{0,39}$. Descriptor must have 1-30 unique fields. ID/Version/Protocol must match manifest. Request fields: Action string (create|list), Payload json.RawMessage, RequestID string, ActorID string. Response fields: Data json.RawMessage, Error string, Code string. For validation errors return Response{Code:"INVALID_ARGUMENT",Error:"Chinese user-readable message"},nil. Transport/internal failures may return a Go error; host then stops module, requiring a start action. Unknown actions return Code NOT_FOUND. 3. Call host capabilities (reverse RPC) Host has Call(modulekit.HostRequest) (modulekit.HostResponse,error). HostRequest fields: Service string, Collection string, Payload json.RawMessage, RequestID string, ActorID string. HostResponse fields: Data json.RawMessage, Error string, Code string. Always propagate RequestID and ActorID from the current Execute input. Host verifies them against the active request and its connection-bound module ID. No calls after Execute returns, no asynchronous host calls or cached actors. Service identity.get: returns JSON object {id,name,mode}. Call it in create to demonstrate using the platform identity service. Service records.list: set Collection, returns JSON array of Record, newest first (empty array if none). Service records.create: set Collection and Payload (JSON object, max 32 KiB). Host generates metadata and atomically persists record + audit + idempotency result. Collection matches ^[a-z][a-z0-9_-]{0,39}$. Host automatically partitions by module ID; no arbitrary module or filesystem path arguments. Record fields: ID string json:id, CreatedAt string json:created_at, CreatedBy string json:created_by, Values json.RawMessage json:values. For list and create return Response{Data: reply.Data, Error: reply.Error, Code: reply.Code},nil after handling transport errors. Example: reply,err := host.Call(modulekit.HostRequest{Service:"records.create",Collection:"tickets",Payload:normalized,RequestID:r.RequestID,ActorID:r.ActorID}) if err != nil { return modulekit.Response{},err } return modulekit.Response{Data:reply.Data,Error:reply.Error,Code:reply.Code},nil 4. Workorder requirements for example Fields: title required text max 120 Unicode codepoints; reporter required text max 60; priority required select with exact options 普通,紧急; description optional textarea max 2000. Apply strings.TrimSpace on business fields and validate server-side. Reject unknown JSON fields and invalid types using json.Decoder.DisallowUnknownFields. Do not accept caller-provided status or metadata. Default priority to 普通 only when omitted/empty. Set status="已登记" in backend. Preserve normalized field order using a struct when marshaling, so create retries have stable payload. Action list uses records.list with tickets collection. Action create validates input, calls identity.get, then records.create. Platform UI renders any registered descriptor; do not edit the platform frontend or store data in the module itself. Request timeout: 10s per RPC. A timeout may happen after commit. Reuse request_id and exact business content when retrying. A reused key with different normalized payload returns CONFLICT. Host serializes actions in this prototype. 5. Package go mod tidy go build -trimpath -o workorder.exe . Create manifest.json: {"id":"workorder","name":"工单登记","version":"0.1.0","protocol":1,"entry":"workorder.exe","capabilities":["identity.get","records.list","records.create"]} Place manifest and EXE inside /modules/workorder/. Platform checks entry path, matching descriptor, protocol, grants. Discovery alone never executes a new module. 6. Register through platform HTTP GET / first and keep collab_local HttpOnly SameSite Strict cookie. GET /api/bootstrap returns csrf_token and requires_setup. Before protected API calls: if requires_setup is true, POST /api/auth/setup {name,password} to initialize local-admin; otherwise POST /api/auth/login {username,password}. Passwords have at least 12 characters. Replace your CSRF token with the token returned by setup/login, and keep the rotated session cookie. POST /api/auth/logout ends the session. POST requests use Content-Type: application/json and X-CSRF-Token plus cookie. POST /api/modules/register body {"id":"workorder","capabilities":["identity.get","records.list","records.create"]} The local administrator approves all listed capabilities by this request. Only install trusted binaries. GET /api/modules returns discovered and registered modules; running ones include descriptor. POST /api/actions/workorder/create body {"request_id":"a-unique-value-at-least-16-chars","payload":{"title":"维修打印机","reporter":"张三","priority":"普通","description":"无法打印"}} POST /api/actions/workorder/list body {"request_id":"a-different-unique-value","payload":{}} GET /api/audit returns latest 100 audit entries. POST /api/modules/workorder/stop body {}: stop now; registered module loads on next platform restart. POST /api/modules/workorder/start body {}: start/restart registered module. POST /api/modules/workorder/unregister body {}: stop and forget registration, retain business data. Unknown browser-supplied actor_id is rejected; host uses the authenticated account ID. Menu visibility and HTTP/RPC permissions follow assigned roles; do not assume local-admin for other accounts. 7. Verify Read these pages from the RUNNING platform before writing module source. Download and use SDK from platform, rather than referencing internal implementation. Record response hashes and build/test results. Verify register; dynamic form; valid create/list; required-field and enum errors; duplicate request returns same record; changed payload same key conflicts; audit; stop/start; platform restart restores registration and data. Module EXE is a child process, not a standalone public web server. Running it directly prints a plugin warning; start through platform. 8. Limits Local-only trusted module demo. Local account login and role authorization are implemented; no tenants, enterprise SSO, sandbox, dynamic SQL, enterprise workflow, arbitrary custom frontend, database, or automatic migration. Department membership does not isolate business records; business.read covers all modules. Persistence is JSON whole-file snapshots with synchronized replace, with a single-process data-directory lock. Stop platform before copying data for backup. Do not run multiple hosts on the same data directory. Registry persists approval; stop is temporary, unregister disables loading on restart. The host supervises unexpected module exits with bounded automatic restart; intentional stop suppresses recovery until explicit start or host restart. See section 11. 9. Administrative console (platform v0.2) GET /api/admin returns role-filtered departments/users/roles and revision. Save via POST /api/admin/departments, /users or /roles with revision from the current view; stale writes return 409. Permissions: modules.read, modules.manage, business.read, business.create, org.manage, users.manage, roles.manage, audit.read. Roles are unioned; business.read grants all-module record access. User managers cannot grant or manage permissions above their own. Built-in system-admin and the initial local-admin account are protected. Account disabling, password resets and permission changes take effect on existing sessions. Sessions expire after eight hours and all sessions expire on platform restart. Password hashes use PBKDF2-HMAC-SHA256 (600000 iterations) with a per-password random salt; API responses omit credentials. Module registration still requires manifest capability approval in addition to user permissions. Protocol v1 and the module SDK are unchanged. 10. Module-scoped permissions and role templates (platform v0.3, RPC remains protocol 1) Manifest may declare permissions with id/name/action, for example: "permissions":[{"id":"reimbursement.read","name":"查看报销","action":"list"},{"id":"reimbursement.create","name":"提交报销","action":"create"}] Declare these two base actions; optional workflow actions are described in section 15. IDs must be .read and .create. Omitting permissions synthesizes these defaults for older modules. Optional role_templates contain id/name/description/permissions. Each role ID starts with - and obeys the normal 40-character identifier limit. Template permissions may ONLY reference that module's declared permissions, never platform or another module's permissions. On successful module registration the platform persists its permission catalog and creates missing role templates. Templates never assign users, and re-registering never overwrites an existing role edited by the administrator. Use the unified Roles and Permissions page to select permissions grouped by module. Assign one or more roles in User Management. A module-only role does not need modules.read: module discovery automatically returns only that account's permitted modules. Both HTTP actions and module reverse calls enforce the connection-bound module's read/create permission. Existing business.read/business.create permissions remain explicit all-module compatibility grants, including future modules. Do not combine these broad roles with a scoped role if the intention is access to just one module. Unregistering retains role assignments and marks catalog entries inactive; reinstall restores availability without resetting administrator edits. Stopping a module retains active registration and catalog entries. Permission namespaces modules/business/audit/org/users/roles are reserved module IDs. A manifest has at most 20 role templates. Optional record_columns: [{"name":"duration_days","label":"请假天数(自然日)"}]. This displays backend-derived values in the standard list; up to 10 unique identifier names, labels up to 80 characters. These are display columns, not new form inputs or trusted client data. Workflow operations are available through the additive protocol-1 extension in section 15; payment and arbitrary operations remain unsupported. 11. Windows service host (platform v0.3.1, module protocol remains 1) Administrator terminal: platform.exe service install, then platform.exe service start. Commands: install|start|stop|restart|status|uninstall. Optional -service-name (default CollabPlatform). The internal "service run" command is for SCM only. Install records absolute -addr/-data-dir/-module-dir/-log-dir paths, delayed automatic start and LocalService account. Per-service SID grants modify on data/logs, read/execute on modules and platform binary. No module is separately installed as a Windows service. The host owns child module processes via Windows Job Objects. On normal stop it drains HTTP requests and closes modules; on host crash the OS cleans its module process trees. Unexpected module exits and RPC failures trigger at most 3 automatic starts with approximately 1/2/4-second backoff. Manual module stop suppresses restarts. Manual start resets the budget; 10 minutes of continuous healthy running clears the budget. SCM recovery restarts the host after 5/15/60 seconds for successive failures, then stops trying; its failure-count reset period is 1 hour. Normal service stop does not trigger recovery. GET /api/modules additionally exposes process_id for running modules and restart_count. These are operational metadata, not authorization inputs. Logs: logs/platform.log (5 MiB threshold, 3 backups), configurable with -log-dir. status prints service account, PID, binary path and startup settings. Stop service before replacing binaries or backing up data. Uninstall removes service registration but preserves data/modules/logs and per-service SID ACL entries. Moving the binary requires reinstalling service with the original data paths. Browser sessions expire on host restart; users log in again. Embedded Chinese operations guide: /docs/service. 11. Self-submission and administrator-controlled publication (platform v0.4) A candidate submission NEVER launches its EXE or installs roles into production. An administrator with modules.manage creates a scoped token: POST /api/publishing/tokens {module_id,expires_hours:24}. The clear token is returned once, stored only as a hash, may be revoked and expires within at most 168 hours. The module developer or deployment script POSTs a ZIP body to /api/module-submissions with Content-Type: application/zip, Authorization: Bearer , and X-Submission-Request-ID (16-128 chars). No browser cookie/CSRF is required for this bearer endpoint. Tokens can only submit/query their module; they cannot test, approve or access business data. The ZIP root contains exactly manifest.json, its declared native executable (Windows EXE or Linux ELF for the server architecture), and optionally simulation.json. Limit: 64MiB compressed AND decompressed; manifest/simulation config max64KiB each. Paths, directories, links, duplicate and undeclared files are rejected. Current publication flow requires identity.get, records.list and records.create capabilities. GET /api/module-submissions/ with the same scoped bearer token returns status. Reusing the same request ID with the same ZIP returns the original submission; different bytes conflict. Administrator browser endpoints (session cookie + X-CSRF-Token, modules.manage required): GET /api/publishing POST /api/publishing/tokens//revoke {} POST /api/publishing/submissions//test {} POST /api/publishing/submissions//approve {capabilities:[...exact requested caps...],note:"..."} POST /api/publishing/submissions//reject {note:"required reason"} Simulation and approval start asynchronous jobs (202); poll the list/status. States: pending -> testing -> ready/failed; ready -> publishing -> published/failed; pending/ready/failed -> rejected. Failed candidates require re-testing. Published/rejected candidates cannot be resubmitted as the same workflow item; submit a new ZIP/request ID instead. simulation.json example: {"valid_payload":{...valid business data...},"conflict_payload":{...different but valid business data...}}. Fixture data is only used in an isolated temporary database. Tests cover package hash, Describe consistency, empty list, required/unknown fields, valid create, idempotence/conflict, reader denial, atomic audit and database/registration reload. This EXECUTES the candidate with the platform service OS identity; data isolation is NOT an OS sandbox or malware check. Review source before starting. Approval is bound to the tested ZIP hash, is audited, requires explicit capability approval, and attempts to restore the previous EXE/registry if startup fails. Prior packages remain under modules/.releases//previous. No migration or production state.json replacement is performed. Interruptions are reconciled at next startup or marked failed for a new simulation. Windows service installs now grant only its dedicated service SID Modify access to modules/ as well as data/logs, so an approved release can be installed. Existing service installations need that module-directory SID grant updated; EXE itself remains read/execute. CLI helpers: scripts/package_module.py and scripts/submit_module.py. Provide MODULE_SUBMISSION_TOKEN via environment, not a public URL or repository file. See docs/module-publishing.md for an end-to-end simulation plan. 12. Linux HTTPS deployment Build with scripts/build-linux.ps1 (GOOS=linux GOARCH=amd64 CGO_ENABLED=0). Linux manifest entry is an extensionless filename such as leave, and the executable must be ELF amd64. Run the platform with -public-origin https://oa.pckee.com behind the loopback HTTPS proxy. Cookies are Secure; the exact HTTPS origin and loopback forwarded HTTPS requests are required. Use systemd for service lifecycle, including SIGTERM draining and child process cleanup. Windows packages must be rebuilt and resubmitted before Linux publication. 13. MANDATORY business module UI style: adminui-v1 Every business module MUST follow /docs/ui-style.md and /docs/ui-style. The SDK includes UI_STYLE.md and ui/index.html. Use the shared purple brand, light sidebar, 16px cards and theme tokens, supporting light/dark modes and narrow screens. Protocol v1 modules declare Describe fields, record_columns, permissions and role_templates; the platform renders all menus/forms/tables. Never bundle independent HTML/CSS/JS or a second login/sidebar in a submission ZIP. Visual checks are required in addition to protocol simulations. 14. Email code login When Resend is configured, email code is the default and password login is disabled. Users must be enabled and have a unique email bound in centralized user management; no public self-registration. POST /api/auth/email/request {email} returns challenge_id, expires_in=300 and resend_after=60. POST /api/auth/email/verify {email,code,challenge_id} rotates the session and CSRF on success. Both endpoints require the browser session cookie, CSRF and same Origin. Codes are single-use, bound to the requesting browser, and allow at most 5 attempts. Only hashed codes live in memory; restart invalidates outstanding codes. Environment: RESEND_API_KEY, OA_MAIL_FROM, OA_ADMIN_EMAIL (initial administrator binding only). Do not put secrets in SDKs or manifests. 15. Descriptor-driven workflow actions (platform v0.6, additive protocol 1) Descriptor optionally declares create_label and actions. Action fields: name (edit|submit|publish|reject), label, fields ([]Field), statuses ([]string), owner_only (bool). Missing actions preserves the original create/list page. Each action MUST have a manifest permission . mapped to the same action. Base read/list and create/create declarations remain required. Up to four additional actions are accepted. HTTP POST /api/actions// uses the existing request_id/payload envelope. Extended actions require their explicit scoped permission: legacy business.create NEVER grants edit/submit/publish/reject. The built-in system-admin resolves module permissions dynamically; no ordinary user receives new rights automatically. Both HTTP and delegated RPC re-check current permissions. identity.get now also returns permissions ([]string). Modules enforce ownership and business state transitions. UI statuses and owner_only only guide button visibility; they are not authorization. records.create may append a revision during any permitted write action, and audit records identify the operation (records.edit/submit/publish/reject). records.list remains read-only. Collection and actor isolation are unchanged. Immutable revisions can be folded by the module into the current article list without adding mutable record storage. Include action, actor and normalized input in idempotence checks, and reject stale revisions before writing. The bundled CMS example uses editor (read/create/edit/submit) and chief (read/publish/reject) templates. Draft -> pending -> published or returned -> pending. Editors edit only their own draft/returned articles. The CMS publishes into the authenticated application's published list; this does not create a public anonymous website. Published articles are locked in this initial version.