corporateai 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (704) hide show
  1. package/.dockerignore +10 -0
  2. package/.env.example +3 -0
  3. package/.github/workflows/publish-cli.yml +49 -0
  4. package/.mailmap +1 -0
  5. package/AGENTS.md +148 -0
  6. package/CONTRIBUTING.md +75 -0
  7. package/Dockerfile +59 -0
  8. package/Dockerfile.onboard-smoke +42 -0
  9. package/LICENSE +21 -0
  10. package/README.md +93 -0
  11. package/cli/esbuild.config.mjs +11 -0
  12. package/cli/package.json +24 -0
  13. package/cli/scripts/build-cli.mjs +5 -0
  14. package/cli/src/index.ts +27 -0
  15. package/docker-compose.quickstart.yml +18 -0
  16. package/docker-compose.untrusted-review.yml +33 -0
  17. package/docker-compose.yml +38 -0
  18. package/package.json +56 -0
  19. package/patches/embedded-postgres@18.1.0-beta.16.patch +0 -0
  20. package/pnpm-workspace.yaml +4 -0
  21. package/releases/.gitkeep +0 -0
  22. package/releases/v0.0.1.md +36 -0
  23. package/releases/v0.2.7.md +15 -0
  24. package/releases/v0.3.0.md +54 -0
  25. package/releases/v0.3.1.md +55 -0
  26. package/releases/v2026.318.0.md +66 -0
  27. package/releases/v2026.325.0.md +78 -0
  28. package/report/2026-03-13-08-46-token-optimization-implementation.md +48 -0
  29. package/scripts/backup-db.sh +17 -0
  30. package/scripts/build-npm.sh +80 -0
  31. package/scripts/check-forbidden-tokens.mjs +115 -0
  32. package/scripts/clean-onboard-git.sh +14 -0
  33. package/scripts/clean-onboard-npm.sh +13 -0
  34. package/scripts/clean-onboard-ref.sh +86 -0
  35. package/scripts/create-github-release.sh +99 -0
  36. package/scripts/dev-runner-paths.mjs +38 -0
  37. package/scripts/dev-runner.mjs +606 -0
  38. package/scripts/docker-onboard-smoke.sh +306 -0
  39. package/scripts/ensure-plugin-build-deps.mjs +47 -0
  40. package/scripts/generate-company-assets.ts +365 -0
  41. package/scripts/generate-npm-package-json.mjs +113 -0
  42. package/scripts/generate-org-chart-images.ts +694 -0
  43. package/scripts/generate-org-chart-satori-comparison.ts +225 -0
  44. package/scripts/generate-ui-package-json.mjs +31 -0
  45. package/scripts/kill-dev.sh +71 -0
  46. package/scripts/migrate-inline-env-secrets.ts +126 -0
  47. package/scripts/prepare-server-ui-dist.sh +22 -0
  48. package/scripts/provision-worktree.sh +333 -0
  49. package/scripts/release-lib.sh +306 -0
  50. package/scripts/release-package-map.mjs +169 -0
  51. package/scripts/release.sh +312 -0
  52. package/scripts/rollback-latest.sh +111 -0
  53. package/scripts/smoke/openclaw-docker-ui.sh +329 -0
  54. package/scripts/smoke/openclaw-gateway-e2e.sh +954 -0
  55. package/scripts/smoke/openclaw-join.sh +295 -0
  56. package/scripts/smoke/openclaw-sse-standalone.sh +146 -0
  57. package/scripts/workspace-compat.mjs +60 -0
  58. package/server/CHANGELOG.md +130 -0
  59. package/server/package.json +96 -0
  60. package/server/scripts/copy-onboarding-assets.mjs +10 -0
  61. package/server/scripts/dev-watch.ts +33 -0
  62. package/server/src/__tests__/activity-routes.test.ts +70 -0
  63. package/server/src/__tests__/adapter-models.test.ts +105 -0
  64. package/server/src/__tests__/adapter-session-codecs.test.ts +194 -0
  65. package/server/src/__tests__/agent-auth-jwt.test.ts +79 -0
  66. package/server/src/__tests__/agent-instructions-routes.test.ts +318 -0
  67. package/server/src/__tests__/agent-instructions-service.test.ts +361 -0
  68. package/server/src/__tests__/agent-permissions-routes.test.ts +275 -0
  69. package/server/src/__tests__/agent-shortname-collision.test.ts +69 -0
  70. package/server/src/__tests__/agent-skill-contract.test.ts +50 -0
  71. package/server/src/__tests__/agent-skills-routes.test.ts +462 -0
  72. package/server/src/__tests__/app-hmr-port.test.ts +19 -0
  73. package/server/src/__tests__/approval-routes-idempotency.test.ts +110 -0
  74. package/server/src/__tests__/approvals-service.test.ts +107 -0
  75. package/server/src/__tests__/assets.test.ts +250 -0
  76. package/server/src/__tests__/attachment-types.test.ts +97 -0
  77. package/server/src/__tests__/board-mutation-guard.test.ts +105 -0
  78. package/server/src/__tests__/budgets-service.test.ts +311 -0
  79. package/server/src/__tests__/claude-local-adapter-environment.test.ts +92 -0
  80. package/server/src/__tests__/claude-local-adapter.test.ts +31 -0
  81. package/server/src/__tests__/claude-local-skill-sync.test.ts +111 -0
  82. package/server/src/__tests__/cli-auth-routes.test.ts +230 -0
  83. package/server/src/__tests__/codex-local-adapter-environment.test.ts +143 -0
  84. package/server/src/__tests__/codex-local-adapter.test.ts +253 -0
  85. package/server/src/__tests__/codex-local-execute.test.ts +391 -0
  86. package/server/src/__tests__/codex-local-skill-injection.test.ts +175 -0
  87. package/server/src/__tests__/codex-local-skill-sync.test.ts +123 -0
  88. package/server/src/__tests__/companies-route-path-guard.test.ts +56 -0
  89. package/server/src/__tests__/company-branding-route.test.ts +196 -0
  90. package/server/src/__tests__/company-portability-routes.test.ts +175 -0
  91. package/server/src/__tests__/company-portability.test.ts +2186 -0
  92. package/server/src/__tests__/company-skills-routes.test.ts +113 -0
  93. package/server/src/__tests__/company-skills.test.ts +229 -0
  94. package/server/src/__tests__/costs-service.test.ts +226 -0
  95. package/server/src/__tests__/cursor-local-adapter-environment.test.ts +196 -0
  96. package/server/src/__tests__/cursor-local-adapter.test.ts +406 -0
  97. package/server/src/__tests__/cursor-local-execute.test.ts +263 -0
  98. package/server/src/__tests__/cursor-local-skill-injection.test.ts +104 -0
  99. package/server/src/__tests__/cursor-local-skill-sync.test.ts +145 -0
  100. package/server/src/__tests__/dev-runner-paths.test.ts +25 -0
  101. package/server/src/__tests__/dev-server-status.test.ts +66 -0
  102. package/server/src/__tests__/dev-watch-ignore.test.ts +42 -0
  103. package/server/src/__tests__/documents.test.ts +29 -0
  104. package/server/src/__tests__/error-handler.test.ts +53 -0
  105. package/server/src/__tests__/execution-workspace-policy.test.ts +170 -0
  106. package/server/src/__tests__/forbidden-tokens.test.ts +77 -0
  107. package/server/src/__tests__/gemini-local-adapter-environment.test.ts +135 -0
  108. package/server/src/__tests__/gemini-local-adapter.test.ts +190 -0
  109. package/server/src/__tests__/gemini-local-execute.test.ts +172 -0
  110. package/server/src/__tests__/gemini-local-skill-sync.test.ts +90 -0
  111. package/server/src/__tests__/health.test.ts +16 -0
  112. package/server/src/__tests__/heartbeat-process-recovery.test.ts +256 -0
  113. package/server/src/__tests__/heartbeat-run-summary.test.ts +33 -0
  114. package/server/src/__tests__/heartbeat-workspace-session.test.ts +334 -0
  115. package/server/src/__tests__/helpers/embedded-postgres.ts +7 -0
  116. package/server/src/__tests__/hire-hook.test.ts +181 -0
  117. package/server/src/__tests__/instance-settings-routes.test.ts +156 -0
  118. package/server/src/__tests__/invite-accept-gateway-defaults.test.ts +119 -0
  119. package/server/src/__tests__/invite-accept-replay.test.ts +92 -0
  120. package/server/src/__tests__/invite-expiry.test.ts +10 -0
  121. package/server/src/__tests__/invite-join-grants.test.ts +57 -0
  122. package/server/src/__tests__/invite-join-manager.test.ts +33 -0
  123. package/server/src/__tests__/invite-onboarding-text.test.ts +116 -0
  124. package/server/src/__tests__/issue-comment-reopen-routes.test.ts +146 -0
  125. package/server/src/__tests__/issue-goal-fallback.test.ts +99 -0
  126. package/server/src/__tests__/issues-checkout-wakeup.test.ts +48 -0
  127. package/server/src/__tests__/issues-goal-context-routes.test.ts +187 -0
  128. package/server/src/__tests__/issues-service.test.ts +317 -0
  129. package/server/src/__tests__/issues-user-context.test.ts +113 -0
  130. package/server/src/__tests__/log-redaction.test.ts +74 -0
  131. package/server/src/__tests__/monthly-spend-service.test.ts +90 -0
  132. package/server/src/__tests__/normalize-agent-mention-token.test.ts +41 -0
  133. package/server/src/__tests__/openclaw-gateway-adapter.test.ts +626 -0
  134. package/server/src/__tests__/openclaw-invite-prompt-route.test.ts +192 -0
  135. package/server/src/__tests__/opencode-local-adapter-environment.test.ts +97 -0
  136. package/server/src/__tests__/opencode-local-adapter.test.ts +226 -0
  137. package/server/src/__tests__/opencode-local-skill-sync.test.ts +91 -0
  138. package/server/src/__tests__/paperclip-env.test.ts +58 -0
  139. package/server/src/__tests__/paperclip-skill-utils.test.ts +63 -0
  140. package/server/src/__tests__/pi-local-adapter-environment.test.ts +102 -0
  141. package/server/src/__tests__/pi-local-skill-sync.test.ts +95 -0
  142. package/server/src/__tests__/plugin-dev-watcher.test.ts +68 -0
  143. package/server/src/__tests__/plugin-worker-manager.test.ts +43 -0
  144. package/server/src/__tests__/private-hostname-guard.test.ts +56 -0
  145. package/server/src/__tests__/project-shortname-resolution.test.ts +45 -0
  146. package/server/src/__tests__/quota-windows-service.test.ts +56 -0
  147. package/server/src/__tests__/quota-windows.test.ts +1109 -0
  148. package/server/src/__tests__/redaction.test.ts +66 -0
  149. package/server/src/__tests__/routines-e2e.test.ts +276 -0
  150. package/server/src/__tests__/routines-routes.test.ts +271 -0
  151. package/server/src/__tests__/routines-service.test.ts +424 -0
  152. package/server/src/__tests__/storage-local-provider.test.ts +78 -0
  153. package/server/src/__tests__/ui-branding.test.ts +82 -0
  154. package/server/src/__tests__/work-products.test.ts +95 -0
  155. package/server/src/__tests__/workspace-runtime.test.ts +1131 -0
  156. package/server/src/__tests__/worktree-config.test.ts +426 -0
  157. package/server/src/adapters/codex-models.ts +105 -0
  158. package/server/src/adapters/cursor-models.ts +171 -0
  159. package/server/src/adapters/http/execute.ts +42 -0
  160. package/server/src/adapters/http/index.ts +21 -0
  161. package/server/src/adapters/http/test.ts +116 -0
  162. package/server/src/adapters/index.ts +18 -0
  163. package/server/src/adapters/process/execute.ts +77 -0
  164. package/server/src/adapters/process/index.ts +24 -0
  165. package/server/src/adapters/process/test.ts +89 -0
  166. package/server/src/adapters/registry.ts +225 -0
  167. package/server/src/adapters/server-utils-compat.ts +57 -0
  168. package/server/src/adapters/types.ts +30 -0
  169. package/server/src/adapters/utils.ts +48 -0
  170. package/server/src/agent-auth-jwt.ts +141 -0
  171. package/server/src/app.ts +321 -0
  172. package/server/src/attachment-types.ts +74 -0
  173. package/server/src/auth/better-auth.ts +148 -0
  174. package/server/src/board-claim.ts +150 -0
  175. package/server/src/config-file.ts +17 -0
  176. package/server/src/config.ts +260 -0
  177. package/server/src/dev-server-status.ts +103 -0
  178. package/server/src/dev-watch-ignore.ts +36 -0
  179. package/server/src/errors.ts +34 -0
  180. package/server/src/home-paths.ts +95 -0
  181. package/server/src/index.ts +799 -0
  182. package/server/src/log-redaction.ts +146 -0
  183. package/server/src/middleware/auth.ts +178 -0
  184. package/server/src/middleware/board-mutation-guard.ts +66 -0
  185. package/server/src/middleware/error-handler.ts +71 -0
  186. package/server/src/middleware/index.ts +3 -0
  187. package/server/src/middleware/logger.ts +90 -0
  188. package/server/src/middleware/private-hostname-guard.ts +92 -0
  189. package/server/src/middleware/validate.ts +9 -0
  190. package/server/src/onboarding-assets/ceo/AGENTS.md +54 -0
  191. package/server/src/onboarding-assets/ceo/HEARTBEAT.md +72 -0
  192. package/server/src/onboarding-assets/ceo/SOUL.md +33 -0
  193. package/server/src/onboarding-assets/ceo/TOOLS.md +3 -0
  194. package/server/src/onboarding-assets/default/AGENTS.md +3 -0
  195. package/server/src/paths.ts +34 -0
  196. package/server/src/realtime/live-events-ws.ts +274 -0
  197. package/server/src/redaction.ts +59 -0
  198. package/server/src/routes/access.ts +2888 -0
  199. package/server/src/routes/activity.ts +89 -0
  200. package/server/src/routes/agents.ts +2313 -0
  201. package/server/src/routes/approvals.ts +346 -0
  202. package/server/src/routes/assets.ts +341 -0
  203. package/server/src/routes/authz.ts +52 -0
  204. package/server/src/routes/companies.ts +343 -0
  205. package/server/src/routes/company-skills.ts +300 -0
  206. package/server/src/routes/costs.ts +335 -0
  207. package/server/src/routes/dashboard.ts +19 -0
  208. package/server/src/routes/execution-workspaces.ts +182 -0
  209. package/server/src/routes/goals.ts +107 -0
  210. package/server/src/routes/health.ts +94 -0
  211. package/server/src/routes/index.ts +17 -0
  212. package/server/src/routes/instance-settings.ts +95 -0
  213. package/server/src/routes/issues-checkout-wakeup.ts +14 -0
  214. package/server/src/routes/issues.ts +1680 -0
  215. package/server/src/routes/llms.ts +86 -0
  216. package/server/src/routes/org-chart-svg.ts +777 -0
  217. package/server/src/routes/plugin-ui-static.ts +497 -0
  218. package/server/src/routes/plugins.ts +2220 -0
  219. package/server/src/routes/projects.ts +295 -0
  220. package/server/src/routes/routines.ts +300 -0
  221. package/server/src/routes/secrets.ts +166 -0
  222. package/server/src/routes/sidebar-badges.ts +52 -0
  223. package/server/src/secrets/external-stub-providers.ts +32 -0
  224. package/server/src/secrets/local-encrypted-provider.ts +135 -0
  225. package/server/src/secrets/provider-registry.ts +31 -0
  226. package/server/src/secrets/types.ts +23 -0
  227. package/server/src/services/access.ts +381 -0
  228. package/server/src/services/activity-log.ts +95 -0
  229. package/server/src/services/activity.ts +164 -0
  230. package/server/src/services/agent-instructions.ts +735 -0
  231. package/server/src/services/agent-permissions.ts +27 -0
  232. package/server/src/services/agents.ts +694 -0
  233. package/server/src/services/approvals.ts +273 -0
  234. package/server/src/services/assets.ts +23 -0
  235. package/server/src/services/board-auth.ts +355 -0
  236. package/server/src/services/budgets.ts +959 -0
  237. package/server/src/services/companies.ts +313 -0
  238. package/server/src/services/company-export-readme.ts +173 -0
  239. package/server/src/services/company-portability.ts +4263 -0
  240. package/server/src/services/company-skills.ts +2356 -0
  241. package/server/src/services/costs.ts +365 -0
  242. package/server/src/services/cron.ts +373 -0
  243. package/server/src/services/dashboard.ts +110 -0
  244. package/server/src/services/default-agent-instructions.ts +27 -0
  245. package/server/src/services/documents.ts +434 -0
  246. package/server/src/services/execution-workspace-policy.ts +210 -0
  247. package/server/src/services/execution-workspaces.ts +100 -0
  248. package/server/src/services/finance.ts +135 -0
  249. package/server/src/services/goals.ts +81 -0
  250. package/server/src/services/heartbeat-run-summary.ts +35 -0
  251. package/server/src/services/heartbeat.ts +3863 -0
  252. package/server/src/services/hire-hook.ts +114 -0
  253. package/server/src/services/index.ts +32 -0
  254. package/server/src/services/instance-settings.ts +138 -0
  255. package/server/src/services/issue-approvals.ts +175 -0
  256. package/server/src/services/issue-assignment-wakeup.ts +48 -0
  257. package/server/src/services/issue-goal-fallback.ts +56 -0
  258. package/server/src/services/issues.ts +1828 -0
  259. package/server/src/services/live-events.ts +55 -0
  260. package/server/src/services/plugin-capability-validator.ts +450 -0
  261. package/server/src/services/plugin-config-validator.ts +55 -0
  262. package/server/src/services/plugin-dev-watcher.ts +339 -0
  263. package/server/src/services/plugin-event-bus.ts +413 -0
  264. package/server/src/services/plugin-host-service-cleanup.ts +59 -0
  265. package/server/src/services/plugin-host-services.ts +1132 -0
  266. package/server/src/services/plugin-job-coordinator.ts +261 -0
  267. package/server/src/services/plugin-job-scheduler.ts +753 -0
  268. package/server/src/services/plugin-job-store.ts +466 -0
  269. package/server/src/services/plugin-lifecycle.ts +822 -0
  270. package/server/src/services/plugin-loader.ts +1955 -0
  271. package/server/src/services/plugin-log-retention.ts +87 -0
  272. package/server/src/services/plugin-manifest-validator.ts +164 -0
  273. package/server/src/services/plugin-registry.ts +683 -0
  274. package/server/src/services/plugin-runtime-sandbox.ts +222 -0
  275. package/server/src/services/plugin-secrets-handler.ts +355 -0
  276. package/server/src/services/plugin-state-store.ts +238 -0
  277. package/server/src/services/plugin-stream-bus.ts +81 -0
  278. package/server/src/services/plugin-tool-dispatcher.ts +449 -0
  279. package/server/src/services/plugin-tool-registry.ts +450 -0
  280. package/server/src/services/plugin-worker-manager.ts +1343 -0
  281. package/server/src/services/projects.ts +860 -0
  282. package/server/src/services/quota-windows.ts +65 -0
  283. package/server/src/services/routines.ts +1269 -0
  284. package/server/src/services/run-log-store.ts +156 -0
  285. package/server/src/services/secrets.ts +370 -0
  286. package/server/src/services/sidebar-badges.ts +56 -0
  287. package/server/src/services/work-products.ts +124 -0
  288. package/server/src/services/workspace-operation-log-store.ts +156 -0
  289. package/server/src/services/workspace-operations.ts +262 -0
  290. package/server/src/services/workspace-runtime.ts +1565 -0
  291. package/server/src/startup-banner.ts +176 -0
  292. package/server/src/storage/index.ts +35 -0
  293. package/server/src/storage/local-disk-provider.ts +89 -0
  294. package/server/src/storage/provider-registry.ts +18 -0
  295. package/server/src/storage/s3-provider.ts +153 -0
  296. package/server/src/storage/service.ts +131 -0
  297. package/server/src/storage/types.ts +63 -0
  298. package/server/src/ui-branding.ts +217 -0
  299. package/server/src/version.ts +10 -0
  300. package/server/src/worktree-config.ts +468 -0
  301. package/server/tsconfig.json +9 -0
  302. package/server/vitest.config.ts +7 -0
  303. package/skills/paperclip/SKILL.md +365 -0
  304. package/skills/paperclip/references/api-reference.md +647 -0
  305. package/skills/paperclip/references/company-skills.md +193 -0
  306. package/skills/paperclip-create-agent/SKILL.md +142 -0
  307. package/skills/paperclip-create-agent/references/api-reference.md +105 -0
  308. package/skills/paperclip-create-plugin/SKILL.md +102 -0
  309. package/skills/para-memory-files/SKILL.md +104 -0
  310. package/skills/para-memory-files/references/schemas.md +35 -0
  311. package/tests/e2e/onboarding.spec.ts +142 -0
  312. package/tests/e2e/playwright.config.ts +35 -0
  313. package/tests/release-smoke/docker-auth-onboarding.spec.ts +141 -0
  314. package/tests/release-smoke/playwright.config.ts +28 -0
  315. package/tsconfig.base.json +18 -0
  316. package/tsconfig.json +18 -0
  317. package/ui/README.md +12 -0
  318. package/ui/components.json +21 -0
  319. package/ui/index.html +47 -0
  320. package/ui/package.json +73 -0
  321. package/ui/public/android-chrome-192x192.png +0 -0
  322. package/ui/public/android-chrome-512x512.png +0 -0
  323. package/ui/public/apple-touch-icon.png +0 -0
  324. package/ui/public/brands/opencode-logo-dark-square.svg +18 -0
  325. package/ui/public/brands/opencode-logo-light-square.svg +18 -0
  326. package/ui/public/favicon-16x16.png +0 -0
  327. package/ui/public/favicon-32x32.png +0 -0
  328. package/ui/public/favicon.ico +0 -0
  329. package/ui/public/favicon.svg +9 -0
  330. package/ui/public/site.webmanifest +30 -0
  331. package/ui/public/sprites/1-D-1.png +0 -0
  332. package/ui/public/sprites/1-D-2.png +0 -0
  333. package/ui/public/sprites/1-D-3.png +0 -0
  334. package/ui/public/sprites/1-L-1.png +0 -0
  335. package/ui/public/sprites/1-R-1.png +0 -0
  336. package/ui/public/sprites/10-D-1.png +0 -0
  337. package/ui/public/sprites/10-D-2.png +0 -0
  338. package/ui/public/sprites/10-D-3.png +0 -0
  339. package/ui/public/sprites/10-L-1.png +0 -0
  340. package/ui/public/sprites/10-R-1.png +0 -0
  341. package/ui/public/sprites/11-D-1.png +0 -0
  342. package/ui/public/sprites/11-D-2.png +0 -0
  343. package/ui/public/sprites/11-D-3.png +0 -0
  344. package/ui/public/sprites/11-L-1.png +0 -0
  345. package/ui/public/sprites/11-R-1.png +0 -0
  346. package/ui/public/sprites/12-D-1.png +0 -0
  347. package/ui/public/sprites/12-D-2.png +0 -0
  348. package/ui/public/sprites/12-D-3.png +0 -0
  349. package/ui/public/sprites/12-L-1.png +0 -0
  350. package/ui/public/sprites/12-R-1.png +0 -0
  351. package/ui/public/sprites/13-D-1.png +0 -0
  352. package/ui/public/sprites/13-D-2.png +0 -0
  353. package/ui/public/sprites/13-D-3.png +0 -0
  354. package/ui/public/sprites/13-L-1.png +0 -0
  355. package/ui/public/sprites/13-R-1.png +0 -0
  356. package/ui/public/sprites/14-D-1.png +0 -0
  357. package/ui/public/sprites/14-D-2.png +0 -0
  358. package/ui/public/sprites/14-D-3.png +0 -0
  359. package/ui/public/sprites/14-L-1.png +0 -0
  360. package/ui/public/sprites/14-R-1.png +0 -0
  361. package/ui/public/sprites/2-D-1.png +0 -0
  362. package/ui/public/sprites/2-D-2.png +0 -0
  363. package/ui/public/sprites/2-D-3.png +0 -0
  364. package/ui/public/sprites/2-L-1.png +0 -0
  365. package/ui/public/sprites/2-R-1.png +0 -0
  366. package/ui/public/sprites/3-D-1.png +0 -0
  367. package/ui/public/sprites/3-D-2.png +0 -0
  368. package/ui/public/sprites/3-D-3.png +0 -0
  369. package/ui/public/sprites/3-L-1.png +0 -0
  370. package/ui/public/sprites/3-R-1.png +0 -0
  371. package/ui/public/sprites/4-D-1.png +0 -0
  372. package/ui/public/sprites/4-D-2.png +0 -0
  373. package/ui/public/sprites/4-D-3.png +0 -0
  374. package/ui/public/sprites/4-L-1.png +0 -0
  375. package/ui/public/sprites/4-R-1.png +0 -0
  376. package/ui/public/sprites/5-D-1.png +0 -0
  377. package/ui/public/sprites/5-D-2.png +0 -0
  378. package/ui/public/sprites/5-D-3.png +0 -0
  379. package/ui/public/sprites/5-L-1.png +0 -0
  380. package/ui/public/sprites/5-R-1.png +0 -0
  381. package/ui/public/sprites/6-D-1.png +0 -0
  382. package/ui/public/sprites/6-D-2.png +0 -0
  383. package/ui/public/sprites/6-D-3.png +0 -0
  384. package/ui/public/sprites/6-L-1.png +0 -0
  385. package/ui/public/sprites/6-R-1.png +0 -0
  386. package/ui/public/sprites/7-D-1.png +0 -0
  387. package/ui/public/sprites/7-D-2.png +0 -0
  388. package/ui/public/sprites/7-D-3.png +0 -0
  389. package/ui/public/sprites/7-L-1.png +0 -0
  390. package/ui/public/sprites/7-R-1.png +0 -0
  391. package/ui/public/sprites/8-D-1.png +0 -0
  392. package/ui/public/sprites/8-D-2.png +0 -0
  393. package/ui/public/sprites/8-D-3.png +0 -0
  394. package/ui/public/sprites/8-L-1.png +0 -0
  395. package/ui/public/sprites/8-R-1.png +0 -0
  396. package/ui/public/sprites/9-D-1.png +0 -0
  397. package/ui/public/sprites/9-D-2.png +0 -0
  398. package/ui/public/sprites/9-D-3.png +0 -0
  399. package/ui/public/sprites/9-L-1.png +0 -0
  400. package/ui/public/sprites/9-R-1.png +0 -0
  401. package/ui/public/sprites/ceo-lobster.png +0 -0
  402. package/ui/public/sw.js +42 -0
  403. package/ui/public/worktree-favicon-16x16.png +0 -0
  404. package/ui/public/worktree-favicon-32x32.png +0 -0
  405. package/ui/public/worktree-favicon.ico +0 -0
  406. package/ui/public/worktree-favicon.svg +9 -0
  407. package/ui/src/App.tsx +354 -0
  408. package/ui/src/adapters/claude-local/config-fields.tsx +138 -0
  409. package/ui/src/adapters/claude-local/index.ts +13 -0
  410. package/ui/src/adapters/codex-local/config-fields.tsx +104 -0
  411. package/ui/src/adapters/codex-local/index.ts +13 -0
  412. package/ui/src/adapters/cursor/config-fields.tsx +49 -0
  413. package/ui/src/adapters/cursor/index.ts +13 -0
  414. package/ui/src/adapters/gemini-local/config-fields.tsx +51 -0
  415. package/ui/src/adapters/gemini-local/index.ts +13 -0
  416. package/ui/src/adapters/http/build-config.ts +9 -0
  417. package/ui/src/adapters/http/config-fields.tsx +38 -0
  418. package/ui/src/adapters/http/index.ts +12 -0
  419. package/ui/src/adapters/http/parse-stdout.ts +5 -0
  420. package/ui/src/adapters/index.ts +9 -0
  421. package/ui/src/adapters/local-workspace-runtime-fields.tsx +5 -0
  422. package/ui/src/adapters/openclaw-gateway/config-fields.tsx +237 -0
  423. package/ui/src/adapters/openclaw-gateway/index.ts +13 -0
  424. package/ui/src/adapters/opencode-local/config-fields.tsx +72 -0
  425. package/ui/src/adapters/opencode-local/index.ts +13 -0
  426. package/ui/src/adapters/pi-local/config-fields.tsx +49 -0
  427. package/ui/src/adapters/pi-local/index.ts +13 -0
  428. package/ui/src/adapters/process/build-config.ts +18 -0
  429. package/ui/src/adapters/process/config-fields.tsx +77 -0
  430. package/ui/src/adapters/process/index.ts +12 -0
  431. package/ui/src/adapters/process/parse-stdout.ts +5 -0
  432. package/ui/src/adapters/registry.ts +34 -0
  433. package/ui/src/adapters/runtime-json-fields.tsx +122 -0
  434. package/ui/src/adapters/transcript.test.ts +30 -0
  435. package/ui/src/adapters/transcript.ts +62 -0
  436. package/ui/src/adapters/types.ts +34 -0
  437. package/ui/src/api/access.ts +160 -0
  438. package/ui/src/api/activity.ts +37 -0
  439. package/ui/src/api/agents.ts +194 -0
  440. package/ui/src/api/approvals.ts +25 -0
  441. package/ui/src/api/assets.ts +30 -0
  442. package/ui/src/api/auth.ts +74 -0
  443. package/ui/src/api/budgets.ts +21 -0
  444. package/ui/src/api/client.ts +50 -0
  445. package/ui/src/api/companies.ts +59 -0
  446. package/ui/src/api/companySkills.ts +55 -0
  447. package/ui/src/api/costs.ts +60 -0
  448. package/ui/src/api/dashboard.ts +7 -0
  449. package/ui/src/api/execution-workspaces.ts +27 -0
  450. package/ui/src/api/goals.ts +12 -0
  451. package/ui/src/api/health.ts +41 -0
  452. package/ui/src/api/heartbeats.ts +62 -0
  453. package/ui/src/api/index.ts +18 -0
  454. package/ui/src/api/instanceSettings.ts +19 -0
  455. package/ui/src/api/issues.ts +115 -0
  456. package/ui/src/api/plugins.ts +424 -0
  457. package/ui/src/api/projects.ts +34 -0
  458. package/ui/src/api/routines.ts +59 -0
  459. package/ui/src/api/secrets.ts +26 -0
  460. package/ui/src/api/sidebarBadges.ts +7 -0
  461. package/ui/src/components/AccountingModelCard.tsx +69 -0
  462. package/ui/src/components/ActiveAgentsPanel.tsx +157 -0
  463. package/ui/src/components/ActivityCharts.tsx +264 -0
  464. package/ui/src/components/ActivityRow.tsx +147 -0
  465. package/ui/src/components/AgentActionButtons.tsx +51 -0
  466. package/ui/src/components/AgentConfigForm.tsx +1468 -0
  467. package/ui/src/components/AgentIconPicker.tsx +81 -0
  468. package/ui/src/components/AgentProperties.tsx +107 -0
  469. package/ui/src/components/ApprovalCard.tsx +107 -0
  470. package/ui/src/components/ApprovalPayload.tsx +134 -0
  471. package/ui/src/components/AsciiArtAnimation.tsx +355 -0
  472. package/ui/src/components/BillerSpendCard.tsx +146 -0
  473. package/ui/src/components/BreadcrumbBar.tsx +113 -0
  474. package/ui/src/components/BudgetIncidentCard.tsx +101 -0
  475. package/ui/src/components/BudgetPolicyCard.tsx +220 -0
  476. package/ui/src/components/BudgetSidebarMarker.tsx +13 -0
  477. package/ui/src/components/ClaudeSubscriptionPanel.tsx +141 -0
  478. package/ui/src/components/CodexSubscriptionPanel.tsx +158 -0
  479. package/ui/src/components/CommandPalette.tsx +239 -0
  480. package/ui/src/components/CommentThread.tsx +503 -0
  481. package/ui/src/components/CompanyPatternIcon.tsx +212 -0
  482. package/ui/src/components/CompanyRail.tsx +329 -0
  483. package/ui/src/components/CompanySwitcher.tsx +81 -0
  484. package/ui/src/components/CopyText.tsx +56 -0
  485. package/ui/src/components/DevRestartBanner.tsx +89 -0
  486. package/ui/src/components/EmptyState.tsx +27 -0
  487. package/ui/src/components/EntityRow.tsx +69 -0
  488. package/ui/src/components/FilterBar.tsx +39 -0
  489. package/ui/src/components/FinanceBillerCard.tsx +45 -0
  490. package/ui/src/components/FinanceKindCard.tsx +44 -0
  491. package/ui/src/components/FinanceTimelineCard.tsx +72 -0
  492. package/ui/src/components/GoalProperties.tsx +165 -0
  493. package/ui/src/components/GoalTree.tsx +118 -0
  494. package/ui/src/components/Identity.tsx +39 -0
  495. package/ui/src/components/InlineEditor.tsx +248 -0
  496. package/ui/src/components/InlineEntitySelector.tsx +206 -0
  497. package/ui/src/components/InstanceSidebar.tsx +53 -0
  498. package/ui/src/components/IssueDocumentsSection.tsx +892 -0
  499. package/ui/src/components/IssueProperties.tsx +621 -0
  500. package/ui/src/components/IssueRow.tsx +149 -0
  501. package/ui/src/components/IssueWorkspaceCard.tsx +404 -0
  502. package/ui/src/components/IssuesList.tsx +889 -0
  503. package/ui/src/components/JsonSchemaForm.tsx +1048 -0
  504. package/ui/src/components/KanbanBoard.tsx +275 -0
  505. package/ui/src/components/Layout.tsx +441 -0
  506. package/ui/src/components/LiveRunWidget.tsx +160 -0
  507. package/ui/src/components/MarkdownBody.test.tsx +50 -0
  508. package/ui/src/components/MarkdownBody.tsx +152 -0
  509. package/ui/src/components/MarkdownEditor.tsx +622 -0
  510. package/ui/src/components/MetricCard.tsx +53 -0
  511. package/ui/src/components/MobileBottomNav.tsx +123 -0
  512. package/ui/src/components/NewAgentDialog.tsx +223 -0
  513. package/ui/src/components/NewGoalDialog.tsx +283 -0
  514. package/ui/src/components/NewIssueDialog.tsx +1473 -0
  515. package/ui/src/components/NewProjectDialog.tsx +451 -0
  516. package/ui/src/components/OnboardingWizard.tsx +1392 -0
  517. package/ui/src/components/OpenCodeLogoIcon.tsx +22 -0
  518. package/ui/src/components/PackageFileTree.tsx +318 -0
  519. package/ui/src/components/PageSkeleton.tsx +180 -0
  520. package/ui/src/components/PageTabBar.tsx +45 -0
  521. package/ui/src/components/PathInstructionsModal.tsx +143 -0
  522. package/ui/src/components/PriorityIcon.tsx +77 -0
  523. package/ui/src/components/ProjectProperties.tsx +1127 -0
  524. package/ui/src/components/PropertiesPanel.tsx +29 -0
  525. package/ui/src/components/ProviderQuotaCard.tsx +417 -0
  526. package/ui/src/components/QuotaBar.tsx +65 -0
  527. package/ui/src/components/ReportsToPicker.tsx +127 -0
  528. package/ui/src/components/ScheduleEditor.tsx +344 -0
  529. package/ui/src/components/ScrollToBottom.tsx +79 -0
  530. package/ui/src/components/Sidebar.tsx +130 -0
  531. package/ui/src/components/SidebarAgents.tsx +146 -0
  532. package/ui/src/components/SidebarNavItem.tsx +92 -0
  533. package/ui/src/components/SidebarProjects.tsx +234 -0
  534. package/ui/src/components/SidebarSection.tsx +17 -0
  535. package/ui/src/components/StatusBadge.tsx +15 -0
  536. package/ui/src/components/StatusIcon.tsx +71 -0
  537. package/ui/src/components/SwipeToArchive.tsx +152 -0
  538. package/ui/src/components/ToastViewport.tsx +99 -0
  539. package/ui/src/components/WorktreeBanner.tsx +25 -0
  540. package/ui/src/components/agent-config-defaults.ts +31 -0
  541. package/ui/src/components/agent-config-primitives.tsx +476 -0
  542. package/ui/src/components/transcript/RunTranscriptView.test.tsx +84 -0
  543. package/ui/src/components/transcript/RunTranscriptView.tsx +1015 -0
  544. package/ui/src/components/transcript/useLiveRunTranscripts.ts +297 -0
  545. package/ui/src/components/ui/avatar.tsx +107 -0
  546. package/ui/src/components/ui/badge.tsx +48 -0
  547. package/ui/src/components/ui/breadcrumb.tsx +109 -0
  548. package/ui/src/components/ui/button.tsx +64 -0
  549. package/ui/src/components/ui/card.tsx +92 -0
  550. package/ui/src/components/ui/checkbox.tsx +32 -0
  551. package/ui/src/components/ui/collapsible.tsx +33 -0
  552. package/ui/src/components/ui/command.tsx +194 -0
  553. package/ui/src/components/ui/dialog.tsx +156 -0
  554. package/ui/src/components/ui/dropdown-menu.tsx +257 -0
  555. package/ui/src/components/ui/input.tsx +21 -0
  556. package/ui/src/components/ui/label.tsx +22 -0
  557. package/ui/src/components/ui/popover.tsx +88 -0
  558. package/ui/src/components/ui/scroll-area.tsx +56 -0
  559. package/ui/src/components/ui/select.tsx +188 -0
  560. package/ui/src/components/ui/separator.tsx +28 -0
  561. package/ui/src/components/ui/sheet.tsx +143 -0
  562. package/ui/src/components/ui/skeleton.tsx +13 -0
  563. package/ui/src/components/ui/tabs.tsx +89 -0
  564. package/ui/src/components/ui/textarea.tsx +18 -0
  565. package/ui/src/components/ui/tooltip.tsx +57 -0
  566. package/ui/src/components/visual-office/AgentAvatar.tsx +99 -0
  567. package/ui/src/components/visual-office/OfficeViewExact.tsx +417 -0
  568. package/ui/src/components/visual-office/i18n.ts +52 -0
  569. package/ui/src/components/visual-office/office-view/CliUsagePanel.tsx +240 -0
  570. package/ui/src/components/visual-office/office-view/VirtualPadOverlay.tsx +104 -0
  571. package/ui/src/components/visual-office/office-view/buildScene-break-room.ts +248 -0
  572. package/ui/src/components/visual-office/office-view/buildScene-ceo-hallway.ts +345 -0
  573. package/ui/src/components/visual-office/office-view/buildScene-department-agent.ts +242 -0
  574. package/ui/src/components/visual-office/office-view/buildScene-departments.ts +360 -0
  575. package/ui/src/components/visual-office/office-view/buildScene-final-layers.ts +113 -0
  576. package/ui/src/components/visual-office/office-view/buildScene-types.ts +91 -0
  577. package/ui/src/components/visual-office/office-view/buildScene.ts +232 -0
  578. package/ui/src/components/visual-office/office-view/drawing-core.ts +374 -0
  579. package/ui/src/components/visual-office/office-view/drawing-furniture-a.ts +338 -0
  580. package/ui/src/components/visual-office/office-view/drawing-furniture-b.ts +241 -0
  581. package/ui/src/components/visual-office/office-view/model.ts +301 -0
  582. package/ui/src/components/visual-office/office-view/officeTicker.ts +455 -0
  583. package/ui/src/components/visual-office/office-view/officeTickerRoomAndDelivery.ts +133 -0
  584. package/ui/src/components/visual-office/office-view/themes-locale.ts +460 -0
  585. package/ui/src/components/visual-office/office-view/useCliUsage.ts +37 -0
  586. package/ui/src/components/visual-office/office-view/useOfficeDeliveryEffects.ts +465 -0
  587. package/ui/src/components/visual-office/office-view/useOfficePixiRuntime.ts +282 -0
  588. package/ui/src/components/visual-office/types.ts +123 -0
  589. package/ui/src/context/BreadcrumbContext.tsx +44 -0
  590. package/ui/src/context/CompanyContext.tsx +151 -0
  591. package/ui/src/context/DialogContext.tsx +135 -0
  592. package/ui/src/context/LiveUpdatesProvider.test.ts +119 -0
  593. package/ui/src/context/LiveUpdatesProvider.tsx +760 -0
  594. package/ui/src/context/PanelContext.tsx +73 -0
  595. package/ui/src/context/SidebarContext.tsx +43 -0
  596. package/ui/src/context/ThemeContext.tsx +83 -0
  597. package/ui/src/context/ToastContext.tsx +172 -0
  598. package/ui/src/fixtures/runTranscriptFixtures.ts +226 -0
  599. package/ui/src/hooks/useAgentOrder.ts +105 -0
  600. package/ui/src/hooks/useAutosaveIndicator.ts +72 -0
  601. package/ui/src/hooks/useCompanyPageMemory.test.ts +90 -0
  602. package/ui/src/hooks/useCompanyPageMemory.ts +79 -0
  603. package/ui/src/hooks/useDateRange.ts +120 -0
  604. package/ui/src/hooks/useInboxBadge.ts +132 -0
  605. package/ui/src/hooks/useKeyboardShortcuts.ts +40 -0
  606. package/ui/src/hooks/useProjectOrder.ts +106 -0
  607. package/ui/src/index.css +770 -0
  608. package/ui/src/lib/agent-icons.ts +99 -0
  609. package/ui/src/lib/agent-order.ts +107 -0
  610. package/ui/src/lib/agent-skills-state.test.ts +90 -0
  611. package/ui/src/lib/agent-skills-state.ts +41 -0
  612. package/ui/src/lib/assignees.test.ts +92 -0
  613. package/ui/src/lib/assignees.ts +82 -0
  614. package/ui/src/lib/color-contrast.ts +107 -0
  615. package/ui/src/lib/company-export-selection.test.ts +41 -0
  616. package/ui/src/lib/company-export-selection.ts +57 -0
  617. package/ui/src/lib/company-page-memory.ts +65 -0
  618. package/ui/src/lib/company-portability-sidebar.test.ts +101 -0
  619. package/ui/src/lib/company-portability-sidebar.ts +62 -0
  620. package/ui/src/lib/company-routes.ts +88 -0
  621. package/ui/src/lib/company-selection.test.ts +34 -0
  622. package/ui/src/lib/company-selection.ts +18 -0
  623. package/ui/src/lib/groupBy.ts +11 -0
  624. package/ui/src/lib/inbox.test.ts +404 -0
  625. package/ui/src/lib/inbox.ts +292 -0
  626. package/ui/src/lib/instance-settings.test.ts +26 -0
  627. package/ui/src/lib/instance-settings.ts +25 -0
  628. package/ui/src/lib/issueDetailBreadcrumb.ts +24 -0
  629. package/ui/src/lib/legacy-agent-config.test.ts +40 -0
  630. package/ui/src/lib/legacy-agent-config.ts +17 -0
  631. package/ui/src/lib/mention-aware-link-node.test.ts +50 -0
  632. package/ui/src/lib/mention-aware-link-node.ts +67 -0
  633. package/ui/src/lib/mention-chips.ts +168 -0
  634. package/ui/src/lib/mention-deletion.test.ts +87 -0
  635. package/ui/src/lib/mention-deletion.ts +143 -0
  636. package/ui/src/lib/model-utils.ts +16 -0
  637. package/ui/src/lib/onboarding-goal.test.ts +22 -0
  638. package/ui/src/lib/onboarding-goal.ts +18 -0
  639. package/ui/src/lib/onboarding-launch.test.ts +131 -0
  640. package/ui/src/lib/onboarding-launch.ts +54 -0
  641. package/ui/src/lib/onboarding-route.test.ts +80 -0
  642. package/ui/src/lib/onboarding-route.ts +51 -0
  643. package/ui/src/lib/portable-files.ts +42 -0
  644. package/ui/src/lib/project-order.ts +71 -0
  645. package/ui/src/lib/queryKeys.ts +140 -0
  646. package/ui/src/lib/recent-assignees.ts +36 -0
  647. package/ui/src/lib/router.tsx +76 -0
  648. package/ui/src/lib/routine-trigger-patch.test.ts +72 -0
  649. package/ui/src/lib/routine-trigger-patch.ts +31 -0
  650. package/ui/src/lib/status-colors.ts +108 -0
  651. package/ui/src/lib/timeAgo.ts +31 -0
  652. package/ui/src/lib/utils.ts +168 -0
  653. package/ui/src/lib/worktree-branding.ts +65 -0
  654. package/ui/src/lib/zip.test.ts +289 -0
  655. package/ui/src/lib/zip.ts +284 -0
  656. package/ui/src/main.tsx +67 -0
  657. package/ui/src/pages/Activity.tsx +141 -0
  658. package/ui/src/pages/AgentDetail.tsx +4053 -0
  659. package/ui/src/pages/Agents.tsx +415 -0
  660. package/ui/src/pages/ApprovalDetail.tsx +369 -0
  661. package/ui/src/pages/Approvals.tsx +132 -0
  662. package/ui/src/pages/Auth.tsx +180 -0
  663. package/ui/src/pages/BoardClaim.tsx +125 -0
  664. package/ui/src/pages/CliAuth.tsx +184 -0
  665. package/ui/src/pages/Companies.tsx +297 -0
  666. package/ui/src/pages/CompanyExport.tsx +1019 -0
  667. package/ui/src/pages/CompanyImport.tsx +1355 -0
  668. package/ui/src/pages/CompanySettings.tsx +661 -0
  669. package/ui/src/pages/CompanySkills.tsx +1171 -0
  670. package/ui/src/pages/Costs.tsx +1103 -0
  671. package/ui/src/pages/Dashboard.tsx +388 -0
  672. package/ui/src/pages/DesignGuide.tsx +1330 -0
  673. package/ui/src/pages/ExecutionWorkspaceDetail.tsx +82 -0
  674. package/ui/src/pages/GoalDetail.tsx +197 -0
  675. package/ui/src/pages/Goals.tsx +63 -0
  676. package/ui/src/pages/Inbox.tsx +1291 -0
  677. package/ui/src/pages/InstanceExperimentalSettings.tsx +139 -0
  678. package/ui/src/pages/InstanceGeneralSettings.tsx +104 -0
  679. package/ui/src/pages/InstanceSettings.tsx +284 -0
  680. package/ui/src/pages/InviteLanding.tsx +320 -0
  681. package/ui/src/pages/IssueDetail.tsx +1201 -0
  682. package/ui/src/pages/Issues.tsx +116 -0
  683. package/ui/src/pages/MyIssues.tsx +72 -0
  684. package/ui/src/pages/NewAgent.tsx +353 -0
  685. package/ui/src/pages/NotFound.tsx +66 -0
  686. package/ui/src/pages/Org.tsx +132 -0
  687. package/ui/src/pages/OrgChart.tsx +447 -0
  688. package/ui/src/pages/PluginManager.tsx +510 -0
  689. package/ui/src/pages/PluginPage.tsx +156 -0
  690. package/ui/src/pages/PluginSettings.tsx +836 -0
  691. package/ui/src/pages/ProjectDetail.tsx +633 -0
  692. package/ui/src/pages/Projects.tsx +87 -0
  693. package/ui/src/pages/RoutineDetail.tsx +1022 -0
  694. package/ui/src/pages/Routines.tsx +661 -0
  695. package/ui/src/pages/RunTranscriptUxLab.tsx +334 -0
  696. package/ui/src/pages/VisualOffice.tsx +243 -0
  697. package/ui/src/plugins/bridge-init.ts +69 -0
  698. package/ui/src/plugins/bridge.ts +476 -0
  699. package/ui/src/plugins/launchers.tsx +834 -0
  700. package/ui/src/plugins/slots.tsx +855 -0
  701. package/ui/tsconfig.json +21 -0
  702. package/ui/vite.config.ts +23 -0
  703. package/ui/vitest.config.ts +14 -0
  704. package/vitest.config.ts +11 -0
