@paperclipai/server 0.2.2

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 (317) hide show
  1. package/dist/adapters/codex-models.d.ts +4 -0
  2. package/dist/adapters/codex-models.d.ts.map +1 -0
  3. package/dist/adapters/codex-models.js +98 -0
  4. package/dist/adapters/codex-models.js.map +1 -0
  5. package/dist/adapters/http/execute.d.ts +3 -0
  6. package/dist/adapters/http/execute.d.ts.map +1 -0
  7. package/dist/adapters/http/execute.js +39 -0
  8. package/dist/adapters/http/execute.js.map +1 -0
  9. package/dist/adapters/http/index.d.ts +3 -0
  10. package/dist/adapters/http/index.d.ts.map +1 -0
  11. package/dist/adapters/http/index.js +20 -0
  12. package/dist/adapters/http/index.js.map +1 -0
  13. package/dist/adapters/http/test.d.ts +3 -0
  14. package/dist/adapters/http/test.d.ts.map +1 -0
  15. package/dist/adapters/http/test.js +106 -0
  16. package/dist/adapters/http/test.js.map +1 -0
  17. package/dist/adapters/index.d.ts +4 -0
  18. package/dist/adapters/index.d.ts.map +1 -0
  19. package/dist/adapters/index.js +3 -0
  20. package/dist/adapters/index.js.map +1 -0
  21. package/dist/adapters/process/execute.d.ts +3 -0
  22. package/dist/adapters/process/execute.d.ts.map +1 -0
  23. package/dist/adapters/process/execute.js +63 -0
  24. package/dist/adapters/process/execute.js.map +1 -0
  25. package/dist/adapters/process/index.d.ts +3 -0
  26. package/dist/adapters/process/index.d.ts.map +1 -0
  27. package/dist/adapters/process/index.js +23 -0
  28. package/dist/adapters/process/index.js.map +1 -0
  29. package/dist/adapters/process/test.d.ts +3 -0
  30. package/dist/adapters/process/test.d.ts.map +1 -0
  31. package/dist/adapters/process/test.js +77 -0
  32. package/dist/adapters/process/test.js.map +1 -0
  33. package/dist/adapters/registry.d.ts +9 -0
  34. package/dist/adapters/registry.d.ts.map +1 -0
  35. package/dist/adapters/registry.js +63 -0
  36. package/dist/adapters/registry.js.map +1 -0
  37. package/dist/adapters/types.d.ts +2 -0
  38. package/dist/adapters/types.d.ts.map +1 -0
  39. package/dist/adapters/types.js +2 -0
  40. package/dist/adapters/types.js.map +1 -0
  41. package/dist/adapters/utils.d.ts +10 -0
  42. package/dist/adapters/utils.d.ts.map +1 -0
  43. package/dist/adapters/utils.js +14 -0
  44. package/dist/adapters/utils.js.map +1 -0
  45. package/dist/agent-auth-jwt.d.ts +14 -0
  46. package/dist/agent-auth-jwt.d.ts.map +1 -0
  47. package/dist/agent-auth-jwt.js +117 -0
  48. package/dist/agent-auth-jwt.js.map +1 -0
  49. package/dist/app.d.ts +20 -0
  50. package/dist/app.d.ts.map +1 -0
  51. package/dist/app.js +127 -0
  52. package/dist/app.js.map +1 -0
  53. package/dist/auth/better-auth.d.ts +23 -0
  54. package/dist/auth/better-auth.d.ts.map +1 -0
  55. package/dist/auth/better-auth.js +80 -0
  56. package/dist/auth/better-auth.js.map +1 -0
  57. package/dist/board-claim.d.ts +23 -0
  58. package/dist/board-claim.d.ts.map +1 -0
  59. package/dist/board-claim.js +115 -0
  60. package/dist/board-claim.js.map +1 -0
  61. package/dist/config-file.d.ts +3 -0
  62. package/dist/config-file.d.ts.map +1 -0
  63. package/dist/config-file.js +16 -0
  64. package/dist/config-file.js.map +1 -0
  65. package/dist/config.d.ts +33 -0
  66. package/dist/config.d.ts.map +1 -0
  67. package/dist/config.js +114 -0
  68. package/dist/config.js.map +1 -0
  69. package/dist/errors.d.ts +12 -0
  70. package/dist/errors.d.ts.map +1 -0
  71. package/dist/errors.js +28 -0
  72. package/dist/errors.js.map +1 -0
  73. package/dist/home-paths.d.ts +11 -0
  74. package/dist/home-paths.d.ts.map +1 -0
  75. package/dist/home-paths.js +54 -0
  76. package/dist/home-paths.js.map +1 -0
  77. package/dist/index.d.ts +2 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +439 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/middleware/auth.d.ts +12 -0
  82. package/dist/middleware/auth.d.ts.map +1 -0
  83. package/dist/middleware/auth.js +124 -0
  84. package/dist/middleware/auth.js.map +1 -0
  85. package/dist/middleware/board-mutation-guard.d.ts +3 -0
  86. package/dist/middleware/board-mutation-guard.d.ts.map +1 -0
  87. package/dist/middleware/board-mutation-guard.js +60 -0
  88. package/dist/middleware/board-mutation-guard.js.map +1 -0
  89. package/dist/middleware/error-handler.d.ts +3 -0
  90. package/dist/middleware/error-handler.d.ts.map +1 -0
  91. package/dist/middleware/error-handler.js +22 -0
  92. package/dist/middleware/error-handler.js.map +1 -0
  93. package/dist/middleware/index.d.ts +4 -0
  94. package/dist/middleware/index.d.ts.map +1 -0
  95. package/dist/middleware/index.js +4 -0
  96. package/dist/middleware/index.js.map +1 -0
  97. package/dist/middleware/logger.d.ts +4 -0
  98. package/dist/middleware/logger.d.ts.map +1 -0
  99. package/dist/middleware/logger.js +37 -0
  100. package/dist/middleware/logger.js.map +1 -0
  101. package/dist/middleware/private-hostname-guard.d.ts +11 -0
  102. package/dist/middleware/private-hostname-guard.d.ts.map +1 -0
  103. package/dist/middleware/private-hostname-guard.js +78 -0
  104. package/dist/middleware/private-hostname-guard.js.map +1 -0
  105. package/dist/middleware/validate.d.ts +4 -0
  106. package/dist/middleware/validate.d.ts.map +1 -0
  107. package/dist/middleware/validate.js +7 -0
  108. package/dist/middleware/validate.js.map +1 -0
  109. package/dist/paths.d.ts +3 -0
  110. package/dist/paths.d.ts.map +1 -0
  111. package/dist/paths.js +31 -0
  112. package/dist/paths.js.map +1 -0
  113. package/dist/realtime/live-events-ws.d.ts +10 -0
  114. package/dist/realtime/live-events-ws.d.ts.map +1 -0
  115. package/dist/realtime/live-events-ws.js +185 -0
  116. package/dist/realtime/live-events-ws.js.map +1 -0
  117. package/dist/redaction.d.ts +4 -0
  118. package/dist/redaction.d.ts.map +1 -0
  119. package/dist/redaction.js +63 -0
  120. package/dist/redaction.js.map +1 -0
  121. package/dist/routes/access.d.ts +9 -0
  122. package/dist/routes/access.d.ts.map +1 -0
  123. package/dist/routes/access.js +887 -0
  124. package/dist/routes/access.js.map +1 -0
  125. package/dist/routes/activity.d.ts +3 -0
  126. package/dist/routes/activity.d.ts.map +1 -0
  127. package/dist/routes/activity.js +87 -0
  128. package/dist/routes/activity.js.map +1 -0
  129. package/dist/routes/agents.d.ts +3 -0
  130. package/dist/routes/agents.d.ts.map +1 -0
  131. package/dist/routes/agents.js +1132 -0
  132. package/dist/routes/agents.js.map +1 -0
  133. package/dist/routes/approvals.d.ts +3 -0
  134. package/dist/routes/approvals.d.ts.map +1 -0
  135. package/dist/routes/approvals.js +271 -0
  136. package/dist/routes/approvals.js.map +1 -0
  137. package/dist/routes/assets.d.ts +4 -0
  138. package/dist/routes/assets.d.ts.map +1 -0
  139. package/dist/routes/assets.js +138 -0
  140. package/dist/routes/assets.js.map +1 -0
  141. package/dist/routes/authz.d.ts +15 -0
  142. package/dist/routes/authz.d.ts.map +1 -0
  143. package/dist/routes/authz.js +40 -0
  144. package/dist/routes/authz.js.map +1 -0
  145. package/dist/routes/companies.d.ts +3 -0
  146. package/dist/routes/companies.d.ts.map +1 -0
  147. package/dist/routes/companies.js +159 -0
  148. package/dist/routes/companies.js.map +1 -0
  149. package/dist/routes/costs.d.ts +3 -0
  150. package/dist/routes/costs.d.ts.map +1 -0
  151. package/dist/routes/costs.js +113 -0
  152. package/dist/routes/costs.js.map +1 -0
  153. package/dist/routes/dashboard.d.ts +3 -0
  154. package/dist/routes/dashboard.d.ts.map +1 -0
  155. package/dist/routes/dashboard.js +15 -0
  156. package/dist/routes/dashboard.js.map +1 -0
  157. package/dist/routes/goals.d.ts +3 -0
  158. package/dist/routes/goals.d.ts.map +1 -0
  159. package/dist/routes/goals.js +95 -0
  160. package/dist/routes/goals.js.map +1 -0
  161. package/dist/routes/health.d.ts +9 -0
  162. package/dist/routes/health.d.ts.map +1 -0
  163. package/dist/routes/health.js +38 -0
  164. package/dist/routes/health.js.map +1 -0
  165. package/dist/routes/index.d.ts +15 -0
  166. package/dist/routes/index.d.ts.map +1 -0
  167. package/dist/routes/index.js +15 -0
  168. package/dist/routes/index.js.map +1 -0
  169. package/dist/routes/issues.d.ts +4 -0
  170. package/dist/routes/issues.d.ts.map +1 -0
  171. package/dist/routes/issues.js +973 -0
  172. package/dist/routes/issues.js.map +1 -0
  173. package/dist/routes/llms.d.ts +3 -0
  174. package/dist/routes/llms.d.ts.map +1 -0
  175. package/dist/routes/llms.js +78 -0
  176. package/dist/routes/llms.js.map +1 -0
  177. package/dist/routes/projects.d.ts +3 -0
  178. package/dist/routes/projects.d.ts.map +1 -0
  179. package/dist/routes/projects.js +253 -0
  180. package/dist/routes/projects.js.map +1 -0
  181. package/dist/routes/secrets.d.ts +3 -0
  182. package/dist/routes/secrets.d.ts.map +1 -0
  183. package/dist/routes/secrets.js +128 -0
  184. package/dist/routes/secrets.js.map +1 -0
  185. package/dist/routes/sidebar-badges.d.ts +3 -0
  186. package/dist/routes/sidebar-badges.d.ts.map +1 -0
  187. package/dist/routes/sidebar-badges.js +47 -0
  188. package/dist/routes/sidebar-badges.js.map +1 -0
  189. package/dist/secrets/external-stub-providers.d.ts +5 -0
  190. package/dist/secrets/external-stub-providers.d.ts.map +1 -0
  191. package/dist/secrets/external-stub-providers.js +21 -0
  192. package/dist/secrets/external-stub-providers.js.map +1 -0
  193. package/dist/secrets/local-encrypted-provider.d.ts +3 -0
  194. package/dist/secrets/local-encrypted-provider.d.ts.map +1 -0
  195. package/dist/secrets/local-encrypted-provider.js +116 -0
  196. package/dist/secrets/local-encrypted-provider.js.map +1 -0
  197. package/dist/secrets/provider-registry.d.ts +5 -0
  198. package/dist/secrets/provider-registry.d.ts.map +1 -0
  199. package/dist/secrets/provider-registry.js +20 -0
  200. package/dist/secrets/provider-registry.js.map +1 -0
  201. package/dist/secrets/types.d.ts +21 -0
  202. package/dist/secrets/types.d.ts.map +1 -0
  203. package/dist/secrets/types.js +2 -0
  204. package/dist/secrets/types.js.map +1 -0
  205. package/dist/services/access.d.ts +81 -0
  206. package/dist/services/access.d.ts.map +1 -0
  207. package/dist/services/access.js +187 -0
  208. package/dist/services/access.js.map +1 -0
  209. package/dist/services/activity-log.d.ts +14 -0
  210. package/dist/services/activity-log.d.ts.map +1 -0
  211. package/dist/services/activity-log.js +32 -0
  212. package/dist/services/activity-log.js.map +1 -0
  213. package/dist/services/activity.d.ts +764 -0
  214. package/dist/services/activity.d.ts.map +1 -0
  215. package/dist/services/activity.js +105 -0
  216. package/dist/services/activity.js.map +1 -0
  217. package/dist/services/agent-permissions.d.ts +6 -0
  218. package/dist/services/agent-permissions.d.ts.map +1 -0
  219. package/dist/services/agent-permissions.js +18 -0
  220. package/dist/services/agent-permissions.js.map +1 -0
  221. package/dist/services/agents.d.ts +1494 -0
  222. package/dist/services/agents.d.ts.map +1 -0
  223. package/dist/services/agents.js +454 -0
  224. package/dist/services/agents.js.map +1 -0
  225. package/dist/services/approvals.d.ts +540 -0
  226. package/dist/services/approvals.d.ts.map +1 -0
  227. package/dist/services/approvals.js +173 -0
  228. package/dist/services/approvals.js.map +1 -0
  229. package/dist/services/assets.d.ts +33 -0
  230. package/dist/services/assets.d.ts.map +1 -0
  231. package/dist/services/assets.js +17 -0
  232. package/dist/services/assets.js.map +1 -0
  233. package/dist/services/companies.d.ts +503 -0
  234. package/dist/services/companies.d.ts.map +1 -0
  235. package/dist/services/companies.js +120 -0
  236. package/dist/services/companies.js.map +1 -0
  237. package/dist/services/company-portability.d.ts +8 -0
  238. package/dist/services/company-portability.d.ts.map +1 -0
  239. package/dist/services/company-portability.js +851 -0
  240. package/dist/services/company-portability.js.map +1 -0
  241. package/dist/services/costs.d.ts +50 -0
  242. package/dist/services/costs.d.ts.map +1 -0
  243. package/dist/services/costs.js +166 -0
  244. package/dist/services/costs.js.map +1 -0
  245. package/dist/services/dashboard.d.ts +21 -0
  246. package/dist/services/dashboard.d.ts.map +1 -0
  247. package/dist/services/dashboard.js +96 -0
  248. package/dist/services/dashboard.js.map +1 -0
  249. package/dist/services/goals.d.ts +407 -0
  250. package/dist/services/goals.d.ts.map +1 -0
  251. package/dist/services/goals.js +29 -0
  252. package/dist/services/goals.js.map +1 -0
  253. package/dist/services/heartbeat.d.ts +1666 -0
  254. package/dist/services/heartbeat.d.ts.map +1 -0
  255. package/dist/services/heartbeat.js +1752 -0
  256. package/dist/services/heartbeat.js.map +1 -0
  257. package/dist/services/index.d.ts +20 -0
  258. package/dist/services/index.d.ts.map +1 -0
  259. package/dist/services/index.js +20 -0
  260. package/dist/services/index.js.map +1 -0
  261. package/dist/services/issue-approvals.d.ts +56 -0
  262. package/dist/services/issue-approvals.d.ts.map +1 -0
  263. package/dist/services/issue-approvals.js +153 -0
  264. package/dist/services/issue-approvals.js.map +1 -0
  265. package/dist/services/issues.d.ts +756 -0
  266. package/dist/services/issues.d.ts.map +1 -0
  267. package/dist/services/issues.js +917 -0
  268. package/dist/services/issues.js.map +1 -0
  269. package/dist/services/live-events.d.ts +12 -0
  270. package/dist/services/live-events.d.ts.map +1 -0
  271. package/dist/services/live-events.js +24 -0
  272. package/dist/services/live-events.js.map +1 -0
  273. package/dist/services/projects.d.ts +66 -0
  274. package/dist/services/projects.d.ts.map +1 -0
  275. package/dist/services/projects.js +472 -0
  276. package/dist/services/projects.js.map +1 -0
  277. package/dist/services/run-log-store.d.ts +34 -0
  278. package/dist/services/run-log-store.d.ts.map +1 -0
  279. package/dist/services/run-log-store.js +112 -0
  280. package/dist/services/run-log-store.js.map +1 -0
  281. package/dist/services/secrets.d.ts +506 -0
  282. package/dist/services/secrets.d.ts.map +1 -0
  283. package/dist/services/secrets.js +284 -0
  284. package/dist/services/secrets.js.map +1 -0
  285. package/dist/services/sidebar-badges.d.ts +9 -0
  286. package/dist/services/sidebar-badges.d.ts.map +1 -0
  287. package/dist/services/sidebar-badges.js +33 -0
  288. package/dist/services/sidebar-badges.js.map +1 -0
  289. package/dist/startup-banner.d.ts +27 -0
  290. package/dist/startup-banner.d.ts.map +1 -0
  291. package/dist/startup-banner.js +112 -0
  292. package/dist/startup-banner.js.map +1 -0
  293. package/dist/storage/index.d.ts +6 -0
  294. package/dist/storage/index.d.ts.map +1 -0
  295. package/dist/storage/index.js +29 -0
  296. package/dist/storage/index.js.map +1 -0
  297. package/dist/storage/local-disk-provider.d.ts +3 -0
  298. package/dist/storage/local-disk-provider.d.ts.map +1 -0
  299. package/dist/storage/local-disk-provider.js +79 -0
  300. package/dist/storage/local-disk-provider.js.map +1 -0
  301. package/dist/storage/provider-registry.d.ts +4 -0
  302. package/dist/storage/provider-registry.d.ts.map +1 -0
  303. package/dist/storage/provider-registry.js +15 -0
  304. package/dist/storage/provider-registry.js.map +1 -0
  305. package/dist/storage/s3-provider.d.ts +11 -0
  306. package/dist/storage/s3-provider.d.ts.map +1 -0
  307. package/dist/storage/s3-provider.js +123 -0
  308. package/dist/storage/s3-provider.js.map +1 -0
  309. package/dist/storage/service.d.ts +3 -0
  310. package/dist/storage/service.d.ts.map +1 -0
  311. package/dist/storage/service.js +120 -0
  312. package/dist/storage/service.js.map +1 -0
  313. package/dist/storage/types.d.ts +55 -0
  314. package/dist/storage/types.d.ts.map +1 -0
  315. package/dist/storage/types.js +2 -0
  316. package/dist/storage/types.js.map +1 -0
  317. package/package.json +62 -0
