@paperclipai/server 2026.817.0 → 2026.818.0-beta.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 (548) hide show
  1. package/dist/adapters/hermes-gateway-doc.d.ts +1 -1
  2. package/dist/adapters/hermes-gateway-doc.d.ts.map +1 -1
  3. package/dist/adapters/hermes-gateway-doc.js +1 -1
  4. package/dist/adapters/plugin-loader.d.ts.map +1 -1
  5. package/dist/adapters/plugin-loader.js +3 -2
  6. package/dist/adapters/plugin-loader.js.map +1 -1
  7. package/dist/agent-auth-jwt.d.ts.map +1 -1
  8. package/dist/agent-auth-jwt.js +8 -2
  9. package/dist/agent-auth-jwt.js.map +1 -1
  10. package/dist/app.d.ts +5 -1
  11. package/dist/app.d.ts.map +1 -1
  12. package/dist/app.js +202 -15
  13. package/dist/app.js.map +1 -1
  14. package/dist/built-ins/agents/summarizer/AGENTS.md +2 -2
  15. package/dist/built-ins/agents/summarizer/routines/refresh-stale-summaries.md +1 -0
  16. package/dist/db-errors.d.ts +9 -0
  17. package/dist/db-errors.d.ts.map +1 -1
  18. package/dist/db-errors.js +19 -0
  19. package/dist/db-errors.js.map +1 -1
  20. package/dist/errors.d.ts +1 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +2 -2
  23. package/dist/errors.js.map +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +162 -38
  26. package/dist/index.js.map +1 -1
  27. package/dist/lib/request-promise-memo.d.ts +6 -0
  28. package/dist/lib/request-promise-memo.d.ts.map +1 -0
  29. package/dist/lib/request-promise-memo.js +25 -0
  30. package/dist/lib/request-promise-memo.js.map +1 -0
  31. package/dist/lib/request-promise-memo.test.d.ts +2 -0
  32. package/dist/lib/request-promise-memo.test.d.ts.map +1 -0
  33. package/dist/lib/request-promise-memo.test.js +74 -0
  34. package/dist/lib/request-promise-memo.test.js.map +1 -0
  35. package/dist/middleware/auth.d.ts +13 -1
  36. package/dist/middleware/auth.d.ts.map +1 -1
  37. package/dist/middleware/auth.js +92 -41
  38. package/dist/middleware/auth.js.map +1 -1
  39. package/dist/middleware/cloud-tenant-actor.test.js +26 -1
  40. package/dist/middleware/cloud-tenant-actor.test.js.map +1 -1
  41. package/dist/middleware/error-handler.d.ts.map +1 -1
  42. package/dist/middleware/error-handler.js +11 -2
  43. package/dist/middleware/error-handler.js.map +1 -1
  44. package/dist/middleware/logger.d.ts.map +1 -1
  45. package/dist/middleware/logger.js +7 -33
  46. package/dist/middleware/logger.js.map +1 -1
  47. package/dist/middleware/private-hostname-guard.d.ts.map +1 -1
  48. package/dist/middleware/private-hostname-guard.js +11 -6
  49. package/dist/middleware/private-hostname-guard.js.map +1 -1
  50. package/dist/middleware/private-json-etag.d.ts +3 -0
  51. package/dist/middleware/private-json-etag.d.ts.map +1 -0
  52. package/dist/middleware/private-json-etag.js +37 -0
  53. package/dist/middleware/private-json-etag.js.map +1 -0
  54. package/dist/middleware/redact-sensitive.d.ts.map +1 -1
  55. package/dist/middleware/redact-sensitive.js +11 -0
  56. package/dist/middleware/redact-sensitive.js.map +1 -1
  57. package/dist/realtime/live-events-ws.d.ts +12 -0
  58. package/dist/realtime/live-events-ws.d.ts.map +1 -1
  59. package/dist/realtime/live-events-ws.js +20 -0
  60. package/dist/realtime/live-events-ws.js.map +1 -1
  61. package/dist/redaction.js +1 -1
  62. package/dist/redaction.js.map +1 -1
  63. package/dist/routes/access.d.ts +2 -0
  64. package/dist/routes/access.d.ts.map +1 -1
  65. package/dist/routes/access.js +21 -10
  66. package/dist/routes/access.js.map +1 -1
  67. package/dist/routes/agents.d.ts +42 -0
  68. package/dist/routes/agents.d.ts.map +1 -1
  69. package/dist/routes/agents.js +875 -8
  70. package/dist/routes/agents.js.map +1 -1
  71. package/dist/routes/companies.d.ts +5 -1
  72. package/dist/routes/companies.d.ts.map +1 -1
  73. package/dist/routes/companies.js +467 -30
  74. package/dist/routes/companies.js.map +1 -1
  75. package/dist/routes/environments.d.ts +12 -3
  76. package/dist/routes/environments.d.ts.map +1 -1
  77. package/dist/routes/environments.js +139 -23
  78. package/dist/routes/environments.js.map +1 -1
  79. package/dist/routes/execution-workspaces.d.ts.map +1 -1
  80. package/dist/routes/execution-workspaces.js +240 -70
  81. package/dist/routes/execution-workspaces.js.map +1 -1
  82. package/dist/routes/file-resources.d.ts +14 -1
  83. package/dist/routes/file-resources.d.ts.map +1 -1
  84. package/dist/routes/file-resources.js +182 -3
  85. package/dist/routes/file-resources.js.map +1 -1
  86. package/dist/routes/index.d.ts +2 -1
  87. package/dist/routes/index.d.ts.map +1 -1
  88. package/dist/routes/index.js +2 -1
  89. package/dist/routes/index.js.map +1 -1
  90. package/dist/routes/instance-settings.d.ts.map +1 -1
  91. package/dist/routes/instance-settings.js +20 -1
  92. package/dist/routes/instance-settings.js.map +1 -1
  93. package/dist/routes/issues.d.ts.map +1 -1
  94. package/dist/routes/issues.js +750 -235
  95. package/dist/routes/issues.js.map +1 -1
  96. package/dist/routes/onboarding-seed.d.ts +27 -0
  97. package/dist/routes/onboarding-seed.d.ts.map +1 -0
  98. package/dist/routes/onboarding-seed.js +56 -0
  99. package/dist/routes/onboarding-seed.js.map +1 -0
  100. package/dist/routes/openapi.d.ts.map +1 -1
  101. package/dist/routes/openapi.js +316 -2
  102. package/dist/routes/openapi.js.map +1 -1
  103. package/dist/routes/secrets.d.ts.map +1 -1
  104. package/dist/routes/secrets.js +11 -1
  105. package/dist/routes/secrets.js.map +1 -1
  106. package/dist/routes/setup-token-route.test.d.ts +2 -0
  107. package/dist/routes/setup-token-route.test.d.ts.map +1 -0
  108. package/dist/routes/setup-token-route.test.js +896 -0
  109. package/dist/routes/setup-token-route.test.js.map +1 -0
  110. package/dist/routes/tool-access.d.ts.map +1 -1
  111. package/dist/routes/tool-access.js +87 -52
  112. package/dist/routes/tool-access.js.map +1 -1
  113. package/dist/routes/workspace-runtime-service-authz.d.ts +11 -1
  114. package/dist/routes/workspace-runtime-service-authz.d.ts.map +1 -1
  115. package/dist/routes/workspace-runtime-service-authz.js +18 -16
  116. package/dist/routes/workspace-runtime-service-authz.js.map +1 -1
  117. package/dist/services/access.d.ts.map +1 -1
  118. package/dist/services/activity.d.ts +1 -1
  119. package/dist/services/agents.d.ts +21 -0
  120. package/dist/services/agents.d.ts.map +1 -1
  121. package/dist/services/agents.js +121 -1
  122. package/dist/services/agents.js.map +1 -1
  123. package/dist/services/attention-resolver-audience.test.d.ts +2 -0
  124. package/dist/services/attention-resolver-audience.test.d.ts.map +1 -0
  125. package/dist/services/attention-resolver-audience.test.js +81 -0
  126. package/dist/services/attention-resolver-audience.test.js.map +1 -0
  127. package/dist/services/attention.d.ts +20 -1
  128. package/dist/services/attention.d.ts.map +1 -1
  129. package/dist/services/attention.js +45 -1
  130. package/dist/services/attention.js.map +1 -1
  131. package/dist/services/authorization.d.ts +2 -2
  132. package/dist/services/authorization.d.ts.map +1 -1
  133. package/dist/services/authorization.js +89 -46
  134. package/dist/services/authorization.js.map +1 -1
  135. package/dist/services/board-auth.d.ts.map +1 -1
  136. package/dist/services/board-auth.js +40 -1
  137. package/dist/services/board-auth.js.map +1 -1
  138. package/dist/services/board-auth.test.d.ts +2 -0
  139. package/dist/services/board-auth.test.d.ts.map +1 -0
  140. package/dist/services/board-auth.test.js +34 -0
  141. package/dist/services/board-auth.test.js.map +1 -0
  142. package/dist/services/built-in-agents.d.ts.map +1 -1
  143. package/dist/services/built-in-agents.js +20 -4
  144. package/dist/services/built-in-agents.js.map +1 -1
  145. package/dist/services/bundled-plugins.d.ts +11 -1
  146. package/dist/services/bundled-plugins.d.ts.map +1 -1
  147. package/dist/services/bundled-plugins.js +46 -1
  148. package/dist/services/bundled-plugins.js.map +1 -1
  149. package/dist/services/codex-device-login-reaper.d.ts +61 -0
  150. package/dist/services/codex-device-login-reaper.d.ts.map +1 -0
  151. package/dist/services/codex-device-login-reaper.js +219 -0
  152. package/dist/services/codex-device-login-reaper.js.map +1 -0
  153. package/dist/services/codex-device-login-service.d.ts +355 -0
  154. package/dist/services/codex-device-login-service.d.ts.map +1 -0
  155. package/dist/services/codex-device-login-service.js +775 -0
  156. package/dist/services/codex-device-login-service.js.map +1 -0
  157. package/dist/services/companies.d.ts.map +1 -1
  158. package/dist/services/companies.js +7 -0
  159. package/dist/services/companies.js.map +1 -1
  160. package/dist/services/company-export-readme.js +1 -1
  161. package/dist/services/company-export-readme.js.map +1 -1
  162. package/dist/services/company-import-transfers.d.ts +55 -0
  163. package/dist/services/company-import-transfers.d.ts.map +1 -0
  164. package/dist/services/company-import-transfers.js +151 -0
  165. package/dist/services/company-import-transfers.js.map +1 -0
  166. package/dist/services/company-portability.d.ts.map +1 -1
  167. package/dist/services/company-portability.js +128 -3
  168. package/dist/services/company-portability.js.map +1 -1
  169. package/dist/services/company-transfer-runs.d.ts +136 -0
  170. package/dist/services/company-transfer-runs.d.ts.map +1 -0
  171. package/dist/services/company-transfer-runs.js +243 -0
  172. package/dist/services/company-transfer-runs.js.map +1 -0
  173. package/dist/services/cross-issue-influence-limit.d.ts +7 -1
  174. package/dist/services/cross-issue-influence-limit.d.ts.map +1 -1
  175. package/dist/services/cross-issue-influence-limit.js.map +1 -1
  176. package/dist/services/environment-config.d.ts +15 -0
  177. package/dist/services/environment-config.d.ts.map +1 -1
  178. package/dist/services/environment-config.js +56 -10
  179. package/dist/services/environment-config.js.map +1 -1
  180. package/dist/services/environment-execution-target.d.ts.map +1 -1
  181. package/dist/services/environment-execution-target.js +61 -11
  182. package/dist/services/environment-execution-target.js.map +1 -1
  183. package/dist/services/environment-run-orchestrator.d.ts +16 -1
  184. package/dist/services/environment-run-orchestrator.d.ts.map +1 -1
  185. package/dist/services/environment-run-orchestrator.js +1 -1
  186. package/dist/services/environment-run-orchestrator.js.map +1 -1
  187. package/dist/services/environment-runtime.d.ts +178 -2
  188. package/dist/services/environment-runtime.d.ts.map +1 -1
  189. package/dist/services/environment-runtime.js +1035 -96
  190. package/dist/services/environment-runtime.js.map +1 -1
  191. package/dist/services/environments.d.ts +75 -0
  192. package/dist/services/environments.d.ts.map +1 -1
  193. package/dist/services/environments.js +199 -8
  194. package/dist/services/environments.js.map +1 -1
  195. package/dist/services/execution-allowlist.d.ts +16 -4
  196. package/dist/services/execution-allowlist.d.ts.map +1 -1
  197. package/dist/services/execution-allowlist.js +35 -16
  198. package/dist/services/execution-allowlist.js.map +1 -1
  199. package/dist/services/execution-allowlist.test.js +33 -0
  200. package/dist/services/execution-allowlist.test.js.map +1 -1
  201. package/dist/services/execution-workspace-policy.d.ts +14 -1
  202. package/dist/services/execution-workspace-policy.d.ts.map +1 -1
  203. package/dist/services/execution-workspace-policy.js +29 -12
  204. package/dist/services/execution-workspace-policy.js.map +1 -1
  205. package/dist/services/execution-workspaces.d.ts +89 -0
  206. package/dist/services/execution-workspaces.d.ts.map +1 -1
  207. package/dist/services/execution-workspaces.js +997 -125
  208. package/dist/services/execution-workspaces.js.map +1 -1
  209. package/dist/services/heartbeat-run-summary.d.ts +1 -0
  210. package/dist/services/heartbeat-run-summary.d.ts.map +1 -1
  211. package/dist/services/heartbeat-run-summary.js +19 -3
  212. package/dist/services/heartbeat-run-summary.js.map +1 -1
  213. package/dist/services/heartbeat.d.ts +18 -2
  214. package/dist/services/heartbeat.d.ts.map +1 -1
  215. package/dist/services/heartbeat.js +369 -16
  216. package/dist/services/heartbeat.js.map +1 -1
  217. package/dist/services/import-write-types.d.ts +12 -0
  218. package/dist/services/import-write-types.d.ts.map +1 -1
  219. package/dist/services/index.d.ts +2 -0
  220. package/dist/services/index.d.ts.map +1 -1
  221. package/dist/services/index.js +2 -0
  222. package/dist/services/index.js.map +1 -1
  223. package/dist/services/instance-settings.d.ts +10 -1
  224. package/dist/services/instance-settings.d.ts.map +1 -1
  225. package/dist/services/instance-settings.js +16 -9
  226. package/dist/services/instance-settings.js.map +1 -1
  227. package/dist/services/issue-review-policy.d.ts +4 -0
  228. package/dist/services/issue-review-policy.d.ts.map +1 -1
  229. package/dist/services/issue-review-policy.js +18 -7
  230. package/dist/services/issue-review-policy.js.map +1 -1
  231. package/dist/services/issue-thread-interaction-resolution.d.ts +92 -0
  232. package/dist/services/issue-thread-interaction-resolution.d.ts.map +1 -0
  233. package/dist/services/issue-thread-interaction-resolution.js +198 -0
  234. package/dist/services/issue-thread-interaction-resolution.js.map +1 -0
  235. package/dist/services/issue-thread-interaction-resolution.test.d.ts +2 -0
  236. package/dist/services/issue-thread-interaction-resolution.test.d.ts.map +1 -0
  237. package/dist/services/issue-thread-interaction-resolution.test.js +152 -0
  238. package/dist/services/issue-thread-interaction-resolution.test.js.map +1 -0
  239. package/dist/services/issue-thread-interactions.d.ts +24 -4
  240. package/dist/services/issue-thread-interactions.d.ts.map +1 -1
  241. package/dist/services/issue-thread-interactions.js +360 -145
  242. package/dist/services/issue-thread-interactions.js.map +1 -1
  243. package/dist/services/issue-thread-interactions.test.js +18 -11
  244. package/dist/services/issue-thread-interactions.test.js.map +1 -1
  245. package/dist/services/issues.d.ts +3 -0
  246. package/dist/services/issues.d.ts.map +1 -1
  247. package/dist/services/issues.js +157 -33
  248. package/dist/services/issues.js.map +1 -1
  249. package/dist/services/local-service-supervisor.d.ts +2 -0
  250. package/dist/services/local-service-supervisor.d.ts.map +1 -1
  251. package/dist/services/local-service-supervisor.js +53 -3
  252. package/dist/services/local-service-supervisor.js.map +1 -1
  253. package/dist/services/managed-environments.d.ts +4 -1
  254. package/dist/services/managed-environments.d.ts.map +1 -1
  255. package/dist/services/managed-environments.js +86 -2
  256. package/dist/services/managed-environments.js.map +1 -1
  257. package/dist/services/managed-environments.test.js +164 -0
  258. package/dist/services/managed-environments.test.js.map +1 -1
  259. package/dist/services/onboarding-greeting.d.ts +7 -0
  260. package/dist/services/onboarding-greeting.d.ts.map +1 -0
  261. package/dist/services/onboarding-greeting.js +27 -0
  262. package/dist/services/onboarding-greeting.js.map +1 -0
  263. package/dist/services/onboarding-greeting.test.d.ts +2 -0
  264. package/dist/services/onboarding-greeting.test.d.ts.map +1 -0
  265. package/dist/services/onboarding-greeting.test.js +32 -0
  266. package/dist/services/onboarding-greeting.test.js.map +1 -0
  267. package/dist/services/onboarding-seed.d.ts +57 -0
  268. package/dist/services/onboarding-seed.d.ts.map +1 -0
  269. package/dist/services/onboarding-seed.js +344 -0
  270. package/dist/services/onboarding-seed.js.map +1 -0
  271. package/dist/services/plan-review-context.d.ts +9 -1
  272. package/dist/services/plan-review-context.d.ts.map +1 -1
  273. package/dist/services/plan-review-context.js +192 -0
  274. package/dist/services/plan-review-context.js.map +1 -1
  275. package/dist/services/plugin-environment-driver.d.ts +46 -0
  276. package/dist/services/plugin-environment-driver.d.ts.map +1 -1
  277. package/dist/services/plugin-environment-driver.js +50 -0
  278. package/dist/services/plugin-environment-driver.js.map +1 -1
  279. package/dist/services/plugin-host-services.d.ts +1 -0
  280. package/dist/services/plugin-host-services.d.ts.map +1 -1
  281. package/dist/services/plugin-host-services.js +51 -28
  282. package/dist/services/plugin-host-services.js.map +1 -1
  283. package/dist/services/plugin-loader.d.ts +10 -1
  284. package/dist/services/plugin-loader.d.ts.map +1 -1
  285. package/dist/services/plugin-loader.js +43 -2
  286. package/dist/services/plugin-loader.js.map +1 -1
  287. package/dist/services/plugin-worker-manager.d.ts +61 -0
  288. package/dist/services/plugin-worker-manager.d.ts.map +1 -1
  289. package/dist/services/plugin-worker-manager.js +248 -1
  290. package/dist/services/plugin-worker-manager.js.map +1 -1
  291. package/dist/services/productivity-review.d.ts.map +1 -1
  292. package/dist/services/productivity-review.js +5 -0
  293. package/dist/services/productivity-review.js.map +1 -1
  294. package/dist/services/recovery/service.d.ts.map +1 -1
  295. package/dist/services/recovery/service.js +5 -1
  296. package/dist/services/recovery/service.js.map +1 -1
  297. package/dist/services/recovery/successful-run-handoff.d.ts +16 -1
  298. package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -1
  299. package/dist/services/recovery/successful-run-handoff.js +32 -0
  300. package/dist/services/recovery/successful-run-handoff.js.map +1 -1
  301. package/dist/services/recovery/successful-run-handoff.test.js +51 -1
  302. package/dist/services/recovery/successful-run-handoff.test.js.map +1 -1
  303. package/dist/services/routines.d.ts.map +1 -1
  304. package/dist/services/routines.js +48 -4
  305. package/dist/services/routines.js.map +1 -1
  306. package/dist/services/runtime-exposure/broker-client.d.ts +88 -0
  307. package/dist/services/runtime-exposure/broker-client.d.ts.map +1 -0
  308. package/dist/services/runtime-exposure/broker-client.js +217 -0
  309. package/dist/services/runtime-exposure/broker-client.js.map +1 -0
  310. package/dist/services/runtime-exposure/broker-client.test.d.ts +2 -0
  311. package/dist/services/runtime-exposure/broker-client.test.d.ts.map +1 -0
  312. package/dist/services/runtime-exposure/broker-client.test.js +185 -0
  313. package/dist/services/runtime-exposure/broker-client.test.js.map +1 -0
  314. package/dist/services/runtime-exposure/exposure-manager.d.ts +135 -0
  315. package/dist/services/runtime-exposure/exposure-manager.d.ts.map +1 -0
  316. package/dist/services/runtime-exposure/exposure-manager.js +247 -0
  317. package/dist/services/runtime-exposure/exposure-manager.js.map +1 -0
  318. package/dist/services/runtime-exposure/exposure-manager.test.d.ts +2 -0
  319. package/dist/services/runtime-exposure/exposure-manager.test.d.ts.map +1 -0
  320. package/dist/services/runtime-exposure/exposure-manager.test.js +384 -0
  321. package/dist/services/runtime-exposure/exposure-manager.test.js.map +1 -0
  322. package/dist/services/runtime-exposure/loopback-listener.d.ts +33 -0
  323. package/dist/services/runtime-exposure/loopback-listener.d.ts.map +1 -0
  324. package/dist/services/runtime-exposure/loopback-listener.js +146 -0
  325. package/dist/services/runtime-exposure/loopback-listener.js.map +1 -0
  326. package/dist/services/runtime-exposure/loopback-listener.test.d.ts +2 -0
  327. package/dist/services/runtime-exposure/loopback-listener.test.d.ts.map +1 -0
  328. package/dist/services/runtime-exposure/loopback-listener.test.js +116 -0
  329. package/dist/services/runtime-exposure/loopback-listener.test.js.map +1 -0
  330. package/dist/services/runtime-exposure/port-pair.d.ts +47 -0
  331. package/dist/services/runtime-exposure/port-pair.d.ts.map +1 -0
  332. package/dist/services/runtime-exposure/port-pair.js +57 -0
  333. package/dist/services/runtime-exposure/port-pair.js.map +1 -0
  334. package/dist/services/runtime-exposure/port-pair.test.d.ts +2 -0
  335. package/dist/services/runtime-exposure/port-pair.test.d.ts.map +1 -0
  336. package/dist/services/runtime-exposure/port-pair.test.js +92 -0
  337. package/dist/services/runtime-exposure/port-pair.test.js.map +1 -0
  338. package/dist/services/runtime-exposure/port-reservation.d.ts +211 -0
  339. package/dist/services/runtime-exposure/port-reservation.d.ts.map +1 -0
  340. package/dist/services/runtime-exposure/port-reservation.js +380 -0
  341. package/dist/services/runtime-exposure/port-reservation.js.map +1 -0
  342. package/dist/services/runtime-exposure/port-reservation.test.d.ts +2 -0
  343. package/dist/services/runtime-exposure/port-reservation.test.d.ts.map +1 -0
  344. package/dist/services/runtime-exposure/port-reservation.test.js +456 -0
  345. package/dist/services/runtime-exposure/port-reservation.test.js.map +1 -0
  346. package/dist/services/runtime-exposure/tailscale-hostname.d.ts +11 -0
  347. package/dist/services/runtime-exposure/tailscale-hostname.d.ts.map +1 -0
  348. package/dist/services/runtime-exposure/tailscale-hostname.js +36 -0
  349. package/dist/services/runtime-exposure/tailscale-hostname.js.map +1 -0
  350. package/dist/services/runtime-exposure/tailscale-hostname.test.d.ts +2 -0
  351. package/dist/services/runtime-exposure/tailscale-hostname.test.d.ts.map +1 -0
  352. package/dist/services/runtime-exposure/tailscale-hostname.test.js +14 -0
  353. package/dist/services/runtime-exposure/tailscale-hostname.test.js.map +1 -0
  354. package/dist/services/sandbox-orphan-cleanup-spool.d.ts +64 -0
  355. package/dist/services/sandbox-orphan-cleanup-spool.d.ts.map +1 -0
  356. package/dist/services/sandbox-orphan-cleanup-spool.js +134 -0
  357. package/dist/services/sandbox-orphan-cleanup-spool.js.map +1 -0
  358. package/dist/services/sandbox-provider-runtime.d.ts +15 -0
  359. package/dist/services/sandbox-provider-runtime.d.ts.map +1 -1
  360. package/dist/services/sandbox-provider-runtime.js +1 -0
  361. package/dist/services/sandbox-provider-runtime.js.map +1 -1
  362. package/dist/services/secrets.d.ts +103 -0
  363. package/dist/services/secrets.d.ts.map +1 -1
  364. package/dist/services/secrets.js +432 -10
  365. package/dist/services/secrets.js.map +1 -1
  366. package/dist/services/setup-token-session.d.ts +630 -0
  367. package/dist/services/setup-token-session.d.ts.map +1 -0
  368. package/dist/services/setup-token-session.js +1005 -0
  369. package/dist/services/setup-token-session.js.map +1 -0
  370. package/dist/services/setup-token-session.test.d.ts +2 -0
  371. package/dist/services/setup-token-session.test.d.ts.map +1 -0
  372. package/dist/services/setup-token-session.test.js +1143 -0
  373. package/dist/services/setup-token-session.test.js.map +1 -0
  374. package/dist/services/setup-token-transport-binding.d.ts +202 -0
  375. package/dist/services/setup-token-transport-binding.d.ts.map +1 -0
  376. package/dist/services/setup-token-transport-binding.js +386 -0
  377. package/dist/services/setup-token-transport-binding.js.map +1 -0
  378. package/dist/services/setup-token-transport-binding.test.d.ts +2 -0
  379. package/dist/services/setup-token-transport-binding.test.d.ts.map +1 -0
  380. package/dist/services/setup-token-transport-binding.test.js +761 -0
  381. package/dist/services/setup-token-transport-binding.test.js.map +1 -0
  382. package/dist/services/summary-slots.d.ts.map +1 -1
  383. package/dist/services/summary-slots.js +40 -6
  384. package/dist/services/summary-slots.js.map +1 -1
  385. package/dist/services/task-watchdogs.js +2 -2
  386. package/dist/services/task-watchdogs.js.map +1 -1
  387. package/dist/services/tool-access-policy.d.ts +1 -1
  388. package/dist/services/tool-access.d.ts.map +1 -1
  389. package/dist/services/tool-access.js +29 -11
  390. package/dist/services/tool-access.js.map +1 -1
  391. package/dist/services/tool-gateway.d.ts +1 -1
  392. package/dist/services/tool-gateway.d.ts.map +1 -1
  393. package/dist/services/tool-gateway.js +56 -5
  394. package/dist/services/tool-gateway.js.map +1 -1
  395. package/dist/services/workspace-file-resources.d.ts +10 -1
  396. package/dist/services/workspace-file-resources.d.ts.map +1 -1
  397. package/dist/services/workspace-file-resources.js +258 -6
  398. package/dist/services/workspace-file-resources.js.map +1 -1
  399. package/dist/services/workspace-git-operation-scheduler.d.ts +135 -0
  400. package/dist/services/workspace-git-operation-scheduler.d.ts.map +1 -0
  401. package/dist/services/workspace-git-operation-scheduler.js +655 -0
  402. package/dist/services/workspace-git-operation-scheduler.js.map +1 -0
  403. package/dist/services/workspace-git-operation-scheduler.test.d.ts +2 -0
  404. package/dist/services/workspace-git-operation-scheduler.test.d.ts.map +1 -0
  405. package/dist/services/workspace-git-operation-scheduler.test.js +367 -0
  406. package/dist/services/workspace-git-operation-scheduler.test.js.map +1 -0
  407. package/dist/services/workspace-operations.d.ts +60 -0
  408. package/dist/services/workspace-operations.d.ts.map +1 -1
  409. package/dist/services/workspace-operations.js +384 -19
  410. package/dist/services/workspace-operations.js.map +1 -1
  411. package/dist/services/workspace-runtime-exposure-backfill.test.d.ts +2 -0
  412. package/dist/services/workspace-runtime-exposure-backfill.test.d.ts.map +1 -0
  413. package/dist/services/workspace-runtime-exposure-backfill.test.js +149 -0
  414. package/dist/services/workspace-runtime-exposure-backfill.test.js.map +1 -0
  415. package/dist/services/workspace-runtime-exposure.test.d.ts +2 -0
  416. package/dist/services/workspace-runtime-exposure.test.d.ts.map +1 -0
  417. package/dist/services/workspace-runtime-exposure.test.js +517 -0
  418. package/dist/services/workspace-runtime-exposure.test.js.map +1 -0
  419. package/dist/services/workspace-runtime-leases.d.ts +90 -0
  420. package/dist/services/workspace-runtime-leases.d.ts.map +1 -0
  421. package/dist/services/workspace-runtime-leases.js +252 -0
  422. package/dist/services/workspace-runtime-leases.js.map +1 -0
  423. package/dist/services/workspace-runtime-read-model.d.ts +12 -0
  424. package/dist/services/workspace-runtime-read-model.d.ts.map +1 -1
  425. package/dist/services/workspace-runtime-read-model.test.js +3 -0
  426. package/dist/services/workspace-runtime-read-model.test.js.map +1 -1
  427. package/dist/services/workspace-runtime-ready-comment.test.js +1 -0
  428. package/dist/services/workspace-runtime-ready-comment.test.js.map +1 -1
  429. package/dist/services/workspace-runtime.d.ts +160 -2
  430. package/dist/services/workspace-runtime.d.ts.map +1 -1
  431. package/dist/services/workspace-runtime.js +1693 -141
  432. package/dist/services/workspace-runtime.js.map +1 -1
  433. package/dist/shutdown.d.ts +24 -0
  434. package/dist/shutdown.d.ts.map +1 -1
  435. package/dist/shutdown.js +37 -0
  436. package/dist/shutdown.js.map +1 -1
  437. package/dist/shutdown.test.js +113 -1
  438. package/dist/shutdown.test.js.map +1 -1
  439. package/dist/startup-banner.js +1 -1
  440. package/dist/startup-banner.js.map +1 -1
  441. package/dist/url-utils.d.ts +26 -0
  442. package/dist/url-utils.d.ts.map +1 -0
  443. package/dist/url-utils.js +59 -0
  444. package/dist/url-utils.js.map +1 -0
  445. package/dist/url-utils.test.d.ts +2 -0
  446. package/dist/url-utils.test.d.ts.map +1 -0
  447. package/dist/url-utils.test.js +55 -0
  448. package/dist/url-utils.test.js.map +1 -0
  449. package/dist/worktree-config.d.ts.map +1 -1
  450. package/dist/worktree-config.js +3 -17
  451. package/dist/worktree-config.js.map +1 -1
  452. package/package.json +20 -20
  453. package/skills/paperclip/SKILL.md +16 -11
  454. package/skills/paperclip/references/api-reference.md +8 -5
  455. package/skills/paperclip-board/SKILL.md +1 -1
  456. package/ui-dist/assets/{abnfDiagram-VRR7QNED-wvSnvD6-.js → abnfDiagram-N423BO3Z-BvgoV-u5.js} +1 -1
  457. package/ui-dist/assets/{arc-Bx0V1JsN.js → arc-BInnUBUU.js} +1 -1
  458. package/ui-dist/assets/architectureDiagram-T3A2C74G-Dg4MziOm.js +36 -0
  459. package/ui-dist/assets/{blockDiagram-677ZJIJ3-BROcLaUk.js → blockDiagram-VBNYF7ZC-D9kzAkLN.js} +1 -1
  460. package/ui-dist/assets/{c4Diagram-LMCZKHZV-D-V3kgju.js → c4Diagram-5PPSVZJV-BwzsgV_0.js} +1 -1
  461. package/ui-dist/assets/channel-qoSYbCn0.js +1 -0
  462. package/ui-dist/assets/{chunk-32BRIVSS-qillH3H6.js → chunk-2GRJ4B5K-D2EQ9vrl.js} +1 -1
  463. package/ui-dist/assets/{chunk-2Q5K7J3B-D19cVnii.js → chunk-2Q5K7J3B-DOA_8fSd.js} +1 -1
  464. package/ui-dist/assets/{chunk-EX3LRPZG-CLUswrZg.js → chunk-5RXB4S5H-CT5ukRiT.js} +1 -1
  465. package/ui-dist/assets/{chunk-5VM5RSS4-Cwlp02Lx.js → chunk-5VM5RSS4-D2gOlkyB.js} +1 -1
  466. package/ui-dist/assets/{chunk-MOJQB5TN-yKciyq6E.js → chunk-6Q2QTUOP-DUb6aG8c.js} +1 -1
  467. package/ui-dist/assets/{chunk-V7JOEXUC-DjJ75g0_.js → chunk-GF5L2VYU-BPabG3Cx.js} +1 -1
  468. package/ui-dist/assets/{chunk-JWPE2WC7-BjepyzEb.js → chunk-JWPE2WC7-scNfhlZz.js} +1 -1
  469. package/ui-dist/assets/{chunk-VR4S4FIN-CiEmy8-R.js → chunk-KBJHAD2P-BcRAzRpq.js} +1 -1
  470. package/ui-dist/assets/{chunk-RYQCIY6F-ed7mQM9j.js → chunk-RYQCIY6F-BiZ0-EGh.js} +1 -1
  471. package/ui-dist/assets/{chunk-XXDRQBXY-BiI83zoO.js → chunk-XXDRQBXY-lJULxFIE.js} +1 -1
  472. package/ui-dist/assets/classDiagram-JCYQIIEL-B24u1REs.js +1 -0
  473. package/ui-dist/assets/classDiagram-v2-OCEON4UE-B24u1REs.js +1 -0
  474. package/ui-dist/assets/{cose-bilkent-JH36ORCC-DIdQf5ta.js → cose-bilkent-JH36ORCC-Do8pe8JB.js} +1 -1
  475. package/ui-dist/assets/{cynefin-VYW2F7L2-B2m9xxh8.js → cynefin-VYW2F7L2-B34jJY1x.js} +1 -1
  476. package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-CGVdB0kX.js → cynefinDiagram-MW4NZA55-DQ8QK8JK.js} +1 -1
  477. package/ui-dist/assets/cytoscape.esm-DShqj__B.js +321 -0
  478. package/ui-dist/assets/{dagre-VKFMJZFB-CskKiZwJ.js → dagre-VZM6K2ZE-B7lSKgxH.js} +1 -1
  479. package/ui-dist/assets/{diagram-OA4YK3LP-D2cn5eIN.js → diagram-7IWD3JNH-B3hLAFGE.js} +1 -1
  480. package/ui-dist/assets/{diagram-FQU43EPY-oa5Zu0XG.js → diagram-B4RE2ZJO-CH9Ro9e0.js} +1 -1
  481. package/ui-dist/assets/{diagram-NH7WQ7WH-fymti5Js.js → diagram-LBJQPF4R-DdPVMii7.js} +1 -1
  482. package/ui-dist/assets/{diagram-G47NLZAW-BWF2ZCsw.js → diagram-Q27KOJAE-f5VXswjc.js} +1 -1
  483. package/ui-dist/assets/diagram-UB23O5K3-DGqxixsR.js +41 -0
  484. package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-CJ1d3D5q.js → ebnfDiagram-BXEA7PRR-C-1klg_w.js} +1 -1
  485. package/ui-dist/assets/{erDiagram-Q63AITRT-BJCPa7kK.js → erDiagram-JOGREHBK-Dt4qK4gv.js} +1 -1
  486. package/ui-dist/assets/{flowDiagram-23GEKE2U-BWjWuz6r.js → flowDiagram-UKHOOZJN-9ZlrTuET.js} +5 -5
  487. package/ui-dist/assets/{ganttDiagram-NO4QXBWP-Dv1KKjoM.js → ganttDiagram-PKOTCBZU-CE3lyvFq.js} +1 -1
  488. package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-9X7vgL0A.js → gitGraphDiagram-DS77QQ5N-HH2fSgad.js} +1 -1
  489. package/ui-dist/assets/{index-CpaK48ok.js → index-B61ORkIb.js} +1 -1
  490. package/ui-dist/assets/index-BHiUiToL.css +1 -0
  491. package/ui-dist/assets/{index-D2YCrA-D.js → index-BY5ITKmG.js} +1 -1
  492. package/ui-dist/assets/{index-9JMDmXc5.js → index-BbAhzCq8.js} +1 -1
  493. package/ui-dist/assets/{index-C5qHatpp.js → index-Bc9g6q63.js} +1 -1
  494. package/ui-dist/assets/{index-DuI6e5mP.js → index-Bf7tyGxE.js} +1 -1
  495. package/ui-dist/assets/{index-BahRsj0s.js → index-BvFx-4KW.js} +1 -1
  496. package/ui-dist/assets/{index-DKlsEDV7.js → index-C1mpWCcn.js} +1 -1
  497. package/ui-dist/assets/{index-DIXaXZrW.js → index-CHXG4GPL.js} +1 -1
  498. package/ui-dist/assets/{index-DcfO6NoY.js → index-CLu3Tfpv.js} +1 -1
  499. package/ui-dist/assets/{index-Cfz6P0Qq.js → index-CQhaM6qd.js} +1 -1
  500. package/ui-dist/assets/{index-D9d3sl6K.js → index-CVTL3aAL.js} +1 -1
  501. package/ui-dist/assets/{index-BexA3NAl.js → index-CcRTJ93J.js} +1 -1
  502. package/ui-dist/assets/{index-CnBrDwGT.js → index-Cm6iEYPt.js} +247 -223
  503. package/ui-dist/assets/{index-ByGcu-MB.js → index-DQIrrn2L.js} +1 -1
  504. package/ui-dist/assets/{index-DKEzHO7c.js → index-DU4U1fuv.js} +1 -1
  505. package/ui-dist/assets/{index-pqJO8FsV.js → index-DV2FOgyD.js} +1 -1
  506. package/ui-dist/assets/{index-tDuieU3u.js → index-DceuCrr8.js} +1 -1
  507. package/ui-dist/assets/{index-CjN2lm_w.js → index-DlVBBF8o.js} +1 -1
  508. package/ui-dist/assets/{index-0yJtUsFh.js → index-DsdVneqe.js} +1 -1
  509. package/ui-dist/assets/{index-C2a4_IKU.js → index-DvFUXI-Q.js} +1 -1
  510. package/ui-dist/assets/{index-BTMiDMYv.js → index-FHzrJ7iQ.js} +1 -1
  511. package/ui-dist/assets/{index-cUQXJtLK.js → index-diFs0CNC.js} +1 -1
  512. package/ui-dist/assets/{index-C96LHV1A.js → index-hkanj4t6.js} +1 -1
  513. package/ui-dist/assets/{index-rDI1Q4r9.js → index-yRJnh4vx.js} +1 -1
  514. package/ui-dist/assets/{infoDiagram-FWYZ7A6U-CHIhutSb.js → infoDiagram-6WML65LV-2jai2fw-.js} +1 -1
  515. package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-ChaD7-5_.js → ishikawaDiagram-WSZJBQD7-CUmGhmEb.js} +1 -1
  516. package/ui-dist/assets/{journeyDiagram-5HDEW3XC-M5T2sMgn.js → journeyDiagram-NVQOT4AX-CGQuPDO1.js} +1 -1
  517. package/ui-dist/assets/{kanban-definition-HUTT4EX6-B8H6R_Wc.js → kanban-definition-27J2QSJJ-zgEPcbHl.js} +1 -1
  518. package/ui-dist/assets/{linear-cpPKlVCq.js → linear-BVf5VtLS.js} +1 -1
  519. package/ui-dist/assets/mermaid.core-D2TFkGfI.js +308 -0
  520. package/ui-dist/assets/{mindmap-definition-LN4V7U3C-Bf6POzvm.js → mindmap-definition-FAOFIHXS-xoAQ0Lb_.js} +1 -1
  521. package/ui-dist/assets/{pegDiagram-2B236MQR-BJbGktAV.js → pegDiagram-VL7TDLO6-CBR1PWLr.js} +1 -1
  522. package/ui-dist/assets/{pieDiagram-ENE6RG2P-BmSN9Bio.js → pieDiagram-7S7Q4E2Y-BMPnzK_e.js} +1 -1
  523. package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-JUpqQCx_.js → quadrantDiagram-CIZ2JOQS-CPtVUPkR.js} +1 -1
  524. package/ui-dist/assets/{railroadDiagram-RFXS5EU6-B8k8T4-2.js → railroadDiagram-AXF67PYL-DC0lsZV1.js} +1 -1
  525. package/ui-dist/assets/{requirementDiagram-TGXJPOKE-b32heq7Q.js → requirementDiagram-LRYGKXZP-BF6k05SB.js} +1 -1
  526. package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-DC3s-xgf.js → sankeyDiagram-W5VNT64P-CRN0ibiP.js} +1 -1
  527. package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-BsCFyv_E.js → sequenceDiagram-SI44F4Z6-CQtTiPWm.js} +1 -1
  528. package/ui-dist/assets/{sizeCapture-X5ZJPWSS-S7xeCud3.js → sizeCapture-X5ZJPWSS-BdjuGZd1.js} +1 -1
  529. package/ui-dist/assets/{stateDiagram-2N3HPSRC-58O3ofoS.js → stateDiagram-OKZ733FA-B99onxyh.js} +1 -1
  530. package/ui-dist/assets/stateDiagram-v2-UEYNNEHI-bzFlYBZ1.js +1 -0
  531. package/ui-dist/assets/{swimlanes-5IMT3BWC-DFCVG0iF.js → swimlanes-SLNWSIFB-BIByPbrL.js} +2 -2
  532. package/ui-dist/assets/swimlanesDiagram-ULZ7WXOC-DgdkBnuK.js +8 -0
  533. package/ui-dist/assets/{timeline-definition-FHXFAJF6-C0c0SkQu.js → timeline-definition-Z64GVDOM-I1NgOcn9.js} +1 -1
  534. package/ui-dist/assets/{vennDiagram-L72KCM5P-B1cEx8GQ.js → vennDiagram-T6HMQDX7-D6t2Njcl.js} +1 -1
  535. package/ui-dist/assets/{wardleyDiagram-EHGQE667-BQhDkOx9.js → wardleyDiagram-T6FBY63Y-BiYKm9gh.js} +1 -1
  536. package/ui-dist/assets/{xychartDiagram-FW5EYKEG-YSQp1N0i.js → xychartDiagram-ELKLHX3M-BhwXKWIa.js} +4 -4
  537. package/ui-dist/index.html +3 -3
  538. package/ui-dist/sw.js +8 -3
  539. package/ui-dist/assets/architectureDiagram-ZJ3FMSHR-BcJt8Ia1.js +0 -36
  540. package/ui-dist/assets/channel-ZRpjyNiZ.js +0 -1
  541. package/ui-dist/assets/classDiagram-OUVF2IWQ-CCzzzCgk.js +0 -1
  542. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CCzzzCgk.js +0 -1
  543. package/ui-dist/assets/cytoscape.esm-D8joxN9f.js +0 -321
  544. package/ui-dist/assets/diagram-WEI45ONY-M3WOC2tK.js +0 -41
  545. package/ui-dist/assets/index-BqfqfoQX.css +0 -1
  546. package/ui-dist/assets/mermaid.core-MCazc_od.js +0 -308
  547. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-_VY5dpDG.js +0 -1
  548. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-BJhUjfns.js +0 -8