@@ -0,0 +1,1343 @@
1
+ /**
2
+ * PluginWorkerManager — spawns and manages out-of-process plugin worker child
3
+ * processes, routes JSON-RPC 2.0 calls over stdio, and handles lifecycle
4
+ * management including crash recovery with exponential backoff.
5
+ *
6
+ * Each installed plugin gets one dedicated worker process. The host sends
7
+ * JSON-RPC requests over the child's stdin and reads responses from stdout.
8
+ * Worker stderr is captured and forwarded to the host logger.
9
+ *
10
+ * Process Model (from PLUGIN_SPEC.md §12):
11
+ * - One worker process per installed plugin
12
+ * - Failure isolation: plugin crashes do not affect the host
13
+ * - Graceful shutdown: 10-second drain, then SIGTERM, then SIGKILL
14
+ * - Automatic restart with exponential backoff on unexpected exits
15
+ *
16
+ * @see PLUGIN_SPEC.md §12 — Process Model
17
+ * @see PLUGIN_SPEC.md §12.5 — Graceful Shutdown Policy
18
+ * @see PLUGIN_SPEC.md §13 — Host-Worker Protocol
19
+ */
20
+
21
+ import { fork, type ChildProcess } from "node:child_process";
22
+ import { EventEmitter } from "node:events";
23
+ import { createInterface, type Interface as ReadlineInterface } from "node:readline";
24
+ import type { PaperclipPluginManifestV1 } from "@corporateai/shared";
25
+ import {
26
+ JSONRPC_VERSION,
27
+ JSONRPC_ERROR_CODES,
28
+ PLUGIN_RPC_ERROR_CODES,
29
+ createRequest,
30
+ createErrorResponse,
31
+ parseMessage,
32
+ serializeMessage,
33
+ isJsonRpcResponse,
34
+ isJsonRpcRequest,
35
+ isJsonRpcNotification,
36
+ isJsonRpcSuccessResponse,
37
+ JsonRpcParseError,
38
+ JsonRpcCallError,
39
+ } from "@corporateai/plugin-sdk";
40
+ import type {
41
+ JsonRpcId,
42
+ JsonRpcResponse,
43
+ JsonRpcRequest,
44
+ JsonRpcNotification,
45
+ HostToWorkerMethodName,
46
+ HostToWorkerMethods,
47
+ WorkerToHostMethodName,
48
+ WorkerToHostMethods,
49
+ InitializeParams,
50
+ } from "@corporateai/plugin-sdk";
51
+ import { logger } from "../middleware/logger.js";
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Constants
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /** Default timeout for RPC calls in milliseconds. */
58
+ const DEFAULT_RPC_TIMEOUT_MS = 30_000;
59
+
60
+ /** Hard upper bound for any RPC timeout (5 minutes). Prevents unbounded waits. */
61
+ const MAX_RPC_TIMEOUT_MS = 5 * 60 * 1_000;
62
+
63
+ /** Timeout for the initialize RPC call. */
64
+ const INITIALIZE_TIMEOUT_MS = 15_000;
65
+
66
+ /** Timeout for the shutdown RPC call before escalating to SIGTERM. */
67
+ const SHUTDOWN_DRAIN_MS = 10_000;
68
+
69
+ /** Time to wait after SIGTERM before sending SIGKILL. */
70
+ const SIGTERM_GRACE_MS = 5_000;
71
+
72
+ /** Minimum backoff delay for crash recovery (1 second). */
73
+ const MIN_BACKOFF_MS = 1_000;
74
+
75
+ /** Maximum backoff delay for crash recovery (5 minutes). */
76
+ const MAX_BACKOFF_MS = 5 * 60 * 1_000;
77
+
78
+ /** Backoff multiplier on each consecutive crash. */
79
+ const BACKOFF_MULTIPLIER = 2;
80
+
81
+ /** Maximum number of consecutive crashes before giving up on auto-restart. */
82
+ const MAX_CONSECUTIVE_CRASHES = 10;
83
+
84
+ /** Time window in which crashes are considered consecutive (10 minutes). */
85
+ const CRASH_WINDOW_MS = 10 * 60 * 1_000;
86
+
87
+ /** Maximum number of stderr characters retained for worker failure context. */
88
+ const MAX_STDERR_EXCERPT_CHARS = 8_000;
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Types
92
+ // ---------------------------------------------------------------------------
93
+
94
+ /**
95
+ * Status of a managed worker process.
96
+ */
97
+ export type WorkerStatus =
98
+ | "stopped"
99
+ | "starting"
100
+ | "running"
101
+ | "stopping"
102
+ | "crashed"
103
+ | "backoff";
104
+
105
+ /**
106
+ * Worker-to-host method handler. The host registers these to service calls
107
+ * that the plugin worker makes back to the host (e.g. state.get, events.emit).
108
+ */
109
+ export type WorkerToHostHandler<M extends WorkerToHostMethodName> = (
110
+ params: WorkerToHostMethods[M][0],
111
+ ) => Promise<WorkerToHostMethods[M][1]>;
112
+
113
+ /**
114
+ * A map of all worker-to-host method handlers provided by the host.
115
+ */
116
+ export type WorkerToHostHandlers = {
117
+ [M in WorkerToHostMethodName]?: WorkerToHostHandler<M>;
118
+ };
119
+
120
+ /**
121
+ * Events emitted by a PluginWorkerHandle.
122
+ */
123
+ export interface WorkerHandleEvents {
124
+ /** Worker process started and is ready (initialize succeeded). */
125
+ "ready": { pluginId: string };
126
+ /** Worker process exited. */
127
+ "exit": { pluginId: string; code: number | null; signal: NodeJS.Signals | null };
128
+ /** Worker process crashed unexpectedly. */
129
+ "crash": { pluginId: string; code: number | null; signal: NodeJS.Signals | null; willRestart: boolean };
130
+ /** Worker process errored (e.g. spawn failure). */
131
+ "error": { pluginId: string; error: Error };
132
+ /** Worker status changed. */
133
+ "status": { pluginId: string; status: WorkerStatus; previousStatus: WorkerStatus };
134
+ }
135
+
136
+ type WorkerHandleEventName = keyof WorkerHandleEvents;
137
+
138
+ export function appendStderrExcerpt(current: string, chunk: string): string {
139
+ const next = current ? `${current}\n${chunk}` : chunk;
140
+ return next.length <= MAX_STDERR_EXCERPT_CHARS
141
+ ? next
142
+ : next.slice(-MAX_STDERR_EXCERPT_CHARS);
143
+ }
144
+
145
+ export function formatWorkerFailureMessage(message: string, stderrExcerpt: string): string {
146
+ const excerpt = stderrExcerpt.trim();
147
+ if (!excerpt) return message;
148
+ if (message.includes(excerpt)) return message;
149
+ return `${message}\n\nWorker stderr:\n${excerpt}`;
150
+ }
151
+
152
+ /**
153
+ * Options for starting a worker process.
154
+ */
155
+ export interface WorkerStartOptions {
156
+ /** Absolute path to the plugin worker entrypoint (CJS bundle). */
157
+ entrypointPath: string;
158
+ /** Plugin manifest. */
159
+ manifest: PaperclipPluginManifestV1;
160
+ /** Resolved plugin configuration. */
161
+ config: Record<string, unknown>;
162
+ /** Host instance information for the initialize call. */
163
+ instanceInfo: {
164
+ instanceId: string;
165
+ hostVersion: string;
166
+ };
167
+ /** Host API version. */
168
+ apiVersion: number;
169
+ /** Handlers for worker→host RPC calls. */
170
+ hostHandlers: WorkerToHostHandlers;
171
+ /** Default timeout for RPC calls (ms). Defaults to 30s. */
172
+ rpcTimeoutMs?: number;
173
+ /** Whether to auto-restart on crash. Defaults to true. */
174
+ autoRestart?: boolean;
175
+ /** Node.js execArgv passed to the child process. */
176
+ execArgv?: string[];
177
+ /** Environment variables passed to the child process. */
178
+ env?: Record<string, string>;
179
+ /**
180
+ * Callback for stream notifications from the worker (streams.open/emit/close).
181
+ * The host wires this to the PluginStreamBus to fan out events to SSE clients.
182
+ */
183
+ onStreamNotification?: (method: string, params: Record<string, unknown>) => void;
184
+ }
185
+
186
+ /**
187
+ * A pending RPC call waiting for a response from the worker.
188
+ */
189
+ interface PendingRequest {
190
+ /** The request ID. */
191
+ id: JsonRpcId;
192
+ /** Method name (for logging). */
193
+ method: string;
194
+ /** Resolve the promise with the response. */
195
+ resolve: (response: JsonRpcResponse) => void;
196
+ /** Timeout timer handle. */
197
+ timer: ReturnType<typeof setTimeout>;
198
+ /** Timestamp when the request was sent. */
199
+ sentAt: number;
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // PluginWorkerHandle — manages a single worker process
204
+ // ---------------------------------------------------------------------------
205
+
206
+ /**
207
+ * Handle for a single plugin worker process.
208
+ *
209
+ * Callers use `start()` to spawn the worker, `call()` to send RPC requests,
210
+ * and `stop()` to gracefully shut down. The handle manages crash recovery
211
+ * with exponential backoff automatically when `autoRestart` is enabled.
212
+ */
213
+ export interface PluginWorkerHandle {
214
+ /** The plugin ID this worker serves. */
215
+ readonly pluginId: string;
216
+
217
+ /** Current worker status. */
218
+ readonly status: WorkerStatus;
219
+
220
+ /** Start the worker process. Resolves when initialize completes. */
221
+ start(): Promise<void>;
222
+
223
+ /**
224
+ * Stop the worker process gracefully.
225
+ *
226
+ * Sends a `shutdown` RPC call, waits up to 10 seconds for the worker to
227
+ * exit, then escalates to SIGTERM, and finally SIGKILL if needed.
228
+ */
229
+ stop(): Promise<void>;
230
+
231
+ /**
232
+ * Restart the worker process (stop + start).
233
+ */
234
+ restart(): Promise<void>;
235
+
236
+ /**
237
+ * Send a typed host→worker RPC call.
238
+ *
239
+ * @param method - The RPC method name
240
+ * @param params - Method parameters
241
+ * @param timeoutMs - Optional per-call timeout override
242
+ * @returns The method result
243
+ * @throws {JsonRpcCallError} if the worker returns an error response
244
+ * @throws {Error} if the worker is not running or the call times out
245
+ */
246
+ call<M extends HostToWorkerMethodName>(
247
+ method: M,
248
+ params: HostToWorkerMethods[M][0],
249
+ timeoutMs?: number,
250
+ ): Promise<HostToWorkerMethods[M][1]>;
251
+
252
+ /**
253
+ * Send a fire-and-forget notification to the worker (no response expected).
254
+ */
255
+ notify(method: string, params: unknown): void;
256
+
257
+ /** Subscribe to worker events. */
258
+ on<K extends WorkerHandleEventName>(
259
+ event: K,
260
+ listener: (payload: WorkerHandleEvents[K]) => void,
261
+ ): void;
262
+
263
+ /** Unsubscribe from worker events. */
264
+ off<K extends WorkerHandleEventName>(
265
+ event: K,
266
+ listener: (payload: WorkerHandleEvents[K]) => void,
267
+ ): void;
268
+
269
+ /** Optional methods the worker reported during initialization. */
270
+ readonly supportedMethods: string[];
271
+
272
+ /** Get diagnostic info about the worker. */
273
+ diagnostics(): WorkerDiagnostics;
274
+ }
275
+
276
+ /**
277
+ * Diagnostic information about a worker process.
278
+ */
279
+ export interface WorkerDiagnostics {
280
+ pluginId: string;
281
+ status: WorkerStatus;
282
+ pid: number | null;
283
+ uptime: number | null;
284
+ consecutiveCrashes: number;
285
+ totalCrashes: number;
286
+ pendingRequests: number;
287
+ lastCrashAt: number | null;
288
+ nextRestartAt: number | null;
289
+ }
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // PluginWorkerManager — manages all plugin workers
293
+ // ---------------------------------------------------------------------------
294
+
295
+ /**
296
+ * The top-level manager that holds all plugin worker handles.
297
+ *
298
+ * Provides a registry of workers keyed by plugin ID, with convenience methods
299
+ * for starting/stopping all workers and routing RPC calls.
300
+ */
301
+ export interface PluginWorkerManager {
302
+ /**
303
+ * Register and start a worker for a plugin.
304
+ *
305
+ * @returns The worker handle
306
+ * @throws if a worker is already registered for this plugin
307
+ */
308
+ startWorker(pluginId: string, options: WorkerStartOptions): Promise<PluginWorkerHandle>;
309
+
310
+ /**
311
+ * Stop and unregister a specific plugin worker.
312
+ */
313
+ stopWorker(pluginId: string): Promise<void>;
314
+
315
+ /**
316
+ * Get the worker handle for a plugin.
317
+ */
318
+ getWorker(pluginId: string): PluginWorkerHandle | undefined;
319
+
320
+ /**
321
+ * Check if a worker is registered and running for a plugin.
322
+ */
323
+ isRunning(pluginId: string): boolean;
324
+
325
+ /**
326
+ * Stop all managed workers. Called during server shutdown.
327
+ */
328
+ stopAll(): Promise<void>;
329
+
330
+ /**
331
+ * Get diagnostic info for all workers.
332
+ */
333
+ diagnostics(): WorkerDiagnostics[];
334
+
335
+ /**
336
+ * Send an RPC call to a specific plugin worker.
337
+ *
338
+ * @throws if the worker is not running
339
+ */
340
+ call<M extends HostToWorkerMethodName>(
341
+ pluginId: string,
342
+ method: M,
343
+ params: HostToWorkerMethods[M][0],
344
+ timeoutMs?: number,
345
+ ): Promise<HostToWorkerMethods[M][1]>;
346
+ }
347
+
348
+ // ---------------------------------------------------------------------------
349
+ // Implementation: createPluginWorkerHandle
350
+ // ---------------------------------------------------------------------------
351
+
352
+ /**
353
+ * Create a handle for a single plugin worker process.
354
+ *
355
+ * @internal Exported for testing; consumers should use `createPluginWorkerManager`.
356
+ */
357
+ export function createPluginWorkerHandle(
358
+ pluginId: string,
359
+ options: WorkerStartOptions,
360
+ ): PluginWorkerHandle {
361
+ const log = logger.child({ service: "plugin-worker", pluginId });
362
+ const emitter = new EventEmitter();
363
+ /**
364
+ * Higher than default (10) to accommodate multiple subscribers to
365
+ * crash/ready/exit events during integration tests and runtime monitoring.
366
+ */
367
+ emitter.setMaxListeners(50);
368
+
369
+ // Worker process state
370
+ let childProcess: ChildProcess | null = null;
371
+ let readline: ReadlineInterface | null = null;
372
+ let stderrReadline: ReadlineInterface | null = null;
373
+ let status: WorkerStatus = "stopped";
374
+ let startedAt: number | null = null;
375
+ let stderrExcerpt = "";
376
+
377
+ // Pending RPC requests awaiting a response
378
+ const pendingRequests = new Map<string | number, PendingRequest>();
379
+ let nextRequestId = 1;
380
+
381
+ // Optional methods reported by the worker during initialization
382
+ let supportedMethods: string[] = [];
383
+
384
+ // Crash tracking for exponential backoff
385
+ let consecutiveCrashes = 0;
386
+ let totalCrashes = 0;
387
+ let lastCrashAt: number | null = null;
388
+ let backoffTimer: ReturnType<typeof setTimeout> | null = null;
389
+ let nextRestartAt: number | null = null;
390
+
391
+ // Track open stream channels so we can emit synthetic close on crash.
392
+ // Maps channel → companyId.
393
+ const openStreamChannels = new Map<string, string>();
394
+
395
+ // Shutdown coordination
396
+ let intentionalStop = false;
397
+
398
+ const rpcTimeoutMs = options.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS;
399
+ const autoRestart = options.autoRestart ?? true;
400
+
401
+ // -----------------------------------------------------------------------
402
+ // Status management
403
+ // -----------------------------------------------------------------------
404
+
405
+ function setStatus(newStatus: WorkerStatus): void {
406
+ const prev = status;
407
+ if (prev === newStatus) return;
408
+ status = newStatus;
409
+ log.debug({ from: prev, to: newStatus }, "worker status change");
410
+ emitter.emit("status", { pluginId, status: newStatus, previousStatus: prev });
411
+ }
412
+
413
+ // -----------------------------------------------------------------------
414
+ // JSON-RPC message sending
415
+ // -----------------------------------------------------------------------
416
+
417
+ function sendMessage(message: unknown): void {
418
+ if (!childProcess?.stdin?.writable) {
419
+ throw new Error(`Worker process for plugin "${pluginId}" is not writable`);
420
+ }
421
+ const serialized = serializeMessage(message as any);
422
+ childProcess.stdin.write(serialized);
423
+ }
424
+
425
+ // -----------------------------------------------------------------------
426
+ // Incoming message handling
427
+ // -----------------------------------------------------------------------
428
+
429
+ function handleLine(line: string): void {
430
+ if (!line.trim()) return;
431
+
432
+ let message: unknown;
433
+ try {
434
+ message = parseMessage(line);
435
+ } catch (err) {
436
+ if (err instanceof JsonRpcParseError) {
437
+ log.warn({ rawLine: line.slice(0, 200) }, "unparseable message from worker");
438
+ } else {
439
+ log.warn({ err }, "error parsing worker message");
440
+ }
441
+ return;
442
+ }
443
+
444
+ if (isJsonRpcResponse(message)) {
445
+ handleResponse(message);
446
+ } else if (isJsonRpcRequest(message)) {
447
+ handleWorkerRequest(message as JsonRpcRequest);
448
+ } else if (isJsonRpcNotification(message)) {
449
+ handleWorkerNotification(message as JsonRpcNotification);
450
+ } else {
451
+ log.warn("unknown message type from worker");
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Handle a JSON-RPC response from the worker (matching a pending request).
457
+ */
458
+ function handleResponse(response: JsonRpcResponse): void {
459
+ const id = response.id;
460
+ if (id === null || id === undefined) {
461
+ log.warn("received response with null/undefined id");
462
+ return;
463
+ }
464
+
465
+ const pending = pendingRequests.get(id);
466
+ if (!pending) {
467
+ log.warn({ id }, "received response for unknown request id");
468
+ return;
469
+ }
470
+
471
+ clearTimeout(pending.timer);
472
+ pendingRequests.delete(id);
473
+ pending.resolve(response);
474
+ }
475
+
476
+ /**
477
+ * Handle a JSON-RPC request from the worker (worker→host call).
478
+ */
479
+ async function handleWorkerRequest(request: JsonRpcRequest): Promise<void> {
480
+ const method = request.method as WorkerToHostMethodName;
481
+ const handler = options.hostHandlers[method] as
482
+ | ((params: unknown) => Promise<unknown>)
483
+ | undefined;
484
+
485
+ if (!handler) {
486
+ log.warn({ method }, "worker called unregistered host method");
487
+ try {
488
+ sendMessage(
489
+ createErrorResponse(
490
+ request.id,
491
+ JSONRPC_ERROR_CODES.METHOD_NOT_FOUND,
492
+ `Host does not handle method "${method}"`,
493
+ ),
494
+ );
495
+ } catch {
496
+ // Worker may have exited, ignore send error
497
+ }
498
+ return;
499
+ }
500
+
501
+ try {
502
+ const result = await handler(request.params);
503
+ sendMessage({
504
+ jsonrpc: JSONRPC_VERSION,
505
+ id: request.id,
506
+ result: result ?? null,
507
+ });
508
+ } catch (err) {
509
+ const errorMessage = err instanceof Error ? err.message : String(err);
510
+ log.error({ method, err: errorMessage }, "host handler error");
511
+ try {
512
+ sendMessage(
513
+ createErrorResponse(
514
+ request.id,
515
+ JSONRPC_ERROR_CODES.INTERNAL_ERROR,
516
+ errorMessage,
517
+ ),
518
+ );
519
+ } catch {
520
+ // Worker may have exited, ignore send error
521
+ }
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Handle a JSON-RPC notification from the worker (fire-and-forget).
527
+ *
528
+ * The `log` notification is the primary case — worker `ctx.logger` calls
529
+ * arrive here. We append structured plugin context (pluginId, timestamp,
530
+ * level) so that every log entry is queryable per the spec (§26.1).
531
+ */
532
+ function handleWorkerNotification(notification: JsonRpcNotification): void {
533
+ if (notification.method === "log") {
534
+ const params = notification.params as {
535
+ level?: string;
536
+ message?: string;
537
+ meta?: Record<string, unknown>;
538
+ } | null;
539
+ const level = params?.level ?? "info";
540
+ const msg = params?.message ?? "";
541
+ const meta = params?.meta;
542
+
543
+ // Build a structured log object that includes the plugin context fields
544
+ // required by §26.1: pluginId, timestamp, level, message, and metadata.
545
+ // The child logger already carries `pluginId` in its bindings, but we
546
+ // add explicit `pluginLogLevel` and `pluginTimestamp` so downstream
547
+ // consumers (log storage, UI queries) can filter without parsing.
548
+ const logFields: Record<string, unknown> = {
549
+ ...meta,
550
+ pluginLogLevel: level,
551
+ pluginTimestamp: new Date().toISOString(),
552
+ };
553
+
554
+ if (level === "error") {
555
+ log.error(logFields, `[plugin] ${msg}`);
556
+ } else if (level === "warn") {
557
+ log.warn(logFields, `[plugin] ${msg}`);
558
+ } else if (level === "debug") {
559
+ log.debug(logFields, `[plugin] ${msg}`);
560
+ } else {
561
+ log.info(logFields, `[plugin] ${msg}`);
562
+ }
563
+ return;
564
+ }
565
+
566
+ // Stream notifications: forward to the stream bus via callback
567
+ if (
568
+ notification.method === "streams.open" ||
569
+ notification.method === "streams.emit" ||
570
+ notification.method === "streams.close"
571
+ ) {
572
+ const params = (notification.params ?? {}) as Record<string, unknown>;
573
+
574
+ // Track open channels so we can emit synthetic close on crash
575
+ if (notification.method === "streams.open") {
576
+ const ch = String(params.channel ?? "");
577
+ const co = String(params.companyId ?? "");
578
+ if (ch) openStreamChannels.set(ch, co);
579
+ } else if (notification.method === "streams.close") {
580
+ openStreamChannels.delete(String(params.channel ?? ""));
581
+ }
582
+
583
+ if (options.onStreamNotification) {
584
+ try {
585
+ options.onStreamNotification(notification.method, params);
586
+ } catch (err) {
587
+ log.error(
588
+ {
589
+ method: notification.method,
590
+ err: err instanceof Error ? err.message : String(err),
591
+ },
592
+ "stream notification handler failed",
593
+ );
594
+ }
595
+ }
596
+ return;
597
+ }
598
+
599
+ log.debug({ method: notification.method }, "received notification from worker");
600
+ }
601
+
602
+ // -----------------------------------------------------------------------
603
+ // Process lifecycle
604
+ // -----------------------------------------------------------------------
605
+
606
+ function spawnProcess(): ChildProcess {
607
+ // Security: Do NOT spread process.env into the worker. Plugins should only
608
+ // receive a minimal, controlled environment to prevent leaking host
609
+ // secrets (like DATABASE_URL, internal API keys, etc.).
610
+ const workerEnv: Record<string, string> = {
611
+ ...options.env,
612
+ PATH: process.env.PATH ?? "",
613
+ NODE_PATH: process.env.NODE_PATH ?? "",
614
+ PAPERCLIP_PLUGIN_ID: pluginId,
615
+ NODE_ENV: process.env.NODE_ENV ?? "production",
616
+ TZ: process.env.TZ ?? "UTC",
617
+ };
618
+
619
+ const child = fork(options.entrypointPath, [], {
620
+ stdio: ["pipe", "pipe", "pipe", "ipc"],
621
+ execArgv: options.execArgv ?? [],
622
+ env: workerEnv,
623
+ // Don't let the child keep the parent alive
624
+ detached: false,
625
+ });
626
+
627
+ return child;
628
+ }
629
+
630
+ function attachStdioHandlers(child: ChildProcess): void {
631
+ // Read NDJSON from stdout
632
+ if (child.stdout) {
633
+ readline = createInterface({ input: child.stdout });
634
+ readline.on("line", handleLine);
635
+ }
636
+
637
+ // Capture stderr for logging
638
+ if (child.stderr) {
639
+ stderrReadline = createInterface({ input: child.stderr });
640
+ stderrReadline.on("line", (line: string) => {
641
+ stderrExcerpt = appendStderrExcerpt(stderrExcerpt, line);
642
+ log.warn({ stream: "stderr" }, `[plugin stderr] ${line}`);
643
+ });
644
+ }
645
+
646
+ // Handle process exit
647
+ child.on("exit", (code, signal) => {
648
+ handleProcessExit(code, signal);
649
+ });
650
+
651
+ // Handle process errors (e.g. spawn failure)
652
+ child.on("error", (err) => {
653
+ log.error({ err: err.message }, "worker process error");
654
+ emitter.emit("error", { pluginId, error: err });
655
+ if (status === "starting") {
656
+ setStatus("crashed");
657
+ rejectAllPending(
658
+ new Error(formatWorkerFailureMessage(
659
+ `Worker process failed to start: ${err.message}`,
660
+ stderrExcerpt,
661
+ )),
662
+ );
663
+ }
664
+ });
665
+ }
666
+
667
+ function handleProcessExit(
668
+ code: number | null,
669
+ signal: NodeJS.Signals | null,
670
+ ): void {
671
+ const wasIntentional = intentionalStop;
672
+
673
+ // Clean up readline interfaces
674
+ if (readline) {
675
+ readline.close();
676
+ readline = null;
677
+ }
678
+ if (stderrReadline) {
679
+ stderrReadline.close();
680
+ stderrReadline = null;
681
+ }
682
+ childProcess = null;
683
+ startedAt = null;
684
+
685
+ // Reject all pending requests
686
+ rejectAllPending(
687
+ new Error(formatWorkerFailureMessage(
688
+ `Worker process exited (code=${code}, signal=${signal})`,
689
+ stderrExcerpt,
690
+ )),
691
+ );
692
+
693
+ // Emit synthetic close for any orphaned stream channels so SSE clients
694
+ // are notified instead of hanging indefinitely.
695
+ if (openStreamChannels.size > 0 && options.onStreamNotification) {
696
+ for (const [channel, companyId] of openStreamChannels) {
697
+ try {
698
+ options.onStreamNotification("streams.close", { channel, companyId });
699
+ } catch {
700
+ // Best-effort cleanup — don't let it interfere with exit handling
701
+ }
702
+ }
703
+ openStreamChannels.clear();
704
+ }
705
+
706
+ emitter.emit("exit", { pluginId, code, signal });
707
+
708
+ if (wasIntentional) {
709
+ // Graceful stop — status is already "stopping" or will be set to "stopped"
710
+ setStatus("stopped");
711
+ log.info({ code, signal }, "worker process stopped");
712
+ return;
713
+ }
714
+
715
+ // Unexpected exit — crash recovery
716
+ totalCrashes++;
717
+ const now = Date.now();
718
+
719
+ // Reset consecutive crash counter if enough time passed
720
+ if (lastCrashAt !== null && now - lastCrashAt > CRASH_WINDOW_MS) {
721
+ consecutiveCrashes = 0;
722
+ }
723
+ consecutiveCrashes++;
724
+ lastCrashAt = now;
725
+
726
+ log.error(
727
+ { code, signal, consecutiveCrashes, totalCrashes },
728
+ "worker process crashed",
729
+ );
730
+
731
+ const willRestart =
732
+ autoRestart && consecutiveCrashes <= MAX_CONSECUTIVE_CRASHES;
733
+
734
+ setStatus("crashed");
735
+ emitter.emit("crash", { pluginId, code, signal, willRestart });
736
+
737
+ if (willRestart) {
738
+ scheduleRestart();
739
+ } else {
740
+ log.error(
741
+ { consecutiveCrashes, maxCrashes: MAX_CONSECUTIVE_CRASHES },
742
+ "max consecutive crashes reached, not restarting",
743
+ );
744
+ }
745
+ }
746
+
747
+ function rejectAllPending(error: Error): void {
748
+ for (const [id, pending] of pendingRequests) {
749
+ clearTimeout(pending.timer);
750
+ pending.resolve(
751
+ createErrorResponse(
752
+ pending.id,
753
+ PLUGIN_RPC_ERROR_CODES.WORKER_UNAVAILABLE,
754
+ error.message,
755
+ ) as JsonRpcResponse,
756
+ );
757
+ }
758
+ pendingRequests.clear();
759
+ }
760
+
761
+ // -----------------------------------------------------------------------
762
+ // Crash recovery with exponential backoff
763
+ // -----------------------------------------------------------------------
764
+
765
+ function computeBackoffMs(): number {
766
+ // Exponential backoff: MIN_BACKOFF * MULTIPLIER^(consecutiveCrashes - 1)
767
+ const delay =
768
+ MIN_BACKOFF_MS * Math.pow(BACKOFF_MULTIPLIER, consecutiveCrashes - 1);
769
+ // Add jitter: ±25%
770
+ const jitter = delay * 0.25 * (Math.random() * 2 - 1);
771
+ return Math.min(Math.round(delay + jitter), MAX_BACKOFF_MS);
772
+ }
773
+
774
+ function scheduleRestart(): void {
775
+ const delay = computeBackoffMs();
776
+ nextRestartAt = Date.now() + delay;
777
+
778
+ setStatus("backoff");
779
+
780
+ log.info(
781
+ { delayMs: delay, consecutiveCrashes },
782
+ "scheduling restart with backoff",
783
+ );
784
+
785
+ backoffTimer = setTimeout(async () => {
786
+ backoffTimer = null;
787
+ nextRestartAt = null;
788
+ try {
789
+ await startInternal();
790
+ } catch (err) {
791
+ log.error(
792
+ { err: err instanceof Error ? err.message : String(err) },
793
+ "restart after backoff failed",
794
+ );
795
+ }
796
+ }, delay);
797
+ }
798
+
799
+ function cancelPendingRestart(): void {
800
+ if (backoffTimer !== null) {
801
+ clearTimeout(backoffTimer);
802
+ backoffTimer = null;
803
+ nextRestartAt = null;
804
+ }
805
+ }
806
+
807
+ // -----------------------------------------------------------------------
808
+ // Start / Stop
809
+ // -----------------------------------------------------------------------
810
+
811
+ async function startInternal(): Promise<void> {
812
+ if (status === "running" || status === "starting") {
813
+ throw new Error(`Worker for plugin "${pluginId}" is already ${status}`);
814
+ }
815
+
816
+ intentionalStop = false;
817
+ setStatus("starting");
818
+ stderrExcerpt = "";
819
+
820
+ const child = spawnProcess();
821
+ childProcess = child;
822
+ attachStdioHandlers(child);
823
+ startedAt = Date.now();
824
+
825
+ // Send the initialize RPC call
826
+ const initParams: InitializeParams = {
827
+ manifest: options.manifest,
828
+ config: options.config,
829
+ instanceInfo: options.instanceInfo,
830
+ apiVersion: options.apiVersion,
831
+ };
832
+
833
+ try {
834
+ const result = await callInternal(
835
+ "initialize",
836
+ initParams,
837
+ INITIALIZE_TIMEOUT_MS,
838
+ ) as { ok?: boolean; supportedMethods?: string[] } | undefined;
839
+ if (!result || !result.ok) {
840
+ throw new Error("Worker initialize returned ok=false");
841
+ }
842
+ supportedMethods = result.supportedMethods ?? [];
843
+ } catch (err) {
844
+ // Initialize failed — kill the process and propagate
845
+ const msg = err instanceof Error ? err.message : String(err);
846
+ log.error({ err: msg }, "worker initialize failed");
847
+ await killProcess();
848
+ setStatus("crashed");
849
+ throw new Error(`Worker initialize failed for "${pluginId}": ${msg}`);
850
+ }
851
+
852
+ // Reset crash counter on successful start
853
+ consecutiveCrashes = 0;
854
+ setStatus("running");
855
+ emitter.emit("ready", { pluginId });
856
+ log.info({ pid: child.pid }, "worker process started and initialized");
857
+ }
858
+
859
+ async function stopInternal(): Promise<void> {
860
+ cancelPendingRestart();
861
+
862
+ if (status === "stopped" || status === "stopping") {
863
+ return;
864
+ }
865
+
866
+ intentionalStop = true;
867
+ setStatus("stopping");
868
+
869
+ if (!childProcess) {
870
+ setStatus("stopped");
871
+ return;
872
+ }
873
+
874
+ // Step 1: Send shutdown RPC and wait for the worker to exit gracefully.
875
+ // We race the shutdown call against a timeout. The worker should process
876
+ // the shutdown and exit on its own within the drain period.
877
+ try {
878
+ await Promise.race([
879
+ callInternal("shutdown", {} as Record<string, never>, SHUTDOWN_DRAIN_MS),
880
+ waitForExit(SHUTDOWN_DRAIN_MS),
881
+ ]);
882
+ } catch {
883
+ // Shutdown call failed or timed out — proceed to kill
884
+ log.warn("shutdown RPC failed or timed out, escalating to SIGTERM");
885
+ }
886
+
887
+ // Give the process a brief moment to exit after the shutdown response
888
+ if (childProcess) {
889
+ await waitForExit(500);
890
+ }
891
+
892
+ // Check if process already exited
893
+ if (!childProcess) {
894
+ setStatus("stopped");
895
+ return;
896
+ }
897
+
898
+ // Step 2: Send SIGTERM and wait
899
+ log.info("worker did not exit after shutdown RPC, sending SIGTERM");
900
+ await killWithSignal("SIGTERM", SIGTERM_GRACE_MS);
901
+
902
+ if (!childProcess) {
903
+ setStatus("stopped");
904
+ return;
905
+ }
906
+
907
+ // Step 3: Forcefully kill with SIGKILL
908
+ log.warn("worker did not exit after SIGTERM, sending SIGKILL");
909
+ await killWithSignal("SIGKILL", 2_000);
910
+
911
+ if (childProcess) {
912
+ log.error("worker process still alive after SIGKILL — this should not happen");
913
+ }
914
+
915
+ setStatus("stopped");
916
+ }
917
+
918
+ /**
919
+ * Wait for the child process to exit, up to `timeoutMs`.
920
+ * Resolves immediately if the process is already gone.
921
+ */
922
+ function waitForExit(timeoutMs: number): Promise<void> {
923
+ return new Promise<void>((resolve) => {
924
+ if (!childProcess) {
925
+ resolve();
926
+ return;
927
+ }
928
+
929
+ let settled = false;
930
+ const timer = setTimeout(() => {
931
+ if (settled) return;
932
+ settled = true;
933
+ resolve();
934
+ }, timeoutMs);
935
+
936
+ childProcess.once("exit", () => {
937
+ if (settled) return;
938
+ settled = true;
939
+ clearTimeout(timer);
940
+ resolve();
941
+ });
942
+ });
943
+ }
944
+
945
+ function killWithSignal(
946
+ signal: NodeJS.Signals,
947
+ waitMs: number,
948
+ ): Promise<void> {
949
+ return new Promise<void>((resolve) => {
950
+ if (!childProcess) {
951
+ resolve();
952
+ return;
953
+ }
954
+
955
+ const timer = setTimeout(() => {
956
+ resolve();
957
+ }, waitMs);
958
+
959
+ childProcess.once("exit", () => {
960
+ clearTimeout(timer);
961
+ resolve();
962
+ });
963
+
964
+ try {
965
+ childProcess.kill(signal);
966
+ } catch {
967
+ clearTimeout(timer);
968
+ resolve();
969
+ }
970
+ });
971
+ }
972
+
973
+ async function killProcess(): Promise<void> {
974
+ if (!childProcess) return;
975
+ intentionalStop = true;
976
+ try {
977
+ childProcess.kill("SIGKILL");
978
+ } catch {
979
+ // Process may already be dead
980
+ }
981
+ // Wait briefly for exit event
982
+ await new Promise<void>((resolve) => {
983
+ if (!childProcess) {
984
+ resolve();
985
+ return;
986
+ }
987
+ const timer = setTimeout(() => {
988
+ resolve();
989
+ }, 1_000);
990
+ childProcess.once("exit", () => {
991
+ clearTimeout(timer);
992
+ resolve();
993
+ });
994
+ });
995
+ }
996
+
997
+ // -----------------------------------------------------------------------
998
+ // RPC call implementation
999
+ // -----------------------------------------------------------------------
1000
+
1001
+ function callInternal<M extends HostToWorkerMethodName>(
1002
+ method: M,
1003
+ params: HostToWorkerMethods[M][0],
1004
+ timeoutMs?: number,
1005
+ ): Promise<HostToWorkerMethods[M][1]> {
1006
+ return new Promise<HostToWorkerMethods[M][1]>((resolve, reject) => {
1007
+ if (!childProcess?.stdin?.writable) {
1008
+ reject(
1009
+ new Error(
1010
+ `Cannot call "${method}" — worker for "${pluginId}" is not running`,
1011
+ ),
1012
+ );
1013
+ return;
1014
+ }
1015
+
1016
+ const id = nextRequestId++;
1017
+ const timeout = Math.min(timeoutMs ?? rpcTimeoutMs, MAX_RPC_TIMEOUT_MS);
1018
+
1019
+ // Guard against double-settlement. When a process exits all pending
1020
+ // requests are rejected via rejectAllPending(), but the timeout timer
1021
+ // may still be running. Without this guard the timer's reject fires on
1022
+ // an already-settled promise, producing an unhandled rejection.
1023
+ let settled = false;
1024
+
1025
+ const settle = <T>(fn: (value: T) => void, value: T): void => {
1026
+ if (settled) return;
1027
+ settled = true;
1028
+ clearTimeout(timer);
1029
+ pendingRequests.delete(id);
1030
+ fn(value);
1031
+ };
1032
+
1033
+ const timer = setTimeout(() => {
1034
+ settle(
1035
+ reject,
1036
+ new JsonRpcCallError({
1037
+ code: PLUGIN_RPC_ERROR_CODES.TIMEOUT,
1038
+ message: `RPC call "${method}" timed out after ${timeout}ms`,
1039
+ }),
1040
+ );
1041
+ }, timeout);
1042
+
1043
+ const pending: PendingRequest = {
1044
+ id,
1045
+ method,
1046
+ resolve: (response: JsonRpcResponse) => {
1047
+ if (isJsonRpcSuccessResponse(response)) {
1048
+ settle(resolve, response.result as HostToWorkerMethods[M][1]);
1049
+ } else if ("error" in response && response.error) {
1050
+ settle(reject, new JsonRpcCallError(response.error));
1051
+ } else {
1052
+ settle(reject, new Error(`Unexpected response format for "${method}"`));
1053
+ }
1054
+ },
1055
+ timer,
1056
+ sentAt: Date.now(),
1057
+ };
1058
+
1059
+ pendingRequests.set(id, pending);
1060
+
1061
+ try {
1062
+ const request = createRequest(method, params, id);
1063
+ sendMessage(request);
1064
+ } catch (err) {
1065
+ clearTimeout(timer);
1066
+ pendingRequests.delete(id);
1067
+ reject(
1068
+ new Error(
1069
+ `Failed to send "${method}" to worker: ${
1070
+ err instanceof Error ? err.message : String(err)
1071
+ }`,
1072
+ ),
1073
+ );
1074
+ }
1075
+ });
1076
+ }
1077
+
1078
+ // -----------------------------------------------------------------------
1079
+ // Public API
1080
+ // -----------------------------------------------------------------------
1081
+
1082
+ const handle: PluginWorkerHandle = {
1083
+ get pluginId() {
1084
+ return pluginId;
1085
+ },
1086
+
1087
+ get status() {
1088
+ return status;
1089
+ },
1090
+
1091
+ get supportedMethods() {
1092
+ return supportedMethods;
1093
+ },
1094
+
1095
+ async start() {
1096
+ await startInternal();
1097
+ },
1098
+
1099
+ async stop() {
1100
+ await stopInternal();
1101
+ },
1102
+
1103
+ async restart() {
1104
+ await stopInternal();
1105
+ await startInternal();
1106
+ },
1107
+
1108
+ call<M extends HostToWorkerMethodName>(
1109
+ method: M,
1110
+ params: HostToWorkerMethods[M][0],
1111
+ timeoutMs?: number,
1112
+ ): Promise<HostToWorkerMethods[M][1]> {
1113
+ if (status !== "running" && status !== "starting") {
1114
+ return Promise.reject(
1115
+ new Error(
1116
+ `Cannot call "${method}" — worker for "${pluginId}" is ${status}`,
1117
+ ),
1118
+ );
1119
+ }
1120
+ return callInternal(method, params, timeoutMs);
1121
+ },
1122
+
1123
+ notify(method: string, params: unknown) {
1124
+ if (status !== "running") return;
1125
+ try {
1126
+ sendMessage({
1127
+ jsonrpc: JSONRPC_VERSION,
1128
+ method,
1129
+ params,
1130
+ });
1131
+ } catch {
1132
+ log.warn({ method }, "failed to send notification to worker");
1133
+ }
1134
+ },
1135
+
1136
+ on<K extends WorkerHandleEventName>(
1137
+ event: K,
1138
+ listener: (payload: WorkerHandleEvents[K]) => void,
1139
+ ) {
1140
+ emitter.on(event, listener);
1141
+ },
1142
+
1143
+ off<K extends WorkerHandleEventName>(
1144
+ event: K,
1145
+ listener: (payload: WorkerHandleEvents[K]) => void,
1146
+ ) {
1147
+ emitter.off(event, listener);
1148
+ },
1149
+
1150
+ diagnostics(): WorkerDiagnostics {
1151
+ return {
1152
+ pluginId,
1153
+ status,
1154
+ pid: childProcess?.pid ?? null,
1155
+ uptime:
1156
+ startedAt !== null && status === "running"
1157
+ ? Date.now() - startedAt
1158
+ : null,
1159
+ consecutiveCrashes,
1160
+ totalCrashes,
1161
+ pendingRequests: pendingRequests.size,
1162
+ lastCrashAt,
1163
+ nextRestartAt,
1164
+ };
1165
+ },
1166
+ };
1167
+
1168
+ return handle;
1169
+ }
1170
+
1171
+ // ---------------------------------------------------------------------------
1172
+ // Implementation: createPluginWorkerManager
1173
+ // ---------------------------------------------------------------------------
1174
+
1175
+ /**
1176
+ * Options for creating a PluginWorkerManager.
1177
+ */
1178
+ export interface PluginWorkerManagerOptions {
1179
+ /**
1180
+ * Optional callback invoked when a worker emits a lifecycle event
1181
+ * (crash, restart). Used by the server to publish global live events.
1182
+ */
1183
+ onWorkerEvent?: (event: {
1184
+ type: "plugin.worker.crashed" | "plugin.worker.restarted";
1185
+ pluginId: string;
1186
+ code?: number | null;
1187
+ signal?: string | null;
1188
+ willRestart?: boolean;
1189
+ }) => void;
1190
+ }
1191
+
1192
+ /**
1193
+ * Create a new PluginWorkerManager.
1194
+ *
1195
+ * The manager holds all plugin worker handles and provides a unified API for
1196
+ * starting, stopping, and communicating with plugin workers.
1197
+ *
1198
+ * @example
1199
+ * ```ts
1200
+ * const manager = createPluginWorkerManager();
1201
+ *
1202
+ * const handle = await manager.startWorker("acme.linear", {
1203
+ * entrypointPath: "/path/to/worker.cjs",
1204
+ * manifest,
1205
+ * config: resolvedConfig,
1206
+ * instanceInfo: { instanceId: "inst-1", hostVersion: "1.0.0" },
1207
+ * apiVersion: 1,
1208
+ * hostHandlers: { "config.get": async () => resolvedConfig, ... },
1209
+ * });
1210
+ *
1211
+ * // Send RPC call to the worker
1212
+ * const health = await manager.call("acme.linear", "health", {});
1213
+ *
1214
+ * // Shutdown all workers on server exit
1215
+ * await manager.stopAll();
1216
+ * ```
1217
+ */
1218
+ export function createPluginWorkerManager(
1219
+ managerOptions?: PluginWorkerManagerOptions,
1220
+ ): PluginWorkerManager {
1221
+ const log = logger.child({ service: "plugin-worker-manager" });
1222
+ const workers = new Map<string, PluginWorkerHandle>();
1223
+ /** Per-plugin startup locks to prevent concurrent spawn races. */
1224
+ const startupLocks = new Map<string, Promise<PluginWorkerHandle>>();
1225
+
1226
+ return {
1227
+ async startWorker(
1228
+ pluginId: string,
1229
+ options: WorkerStartOptions,
1230
+ ): Promise<PluginWorkerHandle> {
1231
+ // Mutex: if a start is already in-flight for this plugin, wait for it
1232
+ const inFlight = startupLocks.get(pluginId);
1233
+ if (inFlight) {
1234
+ log.warn({ pluginId }, "concurrent startWorker call — waiting for in-flight start");
1235
+ return inFlight;
1236
+ }
1237
+
1238
+ const existing = workers.get(pluginId);
1239
+ if (existing && existing.status !== "stopped") {
1240
+ throw new Error(
1241
+ `Worker already registered for plugin "${pluginId}" (status: ${existing.status})`,
1242
+ );
1243
+ }
1244
+
1245
+ const handle = createPluginWorkerHandle(pluginId, options);
1246
+ workers.set(pluginId, handle);
1247
+
1248
+ // Subscribe to crash/ready events for live event forwarding
1249
+ if (managerOptions?.onWorkerEvent) {
1250
+ const notify = managerOptions.onWorkerEvent;
1251
+ handle.on("crash", (payload) => {
1252
+ notify({
1253
+ type: "plugin.worker.crashed",
1254
+ pluginId: payload.pluginId,
1255
+ code: payload.code,
1256
+ signal: payload.signal,
1257
+ willRestart: payload.willRestart,
1258
+ });
1259
+ });
1260
+ handle.on("ready", (payload) => {
1261
+ // Only emit restarted if this was a crash recovery (totalCrashes > 0)
1262
+ const diag = handle.diagnostics();
1263
+ if (diag.totalCrashes > 0) {
1264
+ notify({
1265
+ type: "plugin.worker.restarted",
1266
+ pluginId: payload.pluginId,
1267
+ });
1268
+ }
1269
+ });
1270
+ }
1271
+
1272
+ log.info({ pluginId }, "starting plugin worker");
1273
+
1274
+ // Set the lock before awaiting start() to prevent concurrent spawns
1275
+ const startPromise = handle.start().then(() => handle).finally(() => {
1276
+ startupLocks.delete(pluginId);
1277
+ });
1278
+ startupLocks.set(pluginId, startPromise);
1279
+
1280
+ return startPromise;
1281
+ },
1282
+
1283
+ async stopWorker(pluginId: string): Promise<void> {
1284
+ const handle = workers.get(pluginId);
1285
+ if (!handle) {
1286
+ log.warn({ pluginId }, "no worker registered for plugin, nothing to stop");
1287
+ return;
1288
+ }
1289
+
1290
+ log.info({ pluginId }, "stopping plugin worker");
1291
+ await handle.stop();
1292
+ workers.delete(pluginId);
1293
+ },
1294
+
1295
+ getWorker(pluginId: string): PluginWorkerHandle | undefined {
1296
+ return workers.get(pluginId);
1297
+ },
1298
+
1299
+ isRunning(pluginId: string): boolean {
1300
+ const handle = workers.get(pluginId);
1301
+ return handle?.status === "running";
1302
+ },
1303
+
1304
+ async stopAll(): Promise<void> {
1305
+ log.info({ count: workers.size }, "stopping all plugin workers");
1306
+ const promises = Array.from(workers.values()).map(async (handle) => {
1307
+ try {
1308
+ await handle.stop();
1309
+ } catch (err) {
1310
+ log.error(
1311
+ {
1312
+ pluginId: handle.pluginId,
1313
+ err: err instanceof Error ? err.message : String(err),
1314
+ },
1315
+ "error stopping worker during shutdown",
1316
+ );
1317
+ }
1318
+ });
1319
+ await Promise.all(promises);
1320
+ workers.clear();
1321
+ },
1322
+
1323
+ diagnostics(): WorkerDiagnostics[] {
1324
+ return Array.from(workers.values()).map((h) => h.diagnostics());
1325
+ },
1326
+
1327
+ call<M extends HostToWorkerMethodName>(
1328
+ pluginId: string,
1329
+ method: M,
1330
+ params: HostToWorkerMethods[M][0],
1331
+ timeoutMs?: number,
1332
+ ): Promise<HostToWorkerMethods[M][1]> {
1333
+ const handle = workers.get(pluginId);
1334
+ if (!handle) {
1335
+ return Promise.reject(
1336
+ new Error(`No worker registered for plugin "${pluginId}"`),
1337
+ );
1338
+ }
1339
+ return handle.call(method, params, timeoutMs);
1340
+ },
1341
+ };
1342
+ }
1343
+