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,2313 @@
1
+ import { Router, type Request } from "express";
2
+ import { generateKeyPairSync, randomUUID } from "node:crypto";
3
+ import path from "node:path";
4
+ import type { Db } from "@corporateai/db";
5
+ import { agents as agentsTable, companies, heartbeatRuns } from "@corporateai/db";
6
+ import { and, desc, eq, inArray, not, sql } from "drizzle-orm";
7
+ import {
8
+ agentSkillSyncSchema,
9
+ createAgentKeySchema,
10
+ createAgentHireSchema,
11
+ createAgentSchema,
12
+ deriveAgentUrlKey,
13
+ isUuidLike,
14
+ resetAgentSessionSchema,
15
+ testAdapterEnvironmentSchema,
16
+ type AgentSkillSnapshot,
17
+ type InstanceSchedulerHeartbeatAgent,
18
+ upsertAgentInstructionsFileSchema,
19
+ updateAgentInstructionsBundleSchema,
20
+ updateAgentPermissionsSchema,
21
+ updateAgentInstructionsPathSchema,
22
+ wakeAgentSchema,
23
+ updateAgentSchema,
24
+ } from "@corporateai/shared";
25
+ import {
26
+ readPaperclipSkillSyncPreference,
27
+ writePaperclipSkillSyncPreference,
28
+ } from "../adapters/server-utils-compat.js";
29
+ import { validate } from "../middleware/validate.js";
30
+ import {
31
+ agentService,
32
+ agentInstructionsService,
33
+ accessService,
34
+ approvalService,
35
+ companySkillService,
36
+ budgetService,
37
+ heartbeatService,
38
+ issueApprovalService,
39
+ issueService,
40
+ logActivity,
41
+ secretService,
42
+ syncInstructionsBundleConfigFromFilePath,
43
+ workspaceOperationService,
44
+ } from "../services/index.js";
45
+ import { conflict, forbidden, notFound, unprocessable } from "../errors.js";
46
+ import { assertBoard, assertCompanyAccess, assertInstanceAdmin, getActorInfo } from "./authz.js";
47
+ import { findServerAdapter, listAdapterModels } from "../adapters/index.js";
48
+ import { redactEventPayload } from "../redaction.js";
49
+ import { redactCurrentUserValue } from "../log-redaction.js";
50
+ import { renderOrgChartSvg, renderOrgChartPng, type OrgNode, type OrgChartStyle, ORG_CHART_STYLES } from "./org-chart-svg.js";
51
+ import { instanceSettingsService } from "../services/instance-settings.js";
52
+ import { runClaudeLogin } from "@corporateai/adapter-claude-local/server";
53
+ import {
54
+ DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
55
+ DEFAULT_CODEX_LOCAL_MODEL,
56
+ } from "@corporateai/adapter-codex-local";
57
+ import { DEFAULT_CURSOR_LOCAL_MODEL } from "@corporateai/adapter-cursor-local";
58
+ import { DEFAULT_GEMINI_LOCAL_MODEL } from "@corporateai/adapter-gemini-local";
59
+ import { ensureOpenCodeModelConfiguredAndAvailable } from "@corporateai/adapter-opencode-local/server";
60
+ import {
61
+ loadDefaultAgentInstructionsBundle,
62
+ resolveDefaultAgentInstructionsBundleRole,
63
+ } from "../services/default-agent-instructions.js";
64
+
65
+ export function agentRoutes(db: Db) {
66
+ const DEFAULT_INSTRUCTIONS_PATH_KEYS: Record<string, string> = {
67
+ claude_local: "instructionsFilePath",
68
+ codex_local: "instructionsFilePath",
69
+ gemini_local: "instructionsFilePath",
70
+ opencode_local: "instructionsFilePath",
71
+ cursor: "instructionsFilePath",
72
+ pi_local: "instructionsFilePath",
73
+ };
74
+ const DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES = new Set(Object.keys(DEFAULT_INSTRUCTIONS_PATH_KEYS));
75
+ const KNOWN_INSTRUCTIONS_PATH_KEYS = new Set(["instructionsFilePath", "agentsMdPath"]);
76
+ const KNOWN_INSTRUCTIONS_BUNDLE_KEYS = [
77
+ "instructionsBundleMode",
78
+ "instructionsRootPath",
79
+ "instructionsEntryFile",
80
+ "instructionsFilePath",
81
+ "agentsMdPath",
82
+ ] as const;
83
+
84
+ const router = Router();
85
+ const svc = agentService(db);
86
+ const access = accessService(db);
87
+ const approvalsSvc = approvalService(db);
88
+ const budgets = budgetService(db);
89
+ const heartbeat = heartbeatService(db);
90
+ const issueApprovalsSvc = issueApprovalService(db);
91
+ const secretsSvc = secretService(db);
92
+ const instructions = agentInstructionsService();
93
+ const companySkills = companySkillService(db);
94
+ const workspaceOperations = workspaceOperationService(db);
95
+ const instanceSettings = instanceSettingsService(db);
96
+ const strictSecretsMode = process.env.PAPERCLIP_SECRETS_STRICT_MODE === "true";
97
+
98
+ async function getCurrentUserRedactionOptions() {
99
+ return {
100
+ enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs,
101
+ };
102
+ }
103
+
104
+ function canCreateAgents(agent: { role: string; permissions: Record<string, unknown> | null | undefined }) {
105
+ if (!agent.permissions || typeof agent.permissions !== "object") return false;
106
+ return Boolean((agent.permissions as Record<string, unknown>).canCreateAgents);
107
+ }
108
+
109
+ async function buildAgentAccessState(agent: NonNullable<Awaited<ReturnType<typeof svc.getById>>>) {
110
+ const membership = await access.getMembership(agent.companyId, "agent", agent.id);
111
+ const grants = membership
112
+ ? await access.listPrincipalGrants(agent.companyId, "agent", agent.id)
113
+ : [];
114
+ const hasExplicitTaskAssignGrant = grants.some((grant) => grant.permissionKey === "tasks:assign");
115
+
116
+ if (agent.role === "ceo") {
117
+ return {
118
+ canAssignTasks: true,
119
+ taskAssignSource: "ceo_role" as const,
120
+ membership,
121
+ grants,
122
+ };
123
+ }
124
+
125
+ if (canCreateAgents(agent)) {
126
+ return {
127
+ canAssignTasks: true,
128
+ taskAssignSource: "agent_creator" as const,
129
+ membership,
130
+ grants,
131
+ };
132
+ }
133
+
134
+ if (hasExplicitTaskAssignGrant) {
135
+ return {
136
+ canAssignTasks: true,
137
+ taskAssignSource: "explicit_grant" as const,
138
+ membership,
139
+ grants,
140
+ };
141
+ }
142
+
143
+ return {
144
+ canAssignTasks: false,
145
+ taskAssignSource: "none" as const,
146
+ membership,
147
+ grants,
148
+ };
149
+ }
150
+
151
+ async function buildAgentDetail(
152
+ agent: NonNullable<Awaited<ReturnType<typeof svc.getById>>>,
153
+ options?: { restricted?: boolean },
154
+ ) {
155
+ const [chainOfCommand, accessState] = await Promise.all([
156
+ svc.getChainOfCommand(agent.id),
157
+ buildAgentAccessState(agent),
158
+ ]);
159
+
160
+ return {
161
+ ...(options?.restricted ? redactForRestrictedAgentView(agent) : agent),
162
+ chainOfCommand,
163
+ access: accessState,
164
+ };
165
+ }
166
+
167
+ async function applyDefaultAgentTaskAssignGrant(
168
+ companyId: string,
169
+ agentId: string,
170
+ grantedByUserId: string | null,
171
+ ) {
172
+ await access.ensureMembership(companyId, "agent", agentId, "member", "active");
173
+ await access.setPrincipalPermission(
174
+ companyId,
175
+ "agent",
176
+ agentId,
177
+ "tasks:assign",
178
+ true,
179
+ grantedByUserId,
180
+ );
181
+ }
182
+
183
+ async function assertCanCreateAgentsForCompany(req: Request, companyId: string) {
184
+ assertCompanyAccess(req, companyId);
185
+ if (req.actor.type === "board") {
186
+ if (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin) return null;
187
+ const allowed = await access.canUser(companyId, req.actor.userId, "agents:create");
188
+ if (!allowed) {
189
+ throw forbidden("Missing permission: agents:create");
190
+ }
191
+ return null;
192
+ }
193
+ if (!req.actor.agentId) throw forbidden("Agent authentication required");
194
+ const actorAgent = await svc.getById(req.actor.agentId);
195
+ if (!actorAgent || actorAgent.companyId !== companyId) {
196
+ throw forbidden("Agent key cannot access another company");
197
+ }
198
+ const allowedByGrant = await access.hasPermission(companyId, "agent", actorAgent.id, "agents:create");
199
+ if (!allowedByGrant && !canCreateAgents(actorAgent)) {
200
+ throw forbidden("Missing permission: can create agents");
201
+ }
202
+ return actorAgent;
203
+ }
204
+
205
+ async function assertCanReadConfigurations(req: Request, companyId: string) {
206
+ return assertCanCreateAgentsForCompany(req, companyId);
207
+ }
208
+
209
+ async function actorCanReadConfigurationsForCompany(req: Request, companyId: string) {
210
+ assertCompanyAccess(req, companyId);
211
+ if (req.actor.type === "board") {
212
+ if (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin) return true;
213
+ return access.canUser(companyId, req.actor.userId, "agents:create");
214
+ }
215
+ if (!req.actor.agentId) return false;
216
+ const actorAgent = await svc.getById(req.actor.agentId);
217
+ if (!actorAgent || actorAgent.companyId !== companyId) return false;
218
+ const allowedByGrant = await access.hasPermission(companyId, "agent", actorAgent.id, "agents:create");
219
+ return allowedByGrant || canCreateAgents(actorAgent);
220
+ }
221
+
222
+ async function assertCanUpdateAgent(req: Request, targetAgent: { id: string; companyId: string }) {
223
+ assertCompanyAccess(req, targetAgent.companyId);
224
+ if (req.actor.type === "board") return;
225
+ if (!req.actor.agentId) throw forbidden("Agent authentication required");
226
+
227
+ const actorAgent = await svc.getById(req.actor.agentId);
228
+ if (!actorAgent || actorAgent.companyId !== targetAgent.companyId) {
229
+ throw forbidden("Agent key cannot access another company");
230
+ }
231
+
232
+ if (actorAgent.id === targetAgent.id) return;
233
+ if (actorAgent.role === "ceo") return;
234
+ const allowedByGrant = await access.hasPermission(
235
+ targetAgent.companyId,
236
+ "agent",
237
+ actorAgent.id,
238
+ "agents:create",
239
+ );
240
+ if (allowedByGrant || canCreateAgents(actorAgent)) return;
241
+ throw forbidden("Only CEO or agent creators can modify other agents");
242
+ }
243
+
244
+ async function assertCanReadAgent(req: Request, targetAgent: { companyId: string }) {
245
+ assertCompanyAccess(req, targetAgent.companyId);
246
+ if (req.actor.type === "board") return;
247
+ if (!req.actor.agentId) throw forbidden("Agent authentication required");
248
+
249
+ const actorAgent = await svc.getById(req.actor.agentId);
250
+ if (!actorAgent || actorAgent.companyId !== targetAgent.companyId) {
251
+ throw forbidden("Agent key cannot access another company");
252
+ }
253
+ }
254
+
255
+ async function resolveCompanyIdForAgentReference(req: Request): Promise<string | null> {
256
+ const companyIdQuery = req.query.companyId;
257
+ const requestedCompanyId =
258
+ typeof companyIdQuery === "string" && companyIdQuery.trim().length > 0
259
+ ? companyIdQuery.trim()
260
+ : null;
261
+ if (requestedCompanyId) {
262
+ assertCompanyAccess(req, requestedCompanyId);
263
+ return requestedCompanyId;
264
+ }
265
+ if (req.actor.type === "agent" && req.actor.companyId) {
266
+ return req.actor.companyId;
267
+ }
268
+ return null;
269
+ }
270
+
271
+ async function normalizeAgentReference(req: Request, rawId: string): Promise<string> {
272
+ const raw = rawId.trim();
273
+ if (isUuidLike(raw)) return raw;
274
+
275
+ const companyId = await resolveCompanyIdForAgentReference(req);
276
+ if (!companyId) {
277
+ throw unprocessable("Agent shortname lookup requires companyId query parameter");
278
+ }
279
+
280
+ const resolved = await svc.resolveByReference(companyId, raw);
281
+ if (resolved.ambiguous) {
282
+ throw conflict("Agent shortname is ambiguous in this company. Use the agent ID.");
283
+ }
284
+ if (!resolved.agent) {
285
+ throw notFound("Agent not found");
286
+ }
287
+ return resolved.agent.id;
288
+ }
289
+
290
+ function parseSourceIssueIds(input: {
291
+ sourceIssueId?: string | null;
292
+ sourceIssueIds?: string[];
293
+ }): string[] {
294
+ const values: string[] = [];
295
+ if (Array.isArray(input.sourceIssueIds)) values.push(...input.sourceIssueIds);
296
+ if (typeof input.sourceIssueId === "string" && input.sourceIssueId.length > 0) {
297
+ values.push(input.sourceIssueId);
298
+ }
299
+ return Array.from(new Set(values));
300
+ }
301
+
302
+ function asRecord(value: unknown): Record<string, unknown> | null {
303
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
304
+ return value as Record<string, unknown>;
305
+ }
306
+
307
+ function asNonEmptyString(value: unknown): string | null {
308
+ if (typeof value !== "string") return null;
309
+ const trimmed = value.trim();
310
+ return trimmed.length > 0 ? trimmed : null;
311
+ }
312
+
313
+ function preserveInstructionsBundleConfig(
314
+ existingAdapterConfig: Record<string, unknown>,
315
+ nextAdapterConfig: Record<string, unknown>,
316
+ ) {
317
+ const nextKeys = new Set(Object.keys(nextAdapterConfig));
318
+ if (KNOWN_INSTRUCTIONS_BUNDLE_KEYS.some((key) => nextKeys.has(key))) {
319
+ return nextAdapterConfig;
320
+ }
321
+
322
+ const merged = { ...nextAdapterConfig };
323
+ for (const key of KNOWN_INSTRUCTIONS_BUNDLE_KEYS) {
324
+ if (merged[key] === undefined && existingAdapterConfig[key] !== undefined) {
325
+ merged[key] = existingAdapterConfig[key];
326
+ }
327
+ }
328
+ return merged;
329
+ }
330
+
331
+ function parseBooleanLike(value: unknown): boolean | null {
332
+ if (typeof value === "boolean") return value;
333
+ if (typeof value === "number") {
334
+ if (value === 1) return true;
335
+ if (value === 0) return false;
336
+ return null;
337
+ }
338
+ if (typeof value !== "string") return null;
339
+ const normalized = value.trim().toLowerCase();
340
+ if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") {
341
+ return true;
342
+ }
343
+ if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "off") {
344
+ return false;
345
+ }
346
+ return null;
347
+ }
348
+
349
+ function parseNumberLike(value: unknown): number | null {
350
+ if (typeof value === "number" && Number.isFinite(value)) return value;
351
+ if (typeof value !== "string") return null;
352
+ const parsed = Number(value.trim());
353
+ return Number.isFinite(parsed) ? parsed : null;
354
+ }
355
+
356
+ function parseSchedulerHeartbeatPolicy(runtimeConfig: unknown) {
357
+ const heartbeat = asRecord(asRecord(runtimeConfig)?.heartbeat) ?? {};
358
+ return {
359
+ enabled: parseBooleanLike(heartbeat.enabled) ?? true,
360
+ intervalSec: Math.max(0, parseNumberLike(heartbeat.intervalSec) ?? 0),
361
+ };
362
+ }
363
+
364
+ function generateEd25519PrivateKeyPem(): string {
365
+ const { privateKey } = generateKeyPairSync("ed25519");
366
+ return privateKey.export({ type: "pkcs8", format: "pem" }).toString();
367
+ }
368
+
369
+ function ensureGatewayDeviceKey(
370
+ adapterType: string | null | undefined,
371
+ adapterConfig: Record<string, unknown>,
372
+ ): Record<string, unknown> {
373
+ if (adapterType !== "openclaw_gateway") return adapterConfig;
374
+ const disableDeviceAuth = parseBooleanLike(adapterConfig.disableDeviceAuth) === true;
375
+ if (disableDeviceAuth) return adapterConfig;
376
+ if (asNonEmptyString(adapterConfig.devicePrivateKeyPem)) return adapterConfig;
377
+ return { ...adapterConfig, devicePrivateKeyPem: generateEd25519PrivateKeyPem() };
378
+ }
379
+
380
+ function applyCreateDefaultsByAdapterType(
381
+ adapterType: string | null | undefined,
382
+ adapterConfig: Record<string, unknown>,
383
+ ): Record<string, unknown> {
384
+ const next = { ...adapterConfig };
385
+ if (adapterType === "codex_local") {
386
+ if (!asNonEmptyString(next.model)) {
387
+ next.model = DEFAULT_CODEX_LOCAL_MODEL;
388
+ }
389
+ const hasBypassFlag =
390
+ typeof next.dangerouslyBypassApprovalsAndSandbox === "boolean" ||
391
+ typeof next.dangerouslyBypassSandbox === "boolean";
392
+ if (!hasBypassFlag) {
393
+ next.dangerouslyBypassApprovalsAndSandbox = DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX;
394
+ }
395
+ return ensureGatewayDeviceKey(adapterType, next);
396
+ }
397
+ if (adapterType === "gemini_local" && !asNonEmptyString(next.model)) {
398
+ next.model = DEFAULT_GEMINI_LOCAL_MODEL;
399
+ return ensureGatewayDeviceKey(adapterType, next);
400
+ }
401
+ // OpenCode requires explicit model selection — no default
402
+ if (adapterType === "cursor" && !asNonEmptyString(next.model)) {
403
+ next.model = DEFAULT_CURSOR_LOCAL_MODEL;
404
+ }
405
+ return ensureGatewayDeviceKey(adapterType, next);
406
+ }
407
+
408
+ async function assertAdapterConfigConstraints(
409
+ companyId: string,
410
+ adapterType: string | null | undefined,
411
+ adapterConfig: Record<string, unknown>,
412
+ ) {
413
+ if (adapterType !== "opencode_local") return;
414
+ const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(companyId, adapterConfig);
415
+ const runtimeEnv = asRecord(runtimeConfig.env) ?? {};
416
+ try {
417
+ await ensureOpenCodeModelConfiguredAndAvailable({
418
+ model: runtimeConfig.model,
419
+ command: runtimeConfig.command,
420
+ cwd: runtimeConfig.cwd,
421
+ env: runtimeEnv,
422
+ });
423
+ } catch (err) {
424
+ const reason = err instanceof Error ? err.message : String(err);
425
+ throw unprocessable(`Invalid opencode_local adapterConfig: ${reason}`);
426
+ }
427
+ }
428
+
429
+ function resolveInstructionsFilePath(candidatePath: string, adapterConfig: Record<string, unknown>) {
430
+ const trimmed = candidatePath.trim();
431
+ if (path.isAbsolute(trimmed)) return trimmed;
432
+
433
+ const cwd = asNonEmptyString(adapterConfig.cwd);
434
+ if (!cwd) {
435
+ throw unprocessable(
436
+ "Relative instructions path requires adapterConfig.cwd to be set to an absolute path",
437
+ );
438
+ }
439
+ if (!path.isAbsolute(cwd)) {
440
+ throw unprocessable("adapterConfig.cwd must be an absolute path to resolve relative instructions path");
441
+ }
442
+ return path.resolve(cwd, trimmed);
443
+ }
444
+
445
+ async function materializeDefaultInstructionsBundleForNewAgent<T extends {
446
+ id: string;
447
+ companyId: string;
448
+ name: string;
449
+ role: string;
450
+ adapterType: string;
451
+ adapterConfig: unknown;
452
+ }>(agent: T): Promise<T> {
453
+ if (!DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES.has(agent.adapterType)) {
454
+ return agent;
455
+ }
456
+
457
+ const adapterConfig = asRecord(agent.adapterConfig) ?? {};
458
+ const hasExplicitInstructionsBundle =
459
+ Boolean(asNonEmptyString(adapterConfig.instructionsBundleMode))
460
+ || Boolean(asNonEmptyString(adapterConfig.instructionsRootPath))
461
+ || Boolean(asNonEmptyString(adapterConfig.instructionsEntryFile))
462
+ || Boolean(asNonEmptyString(adapterConfig.instructionsFilePath))
463
+ || Boolean(asNonEmptyString(adapterConfig.agentsMdPath));
464
+ if (hasExplicitInstructionsBundle) {
465
+ return agent;
466
+ }
467
+
468
+ const promptTemplate = typeof adapterConfig.promptTemplate === "string"
469
+ ? adapterConfig.promptTemplate
470
+ : "";
471
+ const files = promptTemplate.trim().length === 0
472
+ ? await loadDefaultAgentInstructionsBundle(resolveDefaultAgentInstructionsBundleRole(agent.role))
473
+ : { "AGENTS.md": promptTemplate };
474
+ const materialized = await instructions.materializeManagedBundle(
475
+ agent,
476
+ files,
477
+ { entryFile: "AGENTS.md", replaceExisting: false },
478
+ );
479
+ const nextAdapterConfig = { ...materialized.adapterConfig };
480
+ delete nextAdapterConfig.promptTemplate;
481
+
482
+ const updated = await svc.update(agent.id, { adapterConfig: nextAdapterConfig });
483
+ return (updated as T | null) ?? { ...agent, adapterConfig: nextAdapterConfig };
484
+ }
485
+
486
+ async function assertCanManageInstructionsPath(req: Request, targetAgent: { id: string; companyId: string }) {
487
+ assertCompanyAccess(req, targetAgent.companyId);
488
+ if (req.actor.type === "board") return;
489
+ if (!req.actor.agentId) throw forbidden("Agent authentication required");
490
+
491
+ const actorAgent = await svc.getById(req.actor.agentId);
492
+ if (!actorAgent || actorAgent.companyId !== targetAgent.companyId) {
493
+ throw forbidden("Agent key cannot access another company");
494
+ }
495
+ if (actorAgent.id === targetAgent.id) return;
496
+
497
+ const chainOfCommand = await svc.getChainOfCommand(targetAgent.id);
498
+ if (chainOfCommand.some((manager) => manager.id === actorAgent.id)) return;
499
+
500
+ throw forbidden("Only the target agent or an ancestor manager can update instructions path");
501
+ }
502
+
503
+ function summarizeAgentUpdateDetails(patch: Record<string, unknown>) {
504
+ const changedTopLevelKeys = Object.keys(patch).sort();
505
+ const details: Record<string, unknown> = { changedTopLevelKeys };
506
+
507
+ const adapterConfigPatch = asRecord(patch.adapterConfig);
508
+ if (adapterConfigPatch) {
509
+ details.changedAdapterConfigKeys = Object.keys(adapterConfigPatch).sort();
510
+ }
511
+
512
+ const runtimeConfigPatch = asRecord(patch.runtimeConfig);
513
+ if (runtimeConfigPatch) {
514
+ details.changedRuntimeConfigKeys = Object.keys(runtimeConfigPatch).sort();
515
+ }
516
+
517
+ return details;
518
+ }
519
+
520
+ function buildUnsupportedSkillSnapshot(
521
+ adapterType: string,
522
+ desiredSkills: string[] = [],
523
+ ): AgentSkillSnapshot {
524
+ return {
525
+ adapterType,
526
+ supported: false,
527
+ mode: "unsupported",
528
+ desiredSkills,
529
+ entries: [],
530
+ warnings: ["This adapter does not implement skill sync yet."],
531
+ };
532
+ }
533
+
534
+ function shouldMaterializeRuntimeSkillsForAdapter(adapterType: string) {
535
+ return adapterType !== "claude_local";
536
+ }
537
+
538
+ async function buildRuntimeSkillConfig(
539
+ companyId: string,
540
+ adapterType: string,
541
+ config: Record<string, unknown>,
542
+ ) {
543
+ const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(companyId, {
544
+ materializeMissing: shouldMaterializeRuntimeSkillsForAdapter(adapterType),
545
+ });
546
+ return {
547
+ ...config,
548
+ paperclipRuntimeSkills: runtimeSkillEntries,
549
+ };
550
+ }
551
+
552
+ async function resolveDesiredSkillAssignment(
553
+ companyId: string,
554
+ adapterType: string,
555
+ adapterConfig: Record<string, unknown>,
556
+ requestedDesiredSkills: string[] | undefined,
557
+ ) {
558
+ if (!requestedDesiredSkills) {
559
+ return {
560
+ adapterConfig,
561
+ desiredSkills: null as string[] | null,
562
+ runtimeSkillEntries: null as Awaited<ReturnType<typeof companySkills.listRuntimeSkillEntries>> | null,
563
+ };
564
+ }
565
+
566
+ const resolvedRequestedSkills = await companySkills.resolveRequestedSkillKeys(
567
+ companyId,
568
+ requestedDesiredSkills,
569
+ );
570
+ const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(companyId, {
571
+ materializeMissing: shouldMaterializeRuntimeSkillsForAdapter(adapterType),
572
+ });
573
+ const requiredSkills = runtimeSkillEntries
574
+ .filter((entry) => entry.required)
575
+ .map((entry) => entry.key);
576
+ const desiredSkills = Array.from(new Set([...requiredSkills, ...resolvedRequestedSkills]));
577
+
578
+ return {
579
+ adapterConfig: writePaperclipSkillSyncPreference(adapterConfig, desiredSkills),
580
+ desiredSkills,
581
+ runtimeSkillEntries,
582
+ };
583
+ }
584
+
585
+ function redactForRestrictedAgentView(agent: Awaited<ReturnType<typeof svc.getById>>) {
586
+ if (!agent) return null;
587
+ return {
588
+ ...agent,
589
+ adapterConfig: {},
590
+ runtimeConfig: {},
591
+ };
592
+ }
593
+
594
+ function redactAgentConfiguration(agent: Awaited<ReturnType<typeof svc.getById>>) {
595
+ if (!agent) return null;
596
+ return {
597
+ id: agent.id,
598
+ companyId: agent.companyId,
599
+ name: agent.name,
600
+ role: agent.role,
601
+ title: agent.title,
602
+ status: agent.status,
603
+ reportsTo: agent.reportsTo,
604
+ adapterType: agent.adapterType,
605
+ adapterConfig: redactEventPayload(agent.adapterConfig),
606
+ runtimeConfig: redactEventPayload(agent.runtimeConfig),
607
+ permissions: agent.permissions,
608
+ updatedAt: agent.updatedAt,
609
+ };
610
+ }
611
+
612
+ function redactRevisionSnapshot(snapshot: unknown): Record<string, unknown> {
613
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return {};
614
+ const record = snapshot as Record<string, unknown>;
615
+ return {
616
+ ...record,
617
+ adapterConfig: redactEventPayload(
618
+ typeof record.adapterConfig === "object" && record.adapterConfig !== null
619
+ ? (record.adapterConfig as Record<string, unknown>)
620
+ : {},
621
+ ),
622
+ runtimeConfig: redactEventPayload(
623
+ typeof record.runtimeConfig === "object" && record.runtimeConfig !== null
624
+ ? (record.runtimeConfig as Record<string, unknown>)
625
+ : {},
626
+ ),
627
+ metadata:
628
+ typeof record.metadata === "object" && record.metadata !== null
629
+ ? redactEventPayload(record.metadata as Record<string, unknown>)
630
+ : record.metadata ?? null,
631
+ };
632
+ }
633
+
634
+ function redactConfigRevision(
635
+ revision: Record<string, unknown> & { beforeConfig: unknown; afterConfig: unknown },
636
+ ) {
637
+ return {
638
+ ...revision,
639
+ beforeConfig: redactRevisionSnapshot(revision.beforeConfig),
640
+ afterConfig: redactRevisionSnapshot(revision.afterConfig),
641
+ };
642
+ }
643
+
644
+ function toLeanOrgNode(node: Record<string, unknown>): Record<string, unknown> {
645
+ const reports = Array.isArray(node.reports)
646
+ ? (node.reports as Array<Record<string, unknown>>).map((report) => toLeanOrgNode(report))
647
+ : [];
648
+ return {
649
+ id: String(node.id),
650
+ name: String(node.name),
651
+ role: String(node.role),
652
+ status: String(node.status),
653
+ reports,
654
+ };
655
+ }
656
+
657
+ router.param("id", async (req, _res, next, rawId) => {
658
+ try {
659
+ req.params.id = await normalizeAgentReference(req, String(rawId));
660
+ next();
661
+ } catch (err) {
662
+ next(err);
663
+ }
664
+ });
665
+
666
+ router.get("/companies/:companyId/adapters/:type/models", async (req, res) => {
667
+ const companyId = req.params.companyId as string;
668
+ assertCompanyAccess(req, companyId);
669
+ const type = req.params.type as string;
670
+ const models = await listAdapterModels(type);
671
+ res.json(models);
672
+ });
673
+
674
+ router.post(
675
+ "/companies/:companyId/adapters/:type/test-environment",
676
+ validate(testAdapterEnvironmentSchema),
677
+ async (req, res) => {
678
+ const companyId = req.params.companyId as string;
679
+ const type = req.params.type as string;
680
+ await assertCanReadConfigurations(req, companyId);
681
+
682
+ const adapter = findServerAdapter(type);
683
+ if (!adapter) {
684
+ res.status(404).json({ error: `Unknown adapter type: ${type}` });
685
+ return;
686
+ }
687
+
688
+ const inputAdapterConfig =
689
+ (req.body?.adapterConfig ?? {}) as Record<string, unknown>;
690
+ const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(
691
+ companyId,
692
+ inputAdapterConfig,
693
+ { strictMode: strictSecretsMode },
694
+ );
695
+ const { config: runtimeAdapterConfig } = await secretsSvc.resolveAdapterConfigForRuntime(
696
+ companyId,
697
+ normalizedAdapterConfig,
698
+ );
699
+
700
+ const result = await adapter.testEnvironment({
701
+ companyId,
702
+ adapterType: type,
703
+ config: runtimeAdapterConfig,
704
+ });
705
+
706
+ res.json(result);
707
+ },
708
+ );
709
+
710
+ router.get("/agents/:id/skills", async (req, res) => {
711
+ const id = req.params.id as string;
712
+ const agent = await svc.getById(id);
713
+ if (!agent) {
714
+ res.status(404).json({ error: "Agent not found" });
715
+ return;
716
+ }
717
+ await assertCanReadConfigurations(req, agent.companyId);
718
+
719
+ const adapter = findServerAdapter(agent.adapterType);
720
+ if (!adapter?.listSkills) {
721
+ const preference = readPaperclipSkillSyncPreference(
722
+ agent.adapterConfig as Record<string, unknown>,
723
+ );
724
+ const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(agent.companyId, {
725
+ materializeMissing: false,
726
+ });
727
+ const requiredSkills = runtimeSkillEntries.filter((entry) => entry.required).map((entry) => entry.key);
728
+ res.json(buildUnsupportedSkillSnapshot(agent.adapterType, Array.from(new Set([...requiredSkills, ...preference.desiredSkills]))));
729
+ return;
730
+ }
731
+
732
+ const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(
733
+ agent.companyId,
734
+ agent.adapterConfig,
735
+ );
736
+ const runtimeSkillConfig = await buildRuntimeSkillConfig(
737
+ agent.companyId,
738
+ agent.adapterType,
739
+ runtimeConfig,
740
+ );
741
+ const snapshot = await adapter.listSkills({
742
+ agentId: agent.id,
743
+ companyId: agent.companyId,
744
+ adapterType: agent.adapterType,
745
+ config: runtimeSkillConfig,
746
+ });
747
+ res.json(snapshot);
748
+ });
749
+
750
+ router.post(
751
+ "/agents/:id/skills/sync",
752
+ validate(agentSkillSyncSchema),
753
+ async (req, res) => {
754
+ const id = req.params.id as string;
755
+ const agent = await svc.getById(id);
756
+ if (!agent) {
757
+ res.status(404).json({ error: "Agent not found" });
758
+ return;
759
+ }
760
+ await assertCanUpdateAgent(req, agent);
761
+
762
+ const requestedSkills = Array.from(
763
+ new Set(
764
+ (req.body.desiredSkills as string[])
765
+ .map((value) => value.trim())
766
+ .filter(Boolean),
767
+ ),
768
+ );
769
+ const {
770
+ adapterConfig: nextAdapterConfig,
771
+ desiredSkills,
772
+ runtimeSkillEntries,
773
+ } = await resolveDesiredSkillAssignment(
774
+ agent.companyId,
775
+ agent.adapterType,
776
+ agent.adapterConfig as Record<string, unknown>,
777
+ requestedSkills,
778
+ );
779
+ if (!desiredSkills || !runtimeSkillEntries) {
780
+ throw unprocessable("Skill sync requires desiredSkills.");
781
+ }
782
+ const actor = getActorInfo(req);
783
+ const updated = await svc.update(agent.id, {
784
+ adapterConfig: nextAdapterConfig,
785
+ }, {
786
+ recordRevision: {
787
+ createdByAgentId: actor.agentId,
788
+ createdByUserId: actor.actorType === "user" ? actor.actorId : null,
789
+ source: "skill-sync",
790
+ },
791
+ });
792
+ if (!updated) {
793
+ res.status(404).json({ error: "Agent not found" });
794
+ return;
795
+ }
796
+
797
+ const adapter = findServerAdapter(updated.adapterType);
798
+ const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(
799
+ updated.companyId,
800
+ updated.adapterConfig,
801
+ );
802
+ const runtimeSkillConfig = {
803
+ ...runtimeConfig,
804
+ paperclipRuntimeSkills: runtimeSkillEntries,
805
+ };
806
+ const snapshot = adapter?.syncSkills
807
+ ? await adapter.syncSkills({
808
+ agentId: updated.id,
809
+ companyId: updated.companyId,
810
+ adapterType: updated.adapterType,
811
+ config: runtimeSkillConfig,
812
+ }, desiredSkills)
813
+ : adapter?.listSkills
814
+ ? await adapter.listSkills({
815
+ agentId: updated.id,
816
+ companyId: updated.companyId,
817
+ adapterType: updated.adapterType,
818
+ config: runtimeSkillConfig,
819
+ })
820
+ : buildUnsupportedSkillSnapshot(updated.adapterType, desiredSkills);
821
+
822
+ await logActivity(db, {
823
+ companyId: updated.companyId,
824
+ actorType: actor.actorType,
825
+ actorId: actor.actorId,
826
+ action: "agent.skills_synced",
827
+ entityType: "agent",
828
+ entityId: updated.id,
829
+ agentId: actor.agentId,
830
+ runId: actor.runId,
831
+ details: {
832
+ adapterType: updated.adapterType,
833
+ desiredSkills,
834
+ mode: snapshot.mode,
835
+ supported: snapshot.supported,
836
+ entryCount: snapshot.entries.length,
837
+ warningCount: snapshot.warnings.length,
838
+ },
839
+ });
840
+
841
+ res.json(snapshot);
842
+ },
843
+ );
844
+
845
+ router.get("/companies/:companyId/agents", async (req, res) => {
846
+ const companyId = req.params.companyId as string;
847
+ assertCompanyAccess(req, companyId);
848
+ const result = await svc.list(companyId);
849
+ const canReadConfigs = await actorCanReadConfigurationsForCompany(req, companyId);
850
+ if (canReadConfigs || req.actor.type === "board") {
851
+ res.json(result);
852
+ return;
853
+ }
854
+ res.json(result.map((agent) => redactForRestrictedAgentView(agent)));
855
+ });
856
+
857
+ router.get("/instance/scheduler-heartbeats", async (req, res) => {
858
+ assertInstanceAdmin(req);
859
+
860
+ const rows = await db
861
+ .select({
862
+ id: agentsTable.id,
863
+ companyId: agentsTable.companyId,
864
+ agentName: agentsTable.name,
865
+ role: agentsTable.role,
866
+ title: agentsTable.title,
867
+ status: agentsTable.status,
868
+ adapterType: agentsTable.adapterType,
869
+ runtimeConfig: agentsTable.runtimeConfig,
870
+ lastHeartbeatAt: agentsTable.lastHeartbeatAt,
871
+ companyName: companies.name,
872
+ companyIssuePrefix: companies.issuePrefix,
873
+ })
874
+ .from(agentsTable)
875
+ .innerJoin(companies, eq(agentsTable.companyId, companies.id))
876
+ .orderBy(companies.name, agentsTable.name);
877
+
878
+ const items: InstanceSchedulerHeartbeatAgent[] = rows
879
+ .map((row) => {
880
+ const policy = parseSchedulerHeartbeatPolicy(row.runtimeConfig);
881
+ const statusEligible =
882
+ row.status !== "paused" &&
883
+ row.status !== "terminated" &&
884
+ row.status !== "pending_approval";
885
+
886
+ return {
887
+ id: row.id,
888
+ companyId: row.companyId,
889
+ companyName: row.companyName,
890
+ companyIssuePrefix: row.companyIssuePrefix,
891
+ agentName: row.agentName,
892
+ agentUrlKey: deriveAgentUrlKey(row.agentName, row.id),
893
+ role: row.role as InstanceSchedulerHeartbeatAgent["role"],
894
+ title: row.title,
895
+ status: row.status as InstanceSchedulerHeartbeatAgent["status"],
896
+ adapterType: row.adapterType,
897
+ intervalSec: policy.intervalSec,
898
+ heartbeatEnabled: policy.enabled,
899
+ schedulerActive: statusEligible && policy.enabled && policy.intervalSec > 0,
900
+ lastHeartbeatAt: row.lastHeartbeatAt,
901
+ };
902
+ })
903
+ .filter((item) =>
904
+ item.status !== "paused" &&
905
+ item.status !== "terminated" &&
906
+ item.status !== "pending_approval",
907
+ )
908
+ .sort((left, right) => {
909
+ if (left.schedulerActive !== right.schedulerActive) {
910
+ return left.schedulerActive ? -1 : 1;
911
+ }
912
+ const companyOrder = left.companyName.localeCompare(right.companyName);
913
+ if (companyOrder !== 0) return companyOrder;
914
+ return left.agentName.localeCompare(right.agentName);
915
+ });
916
+
917
+ res.json(items);
918
+ });
919
+
920
+ router.get("/companies/:companyId/org", async (req, res) => {
921
+ const companyId = req.params.companyId as string;
922
+ assertCompanyAccess(req, companyId);
923
+ const tree = await svc.orgForCompany(companyId);
924
+ const leanTree = tree.map((node) => toLeanOrgNode(node as Record<string, unknown>));
925
+ res.json(leanTree);
926
+ });
927
+
928
+ router.get("/companies/:companyId/org.svg", async (req, res) => {
929
+ const companyId = req.params.companyId as string;
930
+ assertCompanyAccess(req, companyId);
931
+ const style = (ORG_CHART_STYLES.includes(req.query.style as OrgChartStyle) ? req.query.style : "warmth") as OrgChartStyle;
932
+ const tree = await svc.orgForCompany(companyId);
933
+ const leanTree = tree.map((node) => toLeanOrgNode(node as Record<string, unknown>));
934
+ const svg = renderOrgChartSvg(leanTree as unknown as OrgNode[], style);
935
+ res.setHeader("Content-Type", "image/svg+xml");
936
+ res.setHeader("Cache-Control", "no-cache");
937
+ res.send(svg);
938
+ });
939
+
940
+ router.get("/companies/:companyId/org.png", async (req, res) => {
941
+ const companyId = req.params.companyId as string;
942
+ assertCompanyAccess(req, companyId);
943
+ const style = (ORG_CHART_STYLES.includes(req.query.style as OrgChartStyle) ? req.query.style : "warmth") as OrgChartStyle;
944
+ const tree = await svc.orgForCompany(companyId);
945
+ const leanTree = tree.map((node) => toLeanOrgNode(node as Record<string, unknown>));
946
+ const png = await renderOrgChartPng(leanTree as unknown as OrgNode[], style);
947
+ res.setHeader("Content-Type", "image/png");
948
+ res.setHeader("Cache-Control", "no-cache");
949
+ res.send(png);
950
+ });
951
+
952
+ router.get("/companies/:companyId/agent-configurations", async (req, res) => {
953
+ const companyId = req.params.companyId as string;
954
+ await assertCanReadConfigurations(req, companyId);
955
+ const rows = await svc.list(companyId);
956
+ res.json(rows.map((row) => redactAgentConfiguration(row)));
957
+ });
958
+
959
+ router.get("/agents/me", async (req, res) => {
960
+ if (req.actor.type !== "agent" || !req.actor.agentId) {
961
+ res.status(401).json({ error: "Agent authentication required" });
962
+ return;
963
+ }
964
+ const agent = await svc.getById(req.actor.agentId);
965
+ if (!agent) {
966
+ res.status(404).json({ error: "Agent not found" });
967
+ return;
968
+ }
969
+ res.json(await buildAgentDetail(agent));
970
+ });
971
+
972
+ router.get("/agents/me/inbox-lite", async (req, res) => {
973
+ if (req.actor.type !== "agent" || !req.actor.agentId || !req.actor.companyId) {
974
+ res.status(401).json({ error: "Agent authentication required" });
975
+ return;
976
+ }
977
+
978
+ const issuesSvc = issueService(db);
979
+ const rows = await issuesSvc.list(req.actor.companyId, {
980
+ assigneeAgentId: req.actor.agentId,
981
+ status: "todo,in_progress,blocked",
982
+ });
983
+
984
+ res.json(
985
+ rows.map((issue) => ({
986
+ id: issue.id,
987
+ identifier: issue.identifier,
988
+ title: issue.title,
989
+ status: issue.status,
990
+ priority: issue.priority,
991
+ projectId: issue.projectId,
992
+ goalId: issue.goalId,
993
+ parentId: issue.parentId,
994
+ updatedAt: issue.updatedAt,
995
+ activeRun: issue.activeRun,
996
+ })),
997
+ );
998
+ });
999
+
1000
+ router.get("/agents/:id", async (req, res) => {
1001
+ const id = req.params.id as string;
1002
+ const agent = await svc.getById(id);
1003
+ if (!agent) {
1004
+ res.status(404).json({ error: "Agent not found" });
1005
+ return;
1006
+ }
1007
+ assertCompanyAccess(req, agent.companyId);
1008
+ if (req.actor.type === "agent" && req.actor.agentId !== id) {
1009
+ const canRead = await actorCanReadConfigurationsForCompany(req, agent.companyId);
1010
+ if (!canRead) {
1011
+ res.json(await buildAgentDetail(agent, { restricted: true }));
1012
+ return;
1013
+ }
1014
+ }
1015
+ res.json(await buildAgentDetail(agent));
1016
+ });
1017
+
1018
+ router.get("/agents/:id/configuration", async (req, res) => {
1019
+ const id = req.params.id as string;
1020
+ const agent = await svc.getById(id);
1021
+ if (!agent) {
1022
+ res.status(404).json({ error: "Agent not found" });
1023
+ return;
1024
+ }
1025
+ await assertCanReadConfigurations(req, agent.companyId);
1026
+ res.json(redactAgentConfiguration(agent));
1027
+ });
1028
+
1029
+ router.get("/agents/:id/config-revisions", async (req, res) => {
1030
+ const id = req.params.id as string;
1031
+ const agent = await svc.getById(id);
1032
+ if (!agent) {
1033
+ res.status(404).json({ error: "Agent not found" });
1034
+ return;
1035
+ }
1036
+ await assertCanReadConfigurations(req, agent.companyId);
1037
+ const revisions = await svc.listConfigRevisions(id);
1038
+ res.json(revisions.map((revision) => redactConfigRevision(revision)));
1039
+ });
1040
+
1041
+ router.get("/agents/:id/config-revisions/:revisionId", async (req, res) => {
1042
+ const id = req.params.id as string;
1043
+ const revisionId = req.params.revisionId as string;
1044
+ const agent = await svc.getById(id);
1045
+ if (!agent) {
1046
+ res.status(404).json({ error: "Agent not found" });
1047
+ return;
1048
+ }
1049
+ await assertCanReadConfigurations(req, agent.companyId);
1050
+ const revision = await svc.getConfigRevision(id, revisionId);
1051
+ if (!revision) {
1052
+ res.status(404).json({ error: "Revision not found" });
1053
+ return;
1054
+ }
1055
+ res.json(redactConfigRevision(revision));
1056
+ });
1057
+
1058
+ router.post("/agents/:id/config-revisions/:revisionId/rollback", async (req, res) => {
1059
+ const id = req.params.id as string;
1060
+ const revisionId = req.params.revisionId as string;
1061
+ const existing = await svc.getById(id);
1062
+ if (!existing) {
1063
+ res.status(404).json({ error: "Agent not found" });
1064
+ return;
1065
+ }
1066
+ await assertCanUpdateAgent(req, existing);
1067
+
1068
+ const actor = getActorInfo(req);
1069
+ const updated = await svc.rollbackConfigRevision(id, revisionId, {
1070
+ agentId: actor.agentId,
1071
+ userId: actor.actorType === "user" ? actor.actorId : null,
1072
+ });
1073
+ if (!updated) {
1074
+ res.status(404).json({ error: "Revision not found" });
1075
+ return;
1076
+ }
1077
+
1078
+ await logActivity(db, {
1079
+ companyId: updated.companyId,
1080
+ actorType: actor.actorType,
1081
+ actorId: actor.actorId,
1082
+ agentId: actor.agentId,
1083
+ runId: actor.runId,
1084
+ action: "agent.config_rolled_back",
1085
+ entityType: "agent",
1086
+ entityId: updated.id,
1087
+ details: { revisionId },
1088
+ });
1089
+
1090
+ res.json(updated);
1091
+ });
1092
+
1093
+ router.get("/agents/:id/runtime-state", async (req, res) => {
1094
+ assertBoard(req);
1095
+ const id = req.params.id as string;
1096
+ const agent = await svc.getById(id);
1097
+ if (!agent) {
1098
+ res.status(404).json({ error: "Agent not found" });
1099
+ return;
1100
+ }
1101
+ assertCompanyAccess(req, agent.companyId);
1102
+
1103
+ const state = await heartbeat.getRuntimeState(id);
1104
+ res.json(state);
1105
+ });
1106
+
1107
+ router.get("/agents/:id/task-sessions", async (req, res) => {
1108
+ assertBoard(req);
1109
+ const id = req.params.id as string;
1110
+ const agent = await svc.getById(id);
1111
+ if (!agent) {
1112
+ res.status(404).json({ error: "Agent not found" });
1113
+ return;
1114
+ }
1115
+ assertCompanyAccess(req, agent.companyId);
1116
+
1117
+ const sessions = await heartbeat.listTaskSessions(id);
1118
+ res.json(
1119
+ sessions.map((session) => ({
1120
+ ...session,
1121
+ sessionParamsJson: redactEventPayload(session.sessionParamsJson ?? null),
1122
+ })),
1123
+ );
1124
+ });
1125
+
1126
+ router.post("/agents/:id/runtime-state/reset-session", validate(resetAgentSessionSchema), async (req, res) => {
1127
+ assertBoard(req);
1128
+ const id = req.params.id as string;
1129
+ const agent = await svc.getById(id);
1130
+ if (!agent) {
1131
+ res.status(404).json({ error: "Agent not found" });
1132
+ return;
1133
+ }
1134
+ assertCompanyAccess(req, agent.companyId);
1135
+
1136
+ const taskKey =
1137
+ typeof req.body.taskKey === "string" && req.body.taskKey.trim().length > 0
1138
+ ? req.body.taskKey.trim()
1139
+ : null;
1140
+ const state = await heartbeat.resetRuntimeSession(id, { taskKey });
1141
+
1142
+ await logActivity(db, {
1143
+ companyId: agent.companyId,
1144
+ actorType: "user",
1145
+ actorId: req.actor.userId ?? "board",
1146
+ action: "agent.runtime_session_reset",
1147
+ entityType: "agent",
1148
+ entityId: id,
1149
+ details: { taskKey: taskKey ?? null },
1150
+ });
1151
+
1152
+ res.json(state);
1153
+ });
1154
+
1155
+ router.post("/companies/:companyId/agent-hires", validate(createAgentHireSchema), async (req, res) => {
1156
+ const companyId = req.params.companyId as string;
1157
+ await assertCanCreateAgentsForCompany(req, companyId);
1158
+ const sourceIssueIds = parseSourceIssueIds(req.body);
1159
+ const {
1160
+ desiredSkills: requestedDesiredSkills,
1161
+ sourceIssueId: _sourceIssueId,
1162
+ sourceIssueIds: _sourceIssueIds,
1163
+ ...hireInput
1164
+ } = req.body;
1165
+ const requestedAdapterConfig = applyCreateDefaultsByAdapterType(
1166
+ hireInput.adapterType,
1167
+ ((hireInput.adapterConfig ?? {}) as Record<string, unknown>),
1168
+ );
1169
+ const desiredSkillAssignment = await resolveDesiredSkillAssignment(
1170
+ companyId,
1171
+ hireInput.adapterType,
1172
+ requestedAdapterConfig,
1173
+ Array.isArray(requestedDesiredSkills) ? requestedDesiredSkills : undefined,
1174
+ );
1175
+ const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(
1176
+ companyId,
1177
+ desiredSkillAssignment.adapterConfig,
1178
+ { strictMode: strictSecretsMode },
1179
+ );
1180
+ await assertAdapterConfigConstraints(
1181
+ companyId,
1182
+ hireInput.adapterType,
1183
+ normalizedAdapterConfig,
1184
+ );
1185
+ const normalizedHireInput = {
1186
+ ...hireInput,
1187
+ adapterConfig: normalizedAdapterConfig,
1188
+ };
1189
+
1190
+ const company = await db
1191
+ .select()
1192
+ .from(companies)
1193
+ .where(eq(companies.id, companyId))
1194
+ .then((rows) => rows[0] ?? null);
1195
+ if (!company) {
1196
+ res.status(404).json({ error: "Company not found" });
1197
+ return;
1198
+ }
1199
+
1200
+ const requiresApproval = company.requireBoardApprovalForNewAgents;
1201
+ const status = requiresApproval ? "pending_approval" : "idle";
1202
+ const createdAgent = await svc.create(companyId, {
1203
+ ...normalizedHireInput,
1204
+ status,
1205
+ spentMonthlyCents: 0,
1206
+ lastHeartbeatAt: null,
1207
+ });
1208
+ const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent);
1209
+
1210
+ let approval: Awaited<ReturnType<typeof approvalsSvc.getById>> | null = null;
1211
+ const actor = getActorInfo(req);
1212
+
1213
+ if (requiresApproval) {
1214
+ const requestedAdapterType = normalizedHireInput.adapterType ?? agent.adapterType;
1215
+ const requestedAdapterConfig =
1216
+ redactEventPayload(
1217
+ (agent.adapterConfig ?? normalizedHireInput.adapterConfig) as Record<string, unknown>,
1218
+ ) ?? {};
1219
+ const requestedRuntimeConfig =
1220
+ redactEventPayload(
1221
+ (normalizedHireInput.runtimeConfig ?? agent.runtimeConfig) as Record<string, unknown>,
1222
+ ) ?? {};
1223
+ const requestedMetadata =
1224
+ redactEventPayload(
1225
+ ((normalizedHireInput.metadata ?? agent.metadata ?? {}) as Record<string, unknown>),
1226
+ ) ?? {};
1227
+ approval = await approvalsSvc.create(companyId, {
1228
+ type: "hire_agent",
1229
+ requestedByAgentId: actor.actorType === "agent" ? actor.actorId : null,
1230
+ requestedByUserId: actor.actorType === "user" ? actor.actorId : null,
1231
+ status: "pending",
1232
+ payload: {
1233
+ name: normalizedHireInput.name,
1234
+ role: normalizedHireInput.role,
1235
+ title: normalizedHireInput.title ?? null,
1236
+ icon: normalizedHireInput.icon ?? null,
1237
+ reportsTo: normalizedHireInput.reportsTo ?? null,
1238
+ capabilities: normalizedHireInput.capabilities ?? null,
1239
+ adapterType: requestedAdapterType,
1240
+ adapterConfig: requestedAdapterConfig,
1241
+ runtimeConfig: requestedRuntimeConfig,
1242
+ budgetMonthlyCents:
1243
+ typeof normalizedHireInput.budgetMonthlyCents === "number"
1244
+ ? normalizedHireInput.budgetMonthlyCents
1245
+ : agent.budgetMonthlyCents,
1246
+ desiredSkills: desiredSkillAssignment.desiredSkills,
1247
+ metadata: requestedMetadata,
1248
+ agentId: agent.id,
1249
+ requestedByAgentId: actor.actorType === "agent" ? actor.actorId : null,
1250
+ requestedConfigurationSnapshot: {
1251
+ adapterType: requestedAdapterType,
1252
+ adapterConfig: requestedAdapterConfig,
1253
+ runtimeConfig: requestedRuntimeConfig,
1254
+ desiredSkills: desiredSkillAssignment.desiredSkills,
1255
+ },
1256
+ },
1257
+ decisionNote: null,
1258
+ decidedByUserId: null,
1259
+ decidedAt: null,
1260
+ updatedAt: new Date(),
1261
+ });
1262
+
1263
+ if (sourceIssueIds.length > 0) {
1264
+ await issueApprovalsSvc.linkManyForApproval(approval.id, sourceIssueIds, {
1265
+ agentId: actor.actorType === "agent" ? actor.actorId : null,
1266
+ userId: actor.actorType === "user" ? actor.actorId : null,
1267
+ });
1268
+ }
1269
+ }
1270
+
1271
+ await logActivity(db, {
1272
+ companyId,
1273
+ actorType: actor.actorType,
1274
+ actorId: actor.actorId,
1275
+ agentId: actor.agentId,
1276
+ runId: actor.runId,
1277
+ action: "agent.hire_created",
1278
+ entityType: "agent",
1279
+ entityId: agent.id,
1280
+ details: {
1281
+ name: agent.name,
1282
+ role: agent.role,
1283
+ requiresApproval,
1284
+ approvalId: approval?.id ?? null,
1285
+ issueIds: sourceIssueIds,
1286
+ desiredSkills: desiredSkillAssignment.desiredSkills,
1287
+ },
1288
+ });
1289
+
1290
+ await applyDefaultAgentTaskAssignGrant(
1291
+ companyId,
1292
+ agent.id,
1293
+ actor.actorType === "user" ? actor.actorId : null,
1294
+ );
1295
+
1296
+ if (approval) {
1297
+ await logActivity(db, {
1298
+ companyId,
1299
+ actorType: actor.actorType,
1300
+ actorId: actor.actorId,
1301
+ agentId: actor.agentId,
1302
+ runId: actor.runId,
1303
+ action: "approval.created",
1304
+ entityType: "approval",
1305
+ entityId: approval.id,
1306
+ details: { type: approval.type, linkedAgentId: agent.id },
1307
+ });
1308
+ }
1309
+
1310
+ res.status(201).json({ agent, approval });
1311
+ });
1312
+
1313
+ router.post("/companies/:companyId/agents", validate(createAgentSchema), async (req, res) => {
1314
+ const companyId = req.params.companyId as string;
1315
+ assertCompanyAccess(req, companyId);
1316
+
1317
+ if (req.actor.type === "agent") {
1318
+ assertBoard(req);
1319
+ }
1320
+
1321
+ const {
1322
+ desiredSkills: requestedDesiredSkills,
1323
+ ...createInput
1324
+ } = req.body;
1325
+ const requestedAdapterConfig = applyCreateDefaultsByAdapterType(
1326
+ createInput.adapterType,
1327
+ ((createInput.adapterConfig ?? {}) as Record<string, unknown>),
1328
+ );
1329
+ const desiredSkillAssignment = await resolveDesiredSkillAssignment(
1330
+ companyId,
1331
+ createInput.adapterType,
1332
+ requestedAdapterConfig,
1333
+ Array.isArray(requestedDesiredSkills) ? requestedDesiredSkills : undefined,
1334
+ );
1335
+ const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(
1336
+ companyId,
1337
+ desiredSkillAssignment.adapterConfig,
1338
+ { strictMode: strictSecretsMode },
1339
+ );
1340
+ await assertAdapterConfigConstraints(
1341
+ companyId,
1342
+ createInput.adapterType,
1343
+ normalizedAdapterConfig,
1344
+ );
1345
+
1346
+ const createdAgent = await svc.create(companyId, {
1347
+ ...createInput,
1348
+ adapterConfig: normalizedAdapterConfig,
1349
+ status: "idle",
1350
+ spentMonthlyCents: 0,
1351
+ lastHeartbeatAt: null,
1352
+ });
1353
+ const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent);
1354
+
1355
+ const actor = getActorInfo(req);
1356
+ await logActivity(db, {
1357
+ companyId,
1358
+ actorType: actor.actorType,
1359
+ actorId: actor.actorId,
1360
+ agentId: actor.agentId,
1361
+ runId: actor.runId,
1362
+ action: "agent.created",
1363
+ entityType: "agent",
1364
+ entityId: agent.id,
1365
+ details: {
1366
+ name: agent.name,
1367
+ role: agent.role,
1368
+ desiredSkills: desiredSkillAssignment.desiredSkills,
1369
+ },
1370
+ });
1371
+
1372
+ await applyDefaultAgentTaskAssignGrant(
1373
+ companyId,
1374
+ agent.id,
1375
+ req.actor.type === "board" ? (req.actor.userId ?? null) : null,
1376
+ );
1377
+
1378
+ if (agent.budgetMonthlyCents > 0) {
1379
+ await budgets.upsertPolicy(
1380
+ companyId,
1381
+ {
1382
+ scopeType: "agent",
1383
+ scopeId: agent.id,
1384
+ amount: agent.budgetMonthlyCents,
1385
+ windowKind: "calendar_month_utc",
1386
+ },
1387
+ actor.actorType === "user" ? actor.actorId : null,
1388
+ );
1389
+ }
1390
+
1391
+ res.status(201).json(agent);
1392
+ });
1393
+
1394
+ router.patch("/agents/:id/permissions", validate(updateAgentPermissionsSchema), async (req, res) => {
1395
+ const id = req.params.id as string;
1396
+ const existing = await svc.getById(id);
1397
+ if (!existing) {
1398
+ res.status(404).json({ error: "Agent not found" });
1399
+ return;
1400
+ }
1401
+ assertCompanyAccess(req, existing.companyId);
1402
+
1403
+ if (req.actor.type === "agent") {
1404
+ const actorAgent = req.actor.agentId ? await svc.getById(req.actor.agentId) : null;
1405
+ if (!actorAgent || actorAgent.companyId !== existing.companyId) {
1406
+ res.status(403).json({ error: "Forbidden" });
1407
+ return;
1408
+ }
1409
+ if (actorAgent.role !== "ceo") {
1410
+ res.status(403).json({ error: "Only CEO can manage permissions" });
1411
+ return;
1412
+ }
1413
+ }
1414
+
1415
+ const agent = await svc.updatePermissions(id, req.body);
1416
+ if (!agent) {
1417
+ res.status(404).json({ error: "Agent not found" });
1418
+ return;
1419
+ }
1420
+
1421
+ const effectiveCanAssignTasks =
1422
+ agent.role === "ceo" || Boolean(agent.permissions?.canCreateAgents) || req.body.canAssignTasks;
1423
+ await access.ensureMembership(agent.companyId, "agent", agent.id, "member", "active");
1424
+ await access.setPrincipalPermission(
1425
+ agent.companyId,
1426
+ "agent",
1427
+ agent.id,
1428
+ "tasks:assign",
1429
+ effectiveCanAssignTasks,
1430
+ req.actor.type === "board" ? (req.actor.userId ?? null) : null,
1431
+ );
1432
+
1433
+ const actor = getActorInfo(req);
1434
+ await logActivity(db, {
1435
+ companyId: agent.companyId,
1436
+ actorType: actor.actorType,
1437
+ actorId: actor.actorId,
1438
+ agentId: actor.agentId,
1439
+ runId: actor.runId,
1440
+ action: "agent.permissions_updated",
1441
+ entityType: "agent",
1442
+ entityId: agent.id,
1443
+ details: {
1444
+ canCreateAgents: agent.permissions?.canCreateAgents ?? false,
1445
+ canAssignTasks: effectiveCanAssignTasks,
1446
+ },
1447
+ });
1448
+
1449
+ res.json(await buildAgentDetail(agent));
1450
+ });
1451
+
1452
+ router.patch("/agents/:id/instructions-path", validate(updateAgentInstructionsPathSchema), async (req, res) => {
1453
+ const id = req.params.id as string;
1454
+ const existing = await svc.getById(id);
1455
+ if (!existing) {
1456
+ res.status(404).json({ error: "Agent not found" });
1457
+ return;
1458
+ }
1459
+
1460
+ await assertCanManageInstructionsPath(req, existing);
1461
+
1462
+ const existingAdapterConfig = asRecord(existing.adapterConfig) ?? {};
1463
+ const explicitKey = asNonEmptyString(req.body.adapterConfigKey);
1464
+ const defaultKey = DEFAULT_INSTRUCTIONS_PATH_KEYS[existing.adapterType] ?? null;
1465
+ const adapterConfigKey = explicitKey ?? defaultKey;
1466
+ if (!adapterConfigKey) {
1467
+ res.status(422).json({
1468
+ error: `No default instructions path key for adapter type '${existing.adapterType}'. Provide adapterConfigKey.`,
1469
+ });
1470
+ return;
1471
+ }
1472
+
1473
+ const nextAdapterConfig: Record<string, unknown> = { ...existingAdapterConfig };
1474
+ if (req.body.path === null) {
1475
+ delete nextAdapterConfig[adapterConfigKey];
1476
+ } else {
1477
+ nextAdapterConfig[adapterConfigKey] = resolveInstructionsFilePath(req.body.path, existingAdapterConfig);
1478
+ }
1479
+
1480
+ const syncedAdapterConfig = syncInstructionsBundleConfigFromFilePath(existing, nextAdapterConfig);
1481
+ const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(
1482
+ existing.companyId,
1483
+ syncedAdapterConfig,
1484
+ { strictMode: strictSecretsMode },
1485
+ );
1486
+ const actor = getActorInfo(req);
1487
+ const agent = await svc.update(
1488
+ id,
1489
+ { adapterConfig: normalizedAdapterConfig },
1490
+ {
1491
+ recordRevision: {
1492
+ createdByAgentId: actor.agentId,
1493
+ createdByUserId: actor.actorType === "user" ? actor.actorId : null,
1494
+ source: "instructions_path_patch",
1495
+ },
1496
+ },
1497
+ );
1498
+ if (!agent) {
1499
+ res.status(404).json({ error: "Agent not found" });
1500
+ return;
1501
+ }
1502
+
1503
+ const updatedAdapterConfig = asRecord(agent.adapterConfig) ?? {};
1504
+ const pathValue = asNonEmptyString(updatedAdapterConfig[adapterConfigKey]);
1505
+
1506
+ await logActivity(db, {
1507
+ companyId: agent.companyId,
1508
+ actorType: actor.actorType,
1509
+ actorId: actor.actorId,
1510
+ agentId: actor.agentId,
1511
+ runId: actor.runId,
1512
+ action: "agent.instructions_path_updated",
1513
+ entityType: "agent",
1514
+ entityId: agent.id,
1515
+ details: {
1516
+ adapterConfigKey,
1517
+ path: pathValue,
1518
+ cleared: req.body.path === null,
1519
+ },
1520
+ });
1521
+
1522
+ res.json({
1523
+ agentId: agent.id,
1524
+ adapterType: agent.adapterType,
1525
+ adapterConfigKey,
1526
+ path: pathValue,
1527
+ });
1528
+ });
1529
+
1530
+ router.get("/agents/:id/instructions-bundle", async (req, res) => {
1531
+ const id = req.params.id as string;
1532
+ const existing = await svc.getById(id);
1533
+ if (!existing) {
1534
+ res.status(404).json({ error: "Agent not found" });
1535
+ return;
1536
+ }
1537
+ await assertCanReadAgent(req, existing);
1538
+ res.json(await instructions.getBundle(existing));
1539
+ });
1540
+
1541
+ router.patch("/agents/:id/instructions-bundle", validate(updateAgentInstructionsBundleSchema), async (req, res) => {
1542
+ const id = req.params.id as string;
1543
+ const existing = await svc.getById(id);
1544
+ if (!existing) {
1545
+ res.status(404).json({ error: "Agent not found" });
1546
+ return;
1547
+ }
1548
+ await assertCanManageInstructionsPath(req, existing);
1549
+
1550
+ const actor = getActorInfo(req);
1551
+ const { bundle, adapterConfig } = await instructions.updateBundle(existing, req.body);
1552
+ const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(
1553
+ existing.companyId,
1554
+ adapterConfig,
1555
+ { strictMode: strictSecretsMode },
1556
+ );
1557
+ await svc.update(
1558
+ id,
1559
+ { adapterConfig: normalizedAdapterConfig },
1560
+ {
1561
+ recordRevision: {
1562
+ createdByAgentId: actor.agentId,
1563
+ createdByUserId: actor.actorType === "user" ? actor.actorId : null,
1564
+ source: "instructions_bundle_patch",
1565
+ },
1566
+ },
1567
+ );
1568
+
1569
+ await logActivity(db, {
1570
+ companyId: existing.companyId,
1571
+ actorType: actor.actorType,
1572
+ actorId: actor.actorId,
1573
+ agentId: actor.agentId,
1574
+ runId: actor.runId,
1575
+ action: "agent.instructions_bundle_updated",
1576
+ entityType: "agent",
1577
+ entityId: existing.id,
1578
+ details: {
1579
+ mode: bundle.mode,
1580
+ rootPath: bundle.rootPath,
1581
+ entryFile: bundle.entryFile,
1582
+ clearLegacyPromptTemplate: req.body.clearLegacyPromptTemplate === true,
1583
+ },
1584
+ });
1585
+
1586
+ res.json(bundle);
1587
+ });
1588
+
1589
+ router.get("/agents/:id/instructions-bundle/file", async (req, res) => {
1590
+ const id = req.params.id as string;
1591
+ const existing = await svc.getById(id);
1592
+ if (!existing) {
1593
+ res.status(404).json({ error: "Agent not found" });
1594
+ return;
1595
+ }
1596
+ await assertCanReadAgent(req, existing);
1597
+
1598
+ const relativePath = typeof req.query.path === "string" ? req.query.path : "";
1599
+ if (!relativePath.trim()) {
1600
+ res.status(422).json({ error: "Query parameter 'path' is required" });
1601
+ return;
1602
+ }
1603
+
1604
+ res.json(await instructions.readFile(existing, relativePath));
1605
+ });
1606
+
1607
+ router.put("/agents/:id/instructions-bundle/file", validate(upsertAgentInstructionsFileSchema), async (req, res) => {
1608
+ const id = req.params.id as string;
1609
+ const existing = await svc.getById(id);
1610
+ if (!existing) {
1611
+ res.status(404).json({ error: "Agent not found" });
1612
+ return;
1613
+ }
1614
+ await assertCanManageInstructionsPath(req, existing);
1615
+
1616
+ const actor = getActorInfo(req);
1617
+ const result = await instructions.writeFile(existing, req.body.path, req.body.content, {
1618
+ clearLegacyPromptTemplate: req.body.clearLegacyPromptTemplate,
1619
+ });
1620
+ const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(
1621
+ existing.companyId,
1622
+ result.adapterConfig,
1623
+ { strictMode: strictSecretsMode },
1624
+ );
1625
+ await svc.update(
1626
+ id,
1627
+ { adapterConfig: normalizedAdapterConfig },
1628
+ {
1629
+ recordRevision: {
1630
+ createdByAgentId: actor.agentId,
1631
+ createdByUserId: actor.actorType === "user" ? actor.actorId : null,
1632
+ source: "instructions_bundle_file_put",
1633
+ },
1634
+ },
1635
+ );
1636
+
1637
+ await logActivity(db, {
1638
+ companyId: existing.companyId,
1639
+ actorType: actor.actorType,
1640
+ actorId: actor.actorId,
1641
+ agentId: actor.agentId,
1642
+ runId: actor.runId,
1643
+ action: "agent.instructions_file_updated",
1644
+ entityType: "agent",
1645
+ entityId: existing.id,
1646
+ details: {
1647
+ path: result.file.path,
1648
+ size: result.file.size,
1649
+ clearLegacyPromptTemplate: req.body.clearLegacyPromptTemplate === true,
1650
+ },
1651
+ });
1652
+
1653
+ res.json(result.file);
1654
+ });
1655
+
1656
+ router.delete("/agents/:id/instructions-bundle/file", async (req, res) => {
1657
+ const id = req.params.id as string;
1658
+ const existing = await svc.getById(id);
1659
+ if (!existing) {
1660
+ res.status(404).json({ error: "Agent not found" });
1661
+ return;
1662
+ }
1663
+ await assertCanManageInstructionsPath(req, existing);
1664
+
1665
+ const relativePath = typeof req.query.path === "string" ? req.query.path : "";
1666
+ if (!relativePath.trim()) {
1667
+ res.status(422).json({ error: "Query parameter 'path' is required" });
1668
+ return;
1669
+ }
1670
+
1671
+ const actor = getActorInfo(req);
1672
+ const result = await instructions.deleteFile(existing, relativePath);
1673
+ await logActivity(db, {
1674
+ companyId: existing.companyId,
1675
+ actorType: actor.actorType,
1676
+ actorId: actor.actorId,
1677
+ agentId: actor.agentId,
1678
+ runId: actor.runId,
1679
+ action: "agent.instructions_file_deleted",
1680
+ entityType: "agent",
1681
+ entityId: existing.id,
1682
+ details: {
1683
+ path: relativePath,
1684
+ },
1685
+ });
1686
+
1687
+ res.json(result.bundle);
1688
+ });
1689
+
1690
+ router.patch("/agents/:id", validate(updateAgentSchema), async (req, res) => {
1691
+ const id = req.params.id as string;
1692
+ const existing = await svc.getById(id);
1693
+ if (!existing) {
1694
+ res.status(404).json({ error: "Agent not found" });
1695
+ return;
1696
+ }
1697
+ await assertCanUpdateAgent(req, existing);
1698
+
1699
+ if (Object.prototype.hasOwnProperty.call(req.body, "permissions")) {
1700
+ res.status(422).json({ error: "Use /api/agents/:id/permissions for permission changes" });
1701
+ return;
1702
+ }
1703
+
1704
+ const patchData = { ...(req.body as Record<string, unknown>) };
1705
+ const replaceAdapterConfig = patchData.replaceAdapterConfig === true;
1706
+ delete patchData.replaceAdapterConfig;
1707
+ if (Object.prototype.hasOwnProperty.call(patchData, "adapterConfig")) {
1708
+ const adapterConfig = asRecord(patchData.adapterConfig);
1709
+ if (!adapterConfig) {
1710
+ res.status(422).json({ error: "adapterConfig must be an object" });
1711
+ return;
1712
+ }
1713
+ const changingInstructionsPath = Object.keys(adapterConfig).some((key) =>
1714
+ KNOWN_INSTRUCTIONS_PATH_KEYS.has(key),
1715
+ );
1716
+ if (changingInstructionsPath) {
1717
+ await assertCanManageInstructionsPath(req, existing);
1718
+ }
1719
+ patchData.adapterConfig = adapterConfig;
1720
+ }
1721
+
1722
+ const requestedAdapterType =
1723
+ typeof patchData.adapterType === "string" ? patchData.adapterType : existing.adapterType;
1724
+ const touchesAdapterConfiguration =
1725
+ Object.prototype.hasOwnProperty.call(patchData, "adapterType") ||
1726
+ Object.prototype.hasOwnProperty.call(patchData, "adapterConfig");
1727
+ if (touchesAdapterConfiguration) {
1728
+ const existingAdapterConfig = asRecord(existing.adapterConfig) ?? {};
1729
+ const changingAdapterType =
1730
+ typeof patchData.adapterType === "string" && patchData.adapterType !== existing.adapterType;
1731
+ const requestedAdapterConfig = Object.prototype.hasOwnProperty.call(patchData, "adapterConfig")
1732
+ ? (asRecord(patchData.adapterConfig) ?? {})
1733
+ : null;
1734
+ if (
1735
+ requestedAdapterConfig
1736
+ && replaceAdapterConfig
1737
+ && KNOWN_INSTRUCTIONS_BUNDLE_KEYS.some((key) =>
1738
+ existingAdapterConfig[key] !== undefined && requestedAdapterConfig[key] === undefined,
1739
+ )
1740
+ ) {
1741
+ await assertCanManageInstructionsPath(req, existing);
1742
+ }
1743
+ let rawEffectiveAdapterConfig = requestedAdapterConfig ?? existingAdapterConfig;
1744
+ if (requestedAdapterConfig && !changingAdapterType && !replaceAdapterConfig) {
1745
+ rawEffectiveAdapterConfig = { ...existingAdapterConfig, ...requestedAdapterConfig };
1746
+ }
1747
+ if (changingAdapterType) {
1748
+ rawEffectiveAdapterConfig = preserveInstructionsBundleConfig(
1749
+ existingAdapterConfig,
1750
+ rawEffectiveAdapterConfig,
1751
+ );
1752
+ }
1753
+ const effectiveAdapterConfig = applyCreateDefaultsByAdapterType(
1754
+ requestedAdapterType,
1755
+ rawEffectiveAdapterConfig,
1756
+ );
1757
+ const normalizedEffectiveAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(
1758
+ existing.companyId,
1759
+ effectiveAdapterConfig,
1760
+ { strictMode: strictSecretsMode },
1761
+ );
1762
+ patchData.adapterConfig = syncInstructionsBundleConfigFromFilePath(existing, normalizedEffectiveAdapterConfig);
1763
+ }
1764
+ if (touchesAdapterConfiguration && requestedAdapterType === "opencode_local") {
1765
+ const effectiveAdapterConfig = asRecord(patchData.adapterConfig) ?? {};
1766
+ await assertAdapterConfigConstraints(
1767
+ existing.companyId,
1768
+ requestedAdapterType,
1769
+ effectiveAdapterConfig,
1770
+ );
1771
+ }
1772
+
1773
+ const actor = getActorInfo(req);
1774
+ const agent = await svc.update(id, patchData, {
1775
+ recordRevision: {
1776
+ createdByAgentId: actor.agentId,
1777
+ createdByUserId: actor.actorType === "user" ? actor.actorId : null,
1778
+ source: "patch",
1779
+ },
1780
+ });
1781
+ if (!agent) {
1782
+ res.status(404).json({ error: "Agent not found" });
1783
+ return;
1784
+ }
1785
+
1786
+ await logActivity(db, {
1787
+ companyId: agent.companyId,
1788
+ actorType: actor.actorType,
1789
+ actorId: actor.actorId,
1790
+ agentId: actor.agentId,
1791
+ runId: actor.runId,
1792
+ action: "agent.updated",
1793
+ entityType: "agent",
1794
+ entityId: agent.id,
1795
+ details: summarizeAgentUpdateDetails(patchData),
1796
+ });
1797
+
1798
+ res.json(agent);
1799
+ });
1800
+
1801
+ router.post("/agents/:id/pause", async (req, res) => {
1802
+ assertBoard(req);
1803
+ const id = req.params.id as string;
1804
+ const agent = await svc.pause(id);
1805
+ if (!agent) {
1806
+ res.status(404).json({ error: "Agent not found" });
1807
+ return;
1808
+ }
1809
+
1810
+ await heartbeat.cancelActiveForAgent(id);
1811
+
1812
+ await logActivity(db, {
1813
+ companyId: agent.companyId,
1814
+ actorType: "user",
1815
+ actorId: req.actor.userId ?? "board",
1816
+ action: "agent.paused",
1817
+ entityType: "agent",
1818
+ entityId: agent.id,
1819
+ });
1820
+
1821
+ res.json(agent);
1822
+ });
1823
+
1824
+ router.post("/agents/:id/resume", async (req, res) => {
1825
+ assertBoard(req);
1826
+ const id = req.params.id as string;
1827
+ const agent = await svc.resume(id);
1828
+ if (!agent) {
1829
+ res.status(404).json({ error: "Agent not found" });
1830
+ return;
1831
+ }
1832
+
1833
+ await logActivity(db, {
1834
+ companyId: agent.companyId,
1835
+ actorType: "user",
1836
+ actorId: req.actor.userId ?? "board",
1837
+ action: "agent.resumed",
1838
+ entityType: "agent",
1839
+ entityId: agent.id,
1840
+ });
1841
+
1842
+ res.json(agent);
1843
+ });
1844
+
1845
+ router.post("/agents/:id/terminate", async (req, res) => {
1846
+ assertBoard(req);
1847
+ const id = req.params.id as string;
1848
+ const agent = await svc.terminate(id);
1849
+ if (!agent) {
1850
+ res.status(404).json({ error: "Agent not found" });
1851
+ return;
1852
+ }
1853
+
1854
+ await heartbeat.cancelActiveForAgent(id);
1855
+
1856
+ await logActivity(db, {
1857
+ companyId: agent.companyId,
1858
+ actorType: "user",
1859
+ actorId: req.actor.userId ?? "board",
1860
+ action: "agent.terminated",
1861
+ entityType: "agent",
1862
+ entityId: agent.id,
1863
+ });
1864
+
1865
+ res.json(agent);
1866
+ });
1867
+
1868
+ router.delete("/agents/:id", async (req, res) => {
1869
+ assertBoard(req);
1870
+ const id = req.params.id as string;
1871
+ const agent = await svc.remove(id);
1872
+ if (!agent) {
1873
+ res.status(404).json({ error: "Agent not found" });
1874
+ return;
1875
+ }
1876
+
1877
+ await logActivity(db, {
1878
+ companyId: agent.companyId,
1879
+ actorType: "user",
1880
+ actorId: req.actor.userId ?? "board",
1881
+ action: "agent.deleted",
1882
+ entityType: "agent",
1883
+ entityId: agent.id,
1884
+ });
1885
+
1886
+ res.json({ ok: true });
1887
+ });
1888
+
1889
+ router.get("/agents/:id/keys", async (req, res) => {
1890
+ assertBoard(req);
1891
+ const id = req.params.id as string;
1892
+ const keys = await svc.listKeys(id);
1893
+ res.json(keys);
1894
+ });
1895
+
1896
+ router.post("/agents/:id/keys", validate(createAgentKeySchema), async (req, res) => {
1897
+ assertBoard(req);
1898
+ const id = req.params.id as string;
1899
+ const key = await svc.createApiKey(id, req.body.name);
1900
+
1901
+ const agent = await svc.getById(id);
1902
+ if (agent) {
1903
+ await logActivity(db, {
1904
+ companyId: agent.companyId,
1905
+ actorType: "user",
1906
+ actorId: req.actor.userId ?? "board",
1907
+ action: "agent.key_created",
1908
+ entityType: "agent",
1909
+ entityId: agent.id,
1910
+ details: { keyId: key.id, name: key.name },
1911
+ });
1912
+ }
1913
+
1914
+ res.status(201).json(key);
1915
+ });
1916
+
1917
+ router.delete("/agents/:id/keys/:keyId", async (req, res) => {
1918
+ assertBoard(req);
1919
+ const keyId = req.params.keyId as string;
1920
+ const revoked = await svc.revokeKey(keyId);
1921
+ if (!revoked) {
1922
+ res.status(404).json({ error: "Key not found" });
1923
+ return;
1924
+ }
1925
+ res.json({ ok: true });
1926
+ });
1927
+
1928
+ router.post("/agents/:id/wakeup", validate(wakeAgentSchema), async (req, res) => {
1929
+ const id = req.params.id as string;
1930
+ const agent = await svc.getById(id);
1931
+ if (!agent) {
1932
+ res.status(404).json({ error: "Agent not found" });
1933
+ return;
1934
+ }
1935
+ assertCompanyAccess(req, agent.companyId);
1936
+
1937
+ if (req.actor.type === "agent" && req.actor.agentId !== id) {
1938
+ res.status(403).json({ error: "Agent can only invoke itself" });
1939
+ return;
1940
+ }
1941
+
1942
+ const run = await heartbeat.wakeup(id, {
1943
+ source: req.body.source,
1944
+ triggerDetail: req.body.triggerDetail ?? "manual",
1945
+ reason: req.body.reason ?? null,
1946
+ payload: req.body.payload ?? null,
1947
+ idempotencyKey: req.body.idempotencyKey ?? null,
1948
+ requestedByActorType: req.actor.type === "agent" ? "agent" : "user",
1949
+ requestedByActorId: req.actor.type === "agent" ? req.actor.agentId ?? null : req.actor.userId ?? null,
1950
+ contextSnapshot: {
1951
+ triggeredBy: req.actor.type,
1952
+ actorId: req.actor.type === "agent" ? req.actor.agentId : req.actor.userId,
1953
+ forceFreshSession: req.body.forceFreshSession === true,
1954
+ },
1955
+ });
1956
+
1957
+ if (!run) {
1958
+ res.status(202).json({ status: "skipped" });
1959
+ return;
1960
+ }
1961
+
1962
+ const actor = getActorInfo(req);
1963
+ await logActivity(db, {
1964
+ companyId: agent.companyId,
1965
+ actorType: actor.actorType,
1966
+ actorId: actor.actorId,
1967
+ agentId: actor.agentId,
1968
+ runId: actor.runId,
1969
+ action: "heartbeat.invoked",
1970
+ entityType: "heartbeat_run",
1971
+ entityId: run.id,
1972
+ details: { agentId: id },
1973
+ });
1974
+
1975
+ res.status(202).json(run);
1976
+ });
1977
+
1978
+ router.post("/agents/:id/heartbeat/invoke", async (req, res) => {
1979
+ const id = req.params.id as string;
1980
+ const agent = await svc.getById(id);
1981
+ if (!agent) {
1982
+ res.status(404).json({ error: "Agent not found" });
1983
+ return;
1984
+ }
1985
+ assertCompanyAccess(req, agent.companyId);
1986
+
1987
+ if (req.actor.type === "agent" && req.actor.agentId !== id) {
1988
+ res.status(403).json({ error: "Agent can only invoke itself" });
1989
+ return;
1990
+ }
1991
+
1992
+ const run = await heartbeat.invoke(
1993
+ id,
1994
+ "on_demand",
1995
+ {
1996
+ triggeredBy: req.actor.type,
1997
+ actorId: req.actor.type === "agent" ? req.actor.agentId : req.actor.userId,
1998
+ },
1999
+ "manual",
2000
+ {
2001
+ actorType: req.actor.type === "agent" ? "agent" : "user",
2002
+ actorId: req.actor.type === "agent" ? req.actor.agentId ?? null : req.actor.userId ?? null,
2003
+ },
2004
+ );
2005
+
2006
+ if (!run) {
2007
+ res.status(202).json({ status: "skipped" });
2008
+ return;
2009
+ }
2010
+
2011
+ const actor = getActorInfo(req);
2012
+ await logActivity(db, {
2013
+ companyId: agent.companyId,
2014
+ actorType: actor.actorType,
2015
+ actorId: actor.actorId,
2016
+ agentId: actor.agentId,
2017
+ runId: actor.runId,
2018
+ action: "heartbeat.invoked",
2019
+ entityType: "heartbeat_run",
2020
+ entityId: run.id,
2021
+ details: { agentId: id },
2022
+ });
2023
+
2024
+ res.status(202).json(run);
2025
+ });
2026
+
2027
+ router.post("/agents/:id/claude-login", async (req, res) => {
2028
+ assertBoard(req);
2029
+ const id = req.params.id as string;
2030
+ const agent = await svc.getById(id);
2031
+ if (!agent) {
2032
+ res.status(404).json({ error: "Agent not found" });
2033
+ return;
2034
+ }
2035
+ assertCompanyAccess(req, agent.companyId);
2036
+ if (agent.adapterType !== "claude_local") {
2037
+ res.status(400).json({ error: "Login is only supported for claude_local agents" });
2038
+ return;
2039
+ }
2040
+
2041
+ const config = asRecord(agent.adapterConfig) ?? {};
2042
+ const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, config);
2043
+ const result = await runClaudeLogin({
2044
+ runId: `claude-login-${randomUUID()}`,
2045
+ agent: {
2046
+ id: agent.id,
2047
+ companyId: agent.companyId,
2048
+ name: agent.name,
2049
+ adapterType: agent.adapterType,
2050
+ adapterConfig: agent.adapterConfig,
2051
+ },
2052
+ config: runtimeConfig,
2053
+ });
2054
+
2055
+ res.json(result);
2056
+ });
2057
+
2058
+ router.get("/companies/:companyId/heartbeat-runs", async (req, res) => {
2059
+ const companyId = req.params.companyId as string;
2060
+ assertCompanyAccess(req, companyId);
2061
+ const agentId = req.query.agentId as string | undefined;
2062
+ const limitParam = req.query.limit as string | undefined;
2063
+ const limit = limitParam ? Math.max(1, Math.min(1000, parseInt(limitParam, 10) || 200)) : undefined;
2064
+ const runs = await heartbeat.list(companyId, agentId, limit);
2065
+ res.json(runs);
2066
+ });
2067
+
2068
+ router.get("/companies/:companyId/live-runs", async (req, res) => {
2069
+ const companyId = req.params.companyId as string;
2070
+ assertCompanyAccess(req, companyId);
2071
+
2072
+ const minCountParam = req.query.minCount as string | undefined;
2073
+ const minCount = minCountParam ? Math.max(0, Math.min(20, parseInt(minCountParam, 10) || 0)) : 0;
2074
+
2075
+ const columns = {
2076
+ id: heartbeatRuns.id,
2077
+ status: heartbeatRuns.status,
2078
+ invocationSource: heartbeatRuns.invocationSource,
2079
+ triggerDetail: heartbeatRuns.triggerDetail,
2080
+ startedAt: heartbeatRuns.startedAt,
2081
+ finishedAt: heartbeatRuns.finishedAt,
2082
+ createdAt: heartbeatRuns.createdAt,
2083
+ agentId: heartbeatRuns.agentId,
2084
+ agentName: agentsTable.name,
2085
+ adapterType: agentsTable.adapterType,
2086
+ issueId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'issueId'`.as("issueId"),
2087
+ };
2088
+
2089
+ const liveRuns = await db
2090
+ .select(columns)
2091
+ .from(heartbeatRuns)
2092
+ .innerJoin(agentsTable, eq(heartbeatRuns.agentId, agentsTable.id))
2093
+ .where(
2094
+ and(
2095
+ eq(heartbeatRuns.companyId, companyId),
2096
+ inArray(heartbeatRuns.status, ["queued", "running"]),
2097
+ ),
2098
+ )
2099
+ .orderBy(desc(heartbeatRuns.createdAt));
2100
+
2101
+ if (minCount > 0 && liveRuns.length < minCount) {
2102
+ const activeIds = liveRuns.map((r) => r.id);
2103
+ const recentRuns = await db
2104
+ .select(columns)
2105
+ .from(heartbeatRuns)
2106
+ .innerJoin(agentsTable, eq(heartbeatRuns.agentId, agentsTable.id))
2107
+ .where(
2108
+ and(
2109
+ eq(heartbeatRuns.companyId, companyId),
2110
+ not(inArray(heartbeatRuns.status, ["queued", "running"])),
2111
+ ...(activeIds.length > 0 ? [not(inArray(heartbeatRuns.id, activeIds))] : []),
2112
+ ),
2113
+ )
2114
+ .orderBy(desc(heartbeatRuns.createdAt))
2115
+ .limit(minCount - liveRuns.length);
2116
+
2117
+ res.json([...liveRuns, ...recentRuns]);
2118
+ return;
2119
+ }
2120
+
2121
+ res.json(liveRuns);
2122
+ });
2123
+
2124
+ router.get("/heartbeat-runs/:runId", async (req, res) => {
2125
+ const runId = req.params.runId as string;
2126
+ const run = await heartbeat.getRun(runId);
2127
+ if (!run) {
2128
+ res.status(404).json({ error: "Heartbeat run not found" });
2129
+ return;
2130
+ }
2131
+ assertCompanyAccess(req, run.companyId);
2132
+ res.json(redactCurrentUserValue(run, await getCurrentUserRedactionOptions()));
2133
+ });
2134
+
2135
+ router.post("/heartbeat-runs/:runId/cancel", async (req, res) => {
2136
+ assertBoard(req);
2137
+ const runId = req.params.runId as string;
2138
+ const run = await heartbeat.cancelRun(runId);
2139
+
2140
+ if (run) {
2141
+ await logActivity(db, {
2142
+ companyId: run.companyId,
2143
+ actorType: "user",
2144
+ actorId: req.actor.userId ?? "board",
2145
+ action: "heartbeat.cancelled",
2146
+ entityType: "heartbeat_run",
2147
+ entityId: run.id,
2148
+ details: { agentId: run.agentId },
2149
+ });
2150
+ }
2151
+
2152
+ res.json(run);
2153
+ });
2154
+
2155
+ router.get("/heartbeat-runs/:runId/events", async (req, res) => {
2156
+ const runId = req.params.runId as string;
2157
+ const run = await heartbeat.getRun(runId);
2158
+ if (!run) {
2159
+ res.status(404).json({ error: "Heartbeat run not found" });
2160
+ return;
2161
+ }
2162
+ assertCompanyAccess(req, run.companyId);
2163
+
2164
+ const afterSeq = Number(req.query.afterSeq ?? 0);
2165
+ const limit = Number(req.query.limit ?? 200);
2166
+ const events = await heartbeat.listEvents(runId, Number.isFinite(afterSeq) ? afterSeq : 0, Number.isFinite(limit) ? limit : 200);
2167
+ const currentUserRedactionOptions = await getCurrentUserRedactionOptions();
2168
+ const redactedEvents = events.map((event) =>
2169
+ redactCurrentUserValue({
2170
+ ...event,
2171
+ payload: redactEventPayload(event.payload),
2172
+ }, currentUserRedactionOptions),
2173
+ );
2174
+ res.json(redactedEvents);
2175
+ });
2176
+
2177
+ router.get("/heartbeat-runs/:runId/log", async (req, res) => {
2178
+ const runId = req.params.runId as string;
2179
+ const run = await heartbeat.getRun(runId);
2180
+ if (!run) {
2181
+ res.status(404).json({ error: "Heartbeat run not found" });
2182
+ return;
2183
+ }
2184
+ assertCompanyAccess(req, run.companyId);
2185
+
2186
+ const offset = Number(req.query.offset ?? 0);
2187
+ const limitBytes = Number(req.query.limitBytes ?? 256000);
2188
+ const result = await heartbeat.readLog(runId, {
2189
+ offset: Number.isFinite(offset) ? offset : 0,
2190
+ limitBytes: Number.isFinite(limitBytes) ? limitBytes : 256000,
2191
+ });
2192
+
2193
+ res.json(result);
2194
+ });
2195
+
2196
+ router.get("/heartbeat-runs/:runId/workspace-operations", async (req, res) => {
2197
+ const runId = req.params.runId as string;
2198
+ const run = await heartbeat.getRun(runId);
2199
+ if (!run) {
2200
+ res.status(404).json({ error: "Heartbeat run not found" });
2201
+ return;
2202
+ }
2203
+ assertCompanyAccess(req, run.companyId);
2204
+
2205
+ const context = asRecord(run.contextSnapshot);
2206
+ const executionWorkspaceId = asNonEmptyString(context?.executionWorkspaceId);
2207
+ const operations = await workspaceOperations.listForRun(runId, executionWorkspaceId);
2208
+ res.json(redactCurrentUserValue(operations, await getCurrentUserRedactionOptions()));
2209
+ });
2210
+
2211
+ router.get("/workspace-operations/:operationId/log", async (req, res) => {
2212
+ const operationId = req.params.operationId as string;
2213
+ const operation = await workspaceOperations.getById(operationId);
2214
+ if (!operation) {
2215
+ res.status(404).json({ error: "Workspace operation not found" });
2216
+ return;
2217
+ }
2218
+ assertCompanyAccess(req, operation.companyId);
2219
+
2220
+ const offset = Number(req.query.offset ?? 0);
2221
+ const limitBytes = Number(req.query.limitBytes ?? 256000);
2222
+ const result = await workspaceOperations.readLog(operationId, {
2223
+ offset: Number.isFinite(offset) ? offset : 0,
2224
+ limitBytes: Number.isFinite(limitBytes) ? limitBytes : 256000,
2225
+ });
2226
+
2227
+ res.json(result);
2228
+ });
2229
+
2230
+ router.get("/issues/:issueId/live-runs", async (req, res) => {
2231
+ const rawId = req.params.issueId as string;
2232
+ const issueSvc = issueService(db);
2233
+ const isIdentifier = /^[A-Z]+-\d+$/i.test(rawId);
2234
+ const issue = isIdentifier ? await issueSvc.getByIdentifier(rawId) : await issueSvc.getById(rawId);
2235
+ if (!issue) {
2236
+ res.status(404).json({ error: "Issue not found" });
2237
+ return;
2238
+ }
2239
+ assertCompanyAccess(req, issue.companyId);
2240
+
2241
+ const liveRuns = await db
2242
+ .select({
2243
+ id: heartbeatRuns.id,
2244
+ status: heartbeatRuns.status,
2245
+ invocationSource: heartbeatRuns.invocationSource,
2246
+ triggerDetail: heartbeatRuns.triggerDetail,
2247
+ startedAt: heartbeatRuns.startedAt,
2248
+ finishedAt: heartbeatRuns.finishedAt,
2249
+ createdAt: heartbeatRuns.createdAt,
2250
+ agentId: heartbeatRuns.agentId,
2251
+ agentName: agentsTable.name,
2252
+ adapterType: agentsTable.adapterType,
2253
+ })
2254
+ .from(heartbeatRuns)
2255
+ .innerJoin(agentsTable, eq(heartbeatRuns.agentId, agentsTable.id))
2256
+ .where(
2257
+ and(
2258
+ eq(heartbeatRuns.companyId, issue.companyId),
2259
+ inArray(heartbeatRuns.status, ["queued", "running"]),
2260
+ sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issue.id}`,
2261
+ ),
2262
+ )
2263
+ .orderBy(desc(heartbeatRuns.createdAt));
2264
+
2265
+ res.json(liveRuns);
2266
+ });
2267
+
2268
+ router.get("/issues/:issueId/active-run", async (req, res) => {
2269
+ const rawId = req.params.issueId as string;
2270
+ const issueSvc = issueService(db);
2271
+ const isIdentifier = /^[A-Z]+-\d+$/i.test(rawId);
2272
+ const issue = isIdentifier ? await issueSvc.getByIdentifier(rawId) : await issueSvc.getById(rawId);
2273
+ if (!issue) {
2274
+ res.status(404).json({ error: "Issue not found" });
2275
+ return;
2276
+ }
2277
+ assertCompanyAccess(req, issue.companyId);
2278
+
2279
+ let run = issue.executionRunId ? await heartbeat.getRun(issue.executionRunId) : null;
2280
+ if (run && run.status !== "queued" && run.status !== "running") {
2281
+ run = null;
2282
+ }
2283
+
2284
+ if (!run && issue.assigneeAgentId && issue.status === "in_progress") {
2285
+ const candidateRun = await heartbeat.getActiveRunForAgent(issue.assigneeAgentId);
2286
+ const candidateContext = asRecord(candidateRun?.contextSnapshot);
2287
+ const candidateIssueId = asNonEmptyString(candidateContext?.issueId);
2288
+ if (candidateRun && candidateIssueId === issue.id) {
2289
+ run = candidateRun;
2290
+ }
2291
+ }
2292
+ if (!run) {
2293
+ res.json(null);
2294
+ return;
2295
+ }
2296
+
2297
+ const agent = await svc.getById(run.agentId);
2298
+ if (!agent) {
2299
+ res.json(null);
2300
+ return;
2301
+ }
2302
+
2303
+ res.json({
2304
+ ...redactCurrentUserValue(run, await getCurrentUserRedactionOptions()),
2305
+ agentId: agent.id,
2306
+ agentName: agent.name,
2307
+ adapterType: agent.adapterType,
2308
+ });
2309
+ });
2310
+
2311
+ return router;
2312
+ }
2313
+