@@ -3,28 +3,35 @@ import { generateKeyPairSync, randomUUID } from "node:crypto";
3
3
  import path from "node:path";
4
4
  import { agents as agentsTable, companies, heartbeatRuns, issues as issuesTable, projects as projectsTable } from "@paperclipai/db";
5
5
  import { and, desc, eq, inArray, not, sql } from "drizzle-orm";
6
- import { agentSkillSyncSchema, agentMineInboxQuerySchema, ADAPTER_AGNOSTIC_KEYS, AGENT_DEFAULT_MAX_CONCURRENT_RUNS, createAgentKeySchema, createAgentHireSchema, createAgentSchema, deriveAgentUrlKey, isUuidLike, normalizeIssueIdentifier, resetAgentSessionSchema, testAdapterEnvironmentSchema, upsertAgentInstructionsFileSchema, updateAgentInstructionsBundleSchema, updateAgentPermissionsSchema, updateAgentInstructionsPathSchema, wakeAgentSchema, updateAgentSchema, supportedEnvironmentDriversForAdapter, LOW_TRUST_REVIEW_PRESET, } from "@paperclipai/shared";
6
+ import { agentSkillSyncSchema, agentMineInboxQuerySchema, ADAPTER_AGNOSTIC_KEYS, AGENT_DEFAULT_MAX_CONCURRENT_RUNS, createAgentKeySchema, createAgentHireSchema, createAgentSchema, deriveAgentUrlKey, isUuidLike, normalizeIssueIdentifier, resetAgentSessionSchema, testAdapterEnvironmentSchema, upsertAgentInstructionsFileSchema, updateAgentInstructionsBundleSchema, updateAgentPermissionsSchema, updateAgentInstructionsPathSchema, wakeAgentSchema, updateAgentSchema, supportedEnvironmentDriversForAdapter, LOW_TRUST_REVIEW_PRESET, startClaudeSetupTokenSessionRequestSchema, submitBrowserCodeRequestSchema, } from "@paperclipai/shared";
7
7
  import { isForbiddenConfigEnvKey, parseObject, resolvePaperclipInstanceRootForAdapter, readPaperclipSkillSyncPreference, writePaperclipSkillSyncPreference, } from "@paperclipai/adapter-utils/server-utils";