@@ -0,0 +1,1132 @@
1
+ import { Router } from "express";
2
+ import { randomUUID } from "node:crypto";
3
+ import path from "node:path";
4
+ import { agents as agentsTable, companies, heartbeatRuns } from "@paperclipai/db";
5
+ import { and, desc, eq, inArray, not, sql } from "drizzle-orm";
6
+ import { createAgentKeySchema, createAgentHireSchema, createAgentSchema, isUuidLike, resetAgentSessionSchema, testAdapterEnvironmentSchema, updateAgentPermissionsSchema, updateAgentInstructionsPathSchema, wakeAgentSchema, updateAgentSchema, } from "@paperclipai/shared";
7
+ import { validate } from "../middleware/validate.js";
8
+ import { agentService, accessService, approvalService, heartbeatService, issueApprovalService, issueService, logActivity, secretService, } from "../services/index.js";
9
+ import { conflict, forbidden, unprocessable } from "../errors.js";
10
+ import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js";
11
+ import { findServerAdapter, listAdapterModels } from "../adapters/index.js";
12
+ import { redactEventPayload } from "../redaction.js";
13
+ import { runClaudeLogin } from "@paperclipai/adapter-claude-local/server";
14
+ import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, DEFAULT_CODEX_LOCAL_MODEL, } from "@paperclipai/adapter-codex-local";
15
+ export function agentRoutes(db) {
16
+ const DEFAULT_INSTRUCTIONS_PATH_KEYS = {
17
+ claude_local: "instructionsFilePath",
18
+ codex_local: "instructionsFilePath",
19
+ };
20
+ const KNOWN_INSTRUCTIONS_PATH_KEYS = new Set(["instructionsFilePath", "agentsMdPath"]);
21
+ const router = Router();
22
+ const svc = agentService(db);
23
+ const access = accessService(db);
24
+ const approvalsSvc = approvalService(db);
25
+ const heartbeat = heartbeatService(db);
26
+ const issueApprovalsSvc = issueApprovalService(db);
27
+ const secretsSvc = secretService(db);
28
+ const strictSecretsMode = process.env.PAPERCLIP_SECRETS_STRICT_MODE === "true";
29
+ function canCreateAgents(agent) {
30
+ if (!agent.permissions || typeof agent.permissions !== "object")
31
+ return false;
32
+ return Boolean(agent.permissions.canCreateAgents);
33
+ }
34
+ async function assertCanCreateAgentsForCompany(req, companyId) {
35
+ assertCompanyAccess(req, companyId);
36
+ if (req.actor.type === "board") {
37
+ if (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin)
38
+ return null;
39
+ const allowed = await access.canUser(companyId, req.actor.userId, "agents:create");
40
+ if (!allowed) {
41
+ throw forbidden("Missing permission: agents:create");
42
+ }
43
+ return null;
44
+ }
45
+ if (!req.actor.agentId)
46
+ throw forbidden("Agent authentication required");
47
+ const actorAgent = await svc.getById(req.actor.agentId);
48
+ if (!actorAgent || actorAgent.companyId !== companyId) {
49
+ throw forbidden("Agent key cannot access another company");
50
+ }
51
+ const allowedByGrant = await access.hasPermission(companyId, "agent", actorAgent.id, "agents:create");
52
+ if (!allowedByGrant && !canCreateAgents(actorAgent)) {
53
+ throw forbidden("Missing permission: can create agents");
54
+ }
55
+ return actorAgent;
56
+ }
57
+ async function assertCanReadConfigurations(req, companyId) {
58
+ return assertCanCreateAgentsForCompany(req, companyId);
59
+ }
60
+ async function actorCanReadConfigurationsForCompany(req, companyId) {
61
+ assertCompanyAccess(req, companyId);
62
+ if (req.actor.type === "board") {
63
+ if (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin)
64
+ return true;
65
+ return access.canUser(companyId, req.actor.userId, "agents:create");
66
+ }
67
+ if (!req.actor.agentId)
68
+ return false;
69
+ const actorAgent = await svc.getById(req.actor.agentId);
70
+ if (!actorAgent || actorAgent.companyId !== companyId)
71
+ return false;
72
+ const allowedByGrant = await access.hasPermission(companyId, "agent", actorAgent.id, "agents:create");
73
+ return allowedByGrant || canCreateAgents(actorAgent);
74
+ }
75
+ async function assertCanUpdateAgent(req, targetAgent) {
76
+ assertCompanyAccess(req, targetAgent.companyId);
77
+ if (req.actor.type === "board")
78
+ return;
79
+ if (!req.actor.agentId)
80
+ throw forbidden("Agent authentication required");
81
+ const actorAgent = await svc.getById(req.actor.agentId);
82
+ if (!actorAgent || actorAgent.companyId !== targetAgent.companyId) {
83
+ throw forbidden("Agent key cannot access another company");
84
+ }
85
+ if (actorAgent.id === targetAgent.id)
86
+ return;
87
+ if (actorAgent.role === "ceo")
88
+ return;
89
+ const allowedByGrant = await access.hasPermission(targetAgent.companyId, "agent", actorAgent.id, "agents:create");
90
+ if (allowedByGrant || canCreateAgents(actorAgent))
91
+ return;
92
+ throw forbidden("Only CEO or agent creators can modify other agents");
93
+ }
94
+ async function resolveCompanyIdForAgentReference(req) {
95
+ const companyIdQuery = req.query.companyId;
96
+ const requestedCompanyId = typeof companyIdQuery === "string" && companyIdQuery.trim().length > 0
97
+ ? companyIdQuery.trim()
98
+ : null;
99
+ if (requestedCompanyId) {
100
+ assertCompanyAccess(req, requestedCompanyId);
101
+ return requestedCompanyId;
102
+ }
103
+ if (req.actor.type === "agent" && req.actor.companyId) {
104
+ return req.actor.companyId;
105
+ }
106
+ return null;
107
+ }
108
+ async function normalizeAgentReference(req, rawId) {
109
+ const raw = rawId.trim();
110
+ if (isUuidLike(raw))
111
+ return raw;
112
+ const companyId = await resolveCompanyIdForAgentReference(req);
113
+ if (!companyId) {
114
+ throw unprocessable("Agent shortname lookup requires companyId query parameter");
115
+ }
116
+ const resolved = await svc.resolveByReference(companyId, raw);
117
+ if (resolved.ambiguous) {
118
+ throw conflict("Agent shortname is ambiguous in this company. Use the agent ID.");
119
+ }
120
+ return resolved.agent?.id ?? raw;
121
+ }
122
+ function parseSourceIssueIds(input) {
123
+ const values = [];
124
+ if (Array.isArray(input.sourceIssueIds))
125
+ values.push(...input.sourceIssueIds);
126
+ if (typeof input.sourceIssueId === "string" && input.sourceIssueId.length > 0) {
127
+ values.push(input.sourceIssueId);
128
+ }
129
+ return Array.from(new Set(values));
130
+ }
131
+ function asRecord(value) {
132
+ if (typeof value !== "object" || value === null || Array.isArray(value))
133
+ return null;
134
+ return value;
135
+ }
136
+ function asNonEmptyString(value) {
137
+ if (typeof value !== "string")
138
+ return null;
139
+ const trimmed = value.trim();
140
+ return trimmed.length > 0 ? trimmed : null;
141
+ }
142
+ function applyCreateDefaultsByAdapterType(adapterType, adapterConfig) {
143
+ if (adapterType !== "codex_local")
144
+ return adapterConfig;
145
+ const next = { ...adapterConfig };
146
+ if (!asNonEmptyString(next.model)) {
147
+ next.model = DEFAULT_CODEX_LOCAL_MODEL;
148
+ }
149
+ const hasBypassFlag = typeof next.dangerouslyBypassApprovalsAndSandbox === "boolean" ||
150
+ typeof next.dangerouslyBypassSandbox === "boolean";
151
+ if (!hasBypassFlag) {
152
+ next.dangerouslyBypassApprovalsAndSandbox = DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX;
153
+ }
154
+ return next;
155
+ }
156
+ function resolveInstructionsFilePath(candidatePath, adapterConfig) {
157
+ const trimmed = candidatePath.trim();
158
+ if (path.isAbsolute(trimmed))
159
+ return trimmed;
160
+ const cwd = asNonEmptyString(adapterConfig.cwd);
161
+ if (!cwd) {
162
+ throw unprocessable("Relative instructions path requires adapterConfig.cwd to be set to an absolute path");
163
+ }
164
+ if (!path.isAbsolute(cwd)) {
165
+ throw unprocessable("adapterConfig.cwd must be an absolute path to resolve relative instructions path");
166
+ }
167
+ return path.resolve(cwd, trimmed);
168
+ }
169
+ async function assertCanManageInstructionsPath(req, targetAgent) {
170
+ assertCompanyAccess(req, targetAgent.companyId);
171
+ if (req.actor.type === "board")
172
+ return;
173
+ if (!req.actor.agentId)
174
+ throw forbidden("Agent authentication required");
175
+ const actorAgent = await svc.getById(req.actor.agentId);
176
+ if (!actorAgent || actorAgent.companyId !== targetAgent.companyId) {
177
+ throw forbidden("Agent key cannot access another company");
178
+ }
179
+ if (actorAgent.id === targetAgent.id)
180
+ return;
181
+ const chainOfCommand = await svc.getChainOfCommand(targetAgent.id);
182
+ if (chainOfCommand.some((manager) => manager.id === actorAgent.id))
183
+ return;
184
+ throw forbidden("Only the target agent or an ancestor manager can update instructions path");
185
+ }
186
+ function summarizeAgentUpdateDetails(patch) {
187
+ const changedTopLevelKeys = Object.keys(patch).sort();
188
+ const details = { changedTopLevelKeys };
189
+ const adapterConfigPatch = asRecord(patch.adapterConfig);
190
+ if (adapterConfigPatch) {
191
+ details.changedAdapterConfigKeys = Object.keys(adapterConfigPatch).sort();
192
+ }
193
+ const runtimeConfigPatch = asRecord(patch.runtimeConfig);
194
+ if (runtimeConfigPatch) {
195
+ details.changedRuntimeConfigKeys = Object.keys(runtimeConfigPatch).sort();
196
+ }
197
+ return details;
198
+ }
199
+ function redactForRestrictedAgentView(agent) {
200
+ if (!agent)
201
+ return null;
202
+ return {
203
+ ...agent,
204
+ adapterConfig: {},
205
+ runtimeConfig: {},
206
+ };
207
+ }
208
+ function redactAgentConfiguration(agent) {
209
+ if (!agent)
210
+ return null;
211
+ return {
212
+ id: agent.id,
213
+ companyId: agent.companyId,
214
+ name: agent.name,
215
+ role: agent.role,
216
+ title: agent.title,
217
+ status: agent.status,
218
+ reportsTo: agent.reportsTo,
219
+ adapterType: agent.adapterType,
220
+ adapterConfig: redactEventPayload(agent.adapterConfig),
221
+ runtimeConfig: redactEventPayload(agent.runtimeConfig),
222
+ permissions: agent.permissions,
223
+ updatedAt: agent.updatedAt,
224
+ };
225
+ }
226
+ function redactRevisionSnapshot(snapshot) {
227
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
228
+ return {};
229
+ const record = snapshot;
230
+ return {
231
+ ...record,
232
+ adapterConfig: redactEventPayload(typeof record.adapterConfig === "object" && record.adapterConfig !== null
233
+ ? record.adapterConfig
234
+ : {}),
235
+ runtimeConfig: redactEventPayload(typeof record.runtimeConfig === "object" && record.runtimeConfig !== null
236
+ ? record.runtimeConfig
237
+ : {}),
238
+ metadata: typeof record.metadata === "object" && record.metadata !== null
239
+ ? redactEventPayload(record.metadata)
240
+ : record.metadata ?? null,
241
+ };
242
+ }
243
+ function redactConfigRevision(revision) {
244
+ return {
245
+ ...revision,
246
+ beforeConfig: redactRevisionSnapshot(revision.beforeConfig),
247
+ afterConfig: redactRevisionSnapshot(revision.afterConfig),
248
+ };
249
+ }
250
+ function toLeanOrgNode(node) {
251
+ const reports = Array.isArray(node.reports)
252
+ ? node.reports.map((report) => toLeanOrgNode(report))
253
+ : [];
254
+ return {
255
+ id: String(node.id),
256
+ name: String(node.name),
257
+ role: String(node.role),
258
+ status: String(node.status),
259
+ reports,
260
+ };
261
+ }
262
+ router.param("id", async (req, _res, next, rawId) => {
263
+ try {
264
+ req.params.id = await normalizeAgentReference(req, String(rawId));
265
+ next();
266
+ }
267
+ catch (err) {
268
+ next(err);
269
+ }
270
+ });
271
+ router.get("/adapters/:type/models", async (req, res) => {
272
+ const type = req.params.type;
273
+ const models = await listAdapterModels(type);
274
+ res.json(models);
275
+ });
276
+ router.post("/companies/:companyId/adapters/:type/test-environment", validate(testAdapterEnvironmentSchema), async (req, res) => {
277
+ const companyId = req.params.companyId;
278
+ const type = req.params.type;
279
+ await assertCanReadConfigurations(req, companyId);
280
+ const adapter = findServerAdapter(type);
281
+ if (!adapter) {
282
+ res.status(404).json({ error: `Unknown adapter type: ${type}` });
283
+ return;
284
+ }
285
+ const inputAdapterConfig = (req.body?.adapterConfig ?? {});
286
+ const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(companyId, inputAdapterConfig, { strictMode: strictSecretsMode });
287
+ const runtimeAdapterConfig = await secretsSvc.resolveAdapterConfigForRuntime(companyId, normalizedAdapterConfig);
288
+ const result = await adapter.testEnvironment({
289
+ companyId,
290
+ adapterType: type,
291
+ config: runtimeAdapterConfig,
292
+ });
293
+ res.json(result);
294
+ });
295
+ router.get("/companies/:companyId/agents", async (req, res) => {
296
+ const companyId = req.params.companyId;
297
+ assertCompanyAccess(req, companyId);
298
+ const result = await svc.list(companyId);
299
+ const canReadConfigs = await actorCanReadConfigurationsForCompany(req, companyId);
300
+ if (canReadConfigs || req.actor.type === "board") {
301
+ res.json(result);
302
+ return;
303
+ }
304
+ res.json(result.map((agent) => redactForRestrictedAgentView(agent)));
305
+ });
306
+ router.get("/companies/:companyId/org", async (req, res) => {
307
+ const companyId = req.params.companyId;
308
+ assertCompanyAccess(req, companyId);
309
+ const tree = await svc.orgForCompany(companyId);
310
+ const leanTree = tree.map((node) => toLeanOrgNode(node));
311
+ res.json(leanTree);
312
+ });
313
+ router.get("/companies/:companyId/agent-configurations", async (req, res) => {
314
+ const companyId = req.params.companyId;
315
+ await assertCanReadConfigurations(req, companyId);
316
+ const rows = await svc.list(companyId);
317
+ res.json(rows.map((row) => redactAgentConfiguration(row)));
318
+ });
319
+ router.get("/agents/me", async (req, res) => {
320
+ if (req.actor.type !== "agent" || !req.actor.agentId) {
321
+ res.status(401).json({ error: "Agent authentication required" });
322
+ return;
323
+ }
324
+ const agent = await svc.getById(req.actor.agentId);
325
+ if (!agent) {
326
+ res.status(404).json({ error: "Agent not found" });
327
+ return;
328
+ }
329
+ const chainOfCommand = await svc.getChainOfCommand(agent.id);
330
+ res.json({ ...agent, chainOfCommand });
331
+ });
332
+ router.get("/agents/:id", async (req, res) => {
333
+ const id = req.params.id;
334
+ const agent = await svc.getById(id);
335
+ if (!agent) {
336
+ res.status(404).json({ error: "Agent not found" });
337
+ return;
338
+ }
339
+ assertCompanyAccess(req, agent.companyId);
340
+ if (req.actor.type === "agent" && req.actor.agentId !== id) {
341
+ const canRead = await actorCanReadConfigurationsForCompany(req, agent.companyId);
342
+ if (!canRead) {
343
+ const chainOfCommand = await svc.getChainOfCommand(agent.id);
344
+ res.json({ ...redactForRestrictedAgentView(agent), chainOfCommand });
345
+ return;
346
+ }
347
+ }
348
+ const chainOfCommand = await svc.getChainOfCommand(agent.id);
349
+ res.json({ ...agent, chainOfCommand });
350
+ });
351
+ router.get("/agents/:id/configuration", async (req, res) => {
352
+ const id = req.params.id;
353
+ const agent = await svc.getById(id);
354
+ if (!agent) {
355
+ res.status(404).json({ error: "Agent not found" });
356
+ return;
357
+ }
358
+ await assertCanReadConfigurations(req, agent.companyId);
359
+ res.json(redactAgentConfiguration(agent));
360
+ });
361
+ router.get("/agents/:id/config-revisions", async (req, res) => {
362
+ const id = req.params.id;
363
+ const agent = await svc.getById(id);
364
+ if (!agent) {
365
+ res.status(404).json({ error: "Agent not found" });
366
+ return;
367
+ }
368
+ await assertCanReadConfigurations(req, agent.companyId);
369
+ const revisions = await svc.listConfigRevisions(id);
370
+ res.json(revisions.map((revision) => redactConfigRevision(revision)));
371
+ });
372
+ router.get("/agents/:id/config-revisions/:revisionId", async (req, res) => {
373
+ const id = req.params.id;
374
+ const revisionId = req.params.revisionId;
375
+ const agent = await svc.getById(id);
376
+ if (!agent) {
377
+ res.status(404).json({ error: "Agent not found" });
378
+ return;
379
+ }
380
+ await assertCanReadConfigurations(req, agent.companyId);
381
+ const revision = await svc.getConfigRevision(id, revisionId);
382
+ if (!revision) {
383
+ res.status(404).json({ error: "Revision not found" });
384
+ return;
385
+ }
386
+ res.json(redactConfigRevision(revision));
387
+ });
388
+ router.post("/agents/:id/config-revisions/:revisionId/rollback", async (req, res) => {
389
+ const id = req.params.id;
390
+ const revisionId = req.params.revisionId;
391
+ const existing = await svc.getById(id);
392
+ if (!existing) {
393
+ res.status(404).json({ error: "Agent not found" });
394
+ return;
395
+ }
396
+ await assertCanUpdateAgent(req, existing);
397
+ const actor = getActorInfo(req);
398
+ const updated = await svc.rollbackConfigRevision(id, revisionId, {
399
+ agentId: actor.agentId,
400
+ userId: actor.actorType === "user" ? actor.actorId : null,
401
+ });
402
+ if (!updated) {
403
+ res.status(404).json({ error: "Revision not found" });
404
+ return;
405
+ }
406
+ await logActivity(db, {
407
+ companyId: updated.companyId,
408
+ actorType: actor.actorType,
409
+ actorId: actor.actorId,
410
+ agentId: actor.agentId,
411
+ runId: actor.runId,
412
+ action: "agent.config_rolled_back",
413
+ entityType: "agent",
414
+ entityId: updated.id,
415
+ details: { revisionId },
416
+ });
417
+ res.json(updated);
418
+ });
419
+ router.get("/agents/:id/runtime-state", async (req, res) => {
420
+ assertBoard(req);
421
+ const id = req.params.id;
422
+ const agent = await svc.getById(id);
423
+ if (!agent) {
424
+ res.status(404).json({ error: "Agent not found" });
425
+ return;
426
+ }
427
+ assertCompanyAccess(req, agent.companyId);
428
+ const state = await heartbeat.getRuntimeState(id);
429
+ res.json(state);
430
+ });
431
+ router.get("/agents/:id/task-sessions", async (req, res) => {
432
+ assertBoard(req);
433
+ const id = req.params.id;
434
+ const agent = await svc.getById(id);
435
+ if (!agent) {
436
+ res.status(404).json({ error: "Agent not found" });
437
+ return;
438
+ }
439
+ assertCompanyAccess(req, agent.companyId);
440
+ const sessions = await heartbeat.listTaskSessions(id);
441
+ res.json(sessions.map((session) => ({
442
+ ...session,
443
+ sessionParamsJson: redactEventPayload(session.sessionParamsJson ?? null),
444
+ })));
445
+ });
446
+ router.post("/agents/:id/runtime-state/reset-session", validate(resetAgentSessionSchema), async (req, res) => {
447
+ assertBoard(req);
448
+ const id = req.params.id;
449
+ const agent = await svc.getById(id);
450
+ if (!agent) {
451
+ res.status(404).json({ error: "Agent not found" });
452
+ return;
453
+ }
454
+ assertCompanyAccess(req, agent.companyId);
455
+ const taskKey = typeof req.body.taskKey === "string" && req.body.taskKey.trim().length > 0
456
+ ? req.body.taskKey.trim()
457
+ : null;
458
+ const state = await heartbeat.resetRuntimeSession(id, { taskKey });
459
+ await logActivity(db, {
460
+ companyId: agent.companyId,
461
+ actorType: "user",
462
+ actorId: req.actor.userId ?? "board",
463
+ action: "agent.runtime_session_reset",
464
+ entityType: "agent",
465
+ entityId: id,
466
+ details: { taskKey: taskKey ?? null },
467
+ });
468
+ res.json(state);
469
+ });
470
+ router.post("/companies/:companyId/agent-hires", validate(createAgentHireSchema), async (req, res) => {
471
+ const companyId = req.params.companyId;
472
+ await assertCanCreateAgentsForCompany(req, companyId);
473
+ const sourceIssueIds = parseSourceIssueIds(req.body);
474
+ const { sourceIssueId: _sourceIssueId, sourceIssueIds: _sourceIssueIds, ...hireInput } = req.body;
475
+ const requestedAdapterConfig = applyCreateDefaultsByAdapterType(hireInput.adapterType, (hireInput.adapterConfig ?? {}));
476
+ const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(companyId, requestedAdapterConfig, { strictMode: strictSecretsMode });
477
+ const normalizedHireInput = {
478
+ ...hireInput,
479
+ adapterConfig: normalizedAdapterConfig,
480
+ };
481
+ const company = await db
482
+ .select()
483
+ .from(companies)
484
+ .where(eq(companies.id, companyId))
485
+ .then((rows) => rows[0] ?? null);
486
+ if (!company) {
487
+ res.status(404).json({ error: "Company not found" });
488
+ return;
489
+ }
490
+ const requiresApproval = company.requireBoardApprovalForNewAgents;
491
+ const status = requiresApproval ? "pending_approval" : "idle";
492
+ const agent = await svc.create(companyId, {
493
+ ...normalizedHireInput,
494
+ status,
495
+ spentMonthlyCents: 0,
496
+ lastHeartbeatAt: null,
497
+ });
498
+ let approval = null;
499
+ const actor = getActorInfo(req);
500
+ if (requiresApproval) {
501
+ const requestedAdapterType = normalizedHireInput.adapterType ?? agent.adapterType;
502
+ const requestedAdapterConfig = redactEventPayload((normalizedHireInput.adapterConfig ?? agent.adapterConfig)) ?? {};
503
+ const requestedRuntimeConfig = redactEventPayload((normalizedHireInput.runtimeConfig ?? agent.runtimeConfig)) ?? {};
504
+ const requestedMetadata = redactEventPayload((normalizedHireInput.metadata ?? agent.metadata ?? {})) ?? {};
505
+ approval = await approvalsSvc.create(companyId, {
506
+ type: "hire_agent",
507
+ requestedByAgentId: actor.actorType === "agent" ? actor.actorId : null,
508
+ requestedByUserId: actor.actorType === "user" ? actor.actorId : null,
509
+ status: "pending",
510
+ payload: {
511
+ name: normalizedHireInput.name,
512
+ role: normalizedHireInput.role,
513
+ title: normalizedHireInput.title ?? null,
514
+ icon: normalizedHireInput.icon ?? null,
515
+ reportsTo: normalizedHireInput.reportsTo ?? null,
516
+ capabilities: normalizedHireInput.capabilities ?? null,
517
+ adapterType: requestedAdapterType,
518
+ adapterConfig: requestedAdapterConfig,
519
+ runtimeConfig: requestedRuntimeConfig,
520
+ budgetMonthlyCents: typeof normalizedHireInput.budgetMonthlyCents === "number"
521
+ ? normalizedHireInput.budgetMonthlyCents
522
+ : agent.budgetMonthlyCents,
523
+ metadata: requestedMetadata,
524
+ agentId: agent.id,
525
+ requestedByAgentId: actor.actorType === "agent" ? actor.actorId : null,
526
+ requestedConfigurationSnapshot: {
527
+ adapterType: requestedAdapterType,
528
+ adapterConfig: requestedAdapterConfig,
529
+ runtimeConfig: requestedRuntimeConfig,
530
+ },
531
+ },
532
+ decisionNote: null,
533
+ decidedByUserId: null,
534
+ decidedAt: null,
535
+ updatedAt: new Date(),
536
+ });
537
+ if (sourceIssueIds.length > 0) {
538
+ await issueApprovalsSvc.linkManyForApproval(approval.id, sourceIssueIds, {
539
+ agentId: actor.actorType === "agent" ? actor.actorId : null,
540
+ userId: actor.actorType === "user" ? actor.actorId : null,
541
+ });
542
+ }
543
+ }
544
+ await logActivity(db, {
545
+ companyId,
546
+ actorType: actor.actorType,
547
+ actorId: actor.actorId,
548
+ agentId: actor.agentId,
549
+ runId: actor.runId,
550
+ action: "agent.hire_created",
551
+ entityType: "agent",
552
+ entityId: agent.id,
553
+ details: {
554
+ name: agent.name,
555
+ role: agent.role,
556
+ requiresApproval,
557
+ approvalId: approval?.id ?? null,
558
+ issueIds: sourceIssueIds,
559
+ },
560
+ });
561
+ if (approval) {
562
+ await logActivity(db, {
563
+ companyId,
564
+ actorType: actor.actorType,
565
+ actorId: actor.actorId,
566
+ agentId: actor.agentId,
567
+ runId: actor.runId,
568
+ action: "approval.created",
569
+ entityType: "approval",
570
+ entityId: approval.id,
571
+ details: { type: approval.type, linkedAgentId: agent.id },
572
+ });
573
+ }
574
+ res.status(201).json({ agent, approval });
575
+ });
576
+ router.post("/companies/:companyId/agents", validate(createAgentSchema), async (req, res) => {
577
+ const companyId = req.params.companyId;
578
+ assertCompanyAccess(req, companyId);
579
+ if (req.actor.type === "agent") {
580
+ assertBoard(req);
581
+ }
582
+ const requestedAdapterConfig = applyCreateDefaultsByAdapterType(req.body.adapterType, (req.body.adapterConfig ?? {}));
583
+ const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(companyId, requestedAdapterConfig, { strictMode: strictSecretsMode });
584
+ const agent = await svc.create(companyId, {
585
+ ...req.body,
586
+ adapterConfig: normalizedAdapterConfig,
587
+ status: "idle",
588
+ spentMonthlyCents: 0,
589
+ lastHeartbeatAt: null,
590
+ });
591
+ const actor = getActorInfo(req);
592
+ await logActivity(db, {
593
+ companyId,
594
+ actorType: actor.actorType,
595
+ actorId: actor.actorId,
596
+ agentId: actor.agentId,
597
+ runId: actor.runId,
598
+ action: "agent.created",
599
+ entityType: "agent",
600
+ entityId: agent.id,
601
+ details: { name: agent.name, role: agent.role },
602
+ });
603
+ res.status(201).json(agent);
604
+ });
605
+ router.patch("/agents/:id/permissions", validate(updateAgentPermissionsSchema), async (req, res) => {
606
+ const id = req.params.id;
607
+ const existing = await svc.getById(id);
608
+ if (!existing) {
609
+ res.status(404).json({ error: "Agent not found" });
610
+ return;
611
+ }
612
+ assertCompanyAccess(req, existing.companyId);
613
+ if (req.actor.type === "agent") {
614
+ const actorAgent = req.actor.agentId ? await svc.getById(req.actor.agentId) : null;
615
+ if (!actorAgent || actorAgent.companyId !== existing.companyId) {
616
+ res.status(403).json({ error: "Forbidden" });
617
+ return;
618
+ }
619
+ if (actorAgent.role !== "ceo") {
620
+ res.status(403).json({ error: "Only CEO can manage permissions" });
621
+ return;
622
+ }
623
+ }
624
+ const agent = await svc.updatePermissions(id, req.body);
625
+ if (!agent) {
626
+ res.status(404).json({ error: "Agent not found" });
627
+ return;
628
+ }
629
+ const actor = getActorInfo(req);
630
+ await logActivity(db, {
631
+ companyId: agent.companyId,
632
+ actorType: actor.actorType,
633
+ actorId: actor.actorId,
634
+ agentId: actor.agentId,
635
+ runId: actor.runId,
636
+ action: "agent.permissions_updated",
637
+ entityType: "agent",
638
+ entityId: agent.id,
639
+ details: req.body,
640
+ });
641
+ res.json(agent);
642
+ });
643
+ router.patch("/agents/:id/instructions-path", validate(updateAgentInstructionsPathSchema), async (req, res) => {
644
+ const id = req.params.id;
645
+ const existing = await svc.getById(id);
646
+ if (!existing) {
647
+ res.status(404).json({ error: "Agent not found" });
648
+ return;
649
+ }
650
+ await assertCanManageInstructionsPath(req, existing);
651
+ const existingAdapterConfig = asRecord(existing.adapterConfig) ?? {};
652
+ const explicitKey = asNonEmptyString(req.body.adapterConfigKey);
653
+ const defaultKey = DEFAULT_INSTRUCTIONS_PATH_KEYS[existing.adapterType] ?? null;
654
+ const adapterConfigKey = explicitKey ?? defaultKey;
655
+ if (!adapterConfigKey) {
656
+ res.status(422).json({
657
+ error: `No default instructions path key for adapter type '${existing.adapterType}'. Provide adapterConfigKey.`,
658
+ });
659
+ return;
660
+ }
661
+ const nextAdapterConfig = { ...existingAdapterConfig };
662
+ if (req.body.path === null) {
663
+ delete nextAdapterConfig[adapterConfigKey];
664
+ }
665
+ else {
666
+ nextAdapterConfig[adapterConfigKey] = resolveInstructionsFilePath(req.body.path, existingAdapterConfig);
667
+ }
668
+ const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(existing.companyId, nextAdapterConfig, { strictMode: strictSecretsMode });
669
+ const actor = getActorInfo(req);
670
+ const agent = await svc.update(id, { adapterConfig: normalizedAdapterConfig }, {
671
+ recordRevision: {
672
+ createdByAgentId: actor.agentId,
673
+ createdByUserId: actor.actorType === "user" ? actor.actorId : null,
674
+ source: "instructions_path_patch",
675
+ },
676
+ });
677
+ if (!agent) {
678
+ res.status(404).json({ error: "Agent not found" });
679
+ return;
680
+ }
681
+ const updatedAdapterConfig = asRecord(agent.adapterConfig) ?? {};
682
+ const pathValue = asNonEmptyString(updatedAdapterConfig[adapterConfigKey]);
683
+ await logActivity(db, {
684
+ companyId: agent.companyId,
685
+ actorType: actor.actorType,
686
+ actorId: actor.actorId,
687
+ agentId: actor.agentId,
688
+ runId: actor.runId,
689
+ action: "agent.instructions_path_updated",
690
+ entityType: "agent",
691
+ entityId: agent.id,
692
+ details: {
693
+ adapterConfigKey,
694
+ path: pathValue,
695
+ cleared: req.body.path === null,
696
+ },
697
+ });
698
+ res.json({
699
+ agentId: agent.id,
700
+ adapterType: agent.adapterType,
701
+ adapterConfigKey,
702
+ path: pathValue,
703
+ });
704
+ });
705
+ router.patch("/agents/:id", validate(updateAgentSchema), async (req, res) => {
706
+ const id = req.params.id;
707
+ const existing = await svc.getById(id);
708
+ if (!existing) {
709
+ res.status(404).json({ error: "Agent not found" });
710
+ return;
711
+ }
712
+ await assertCanUpdateAgent(req, existing);
713
+ if (Object.prototype.hasOwnProperty.call(req.body, "permissions")) {
714
+ res.status(422).json({ error: "Use /api/agents/:id/permissions for permission changes" });
715
+ return;
716
+ }
717
+ const patchData = { ...req.body };
718
+ if (Object.prototype.hasOwnProperty.call(patchData, "adapterConfig")) {
719
+ const adapterConfig = asRecord(patchData.adapterConfig);
720
+ if (!adapterConfig) {
721
+ res.status(422).json({ error: "adapterConfig must be an object" });
722
+ return;
723
+ }
724
+ const changingInstructionsPath = Object.keys(adapterConfig).some((key) => KNOWN_INSTRUCTIONS_PATH_KEYS.has(key));
725
+ if (changingInstructionsPath) {
726
+ await assertCanManageInstructionsPath(req, existing);
727
+ }
728
+ patchData.adapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(existing.companyId, adapterConfig, { strictMode: strictSecretsMode });
729
+ }
730
+ const actor = getActorInfo(req);
731
+ const agent = await svc.update(id, patchData, {
732
+ recordRevision: {
733
+ createdByAgentId: actor.agentId,
734
+ createdByUserId: actor.actorType === "user" ? actor.actorId : null,
735
+ source: "patch",
736
+ },
737
+ });
738
+ if (!agent) {
739
+ res.status(404).json({ error: "Agent not found" });
740
+ return;
741
+ }
742
+ await logActivity(db, {
743
+ companyId: agent.companyId,
744
+ actorType: actor.actorType,
745
+ actorId: actor.actorId,
746
+ agentId: actor.agentId,
747
+ runId: actor.runId,
748
+ action: "agent.updated",
749
+ entityType: "agent",
750
+ entityId: agent.id,
751
+ details: summarizeAgentUpdateDetails(patchData),
752
+ });
753
+ res.json(agent);
754
+ });
755
+ router.post("/agents/:id/pause", async (req, res) => {
756
+ assertBoard(req);
757
+ const id = req.params.id;
758
+ const agent = await svc.pause(id);
759
+ if (!agent) {
760
+ res.status(404).json({ error: "Agent not found" });
761
+ return;
762
+ }
763
+ await heartbeat.cancelActiveForAgent(id);
764
+ await logActivity(db, {
765
+ companyId: agent.companyId,
766
+ actorType: "user",
767
+ actorId: req.actor.userId ?? "board",
768
+ action: "agent.paused",
769
+ entityType: "agent",
770
+ entityId: agent.id,
771
+ });
772
+ res.json(agent);
773
+ });
774
+ router.post("/agents/:id/resume", async (req, res) => {
775
+ assertBoard(req);
776
+ const id = req.params.id;
777
+ const agent = await svc.resume(id);
778
+ if (!agent) {
779
+ res.status(404).json({ error: "Agent not found" });
780
+ return;
781
+ }
782
+ await logActivity(db, {
783
+ companyId: agent.companyId,
784
+ actorType: "user",
785
+ actorId: req.actor.userId ?? "board",
786
+ action: "agent.resumed",
787
+ entityType: "agent",
788
+ entityId: agent.id,
789
+ });
790
+ res.json(agent);
791
+ });
792
+ router.post("/agents/:id/terminate", async (req, res) => {
793
+ assertBoard(req);
794
+ const id = req.params.id;
795
+ const agent = await svc.terminate(id);
796
+ if (!agent) {
797
+ res.status(404).json({ error: "Agent not found" });
798
+ return;
799
+ }
800
+ await heartbeat.cancelActiveForAgent(id);
801
+ await logActivity(db, {
802
+ companyId: agent.companyId,
803
+ actorType: "user",
804
+ actorId: req.actor.userId ?? "board",
805
+ action: "agent.terminated",
806
+ entityType: "agent",
807
+ entityId: agent.id,
808
+ });
809
+ res.json(agent);
810
+ });
811
+ router.delete("/agents/:id", async (req, res) => {
812
+ assertBoard(req);
813
+ const id = req.params.id;
814
+ const agent = await svc.remove(id);
815
+ if (!agent) {
816
+ res.status(404).json({ error: "Agent not found" });
817
+ return;
818
+ }
819
+ await logActivity(db, {
820
+ companyId: agent.companyId,
821
+ actorType: "user",
822
+ actorId: req.actor.userId ?? "board",
823
+ action: "agent.deleted",
824
+ entityType: "agent",
825
+ entityId: agent.id,
826
+ });
827
+ res.json({ ok: true });
828
+ });
829
+ router.get("/agents/:id/keys", async (req, res) => {
830
+ assertBoard(req);
831
+ const id = req.params.id;
832
+ const keys = await svc.listKeys(id);
833
+ res.json(keys);
834
+ });
835
+ router.post("/agents/:id/keys", validate(createAgentKeySchema), async (req, res) => {
836
+ assertBoard(req);
837
+ const id = req.params.id;
838
+ const key = await svc.createApiKey(id, req.body.name);
839
+ const agent = await svc.getById(id);
840
+ if (agent) {
841
+ await logActivity(db, {
842
+ companyId: agent.companyId,
843
+ actorType: "user",
844
+ actorId: req.actor.userId ?? "board",
845
+ action: "agent.key_created",
846
+ entityType: "agent",
847
+ entityId: agent.id,
848
+ details: { keyId: key.id, name: key.name },
849
+ });
850
+ }
851
+ res.status(201).json(key);
852
+ });
853
+ router.delete("/agents/:id/keys/:keyId", async (req, res) => {
854
+ assertBoard(req);
855
+ const keyId = req.params.keyId;
856
+ const revoked = await svc.revokeKey(keyId);
857
+ if (!revoked) {
858
+ res.status(404).json({ error: "Key not found" });
859
+ return;
860
+ }
861
+ res.json({ ok: true });
862
+ });
863
+ router.post("/agents/:id/wakeup", validate(wakeAgentSchema), async (req, res) => {
864
+ const id = req.params.id;
865
+ const agent = await svc.getById(id);
866
+ if (!agent) {
867
+ res.status(404).json({ error: "Agent not found" });
868
+ return;
869
+ }
870
+ assertCompanyAccess(req, agent.companyId);
871
+ if (req.actor.type === "agent" && req.actor.agentId !== id) {
872
+ res.status(403).json({ error: "Agent can only invoke itself" });
873
+ return;
874
+ }
875
+ const run = await heartbeat.wakeup(id, {
876
+ source: req.body.source,
877
+ triggerDetail: req.body.triggerDetail ?? "manual",
878
+ reason: req.body.reason ?? null,
879
+ payload: req.body.payload ?? null,
880
+ idempotencyKey: req.body.idempotencyKey ?? null,
881
+ requestedByActorType: req.actor.type === "agent" ? "agent" : "user",
882
+ requestedByActorId: req.actor.type === "agent" ? req.actor.agentId ?? null : req.actor.userId ?? null,
883
+ contextSnapshot: {
884
+ triggeredBy: req.actor.type,
885
+ actorId: req.actor.type === "agent" ? req.actor.agentId : req.actor.userId,
886
+ },
887
+ });
888
+ if (!run) {
889
+ res.status(202).json({ status: "skipped" });
890
+ return;
891
+ }
892
+ const actor = getActorInfo(req);
893
+ await logActivity(db, {
894
+ companyId: agent.companyId,
895
+ actorType: actor.actorType,
896
+ actorId: actor.actorId,
897
+ agentId: actor.agentId,
898
+ runId: actor.runId,
899
+ action: "heartbeat.invoked",
900
+ entityType: "heartbeat_run",
901
+ entityId: run.id,
902
+ details: { agentId: id },
903
+ });
904
+ res.status(202).json(run);
905
+ });
906
+ router.post("/agents/:id/heartbeat/invoke", async (req, res) => {
907
+ const id = req.params.id;
908
+ const agent = await svc.getById(id);
909
+ if (!agent) {
910
+ res.status(404).json({ error: "Agent not found" });
911
+ return;
912
+ }
913
+ assertCompanyAccess(req, agent.companyId);
914
+ if (req.actor.type === "agent" && req.actor.agentId !== id) {
915
+ res.status(403).json({ error: "Agent can only invoke itself" });
916
+ return;
917
+ }
918
+ const run = await heartbeat.invoke(id, "on_demand", {
919
+ triggeredBy: req.actor.type,
920
+ actorId: req.actor.type === "agent" ? req.actor.agentId : req.actor.userId,
921
+ }, "manual", {
922
+ actorType: req.actor.type === "agent" ? "agent" : "user",
923
+ actorId: req.actor.type === "agent" ? req.actor.agentId ?? null : req.actor.userId ?? null,
924
+ });
925
+ if (!run) {
926
+ res.status(202).json({ status: "skipped" });
927
+ return;
928
+ }
929
+ const actor = getActorInfo(req);
930
+ await logActivity(db, {
931
+ companyId: agent.companyId,
932
+ actorType: actor.actorType,
933
+ actorId: actor.actorId,
934
+ agentId: actor.agentId,
935
+ runId: actor.runId,
936
+ action: "heartbeat.invoked",
937
+ entityType: "heartbeat_run",
938
+ entityId: run.id,
939
+ details: { agentId: id },
940
+ });
941
+ res.status(202).json(run);
942
+ });
943
+ router.post("/agents/:id/claude-login", async (req, res) => {
944
+ assertBoard(req);
945
+ const id = req.params.id;
946
+ const agent = await svc.getById(id);
947
+ if (!agent) {
948
+ res.status(404).json({ error: "Agent not found" });
949
+ return;
950
+ }
951
+ assertCompanyAccess(req, agent.companyId);
952
+ if (agent.adapterType !== "claude_local") {
953
+ res.status(400).json({ error: "Login is only supported for claude_local agents" });
954
+ return;
955
+ }
956
+ const config = asRecord(agent.adapterConfig) ?? {};
957
+ const runtimeConfig = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, config);
958
+ const result = await runClaudeLogin({
959
+ runId: `claude-login-${randomUUID()}`,
960
+ agent: {
961
+ id: agent.id,
962
+ companyId: agent.companyId,
963
+ name: agent.name,
964
+ adapterType: agent.adapterType,
965
+ adapterConfig: agent.adapterConfig,
966
+ },
967
+ config: runtimeConfig,
968
+ });
969
+ res.json(result);
970
+ });
971
+ router.get("/companies/:companyId/heartbeat-runs", async (req, res) => {
972
+ const companyId = req.params.companyId;
973
+ assertCompanyAccess(req, companyId);
974
+ const agentId = req.query.agentId;
975
+ const limitParam = req.query.limit;
976
+ const limit = limitParam ? Math.max(1, Math.min(1000, parseInt(limitParam, 10) || 200)) : undefined;
977
+ const runs = await heartbeat.list(companyId, agentId, limit);
978
+ res.json(runs);
979
+ });
980
+ router.get("/companies/:companyId/live-runs", async (req, res) => {
981
+ const companyId = req.params.companyId;
982
+ assertCompanyAccess(req, companyId);
983
+ const minCountParam = req.query.minCount;
984
+ const minCount = minCountParam ? Math.max(0, Math.min(20, parseInt(minCountParam, 10) || 0)) : 0;
985
+ const columns = {
986
+ id: heartbeatRuns.id,
987
+ status: heartbeatRuns.status,
988
+ invocationSource: heartbeatRuns.invocationSource,
989
+ triggerDetail: heartbeatRuns.triggerDetail,
990
+ startedAt: heartbeatRuns.startedAt,
991
+ finishedAt: heartbeatRuns.finishedAt,
992
+ createdAt: heartbeatRuns.createdAt,
993
+ agentId: heartbeatRuns.agentId,
994
+ agentName: agentsTable.name,
995
+ adapterType: agentsTable.adapterType,
996
+ issueId: sql `${heartbeatRuns.contextSnapshot} ->> 'issueId'`.as("issueId"),
997
+ };
998
+ const liveRuns = await db
999
+ .select(columns)
1000
+ .from(heartbeatRuns)
1001
+ .innerJoin(agentsTable, eq(heartbeatRuns.agentId, agentsTable.id))
1002
+ .where(and(eq(heartbeatRuns.companyId, companyId), inArray(heartbeatRuns.status, ["queued", "running"])))
1003
+ .orderBy(desc(heartbeatRuns.createdAt));
1004
+ if (minCount > 0 && liveRuns.length < minCount) {
1005
+ const activeIds = liveRuns.map((r) => r.id);
1006
+ const recentRuns = await db
1007
+ .select(columns)
1008
+ .from(heartbeatRuns)
1009
+ .innerJoin(agentsTable, eq(heartbeatRuns.agentId, agentsTable.id))
1010
+ .where(and(eq(heartbeatRuns.companyId, companyId), not(inArray(heartbeatRuns.status, ["queued", "running"])), ...(activeIds.length > 0 ? [not(inArray(heartbeatRuns.id, activeIds))] : [])))
1011
+ .orderBy(desc(heartbeatRuns.createdAt))
1012
+ .limit(minCount - liveRuns.length);
1013
+ res.json([...liveRuns, ...recentRuns]);
1014
+ return;
1015
+ }
1016
+ res.json(liveRuns);
1017
+ });
1018
+ router.post("/heartbeat-runs/:runId/cancel", async (req, res) => {
1019
+ assertBoard(req);
1020
+ const runId = req.params.runId;
1021
+ const run = await heartbeat.cancelRun(runId);
1022
+ if (run) {
1023
+ await logActivity(db, {
1024
+ companyId: run.companyId,
1025
+ actorType: "user",
1026
+ actorId: req.actor.userId ?? "board",
1027
+ action: "heartbeat.cancelled",
1028
+ entityType: "heartbeat_run",
1029
+ entityId: run.id,
1030
+ details: { agentId: run.agentId },
1031
+ });
1032
+ }
1033
+ res.json(run);
1034
+ });
1035
+ router.get("/heartbeat-runs/:runId/events", async (req, res) => {
1036
+ const runId = req.params.runId;
1037
+ const run = await heartbeat.getRun(runId);
1038
+ if (!run) {
1039
+ res.status(404).json({ error: "Heartbeat run not found" });
1040
+ return;
1041
+ }
1042
+ assertCompanyAccess(req, run.companyId);
1043
+ const afterSeq = Number(req.query.afterSeq ?? 0);
1044
+ const limit = Number(req.query.limit ?? 200);
1045
+ const events = await heartbeat.listEvents(runId, Number.isFinite(afterSeq) ? afterSeq : 0, Number.isFinite(limit) ? limit : 200);
1046
+ const redactedEvents = events.map((event) => ({
1047
+ ...event,
1048
+ payload: redactEventPayload(event.payload),
1049
+ }));
1050
+ res.json(redactedEvents);
1051
+ });
1052
+ router.get("/heartbeat-runs/:runId/log", async (req, res) => {
1053
+ const runId = req.params.runId;
1054
+ const run = await heartbeat.getRun(runId);
1055
+ if (!run) {
1056
+ res.status(404).json({ error: "Heartbeat run not found" });
1057
+ return;
1058
+ }
1059
+ assertCompanyAccess(req, run.companyId);
1060
+ const offset = Number(req.query.offset ?? 0);
1061
+ const limitBytes = Number(req.query.limitBytes ?? 256000);
1062
+ const result = await heartbeat.readLog(runId, {
1063
+ offset: Number.isFinite(offset) ? offset : 0,
1064
+ limitBytes: Number.isFinite(limitBytes) ? limitBytes : 256000,
1065
+ });
1066
+ res.json(result);
1067
+ });
1068
+ router.get("/issues/:issueId/live-runs", async (req, res) => {
1069
+ const rawId = req.params.issueId;
1070
+ const issueSvc = issueService(db);
1071
+ const isIdentifier = /^[A-Z]+-\d+$/i.test(rawId);
1072
+ const issue = isIdentifier ? await issueSvc.getByIdentifier(rawId) : await issueSvc.getById(rawId);
1073
+ if (!issue) {
1074
+ res.status(404).json({ error: "Issue not found" });
1075
+ return;
1076
+ }
1077
+ assertCompanyAccess(req, issue.companyId);
1078
+ const liveRuns = await db
1079
+ .select({
1080
+ id: heartbeatRuns.id,
1081
+ status: heartbeatRuns.status,
1082
+ invocationSource: heartbeatRuns.invocationSource,
1083
+ triggerDetail: heartbeatRuns.triggerDetail,
1084
+ startedAt: heartbeatRuns.startedAt,
1085
+ finishedAt: heartbeatRuns.finishedAt,
1086
+ createdAt: heartbeatRuns.createdAt,
1087
+ agentId: heartbeatRuns.agentId,
1088
+ agentName: agentsTable.name,
1089
+ adapterType: agentsTable.adapterType,
1090
+ })
1091
+ .from(heartbeatRuns)
1092
+ .innerJoin(agentsTable, eq(heartbeatRuns.agentId, agentsTable.id))
1093
+ .where(and(eq(heartbeatRuns.companyId, issue.companyId), inArray(heartbeatRuns.status, ["queued", "running"]), sql `${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issue.id}`))
1094
+ .orderBy(desc(heartbeatRuns.createdAt));
1095
+ res.json(liveRuns);
1096
+ });
1097
+ router.get("/issues/:issueId/active-run", async (req, res) => {
1098
+ const rawId = req.params.issueId;
1099
+ const issueSvc = issueService(db);
1100
+ const isIdentifier = /^[A-Z]+-\d+$/i.test(rawId);
1101
+ const issue = isIdentifier ? await issueSvc.getByIdentifier(rawId) : await issueSvc.getById(rawId);
1102
+ if (!issue) {
1103
+ res.status(404).json({ error: "Issue not found" });
1104
+ return;
1105
+ }
1106
+ assertCompanyAccess(req, issue.companyId);
1107
+ let run = issue.executionRunId ? await heartbeat.getRun(issue.executionRunId) : null;
1108
+ if (run && run.status !== "queued" && run.status !== "running") {
1109
+ run = null;
1110
+ }
1111
+ if (!run && issue.assigneeAgentId && issue.status === "in_progress") {
1112
+ run = await heartbeat.getActiveRunForAgent(issue.assigneeAgentId);
1113
+ }
1114
+ if (!run) {
1115
+ res.json(null);
1116
+ return;
1117
+ }
1118
+ const agent = await svc.getById(run.agentId);
1119
+ if (!agent) {
1120
+ res.json(null);
1121
+ return;
1122
+ }
1123
+ res.json({
1124
+ ...run,
1125
+ agentId: agent.id,
1126
+ agentName: agent.name,
1127
+ adapterType: agent.adapterType,
1128
+ });
1129
+ });
1130
+ return router;
1131
+ }
1132
+ //# sourceMappingURL=agents.js.map