{
  "title": "Collab platform executable module contract",
  "protocol_version": 2,
  "service_host": {
    "guide": "/docs/service",
    "cli": "platform.exe service install|start|stop|restart|status|uninstall",
    "default_service_name": "CollabPlatform",
    "account": "LocalService with per-service SID directory grants",
    "module_supervision": "At most 3 automatic restart attempts; intentional stop is respected",
    "module_status_fields": [
      "process_id",
      "restart_count"
    ],
    "logs": "logs/platform.log; 5 MiB threshold and 3 backups"
  },
  "transport": "Public v2 JSON-RPC 2.0 stdio; optional v1 Go adapter",
  "guide": "/docs/module-guide.txt",
  "sdk": "/devkit.zip",
  "go_import": "collab.local/platform/modulekit",
  "go_business_interface": [
    "Describe() Descriptor",
    "Execute(Request, Host) (Response, error)"
  ],
  "entrypoint": "v2: implement module.describe / module.execute; Go ServeJSON or Python oa_module.serve. Optional Go v1: modulekit.Serve",
  "host_services": [
    "identity.get",
    "records.list",
    "records.create",
    "directory.read"
  ],
  "actions": [
    "create",
    "list"
  ],
  "limits": {
    "request_body_bytes": 65536,
    "record_bytes": 32768,
    "rpc_wait_seconds": 10,
    "multi_user_auth": true,
    "sandbox": "operator-selected Docker on Linux; native mode is trusted-code only",
    "custom_frontend": true
  },
  "http_endpoints": [
    {
      "method": "GET",
      "path": "/",
      "description": "Set local browser session cookie"
    },
    {
      "method": "GET",
      "path": "/api/bootstrap",
      "description": "Get session CSRF token, authenticated actor with permissions, and requires_setup"
    },
    {
      "method": "GET",
      "path": "/api/modules",
      "description": "Module discovery, status, descriptor and grants"
    },
    {
      "method": "POST",
      "path": "/api/modules/register",
      "body": {
        "id": "workorder",
        "capabilities": [
          "identity.get",
          "records.list",
          "records.create"
        ]
      }
    },
    {
      "method": "POST",
      "path": "/api/modules/{id}/start",
      "body": {}
    },
    {
      "method": "POST",
      "path": "/api/modules/{id}/stop",
      "body": {}
    },
    {
      "method": "POST",
      "path": "/api/modules/{id}/unregister",
      "body": {}
    },
    {
      "method": "POST",
      "path": "/api/actions/{id}/create",
      "body": {
        "request_id": "unique-request-id-min-16",
        "payload": {}
      }
    },
    {
      "method": "POST",
      "path": "/api/actions/{id}/list",
      "body": {
        "request_id": "unique-request-id-min-16",
        "payload": {}
      }
    },
    {
      "method": "GET",
      "path": "/api/audit"
    },
    {
      "method": "POST",
      "path": "/api/auth/setup",
      "body": {
        "name": "管理员",
        "password": "<at-least-12-characters>"
      }
    },
    {
      "method": "POST",
      "path": "/api/auth/login",
      "body": {
        "username": "local-admin",
        "password": "<password>"
      }
    },
    {
      "method": "POST",
      "path": "/api/auth/logout",
      "body": {}
    },
    {
      "method": "POST",
      "path": "/api/auth/password",
      "body": {
        "current_password": "<current>",
        "password": "<new>"
      }
    },
    {
      "method": "GET",
      "path": "/api/admin",
      "description": "Role-filtered departments, users, roles, permission catalog and configuration revision"
    },
    {
      "method": "POST",
      "path": "/api/admin/departments",
      "body": {
        "revision": 0,
        "department": {
          "id": "finance",
          "name": "财务部",
          "parent_id": ""
        }
      }
    },
    {
      "method": "POST",
      "path": "/api/admin/users",
      "body": {
        "revision": 1,
        "user": {
          "id": "alice",
          "name": "员工",
          "department_id": "finance",
          "role_ids": [
            "employee"
          ],
          "enabled": true
        },
        "password": "<initial-or-reset-password>"
      }
    },
    {
      "method": "POST",
      "path": "/api/admin/roles",
      "body": {
        "revision": 2,
        "role": {
          "id": "custom",
          "name": "业务只读",
          "description": "",
          "permissions": [
            "modules.read",
            "business.read"
          ],
          "builtin": false
        }
      }
    },
    {
      "method": "GET",
      "path": "/api/publishing"
    },
    {
      "method": "POST",
      "path": "/api/publishing/tokens",
      "body": {
        "module_id": "leave",
        "expires_hours": 24
      }
    },
    {
      "method": "POST",
      "path": "/api/publishing/tokens/{id}/revoke",
      "body": {}
    },
    {
      "method": "POST",
      "path": "/api/publishing/submissions/{id}/test",
      "body": {}
    },
    {
      "method": "POST",
      "path": "/api/publishing/submissions/{id}/approve",
      "body": {
        "capabilities": [
          "identity.get",
          "records.list",
          "records.create"
        ],
        "note": "批准发布"
      }
    },
    {
      "method": "POST",
      "path": "/api/publishing/submissions/{id}/reject",
      "body": {
        "note": "请补充测试样例"
      }
    }
  ],
  "error_codes": [
    "INVALID_ARGUMENT",
    "UNAUTHENTICATED",
    "FORBIDDEN",
    "CONFLICT",
    "NOT_FOUND",
    "UNAVAILABLE",
    "INTERNAL"
  ],
  "auth": {
    "mode": "local-rbac",
    "cookie": "collab_local",
    "csrf_header": "X-CSRF-Token",
    "registration": "trusted binaries only, capability grant is explicit administrator approval",
    "login": "POST /api/auth/login with username/password; returned csrf_token replaces bootstrap token",
    "setup": "First run: POST /api/auth/setup with name/password creates local-admin; password minimum 12 characters",
    "permissions": "Module-scoped <id>.read/create checked in HTTP and reverse RPC. Legacy business.read/create explicitly cover all present and future modules. Roles union; module-scoped users discover only allowed modules unless granted modules.read."
  },
  "manifest_example": {
    "id": "reimbursement",
    "name": "费用报销",
    "version": "0.2.0",
    "protocol": 1,
    "entry": "reimbursement.exe",
    "capabilities": [
      "identity.get",
      "records.list",
      "records.create"
    ],
    "permissions": [
      {
        "id": "reimbursement.read",
        "name": "查看报销",
        "action": "list"
      },
      {
        "id": "reimbursement.create",
        "name": "提交报销",
        "action": "create"
      }
    ],
    "role_templates": [
      {
        "id": "reimbursement-user",
        "name": "报销申请人",
        "description": "查看及登记本模块业务，可在后台调整",
        "permissions": [
          "reimbursement.read",
          "reimbursement.create"
        ]
      },
      {
        "id": "reimbursement-viewer",
        "name": "报销只读用户",
        "description": "只允许查看本模块业务记录",
        "permissions": [
          "reimbursement.read"
        ]
      }
    ],
    "ui": {
      "mode": "h5",
      "entry": "ui/index.html",
      "bridge_version": 1,
      "files": [
        "ui/index.html",
        "ui/app.js",
        "ui/common.js",
        "ui/common.css"
      ]
    }
  },
  "module_permissions": {
    "actions": {
      "list": "<module-id>.read",
      "create": "<module-id>.create"
    },
    "role_templates": "Optional namespaced roles, limited to own module permissions. Create missing roles on registration, never auto-assign or overwrite edits.",
    "unregister": "Retains role grants and inactive catalog entries."
  },
  "module_publication": {
    "version": "0.4",
    "submit": {
      "method": "POST",
      "path": "/api/module-submissions",
      "content_type": "application/zip",
      "auth": "Scoped Bearer token; browser cookie is not required",
      "headers": [
        "Authorization",
        "X-Submission-Request-ID"
      ],
      "max_bytes": 67108864
    },
    "status": {
      "method": "GET",
      "path": "/api/module-submissions/{id}",
      "auth": "same module token or module administrator"
    },
    "states": [
      "pending",
      "testing",
      "ready",
      "publishing",
      "published",
      "failed",
      "rejected"
    ],
    "simulation": "Independent data/module directories; Docker runtime when configured; real cloud connectors disabled; report bound to SHA256",
    "rollback": "Previous package backed up and restored on failed startup; production records not replaced",
    "admin_permission": "modules.manage",
    "simulation_payloads": {
      "invalid_payload": "optional explicit invalid sample; defaults to {}",
      "valid_payload": "required valid create object for writable modules",
      "conflict_payload": "different valid object used with the same request_id"
    }
  },
  "email_login": {
    "configuration": [
      "RESEND_API_KEY",
      "OA_MAIL_FROM",
      "OA_ADMIN_EMAIL (initial binding only)"
    ],
    "default_when_configured": "email_code",
    "password_login": false,
    "requires": "enabled existing user with unique email; browser session, CSRF and same Origin",
    "request": {
      "method": "POST",
      "path": "/api/auth/email/request",
      "body": {
        "email": "user@example.com"
      },
      "response": {
        "challenge_id": "opaque id",
        "expires_in": 300,
        "resend_after": 60
      }
    },
    "verify": {
      "method": "POST",
      "path": "/api/auth/email/verify",
      "body": {
        "email": "user@example.com",
        "code": "six digits",
        "challenge_id": "opaque id"
      }
    },
    "limits": {
      "expiry_seconds": 300,
      "resend_seconds": 60,
      "max_attempts": 5,
      "email_sends_per_hour": 5,
      "ip_sends_per_hour": 20,
      "global_sends_per_hour": 100
    },
    "security": "single use, browser binding, hashed codes in memory, no public registration"
  },
  "ui_style": {
    "id": "adminui-v1",
    "required": true,
    "guide": "/docs/ui-style.md",
    "preview": "/docs/ui-style",
    "sdk_files": [
      "UI_STYLE.md",
      "ui/index.html",
      "H5_UI.md",
      "h5/index.html",
      "h5/app.js"
    ],
    "rendering": "H5 only: manifest.ui is mandatory. The host mounts an isolated iframe; each app owns its HTML forms, tables and layout. generated mode is rejected."
  },
  "workflow_extension": {
    "platform_version": "0.6.0",
    "protocol": 1,
    "actions": [
      "edit",
      "submit",
      "publish",
      "reject"
    ],
    "permission": "<module-id>.<action>",
    "request": "POST /api/actions/{id}/{action} with request_id and payload",
    "descriptor_action": {
      "name": "publish",
      "label": "审核并发布"
    },
    "identity_get_addition": "permissions: string[]",
    "storage": "records.create appends immutable revisions; records.list reads them",
    "legacy_grants": "business.create does not grant workflow permissions"
  },
  "personal_api_keys": {
    "create": "POST /api/account/api-keys",
    "list": "GET /api/account/api-keys",
    "revoke": "POST /api/account/api-keys/{id}/revoke",
    "create_payload": {
      "name": "Agent",
      "expires_days": 30
    },
    "optional_scopes": "permissions: null follows live account; [] grants none; array narrows to named grants",
    "authentication": "Bearer API Key for business and administrative APIs; browser session and CSRF for key lifecycle",
    "applications": "All enabled users can submit, simulate, approve and publish their own module IDs; modules.manage can govern all modules"
  },
  "email_registration": {
    "enabled_when": "email login configured",
    "request": "POST /api/auth/email/request {email}",
    "verify": "POST /api/auth/email/verify {email,code,challenge_id}",
    "identity": "stable User.ID; unique changeable email binding",
    "default_role": "registered-user",
    "default_permissions": [
      "modules.submit"
    ],
    "disabled_accounts": "cannot login or re-register",
    "verification_response": "registered: true for a new account"
  },
  "directory": {
    "since": "0.7.0",
    "read_only": true,
    "permission": "<module-id>.read",
    "fields": [
      "departments[].id",
      "departments[].name",
      "departments[].parent_id",
      "contacts[].id",
      "contacts[].name",
      "contacts[].email",
      "contacts[].department_id"
    ],
    "ui": "Packaged custom H5; no host directory renderer"
  },
  "browser_sessions": {
    "idle_days": 7,
    "absolute_days": 30,
    "persist_across_restart": true,
    "cookie_same_site": "Lax",
    "csrf_required": true
  },
  "h5_ui": {
    "platform_version": "0.10.2",
    "guide": "/docs/h5-ui.md",
    "manifest": {
      "mode": "h5",
      "entry": "ui/index.html",
      "bridge_version": 1,
      "files": [
        "ui/index.html",
        "ui/app.js"
      ]
    },
    "session": {
      "method": "POST",
      "path": "/api/modules/{id}/ui-session",
      "auth": "Browser session, CSRF and own module read/create permission; API Key is not accepted",
      "body": {},
      "close_body": {
        "close": "session_id"
      },
      "response": [
        "url",
        "session_id",
        "expires_at",
        "bridge_version"
      ]
    },
    "assets": {
      "method": "GET",
      "path": "/h5/{ticket}/{asset...}",
      "scope": "Declared assets of one running module and current browser session; 30-minute maximum; revoked on logout, role change or module restart",
      "max_files": 128,
      "max_file_bytes": 2097152,
      "max_total_bytes": 8388608
    },
    "sandbox": "allow-scripts allow-forms; no allow-same-origin; CSP blocks direct network calls and native form submission",
    "sdk": [
      "ready",
      "context()",
      "execute(action,payload,requestId)",
      "notice(message,error)",
      "resize(height)",
      "onTheme(listener)"
    ],
    "bridge_methods": [
      "context.get",
      "actions.execute",
      "notice.show",
      "frame.resize"
    ],
    "backend_sandbox": "Docker when OA_MODULE_RUNTIME=docker; never fallback on failure",
    "required": true,
    "migration": "Upgrade host and all app packages together. Missing ui and generated mode are rejected; retain old host and all old packages for rollback.",
    "removed_presentation_metadata": [
      "Field",
      "Descriptor.Fields",
      "Descriptor.View",
      "Descriptor.CreateLabel",
      "Action.Fields",
      "Action.Statuses",
      "Action.OwnerOnly",
      "manifest.record_columns"
    ]
  },
  "descriptor": {
    "fields": [
      "id",
      "name",
      "version",
      "protocol",
      "description",
      "actions"
    ],
    "action_fields": [
      "name",
      "label"
    ],
    "presentation_schema": false
  },
  "docker_runtime": {
    "platform_version": "0.10.2",
    "permission": "modules.manage",
    "overview": "GET /api/docker",
    "logs": "GET /api/docker/{module}/logs",
    "control": "POST /api/docker/{module}/{start|stop|restart} {}",
    "policy": {
      "network": "none",
      "rootfs": "read-only",
      "user": "non-root",
      "memory_mb": 192,
      "cpus": 0.5,
      "pids": 64,
      "aggregate_memory_mb": 256,
      "aggregate_cpus": 1.5
    },
    "arbitrary_engine_commands": false
  },
  "data_scopes": {
    "guide": "/docs/data-scopes.md",
    "version": 1,
    "modes": [
      "self",
      "department",
      "department_tree",
      "custom",
      "all"
    ],
    "role_field": "data_scopes: map[module_id or *] -> {mode, department_ids?}",
    "update": "POST /api/admin/roles/{id}/data-scopes {revision,data_scopes}",
    "administrator": "builtin system-admin required; API Key must also cover role permissions",
    "records": "host assigns owner_id and department_id; immutable revisions inherit target ownership",
    "revision_target": "HostRequest.record_id required for restricted non-create writes",
    "default": "new roles self; legacy roles migrate to explicit all",
    "coverage": "records.* only; external connectors and directory services retain dedicated authorization"
  },
  "module_inspection": {
    "method": "GET",
    "path": "/api/modules/{id}/details",
    "permission": "modules.manage",
    "roles_permission": "roles.manage OR users.manage",
    "user_counts_permission": "users.manage",
    "read_only": true,
    "configuration": "Allowlisted status only; credentials and environment are never returned"
  },
  "execution_limits": {
    "platform_version": "0.13.0",
    "cross_module_parallel": true,
    "per_module_execution": 1,
    "global_admitted": 16,
    "per_actor_admitted": 4,
    "per_module_admitted": 4,
    "queue_wait_ms": 250,
    "timeout_ms": 10000,
    "per_actor_rate_per_second": 20,
    "per_actor_burst": 40,
    "identity_binding": "fresh callback connection per invocation; closed on completion/cancellation",
    "errors": {
      "RATE_LIMITED": 429,
      "DEADLINE_EXCEEDED": 504,
      "OUTCOME_UNKNOWN": 504,
      "CANCELED": 408
    },
    "write_retry": "verify outcome; reuse original request_id; never automatically replace ID after an unknown result"
  },
  "observability": {
    "version": "0.14.0",
    "permission": "monitor.read",
    "json": "GET /api/monitoring",
    "prometheus": "GET /api/monitoring/metrics",
    "readiness": "GET /readyz",
    "trace_header": "X-Trace-ID",
    "recent_request_limit": 100,
    "series_limit": 513,
    "container_cache_seconds": 15,
    "container_limit": 64,
    "persistent_history": false,
    "external_alerts": false,
    "guide": "/docs/observability.md"
  },
  "module_api": {
    "version": "0.15.0",
    "prefix": "/apps/{module}/api",
    "manifest": "api_routes",
    "methods": [
      "GET",
      "POST",
      "PUT",
      "PATCH",
      "DELETE"
    ],
    "transport": "declared JSON route to Execute RPC",
    "identity": "host-bound actor; route action authorization; scoped callbacks",
    "write_request_id": "X-Request-ID (16-128 chars)",
    "h5": "PlatformUI.request(path,options)",
    "guide": "/docs/module-api.md"
  },
  "module_database": {
    "version": "0.16.0",
    "engine": "postgres",
    "capability": "database.connect",
    "allocation": "per scope+module account and schema",
    "transport": "Unix socket, Docker only",
    "connections_per_role": 2,
    "admin": "GET /api/databases; POST /api/databases/{id}/rotate",
    "automatic_existing_data_migration": false,
    "automatic_sql_data_scope": false,
    "guide": "/docs/module-database.md"
  },
  "supported_protocols": {
    "2": "JSON-RPC 2.0 stdio; language-neutral; string IDs; one UTF-8 JSON object per line; max frame <4MiB",
    "1": "optional Go compatibility: go-plugin net/rpc/gob"
  },
  "language_neutral_guide": "/docs/module-jsonrpc.md",
  "runtime_profiles": "GET /api/runtime-profiles; modules.manage; profiles controlled only by operator",
  "language_neutral_manifest_example": {
    "id": "python-notes",
    "name": "Python 便笺",
    "version": "1.0.0",
    "protocol": 2,
    "entry": "main.py",
    "runtime": {
      "profile": "python3",
      "files": [
        "code/oa_module.py"
      ]
    },
    "capabilities": [
      "identity.get",
      "records.list",
      "records.create"
    ],
    "permissions": [
      {
        "id": "python-notes.read",
        "name": "阅读本人便笺",
        "action": "list"
      },
      {
        "id": "python-notes.create",
        "name": "新建本人便笺",
        "action": "create"
      }
    ],
    "ui": {
      "mode": "h5",
      "entry": "ui/index.html",
      "bridge_version": 1,
      "files": [
        "ui/index.html",
        "ui/app.js"
      ]
    }
  },
  "self_service_publishing": {
    "permission": "modules.submit (all enabled accounts)",
    "ownership": "first successful submission claims an unused module ID; other owners and installed unowned modules are protected",
    "automatic_registration_key": {
      "expires_days": 90,
      "delivery": "verified account email; full key and curl examples; encrypted durable outbox"
    },
    "mail_events": [
      "registration",
      "submitted",
      "published",
      "publish-failed"
    ],
    "guide": "/docs/self-service-publishing.md"
  },
  "module_developer": {
    "response": "GET /api/modules: items[].developer; GET /api/modules/{id}/details: module.developer",
    "fields": [
      "name",
      "email",
      "source"
    ],
    "source": [
      "application_owner",
      "published_submitter"
    ],
    "identity": "Platform-bound account, not self-declared manifest metadata. Omitted when unknown. Authenticated directory visibility required."
  },
  "module_release": {
    "response": "GET /api/modules: items[].release; GET /api/modules/{id}/details: module.release",
    "fields": [
      "version",
      "submitter",
      "reviewer",
      "submitted_at",
      "reviewed_at"
    ],
    "contact_fields": [
      "name",
      "email",
      "source"
    ],
    "matching": "Successful publication matching installed module ID, version and manifest SHA256; omitted if no matching record. No unverified manifest contact identities."
  },
  "registration_role_policy": {
    "environment": "OA_REGISTRATION_ROLE",
    "default": "registered-user",
    "test_system_value": "system-admin",
    "migration": "Promote enabled existing registered-user accounts; preserve all other account fields and roles. Disabled users remain disabled.",
    "bootstrap_flag": "test_registration_admin",
    "api_keys": "Keys following live owner permissions inherit administrator access in test mode.",
    "disable": "Changing configuration back affects future registrations; existing administrator roles are not automatically revoked."
  },
  "scheduled_jobs": {
    "permissions": [
      "jobs.read",
      "jobs.manage"
    ],
    "guide": "/docs/scheduled-jobs.md",
    "api": [
      "GET /api/jobs",
      "POST /api/jobs",
      "POST /api/jobs/preview",
      "POST /api/jobs/{id}/{enable|disable|run|delete}",
      "GET /api/jobs/logs"
    ],
    "cron": "5 or 6 fields; IANA timezone; no Quartz year/L/W/#.",
    "targets": [
      "health",
      "registered module action"
    ],
    "identity": "Current editor bound as run_as. Revalidate account, current jobs.manage/action, saved permission ceiling and source API Key each trigger.",
    "limits": {
      "jobs": 200,
      "logs": 2000,
      "concurrent_total": 8,
      "concurrent_per_job": 4,
      "timeout_seconds": 10,
      "misfire_grace_seconds": 5
    },
    "recovery": "Persist before dispatch. Interrupted or timed out business may already have committed; no automatic retry. Manual trigger dedup is bounded by retained log window.",
    "storage": "scheduled-jobs.json; single host protected data lock; no distributed scheduling"
  },
  "public_extensions": {
    "contract_version": 1,
    "guide": "/docs/public-extensions.md",
    "catalog": {
      "method": "GET",
      "path": "/api/extensions",
      "permission": "modules.manage"
    },
    "provider": {
      "extension": {
        "api_version": 1,
        "provides": [
          {
            "name": "workflow.start",
            "action": "create"
          }
        ]
      },
      "registration_approval": "extension_approved=true, operator-installed only"
    },
    "consumer": {
      "dependencies": [
        {
          "extension": "workflow",
          "api_version": 1
        }
      ],
      "capabilities": [
        "workflow.start"
      ]
    },
    "constraints": [
      "inherits original actor and delegated permission ceiling",
      "no arbitrary URLs",
      "no read-to-write escalation",
      "exact API major version",
      "cycle and dependency guards",
      "10-second inherited deadline",
      "no automatic business retries"
    ],
    "collaboration_guide": "/docs/collaboration-extensions.md",
    "available_providers": [
      "workflow",
      "todos",
      "notifications",
      "attachments"
    ],
    "workflow_engine": "sequential approval v1; independent operator-installed provider",
    "installation_status_source": "GET /api/extensions (authenticated); available source code does not mean installed on every deployment"
  }
}