8
8
  import { trackAgentCreated } from "@paperclipai/shared/telemetry";
9
9
  import { validate } from "../middleware/validate.js";
10
10
  import { agentService, agentInstructionsService, accessService, approvalService, builtInAgentService, companySkillService, budgetService, heartbeatService, ISSUE_LIST_DEFAULT_LIMIT, issueApprovalService, issueRecoveryActionService, issueService, logActivity, syncInstructionsBundleConfigFromFilePath, workspaceOperationService, } from "../services/index.js";
11
11
  import { badRequest, conflict, forbidden, HttpError, notFound, unprocessable } from "../errors.js";
12
12
  import { createRunSecretRedactionRegistry } from "../services/run-secret-redaction.js";
13
- import { assertBoard, assertCompanyAccess, assertInstanceAdmin, buildActorSecretContext, getAccessibleResource, getActorInfo, hasCompanyAccess } from "./authz.js";
13
+ import { assertAuthenticated, assertBoard, assertCompanyAccess, assertInstanceAdmin, buildActorSecretContext, getAccessibleResource, getActorInfo, hasCompanyAccess } from "./authz.js";
14
14
  import { assertNoAgentHostWorkspaceCommandMutation, collectAgentAdapterWorkspaceCommandPaths, } from "./workspace-command-authz.js";
15
15
  import { environmentService } from "../services/environments.js";
16
16
  import { resolveEnvironmentExecutionTarget } from "../services/environment-execution-target.js";
17
17
  import { environmentRuntimeService } from "../services/environment-runtime.js";
18
+ import { resolvePluginSandboxProviderDriverByKey } from "../services/plugin-environment-driver.js";
19
+ import { getDisabledAdapterTypes } from "../services/adapter-plugin-store.js";
18
20
  import { skillVersionSelectionMap } from "../services/runtime-skill-selections.js";
19
21
  import { secretService } from "../services/secrets.js";
20
22
  import { authorizationDeniedDetails } from "../services/authorization.js";
21
- import { detectAdapterModel, findActiveServerAdapter, findServerAdapter, listAdapterModels, listAdapterModelProfiles, refreshAdapterModels, requireServerAdapter, } from "../adapters/index.js";
23
+ import { detectAdapterModel, findActiveServerAdapter, findServerAdapter, listServerAdapters, listAdapterModels, listAdapterModelProfiles, refreshAdapterModels, requireServerAdapter, } from "../adapters/index.js";
22
24
  import { redactEventPayload } from "../redaction.js";
23
25
  import { redactCurrentUserValue } from "../log-redaction.js";
24
26
  import { renderOrgChartSvg, renderOrgChartPng, ORG_CHART_STYLES } from "./org-chart-svg.js";
25
27
  import { instanceSettingsService, isTruthyRuntimeEnvValue, resolveWorktreeRunExecutionActivationState, } from "../services/instance-settings.js";
26
28
  import { runClaudeLogin } from "@paperclipai/adapter-claude-local/server";
29
+ import { createInviteRateLimiter } from "../services/invite-rate-limit.js";
30
+ import { SetupTokenSessionService, SetupTokenSessionError, assessConfidentialStartup, evaluateConfidentialTransport, SETUP_TOKEN_START_FAILED, SETUP_TOKEN_SESSION_NOT_FOUND, SETUP_TOKEN_PROVIDER_UNSUPPORTED, SETUP_TOKEN_PROVIDER_UNSUPPORTED_CODE, } from "../services/setup-token-session.js";
31
+ import { SETUP_TOKEN_TRANSPORT_ADVISORY_CODE } from "@paperclipai/shared";
27
32
  import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX } from "@paperclipai/adapter-codex-local";
33
+ import { checkStagedCredentialReadiness, promoteDeviceLoginCredential, } from "@paperclipai/adapter-codex-local/server";
34
+ import { AdapterAuthSessionConflictError, CODEX_DEVICE_LOGIN_ADAPTER_TYPE, createCodexDeviceLoginService, createDbAdapterAuthSessionStore, createProductionLoginSessionRuntime, } from "../services/codex-device-login-service.js";
28
35
  import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local";
29
36
  import { DEFAULT_GEMINI_LOCAL_MODEL } from "@paperclipai/adapter-gemini-local";
30
37
  import { DEFAULT_OPENCODE_LOCAL_MODEL } from "@paperclipai/adapter-opencode-local";
@@ -131,6 +138,134 @@ export function agentRoutes(db, options = {}) {
131
138
  const environmentRuntime = environmentRuntimeService(db, {
132
139
  pluginWorkerManager: options.pluginWorkerManager,
133
140
  });
141
+ // --- Setup-token login session (Claude in-product login) -------------------
142
+ //
143
+ // The service owns a company-scoped, owner-bound login session, the
144
+ // confidential transport guard (SR-6, SR-7), the session caps, and the start
145
+ // rate limit. The `options.setupTokenLogin` transport binds the live sandbox
146
+ // pseudo-terminal login process and the real sandbox-lease acquisition. When a
147
+ // caller provides the transport, the session route is the live login path and
148
+ // `SETUP_TOKEN_LOGIN_TRANSPORT_READY` is true. When a caller omits it, the
149
+ // start route returns the fixed no-secret error and the login never spawns a
150
+ // process or holds a lease. The full session state machine, the cleanup order,
151
+ // and the reaper are covered by setup-token-session.test.ts.
152
+ const SETUP_TOKEN_LOGIN_TRANSPORT_READY = options.setupTokenLogin != null;
153
+ const setupTokenConfidentialConfig = {
154
+ deploymentMode: options.deploymentMode ?? "local_trusted",
155
+ trustedProxies: options.confidentialProxyAllowlist ?? [],
156
+ edgeTlsTerminated: options.confidentialEdgeTlsTerminated ?? false,
157
+ };
158
+ // Rate-limit the start route: a small window per company and owner (SR-4).
159
+ const setupTokenRateLimiter = createInviteRateLimiter({ windowMs: 60_000, maxRequests: 5 });
160
+ // The deferred lease manager. It fails closed on acquire until a caller binds
161
+ // the live transport. It still releases a lease by handle or by id, so a
162
+ // reaper or a shutdown can free a lease that an injected transport acquired.
163
+ const deferredSetupTokenLeaseManager = {
164
+ async acquire() {
165
+ // The real sandbox-lease acquisition binds through `options.setupTokenLogin`.
166
+ // Until then the start route fails closed before it reaches here.
167
+ throw new SetupTokenSessionError(503, SETUP_TOKEN_START_FAILED);
168
+ },
169
+ async release(lease) {
170
+ await environmentsSvc.releaseLease(lease.id, "released").catch(() => { });
171
+ },
172
+ async releaseById(leaseId) {
173
+ await environmentsSvc.releaseLease(leaseId, "released").catch(() => { });
174
+ },
175
+ };
176
+ // The in-memory non-secret cleanup record store. It is the default store when a
177
+ // caller does not inject a durable database-backed store.
178
+ const setupTokenCleanupRows = new Map();
179
+ const scopeMatchesRow = (row, identity) => row.companyId === identity.companyId &&
180
+ row.ownerUserId === identity.ownerUserId &&
181
+ row.adapterType === identity.adapterType &&
182
+ row.environmentId === identity.environmentId;
183
+ const inMemorySetupTokenCleanupStore = {
184
+ async record(record) {
185
+ setupTokenCleanupRows.set(record.sessionId, { ...record });
186
+ },
187
+ async markState(identity, state) {
188
+ const row = setupTokenCleanupRows.get(identity.sessionId);
189
+ if (row && scopeMatchesRow(row, identity))
190
+ row.state = state;
191
+ },
192
+ async remove(identity) {
193
+ // The delete matches the full owner scope, so it never removes a row by the
194
+ // session id alone.
195
+ const row = setupTokenCleanupRows.get(identity.sessionId);
196
+ if (row && scopeMatchesRow(row, identity))
197
+ setupTokenCleanupRows.delete(identity.sessionId);
198
+ },
199
+ async listReapable() {
200
+ return [];
201
+ },
202
+ async consumeStoredClaim(identity) {
203
+ const row = setupTokenCleanupRows.get(identity.sessionId);
204
+ if (!row ||
205
+ !scopeMatchesRow(row, identity) ||
206
+ row.state !== "stored" ||
207
+ row.boundAt !== null ||
208
+ row.deadline <= Date.now()) {
209
+ return null;
210
+ }
211
+ row.boundAt = Date.now();
212
+ return { ...row };
213
+ },
214
+ };
215
+ const deferredSetupTokenLoginFactory = () => {
216
+ // The runner-over-pseudo-terminal binding arrives through
217
+ // `options.setupTokenLogin`. Until then the start route fails closed.
218
+ throw new SetupTokenSessionError(503, SETUP_TOKEN_START_FAILED);
219
+ };
220
+ const deferredSetupTokenSecretWriter = async () => {
221
+ // The owner-bound secret writer arrives through `options.setupTokenLogin`.
222
+ // Until then the completion fails closed, so the session never reports a
223
+ // stored credential without a real secret write.
224
+ throw new SetupTokenSessionError(503, SETUP_TOKEN_START_FAILED);
225
+ };
226
+ // Resolve the transport: use the injected factory, lease manager, store, and
227
+ // secret writer when a caller binds them; otherwise use the deferred,
228
+ // fail-closed defaults.
229
+ const setupTokenLoginFactory = options.setupTokenLogin?.factory ?? deferredSetupTokenLoginFactory;
230
+ const setupTokenLeaseManager = options.setupTokenLogin?.leases ?? deferredSetupTokenLeaseManager;
231
+ const setupTokenCleanupStore = options.setupTokenLogin?.store ?? inMemorySetupTokenCleanupStore;
232
+ const setupTokenSecretWriter = options.setupTokenLogin?.completeCredential ?? deferredSetupTokenSecretWriter;
233
+ // Re-check the environment company binding at lease acquisition. The start
234
+ // route runs `assertSandboxLoginEnvironment` before the session begins, but
235
+ // managed-environment reconciliation can bind the sandbox to another company
236
+ // between that guard and the lease acquire. This wrapper re-runs the same
237
+ // guard at acquire time and fails closed with the 403
238
+ // `environment_company_mismatch` before the transport provisions a sandbox.
239
+ // The lease insert transaction re-checks the binding once more inside the
240
+ // insert, so a bind that lands during the provider call still holds no lease.
241
+ const guardedSetupTokenLeaseManager = {
242
+ async acquire(input) {
243
+ await assertSandboxLoginEnvironment(input.scope.companyId, input.scope.environmentId, {
244
+ requireSetupTokenLoginProvider: true,
245
+ });
246
+ return setupTokenLeaseManager.acquire(input);
247
+ },
248
+ release: (lease) => setupTokenLeaseManager.release(lease),
249
+ releaseById: (leaseId) => setupTokenLeaseManager.releaseById(leaseId),
250
+ };
251
+ const setupTokenLoginService = new SetupTokenSessionService({
252
+ factory: setupTokenLoginFactory,
253
+ leases: guardedSetupTokenLeaseManager,
254
+ store: setupTokenCleanupStore,
255
+ completeCredential: setupTokenSecretWriter,
256
+ rateLimiter: setupTokenRateLimiter,
257
+ });
258
+ {
259
+ // Log the startup transport assessment, so an operator can see whether a
260
+ // forwarded proxy protocol is trusted for the confidential routes (SR-7).
261
+ const startupAssessment = assessConfidentialStartup(setupTokenConfidentialConfig);
262
+ logger.info({
263
+ proxyForwardingEnabled: startupAssessment.proxyForwardingEnabled,
264
+ reason: startupAssessment.reason,
265
+ deploymentMode: setupTokenConfidentialConfig.deploymentMode,
266
+ }, "Setup-token login confidential transport startup assessment");
267
+ }
268
+ options.onSetupTokenLoginService?.(setupTokenLoginService);
134
269
  const runRedactions = createRunSecretRedactionRegistry(db);
135
270
  const heartbeat = heartbeatService(db, {
136
271
  pluginWorkerManager: options.pluginWorkerManager,
@@ -143,6 +278,75 @@ export function agentRoutes(db, options = {}) {
143
278
  const workspaceOperations = workspaceOperationService(db);
144
279
  const instanceSettings = instanceSettingsService(db);
145
280
  const strictSecretsMode = process.env.PAPERCLIP_SECRETS_STRICT_MODE === "true";
281
+ // The company-scoped adapter login-session service. It runs the device-login
282
+ // flow in a fresh trusted sandbox and holds the one-time prompt in memory. The
283
+ // process owns one instance, so the in-memory prompt and the cancellation
284
+ // controllers persist across requests.
285
+ const adapterLoginStore = createDbAdapterAuthSessionStore(db);
286
+ const adapterLoginService = createCodexDeviceLoginService({
287
+ store: adapterLoginStore,
288
+ runtime: createProductionLoginSessionRuntime({ db, environmentRuntime }),
289
+ // The mandatory credential promotion. A successful login authenticates only
290
+ // after this promotion validates the exact staged credential, runs an
291
+ // independent readiness check, confirms the session still holds the sole
292
+ // active claim, and writes the credential into the company scope. A rejected
293
+ // or unready credential fails the session and writes nothing.
294
+ promotion: {
295
+ async promote(authBytes, context) {
296
+ // Hold the promotion critical-section lock across the ownership check and
297
+ // the credential write. The reaper takes the same lock before it reclaims
298
+ // a stale `promoting` row. So a reclaim never interleaves with a live
299
+ // write: the reaper either wins the lock first and the ownership check
300
+ // then reads a reclaimed row and writes nothing, or the write finishes
301
+ // first under the lock and the reaper reclaims only after it completes. A
302
+ // read-only fence is not enough, because the filesystem write can start
303
+ // after the fence; the lock spans the whole section.
304
+ const outcome = await adapterLoginStore.withCompanyAdapterPromotionLock(context.companyId, context.adapterType, () => promoteDeviceLoginCredential({
305
+ authBytes,
306
+ companyId: context.companyId,
307
+ userInitiated: true,
308
+ checkReadiness: (bytes) => checkStagedCredentialReadiness(bytes),
309
+ isSoleActiveOwner: async () => {
310
+ // The partial unique index allows one active row per company and
311
+ // adapter. So a `promoting` row for this session is the sole
312
+ // active owner of the company credential slot. The read runs
313
+ // inside the lock, so it observes a reaper reclaim that committed
314
+ // before this section acquired the lock.
315
+ const row = await adapterLoginStore.get(context.sessionId);
316
+ return row?.status === "promoting" && row.companyId === context.companyId;
317
+ },
318
+ log: (line) => {
319
+ // The promotion lines carry no token bytes and no raw account id,
320
+ // so it is safe to log them with the session identifier.
321
+ logger.info({ sessionId: context.sessionId }, line);
322
+ },
323
+ }));
324
+ // A resolved promotion is not necessarily an accepted promotion. In
325
+ // particular, a reaper/expiry race can revoke this session's sole
326
+ // ownership between the service transition and Decision H. Fail closed:
327
+ // only a credential write or a deliberate safe keep can authenticate.
328
+ if (outcome === "kept_foreign_identity") {
329
+ // The login produced a different account than the one the company
330
+ // credential home already holds. The promotion never clobbers an
331
+ // occupied home, so this login installed nothing durable, and the
332
+ // identity-anchored vend can never select it: a later run keeps the
333
+ // existing account. Fail the session, so the operator never sees a
334
+ // false `authenticated` for an account the system will not use.
335
+ throw new Error("device-login credential promotion rejected: the login is a different account than the one already set for this company; the existing account was kept");
336
+ }
337
+ if (outcome !== "promoted" && outcome !== "kept") {
338
+ throw new Error(`device-login credential promotion rejected: ${outcome}`);
339
+ }
340
+ },
341
+ },
342
+ recordActivity: (event) => {
343
+ // The event carries no URL, no code, no credential, no account identifier,
344
+ // and no lease identifier, so it is safe to log.
345
+ logger.info(event, "adapter login session lifecycle");
346
+ },
347
+ });
348
+ // The cancellation controllers for the in-flight login runs this process owns.
349
+ const adapterLoginAbortControllers = new Map();
146
350
  async function assertAgentEnvironmentSelection(companyId, adapterType, environmentId) {
147
351
  if (environmentId === undefined || environmentId === null)
148
352
  return;
@@ -653,6 +857,113 @@ export function agentRoutes(db, options = {}) {
653
857
  return;
654
858
  throw forbidden(decision.explanation, authorizationDeniedDetails(decision));
655
859
  }
860
+ // The single owner-authorization helper for the three adapter login routes. It
861
+ // requires a board actor, company access, and the same configuration
862
+ // permission as the adapter Test route (`agents:create`). It returns the
863
+ // immutable owner identifier: the board user that starts, reads, or cancels the
864
+ // session. The start route persists this identifier; the status and cancel
865
+ // routes compare it to the session owner and return 404 on a mismatch, so a
866
+ // non-owner cannot enumerate a session.
867
+ async function assertCanManageAdapterLogin(req, companyId) {
868
+ assertBoard(req);
869
+ assertCompanyAccess(req, companyId);
870
+ const decision = await access.decide({
871
+ actor: req.actor,
872
+ action: "agents:create",
873
+ resource: { type: "company", companyId },
874
+ });
875
+ if (!decision.allowed) {
876
+ throw forbidden(decision.explanation, authorizationDeniedDetails(decision));
877
+ }
878
+ const userId = req.actor.userId;
879
+ if (!userId) {
880
+ throw forbidden("A board user identity is required to manage an adapter login session.");
881
+ }
882
+ return userId;
883
+ }
884
+ // The device-login flow supports only the Codex adapter. Reject any other type.
885
+ function assertCodexLoginAdapter(type) {
886
+ if (type !== CODEX_DEVICE_LOGIN_ADAPTER_TYPE) {
887
+ throw badRequest(`Adapter "${type}" does not support a device login.`);
888
+ }
889
+ }
890
+ // The environment-eligibility guard for an adapter login. A device login runs
891
+ // only in an active sandbox environment. This reuses the shared environment
892
+ // selection guard, so it rejects a missing, archived (inactive), local, SSH, or
893
+ // plugin environment the same way the agent configuration routes do.
894
+ //
895
+ // The execution environment catalog is instance-scoped, not company-owned. PR
896
+ // #8375 moved the catalog to one shared instance catalog, so an environment row
897
+ // carries no single owning company. The shared selection guard therefore checks
898
+ // only the driver and the status. The company-binding check below then rejects
899
+ // an environment that binds to other companies. The route caller is already
900
+ // bound to the path company by `assertCompanyAccess`, and the acquired lease
901
+ // records that same company, so the login stays attributed to the caller.
902
+ async function assertSandboxLoginEnvironment(companyId, environmentId, options) {
903
+ await assertEnvironmentSelectionForCompany(environmentsSvc, companyId, environmentId, {
904
+ allowedDrivers: ["sandbox"],
905
+ });
906
+ // Reject an environment that another company owns. A managed sandbox
907
+ // environment binds to the companies that the instance provisions it for.
908
+ // When the environment binds to companies but not the request company, the
909
+ // environment belongs to another company. A login there runs the process in
910
+ // a foreign company sandbox, so the guard fails closed. An environment with
911
+ // no company binding is instance-global and stays open to every member.
912
+ const boundCompanyIds = await environmentsSvc.listBoundCompanyIds(environmentId);
913
+ if (boundCompanyIds.length > 0 && !boundCompanyIds.includes(companyId)) {
914
+ throw forbidden("The selected environment belongs to another company.", {
915
+ code: "environment_company_mismatch",
916
+ });
917
+ }
918
+ // Gate the Claude setup-token login on the provider capability. Only a
919
+ // sandbox provider that advertises the setup-token login capability
920
+ // implements the setup-token pseudo-terminal methods. The setup-token start
921
+ // routes pass this option, so an unsupported provider fails closed here
922
+ // before the session starts. The lease guard passes it too, so a
923
+ // reconciliation that rebinds the environment to an unsupported provider
924
+ // still fails closed before the lease and the pseudo-terminal.
925
+ if (options?.requireSetupTokenLoginProvider) {
926
+ await assertSetupTokenLoginProviderCapability(environmentId);
927
+ }
928
+ }
929
+ /**
930
+ * Fails closed when the environment provider does not advertise the Claude
931
+ * setup-token login capability. It resolves the effective provider from the
932
+ * environment config, then reads the static capability from the provider
933
+ * plugin manifest. It never checks the provider by name. A missing plugin, a
934
+ * non-plugin provider, and a provider without the flag all fail closed with
935
+ * the fixed, typed error, so no session row, lease, or pseudo-terminal starts.
936
+ */
937
+ async function assertSetupTokenLoginProviderCapability(environmentId) {
938
+ const environment = await environmentsSvc.getById(environmentId);
939
+ const config = environment?.config && typeof environment.config === "object"
940
+ ? environment.config
941
+ : {};
942
+ const provider = typeof config.provider === "string" ? config.provider : "";
943
+ const resolved = provider
944
+ ? await resolvePluginSandboxProviderDriverByKey({ db, driverKey: provider })
945
+ : null;
946
+ if (!resolved?.driver.supportsSetupTokenLogin) {
947
+ throw unprocessable(SETUP_TOKEN_PROVIDER_UNSUPPORTED, {
948
+ code: SETUP_TOKEN_PROVIDER_UNSUPPORTED_CODE,
949
+ });
950
+ }
951
+ }
952
+ // Read a login session for its owner. The durable row is the authority for the
953
+ // company and the owner. This returns null when the row is absent, when it
954
+ // belongs to another company or adapter, or when the requesting user is not the
955
+ // owner. So a non-owner and a cross-company caller both receive a 404 and cannot
956
+ // enumerate a session. Only the owner path reads the one-time prompt.
957
+ async function readOwnerLoginSession(companyId, adapterType, sessionId, requestingUserId) {
958
+ const row = await adapterLoginStore.get(sessionId);
959
+ if (!row ||
960
+ row.companyId !== companyId ||
961
+ row.adapterType !== adapterType ||
962
+ row.startedByUserId !== requestingUserId) {
963
+ return null;
964
+ }
965
+ return adapterLoginService.readOwnerSession(sessionId, requestingUserId);
966
+ }
656
967
  async function assertCanReadConfigurations(req, companyId) {
657
968
  // Reading agent configurations, skills, and config revisions is a
658
969
  // read-only operation available to any board (human) member of the
@@ -812,6 +1123,35 @@ export function agentRoutes(db, options = {}) {
812
1123
  }
813
1124
  return adapterType;
814
1125
  }
1126
+ /**
1127
+ * Adapter validation for the paths that CHOOSE a harness for a new agent
1128
+ * (hire + create), as opposed to the paths that operate on an existing one.
1129
+ *
1130
+ * A disabled adapter is one this instance cannot run — most often because a
1131
+ * declarative registry (PAPERCLIP_ADAPTERS) curated it out, which
1132
+ * reconcileAdapterAvailability turns into a disabled type at boot. Registered
1133
+ * but disabled still passes assertKnownAdapterType, so an agent could be
1134
+ * created on it and then fail EVERY run at lease time with
1135
+ * `Adapter "..." is not in the configured adapter registry` — an error that
1136
+ * arrives minutes later, in a run log, with no way back to the choice that
1137
+ * caused it. Refuse at selection time instead, and name what can be chosen.
1138
+ *
1139
+ * Existing agents on a now-disabled adapter are deliberately untouched
1140
+ * (listEnabledServerAdapters documents the same rule: hidden from selection,
1141
+ * still functional for agents that already use them).
1142
+ */
1143
+ function assertSelectableAdapterType(type) {
1144
+ const adapterType = assertKnownAdapterType(type);
1145
+ const disabled = new Set(getDisabledAdapterTypes());
1146
+ if (!disabled.has(adapterType))
1147
+ return adapterType;
1148
+ const available = listServerAdapters()
1149
+ .map((a) => a.type)
1150
+ .filter((t) => !disabled.has(t))
1151
+ .sort();
1152
+ throw unprocessable(`Adapter "${adapterType}" is not available on this instance. `
1153
+ + `Available adapters: ${available.length > 0 ? available.join(", ") : "(none configured)"}`);
1154
+ }
815
1155
  async function assertAgentDefaultEnvironmentSelection(companyId, environmentId, options) {
816
1156
  if (environmentId === undefined || environmentId === null)
817
1157
  return;
@@ -1598,6 +1938,99 @@ export function agentRoutes(db, options = {}) {
1598
1938
  await release(releaseStatus);
1599
1939
  }
1600
1940
  });
1941
+ // Start a company-scoped adapter device login. The create form has no agent
1942
+ // identifier, so the route keys on the company and the adapter. The owner
1943
+ // helper requires a board actor with the configuration permission, and it
1944
+ // returns the immutable owner identifier that the service persists on the row.
1945
+ router.post("/companies/:companyId/adapters/:type/login-sessions", async (req, res) => {
1946
+ const companyId = req.params.companyId;
1947
+ const type = req.params.type;
1948
+ const startedByUserId = await assertCanManageAdapterLogin(req, companyId);
1949
+ assertCodexLoginAdapter(type);
1950
+ const environmentId = typeof req.body?.environmentId === "string" && req.body.environmentId.trim().length > 0
1951
+ ? req.body.environmentId
1952
+ : null;
1953
+ if (!environmentId) {
1954
+ throw badRequest("A sandbox environment is required to start a device login.");
1955
+ }
1956
+ const ttlSeconds = typeof req.body?.ttlSeconds === "number" && Number.isFinite(req.body.ttlSeconds)
1957
+ ? req.body.ttlSeconds
1958
+ : undefined;
1959
+ // Reject a non-sandbox or inactive environment before the service starts.
1960
+ await assertSandboxLoginEnvironment(companyId, environmentId);
1961
+ const controller = new AbortController();
1962
+ let result;
1963
+ try {
1964
+ result = await adapterLoginService.start({
1965
+ companyId,
1966
+ environmentId,
1967
+ adapterType: type,
1968
+ startedByUserId,
1969
+ ttlSeconds,
1970
+ signal: controller.signal,
1971
+ });
1972
+ }
1973
+ catch (error) {
1974
+ // A second active login for the same company and adapter loses the
1975
+ // credential slot. Map the service conflict to a 409 response.
1976
+ if (error instanceof AdapterAuthSessionConflictError) {
1977
+ throw conflict(error.message);
1978
+ }
1979
+ throw error;
1980
+ }
1981
+ // Keep the controller so the cancel route can abort the in-flight run.
1982
+ // Drop it when the run ends. The completion runs the terminal handling in
1983
+ // the background; the response returns the initial session at once.
1984
+ const startedSessionId = result.session.sessionId;
1985
+ adapterLoginAbortControllers.set(startedSessionId, controller);
1986
+ void result.completed
1987
+ .catch(() => { })
1988
+ .finally(() => {
1989
+ adapterLoginAbortControllers.delete(startedSessionId);
1990
+ });
1991
+ res.status(201).json(result.session);
1992
+ });
1993
+ // Read a login session. The owner receives the status and the one-time prompt.
1994
+ // A non-owner or a cross-company caller receives a 404.
1995
+ router.get("/companies/:companyId/adapters/:type/login-sessions/:sessionId", async (req, res) => {
1996
+ const companyId = req.params.companyId;
1997
+ const type = req.params.type;
1998
+ const sessionId = req.params.sessionId;
1999
+ const ownerUserId = await assertCanManageAdapterLogin(req, companyId);
2000
+ assertCodexLoginAdapter(type);
2001
+ const owner = await readOwnerLoginSession(companyId, type, sessionId, ownerUserId);
2002
+ if (!owner) {
2003
+ res.status(404).json({ error: "Adapter login session not found" });
2004
+ return;
2005
+ }
2006
+ res.json(owner);
2007
+ });
2008
+ // Cancel a login session. The owner aborts the in-flight run. A non-owner or a
2009
+ // cross-company caller receives a 404.
2010
+ router.post("/companies/:companyId/adapters/:type/login-sessions/:sessionId/cancel", async (req, res) => {
2011
+ const companyId = req.params.companyId;
2012
+ const type = req.params.type;
2013
+ const sessionId = req.params.sessionId;
2014
+ const ownerUserId = await assertCanManageAdapterLogin(req, companyId);
2015
+ assertCodexLoginAdapter(type);
2016
+ // Scope the cancel to this company, adapter, and owner. A non-owner and a
2017
+ // cross-company caller both receive a 404 and cannot cancel a session.
2018
+ const owner = await readOwnerLoginSession(companyId, type, sessionId, ownerUserId);
2019
+ if (!owner) {
2020
+ res.status(404).json({ error: "Adapter login session not found" });
2021
+ return;
2022
+ }
2023
+ // Durably release the company slot. The durable write terminates the row
2024
+ // even when this process does not own the in-flight run, so a cross-process
2025
+ // cancel or a cancel after a restart does not leave the slot held until the
2026
+ // expiry. The reaper deletes the sandbox and finalizes the terminal.
2027
+ const cancelled = await adapterLoginService.cancelOwnerSession(sessionId, ownerUserId);
2028
+ // Abort the in-flight run this process owns, so the local login stops at
2029
+ // once instead of waiting for the reaper. A run in another process, or an
2030
+ // already-terminal run, has no controller here.
2031
+ adapterLoginAbortControllers.get(sessionId)?.abort();
2032
+ res.json(cancelled ?? owner);
2033
+ });
1601
2034
  router.get("/agents/:id/skills", async (req, res) => {
1602
2035
  const id = req.params.id;
1603
2036
  const agent = await svc.getById(id);
@@ -2041,8 +2474,15 @@ export function agentRoutes(db, options = {}) {
2041
2474
  const companyId = req.params.companyId;
2042
2475
  await assertCanCreateAgentsForCompany(req, companyId);
2043
2476
  const sourceIssueIds = parseSourceIssueIds(req.body);
2044
- const { desiredSkills: requestedDesiredSkills, instructionsBundle, sourceIssueId: _sourceIssueId, sourceIssueIds: _sourceIssueIds, ...hireInput } = req.body;
2045
- hireInput.adapterType = assertKnownAdapterType(hireInput.adapterType);
2477
+ const { desiredSkills: requestedDesiredSkills, instructionsBundle, sourceIssueId: _sourceIssueId, sourceIssueIds: _sourceIssueIds,
2478
+ // The stored-session claim is not an agent column. The server derives the
2479
+ // owner from the authenticated actor and consumes the claim in the create
2480
+ // transaction, so it never reaches the insert values.
2481
+ storedSessionId: hireStoredSessionId,
2482
+ // The apply-existing flag is not an agent column. The server binds the
2483
+ // fixed reference to the owner stored value with no login round trip.
2484
+ applyStoredClaudeLogin: hireApplyStoredClaudeLogin, ...hireInput } = req.body;
2485
+ hireInput.adapterType = assertSelectableAdapterType(hireInput.adapterType);
2046
2486
  const rawHireAdapterConfig = (hireInput.adapterConfig ?? {});
2047
2487
  assertNoNewAgentLegacyPromptTemplate(hireInput.adapterType, rawHireAdapterConfig);
2048
2488
  assertNoAgentAdapterConfigMutation(req, rawHireAdapterConfig);
@@ -2078,6 +2518,14 @@ export function agentRoutes(db, options = {}) {
2078
2518
  status,
2079
2519
  spentMonthlyCents: 0,
2080
2520
  lastHeartbeatAt: null,
2521
+ }, {
2522
+ claudeLogin: {
2523
+ storedSessionId: hireStoredSessionId ?? null,
2524
+ ownerUserId: req.actor.type === "agent" ? null : (req.actor.userId ?? null),
2525
+ // The apply-existing path runs only for a user actor. The owner comes
2526
+ // from the actor, so an agent actor never reaches the no-claim bind.
2527
+ applyExistingWithoutClaim: req.actor.type !== "agent" && hireApplyStoredClaudeLogin === true,
2528
+ },
2081
2529
  });
2082
2530
  const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent, instructionsBundle);
2083
2531
  let approval = null;
@@ -2183,8 +2631,15 @@ export function agentRoutes(db, options = {}) {
2183
2631
  if (company.requireBoardApprovalForNewAgents) {
2184
2632
  throw conflict("Direct agent creation requires board approval. Use POST /api/companies/:companyId/agent-hires to create a pending hire approval.");
2185
2633
  }
2186
- const { desiredSkills: requestedDesiredSkills, instructionsBundle, ...createInput } = req.body;
2187
- createInput.adapterType = assertKnownAdapterType(createInput.adapterType);
2634
+ const { desiredSkills: requestedDesiredSkills, instructionsBundle,
2635
+ // The stored-session claim is not an agent column. The server derives the
2636
+ // owner from the authenticated actor and consumes the claim in the create
2637
+ // transaction, so it never reaches the insert values.
2638
+ storedSessionId: createStoredSessionId,
2639
+ // The apply-existing flag is not an agent column. The server binds the
2640
+ // fixed reference to the owner stored value with no login round trip.
2641
+ applyStoredClaudeLogin: createApplyStoredClaudeLogin, ...createInput } = req.body;
2642
+ createInput.adapterType = assertSelectableAdapterType(createInput.adapterType);
2188
2643
  const rawCreateAdapterConfig = (createInput.adapterConfig ?? {});
2189
2644
  assertNoNewAgentLegacyPromptTemplate(createInput.adapterType, rawCreateAdapterConfig);
2190
2645
  assertNoAgentAdapterConfigMutation(req, rawCreateAdapterConfig);
@@ -2211,6 +2666,14 @@ export function agentRoutes(db, options = {}) {
2211
2666
  status: "idle",
2212
2667
  spentMonthlyCents: 0,
2213
2668
  lastHeartbeatAt: null,
2669
+ }, {
2670
+ claudeLogin: {
2671
+ storedSessionId: createStoredSessionId ?? null,
2672
+ ownerUserId: req.actor.type === "agent" ? null : (req.actor.userId ?? null),
2673
+ // The apply-existing path runs only for a user actor. The owner comes
2674
+ // from the actor, so an agent actor never reaches the no-claim bind.
2675
+ applyExistingWithoutClaim: req.actor.type !== "agent" && createApplyStoredClaudeLogin === true,
2676
+ },
2214
2677
  });
2215
2678
  const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent, instructionsBundle);
2216
2679
  const actor = getActorInfo(req);
@@ -2491,6 +2954,11 @@ export function agentRoutes(db, options = {}) {
2491
2954
  const patchData = { ...req.body };
2492
2955
  const replaceAdapterConfig = patchData.replaceAdapterConfig === true;
2493
2956
  delete patchData.replaceAdapterConfig;
2957
+ // The apply-existing flag is not an agent column. The server binds the fixed
2958
+ // reference to the owner stored value with no login round trip. Remove it
2959
+ // from the patch so it never reaches the update values.
2960
+ const applyStoredClaudeLogin = patchData.applyStoredClaudeLogin === true;
2961
+ delete patchData.applyStoredClaudeLogin;
2494
2962
  if (hasOwn(patchData, "adapterConfig")) {
2495
2963
  const adapterConfig = asRecord(patchData.adapterConfig);
2496
2964
  if (!adapterConfig) {
@@ -2504,8 +2972,15 @@ export function agentRoutes(db, options = {}) {
2504
2972
  }
2505
2973
  patchData.adapterConfig = adapterConfig;
2506
2974
  }
2975
+ // Switching an existing agent ONTO another adapter is a new selection, so
2976
+ // it gets the selectable check; keeping the agent's current adapter (even
2977
+ // one since disabled) stays allowed, so a disabled harness does not make an
2978
+ // existing agent uneditable.
2507
2979
  const requestedAdapterType = hasOwn(patchData, "adapterType")
2508
- ? assertKnownAdapterType(patchData.adapterType)
2980
+ ? (() => {
2981
+ const next = assertKnownAdapterType(patchData.adapterType);
2982
+ return next === existing.adapterType ? next : assertSelectableAdapterType(next);
2983
+ })()
2509
2984
  : existing.adapterType;
2510
2985
  let requestedRuntimeConfig = null;
2511
2986
  if (hasOwn(patchData, "runtimeConfig")) {
@@ -2582,6 +3057,12 @@ export function agentRoutes(db, options = {}) {
2582
3057
  createdByUserId: actor.actorType === "user" ? actor.actorId : null,
2583
3058
  source: "patch",
2584
3059
  },
3060
+ claudeLogin: {
3061
+ ownerUserId: req.actor.type === "agent" ? null : (req.actor.userId ?? null),
3062
+ // The apply-existing path runs only for a user actor. The owner comes
3063
+ // from the actor, so an agent actor never reaches the no-claim bind.
3064
+ applyExistingWithoutClaim: req.actor.type !== "agent" && applyStoredClaudeLogin,
3065
+ },
2585
3066
  });
2586
3067
  if (!agent) {
2587
3068
  res.status(404).json({ error: "Agent not found" });
@@ -3032,6 +3513,392 @@ export function agentRoutes(db, options = {}) {
3032
3513
  });
3033
3514
  res.json(result);
3034
3515
  });
3516
+ // --- Setup-token login session routes --------------------------------------
3517
+ //
3518
+ // The routes give the UI operations against one live login session. Every
3519
+ // operation verifies the company and owner user through the session scope. A
3520
+ // missing session and a cross-scope session both return the same 404. The
3521
+ // confidential responses pass through the transport assessment and set
3522
+ // `Cache-Control: no-store`. The routes write no prompt, code, token, or raw
3523
+ // process chunk to a log or an activity detail, and they return fixed error
3524
+ // text only.
3525
+ //
3526
+ // Operator requirement (SR-7): to serve the confidential responses behind a
3527
+ // TLS-terminating reverse proxy, set `CLAUDE_LOGIN_TRUSTED_PROXIES` to the
3528
+ // explicit proxy IP or CIDR allowlist — or, on a managed platform whose edge
3529
+ // always terminates TLS and whose proxy peer addresses cannot be allowlisted,
3530
+ // declare `CLAUDE_LOGIN_EDGE_TLS_TERMINATED=true`. The global `TRUST_PROXY`
3531
+ // setting, including `TRUST_PROXY=true` and a hop-count value, does not
3532
+ // satisfy the guard. A direct TLS request is always valid; a non-TLS request
3533
+ // is valid only on a loopback peer in the `local_trusted` deployment mode.
3534
+ //
3535
+ // Each route below writes its full path as a plain string literal. The static
3536
+ // OpenAPI coverage test reads the route paths from the source text; it does
3537
+ // not evaluate a template variable. A shared base constant would leave the
3538
+ // test with an unresolved path, so the routes repeat the base path instead.
3539
+ /**
3540
+ * Derives the immutable owner of a setup-token login session from the actor.
3541
+ * Only a board user owns a login session. It returns the owner id, or it
3542
+ * throws a forbidden error. The owner is never a client field; it comes only
3543
+ * from the authenticated actor.
3544
+ */
3545
+ const deriveSetupTokenOwnerUserId = (req) => {
3546
+ const actor = getActorInfo(req);
3547
+ if (actor.actorType !== "user") {
3548
+ throw forbidden("A user must own a setup-token login session.");
3549
+ }
3550
+ return actor.actorId;
3551
+ };
3552
+ /**
3553
+ * Read-access gate for the company-scoped setup-token session routes. It runs
3554
+ * before a route resolves a session. The session id is an opaque secret-bearing
3555
+ * reference, so a cross-company reference must fail closed like a missing
3556
+ * session. This gate returns the same fixed not-found error for a cross-company
3557
+ * reference by an authenticated non-member as for a missing session, so the
3558
+ * route is not a company-membership oracle. It keeps the not-found equivalence
3559
+ * the session lookups use.
3560
+ *
3561
+ * The gate keeps the actor rules unchanged. It throws 401 for an unauthenticated
3562
+ * caller and 403 for a non-user actor through the owner derivation. For an
3563
+ * authorized member it runs the full `assertCompanyAccess` write-path checks and
3564
+ * returns the owner user id. For a non-member it sends the fixed 404 and returns
3565
+ * null; the route must stop.
3566
+ */
3567
+ const resolveCompanySessionOwner = (req, companyId, res) => {
3568
+ assertAuthenticated(req);
3569
+ const ownerUserId = deriveSetupTokenOwnerUserId(req);
3570
+ if (!hasCompanyAccess(req, companyId)) {
3571
+ res.setHeader("Cache-Control", "no-store");
3572
+ res.status(404).json({ error: SETUP_TOKEN_SESSION_NOT_FOUND });
3573
+ return null;
3574
+ }
3575
+ assertCompanyAccess(req, companyId);
3576
+ return ownerUserId;
3577
+ };
3578
+ /**
3579
+ * Assesses the setup-token confidential transport. The product
3580
+ * owner set a non-negotiable requirement: do not force TLS. Many users run
3581
+ * Paperclip over plain HTTP on a home server or a Tailscale tailnet. So the
3582
+ * route does not block a non-confidential transport. It returns a non-blocking
3583
+ * advisory instead, and the route attaches it to the confidential response.
3584
+ * The client shows a visible disclaimer and lets the login proceed. The
3585
+ * function reads the raw socket TLS bit and the immediate peer address, so the
3586
+ * global `trust proxy` setting cannot change the result. It returns null when
3587
+ * the transport is confidential (direct TLS, a local-trusted loopback, or an
3588
+ * allowlisted TLS proxy), so a confidential response shows no disclaimer.
3589
+ */
3590
+ const assessSetupTokenTransport = (req) => {
3591
+ const socket = req.socket;
3592
+ const forwardedProto = req.headers["x-forwarded-proto"];
3593
+ const decision = evaluateConfidentialTransport(setupTokenConfidentialConfig, {
3594
+ socketEncrypted: socket?.encrypted === true,
3595
+ remoteAddress: socket?.remoteAddress,
3596
+ forwardedProto: Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto,
3597
+ });
3598
+ return decision.allowed ? null : { code: SETUP_TOKEN_TRANSPORT_ADVISORY_CODE };
3599
+ };
3600
+ const sendSetupTokenError = (res, err) => {
3601
+ if (err instanceof SetupTokenSessionError) {
3602
+ res.status(err.status).json({ error: err.message });
3603
+ return;
3604
+ }
3605
+ throw err;
3606
+ };
3607
+ // --- Company-and-environment setup-token login routes ----------------------
3608
+ //
3609
+ // These routes serve the agentless Claude login. The scope binds one login to
3610
+ // one company, one owner user, one adapter, and one environment. The scope
3611
+ // carries no agent id, so a hire flow starts one login before an agent exists.
3612
+ //
3613
+ // Object-level authorization: every action derives the owner from
3614
+ // the authenticated actor, fixes the adapter to `claude_local`, and resolves
3615
+ // the environment server-side. The lookup scopes by the immutable tuple
3616
+ // company, owner, adapter, environment, and session. A foreign session returns
3617
+ // the same not-found error as a missing session, so a caller cannot enumerate
3618
+ // a session across a company, an owner, an adapter, or an environment.
3619
+ //
3620
+ // Each route writes its full path as a plain string literal, so the static
3621
+ // OpenAPI coverage test can read the path from the source text.
3622
+ // The company-and-environment login serves only the Claude adapter.
3623
+ const CLAUDE_SETUP_TOKEN_ADAPTER_TYPE = "claude_local";
3624
+ // Maps the internal session state to the public login status. The public union
3625
+ // carries no server-only state, so the route never returns the internal
3626
+ // `submitting` or `stored` state to a client.
3627
+ const toClaudeLoginStatus = (state) => {
3628
+ switch (state) {
3629
+ case "starting":
3630
+ return "starting";
3631
+ case "awaiting_code":
3632
+ case "submitting":
3633
+ case "stored":
3634
+ return "waiting_for_user";
3635
+ case "completed":
3636
+ return "authenticated";
3637
+ case "failed":
3638
+ return "failed";
3639
+ case "timed_out":
3640
+ return "timed_out";
3641
+ case "cancelled":
3642
+ return "cancelled";
3643
+ }
3644
+ };
3645
+ // Builds the fixed, non-secret failure for a terminal failure state. A live or
3646
+ // a completed session has no failure. The failure carries a stable reason and
3647
+ // no secret detail.
3648
+ const toClaudeLoginFailure = (state) => {
3649
+ switch (state) {
3650
+ case "failed":
3651
+ return { reason: "failed", message: null };
3652
+ case "timed_out":
3653
+ return { reason: "timed_out", message: null };
3654
+ case "cancelled":
3655
+ return { reason: "cancelled", message: null };
3656
+ default:
3657
+ return null;
3658
+ }
3659
+ };
3660
+ // The public login-session response. It carries no prompt and no secret.
3661
+ const toClaudePublicResponse = (descriptor) => ({
3662
+ sessionId: descriptor.sessionId,
3663
+ environmentId: descriptor.environmentId,
3664
+ status: toClaudeLoginStatus(descriptor.state),
3665
+ expiresAt: new Date(descriptor.deadline).toISOString(),
3666
+ failure: toClaudeLoginFailure(descriptor.state),
3667
+ });
3668
+ // The company-and-environment login key the non-start routes derive. The route
3669
+ // path gives the company, the actor gives the owner, and the route fixes the
3670
+ // adapter. The service matches this key and the agentless marker.
3671
+ const companySetupTokenKey = (companyId, ownerUserId) => ({
3672
+ companyId,
3673
+ ownerUserId,
3674
+ adapterType: CLAUDE_SETUP_TOKEN_ADAPTER_TYPE,
3675
+ });
3676
+ // The stored Claude OAuth token status read. It returns
3677
+ // only the secret id and the latest version of the owner value; it returns no
3678
+ // token. The client reads the version, applies the stored token first, and
3679
+ // captures the version for a later confirmed overwrite. The route derives the
3680
+ // owner only from the authenticated actor and reads the fixed Claude
3681
+ // definition; it accepts no owner, no definition, and no secret id as input.
3682
+ //
3683
+ // The route returns the same fixed 404 for a missing owner value as the
3684
+ // company gate returns for a non-member, so it discloses no existence
3685
+ // distinction across owners or companies. It sets `Cache-Control: no-store`,
3686
+ // so no cache holds the metadata.
3687
+ router.get("/companies/:companyId/claude-oauth-token-status", async (req, res) => {
3688
+ const companyId = req.params.companyId;
3689
+ const ownerUserId = resolveCompanySessionOwner(req, companyId, res);
3690
+ if (ownerUserId === null)
3691
+ return;
3692
+ res.setHeader("Cache-Control", "no-store");
3693
+ const status = await secretsSvc.readClaudeOAuthUserSecretStatus(companyId, ownerUserId);
3694
+ if (!status) {
3695
+ // A missing owner value returns the same fixed not-found as the non-member
3696
+ // gate, so a member without a value and a non-member look the same.
3697
+ res.status(404).json({ error: SETUP_TOKEN_SESSION_NOT_FOUND });
3698
+ return;
3699
+ }
3700
+ const body = status;
3701
+ res.json(body);
3702
+ });
3703
+ router.post("/companies/:companyId/setup-token-login-sessions", async (req, res) => {
3704
+ const companyId = req.params.companyId;
3705
+ assertCompanyAccess(req, companyId);
3706
+ const ownerUserId = deriveSetupTokenOwnerUserId(req);
3707
+ res.setHeader("Cache-Control", "no-store");
3708
+ // Parse the request with the shared strict validator before any session,
3709
+ // lease, or pseudo-terminal side effect. `.strict()` rejects an unknown field,
3710
+ // including a legacy `ttlSeconds`, with a fixed 400. The route echoes no input.
3711
+ const parsed = startClaudeSetupTokenSessionRequestSchema.safeParse(req.body);
3712
+ if (!parsed.success) {
3713
+ res.status(400).json({ error: "The Claude login start request is invalid." });
3714
+ return;
3715
+ }
3716
+ const { environmentId, adapterType } = parsed.data;
3717
+ const confirmedOverwrite = parsed.data.overwrite ?? null;
3718
+ // The company-and-environment login serves only the Claude adapter. A
3719
+ // different adapter type gets a fixed 400.
3720
+ if (adapterType !== CLAUDE_SETUP_TOKEN_ADAPTER_TYPE) {
3721
+ res.status(400).json({ error: "Only the claude_local adapter supports a setup-token login." });
3722
+ return;
3723
+ }
3724
+ if (!SETUP_TOKEN_LOGIN_TRANSPORT_READY) {
3725
+ // The live login transport binds at a later call site. Fail closed with the
3726
+ // fixed no-secret error until then.
3727
+ res.status(503).json({ error: SETUP_TOKEN_START_FAILED });
3728
+ return;
3729
+ }
3730
+ // Resolve the environment server-side to a live instance sandbox. It fails
3731
+ // closed on a missing, archived, non-sandbox, or fake-provider environment, so
3732
+ // the route never persists an empty or invalid environment scope. It runs
3733
+ // before the session starts, so no store holds a rejected environment. It
3734
+ // also rejects an environment that another company owns (see
3735
+ // `assertSandboxLoginEnvironment`), so a login never runs in a foreign
3736
+ // company sandbox. It also fails closed when the provider does not advertise
3737
+ // the setup-token login capability, so an unsupported provider never reaches
3738
+ // a session row, a lease, or a pseudo-terminal.
3739
+ await assertSandboxLoginEnvironment(companyId, environmentId, {
3740
+ requireSetupTokenLoginProvider: true,
3741
+ });
3742
+ const scope = {
3743
+ companyId,
3744
+ ownerUserId,
3745
+ targetAgentId: null,
3746
+ adapterType,
3747
+ environmentId,
3748
+ confirmedOverwrite,
3749
+ };
3750
+ try {
3751
+ const started = await setupTokenLoginService.start(scope);
3752
+ const descriptor = setupTokenLoginService.describeOwned(started.sessionId, scope);
3753
+ // The start response carries the panel mode, so the client renders the
3754
+ // browser-code panel. The full login URL rides only through the
3755
+ // guarded prompt read, not the start response, so the prompt is null here.
3756
+ // The client reads the prompt route for the login URL.
3757
+ const body = {
3758
+ ...toClaudePublicResponse(descriptor),
3759
+ panelMode: "submitted_browser_code",
3760
+ prompt: null,
3761
+ };
3762
+ res.status(201).json(body);
3763
+ }
3764
+ catch (err) {
3765
+ sendSetupTokenError(res, err);
3766
+ }
3767
+ });
3768
+ router.get("/companies/:companyId/setup-token-login-sessions/:sessionId", async (req, res) => {
3769
+ const companyId = req.params.companyId;
3770
+ const ownerUserId = resolveCompanySessionOwner(req, companyId, res);
3771
+ if (ownerUserId === null)
3772
+ return;
3773
+ res.setHeader("Cache-Control", "no-store");
3774
+ try {
3775
+ const sessionId = req.params.sessionId;
3776
+ const scope = setupTokenLoginService.resolveCompanyScope(sessionId, companySetupTokenKey(companyId, ownerUserId));
3777
+ const descriptor = setupTokenLoginService.describeOwned(sessionId, scope);
3778
+ // The status response is public. It carries no prompt and no secret.
3779
+ res.json(toClaudePublicResponse(descriptor));
3780
+ }
3781
+ catch (err) {
3782
+ sendSetupTokenError(res, err);
3783
+ }
3784
+ });
3785
+ router.get("/companies/:companyId/setup-token-login-sessions/:sessionId/prompt", async (req, res) => {
3786
+ const companyId = req.params.companyId;
3787
+ const ownerUserId = resolveCompanySessionOwner(req, companyId, res);
3788
+ if (ownerUserId === null)
3789
+ return;
3790
+ res.setHeader("Cache-Control", "no-store");
3791
+ // The full login URL is a confidential response. The route
3792
+ // does not force TLS. It attaches a non-blocking advisory instead.
3793
+ const transportAdvisory = assessSetupTokenTransport(req);
3794
+ try {
3795
+ const sessionId = req.params.sessionId;
3796
+ const scope = setupTokenLoginService.resolveCompanyScope(sessionId, companySetupTokenKey(companyId, ownerUserId));
3797
+ const descriptor = setupTokenLoginService.describeOwned(sessionId, scope);
3798
+ if (!descriptor.loginUrl) {
3799
+ // The prompt has not surfaced yet. Return the same not-found error as a
3800
+ // missing or a foreign session, so the route never confirms the session
3801
+ // exists before the URL is ready.
3802
+ res.status(404).json({ error: SETUP_TOKEN_SESSION_NOT_FOUND });
3803
+ return;
3804
+ }
3805
+ // The full login URL rides only in this authorized owner response.
3806
+ const body = {
3807
+ authorizationUrl: descriptor.loginUrl,
3808
+ transportAdvisory,
3809
+ };
3810
+ res.json(body);
3811
+ }
3812
+ catch (err) {
3813
+ sendSetupTokenError(res, err);
3814
+ }
3815
+ });
3816
+ router.post("/companies/:companyId/setup-token-login-sessions/:sessionId/code", async (req, res) => {
3817
+ const companyId = req.params.companyId;
3818
+ const ownerUserId = resolveCompanySessionOwner(req, companyId, res);
3819
+ if (ownerUserId === null)
3820
+ return;
3821
+ res.setHeader("Cache-Control", "no-store");
3822
+ // The browser code is the confidential OAuth authorization
3823
+ // secret. The route does not force TLS. It attaches a non-blocking advisory
3824
+ // to the response instead, so the client can show a disclaimer.
3825
+ const transportAdvisory = assessSetupTokenTransport(req);
3826
+ // Parse the request with the shared strict validator before the route forwards
3827
+ // the code to the live process. `.strict()` rejects an unknown field, and the
3828
+ // grammar rejects an empty, an oversized, or a control-byte code. The route
3829
+ // echoes no input; it returns fixed error text only.
3830
+ const parsed = submitBrowserCodeRequestSchema.safeParse(req.body);
3831
+ if (!parsed.success) {
3832
+ res.status(400).json({ error: "A valid browser code is required." });
3833
+ return;
3834
+ }
3835
+ try {
3836
+ const sessionId = req.params.sessionId;
3837
+ const scope = setupTokenLoginService.resolveCompanyScope(sessionId, companySetupTokenKey(companyId, ownerUserId));
3838
+ setupTokenLoginService.submitCode(sessionId, scope, parsed.data.browserCode);
3839
+ const descriptor = setupTokenLoginService.describeOwned(sessionId, scope);
3840
+ const body = {
3841
+ ...toClaudePublicResponse(descriptor),
3842
+ transportAdvisory,
3843
+ };
3844
+ res.json(body);
3845
+ }
3846
+ catch (err) {
3847
+ sendSetupTokenError(res, err);
3848
+ }
3849
+ });
3850
+ router.post("/companies/:companyId/setup-token-login-sessions/:sessionId/completion", async (req, res) => {
3851
+ const companyId = req.params.companyId;
3852
+ const ownerUserId = resolveCompanySessionOwner(req, companyId, res);
3853
+ if (ownerUserId === null)
3854
+ return;
3855
+ res.setHeader("Cache-Control", "no-store");
3856
+ try {
3857
+ const sessionId = req.params.sessionId;
3858
+ const scope = setupTokenLoginService.resolveCompanyScope(sessionId, companySetupTokenKey(companyId, ownerUserId));
3859
+ // The service returns the non-secret `storedSessionId` claim from a
3860
+ // completed session whose owner-bound secret write succeeded. The response
3861
+ // carries no token.
3862
+ const result = setupTokenLoginService.completeSession(sessionId, scope);
3863
+ const body = { storedSessionId: result.storedSessionId };
3864
+ res.json(body);
3865
+ }
3866
+ catch (err) {
3867
+ sendSetupTokenError(res, err);
3868
+ }
3869
+ });
3870
+ router.post("/companies/:companyId/setup-token-login-sessions/:sessionId/cancel", async (req, res) => {
3871
+ const companyId = req.params.companyId;
3872
+ const ownerUserId = resolveCompanySessionOwner(req, companyId, res);
3873
+ if (ownerUserId === null)
3874
+ return;
3875
+ res.setHeader("Cache-Control", "no-store");
3876
+ const sessionId = req.params.sessionId;
3877
+ try {
3878
+ const scope = setupTokenLoginService.resolveCompanyScope(sessionId, companySetupTokenKey(companyId, ownerUserId));
3879
+ await setupTokenLoginService.cancel(sessionId, scope);
3880
+ res.status(200).json({});
3881
+ }
3882
+ catch (err) {
3883
+ // Cancel is idempotent. The service removes a session when it reaches a
3884
+ // terminal state, so a repeat cancel, a cancel after a timeout, or a
3885
+ // cancel of an unknown session finds no record and throws the fixed
3886
+ // not-found error. Return the same success as an active cancel, so the
3887
+ // client stops the poll and returns to its start state.
3888
+ //
3889
+ // This keeps the not-found uniform. The 200 response is identical
3890
+ // for a missing session, an already-terminal session, and a foreign
3891
+ // session, so the route never confirms a session exists and cancels
3892
+ // nothing for a foreign id. A non-member still fails closed with a 404 at
3893
+ // the company-access gate above, before this handler runs. A non-404
3894
+ // error still surfaces.
3895
+ if (err instanceof SetupTokenSessionError && err.status === 404) {
3896
+ res.status(200).json({});
3897
+ return;
3898
+ }
3899
+ sendSetupTokenError(res, err);
3900
+ }
3901
+ });
3035
3902
  router.get("/companies/:companyId/heartbeat-runs", async (req, res) => {
3036
3903
  const companyId = req.params.companyId;
3037
3904
  assertCompanyAccess(req, companyId);