@capybara-hq/kernel 0.0.0-stage → 0.1.0-rc.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 (637) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +20 -2
  4. package/bootstrap.json +26 -0
  5. package/dist/admin/admin.controller.d.ts +139 -0
  6. package/dist/admin/admin.controller.js +238 -0
  7. package/dist/admin/extensions.controller.d.ts +100 -0
  8. package/dist/admin/extensions.controller.js +226 -0
  9. package/dist/admin/setup.controller.d.ts +36 -0
  10. package/dist/admin/setup.controller.js +106 -0
  11. package/dist/admin/setup.service.d.ts +47 -0
  12. package/dist/admin/setup.service.js +90 -0
  13. package/dist/agents/agents.controller.d.ts +205 -0
  14. package/dist/agents/agents.controller.js +144 -0
  15. package/dist/agents/agents.service.d.ts +177 -0
  16. package/dist/agents/agents.service.js +677 -0
  17. package/dist/agents/definition.d.ts +198 -0
  18. package/dist/agents/definition.js +152 -0
  19. package/dist/agents/models.controller.d.ts +13 -0
  20. package/dist/agents/models.controller.js +31 -0
  21. package/dist/agents/templates.controller.d.ts +15 -0
  22. package/dist/agents/templates.controller.js +37 -0
  23. package/dist/agents/templates.d.ts +83 -0
  24. package/dist/agents/templates.js +173 -0
  25. package/dist/catalog/admin-catalog.controller.d.ts +143 -0
  26. package/dist/catalog/admin-catalog.controller.js +245 -0
  27. package/dist/catalog/catalog.service.d.ts +430 -0
  28. package/dist/catalog/catalog.service.js +882 -0
  29. package/dist/catalog/claude/agents.d.ts +27 -0
  30. package/dist/catalog/claude/agents.js +270 -0
  31. package/dist/catalog/claude/commands.d.ts +50 -0
  32. package/dist/catalog/claude/commands.js +179 -0
  33. package/dist/catalog/claude/manifest.d.ts +69 -0
  34. package/dist/catalog/claude/manifest.js +134 -0
  35. package/dist/catalog/claude/marketplace.d.ts +35 -0
  36. package/dist/catalog/claude/marketplace.js +194 -0
  37. package/dist/catalog/claude/mcp.d.ts +33 -0
  38. package/dist/catalog/claude/mcp.js +226 -0
  39. package/dist/catalog/claude/report.d.ts +86 -0
  40. package/dist/catalog/claude/report.js +121 -0
  41. package/dist/catalog/claude/translate.d.ts +56 -0
  42. package/dist/catalog/claude/translate.js +857 -0
  43. package/dist/catalog/cli.d.ts +46 -0
  44. package/dist/catalog/cli.js +211 -0
  45. package/dist/catalog/git-source.d.ts +127 -0
  46. package/dist/catalog/git-source.js +520 -0
  47. package/dist/catalog/marketplace-fetch.d.ts +99 -0
  48. package/dist/catalog/marketplace-fetch.js +203 -0
  49. package/dist/catalog/marketplaces.controller.d.ts +97 -0
  50. package/dist/catalog/marketplaces.controller.js +197 -0
  51. package/dist/catalog/marketplaces.service.d.ts +340 -0
  52. package/dist/catalog/marketplaces.service.js +1217 -0
  53. package/dist/catalog/org-catalog.controller.d.ts +128 -0
  54. package/dist/catalog/org-catalog.controller.js +277 -0
  55. package/dist/catalog/org-catalog.service.d.ts +246 -0
  56. package/dist/catalog/org-catalog.service.js +625 -0
  57. package/dist/catalog/plugin-import.d.ts +436 -0
  58. package/dist/catalog/plugin-import.js +1069 -0
  59. package/dist/catalog/plugin-install.d.ts +447 -0
  60. package/dist/catalog/plugin-install.js +1333 -0
  61. package/dist/catalog/plugin-package.d.ts +143 -0
  62. package/dist/catalog/plugin-package.js +382 -0
  63. package/dist/catalog/plugin-upgrade.d.ts +222 -0
  64. package/dist/catalog/plugin-upgrade.js +1219 -0
  65. package/dist/catalog/requests.service.d.ts +162 -0
  66. package/dist/catalog/requests.service.js +504 -0
  67. package/dist/catalog/schema-path.d.ts +7 -0
  68. package/dist/catalog/schema-path.js +63 -0
  69. package/dist/catalog/upgrade-targets.d.ts +59 -0
  70. package/dist/catalog/upgrade-targets.js +90 -0
  71. package/dist/catalog/versions.d.ts +31 -0
  72. package/dist/catalog/versions.js +44 -0
  73. package/dist/channels/actions.d.ts +37 -0
  74. package/dist/channels/actions.js +121 -0
  75. package/dist/channels/cards.d.ts +44 -0
  76. package/dist/channels/cards.js +156 -0
  77. package/dist/channels/connections.controller.d.ts +59 -0
  78. package/dist/channels/connections.controller.js +115 -0
  79. package/dist/channels/connections.service.d.ts +187 -0
  80. package/dist/channels/connections.service.js +534 -0
  81. package/dist/channels/features.d.ts +12 -0
  82. package/dist/channels/features.js +22 -0
  83. package/dist/channels/inbound.d.ts +120 -0
  84. package/dist/channels/inbound.js +332 -0
  85. package/dist/channels/outbound.d.ts +140 -0
  86. package/dist/channels/outbound.js +1126 -0
  87. package/dist/channels/phrases.d.ts +100 -0
  88. package/dist/channels/phrases.js +81 -0
  89. package/dist/channels/supervisor.d.ts +182 -0
  90. package/dist/channels/supervisor.js +625 -0
  91. package/dist/channels/threads.d.ts +29 -0
  92. package/dist/channels/threads.js +104 -0
  93. package/dist/channels/webhook-url.d.ts +16 -0
  94. package/dist/channels/webhook-url.js +34 -0
  95. package/dist/channels/webhook.controller.d.ts +19 -0
  96. package/dist/channels/webhook.controller.js +72 -0
  97. package/dist/channels/webhook.d.ts +73 -0
  98. package/dist/channels/webhook.js +274 -0
  99. package/dist/config/docs.d.ts +9 -0
  100. package/dist/config/docs.js +85 -0
  101. package/dist/config/env.d.ts +6 -0
  102. package/dist/config/env.js +73 -0
  103. package/dist/config/fields.d.ts +42 -0
  104. package/dist/config/fields.js +214 -0
  105. package/dist/config/file-source.d.ts +3 -0
  106. package/dist/config/file-source.js +31 -0
  107. package/dist/config/load.d.ts +20 -0
  108. package/dist/config/load.js +98 -0
  109. package/dist/config/nacos-source.d.ts +31 -0
  110. package/dist/config/nacos-source.js +229 -0
  111. package/dist/config/schema.d.ts +237 -0
  112. package/dist/config/schema.js +1192 -0
  113. package/dist/config/sources.d.ts +18 -0
  114. package/dist/config/sources.js +59 -0
  115. package/dist/config/types.d.ts +9 -0
  116. package/dist/config/types.js +7 -0
  117. package/dist/conversations/artifacts.d.ts +52 -0
  118. package/dist/conversations/artifacts.js +137 -0
  119. package/dist/conversations/attachments.controller.d.ts +28 -0
  120. package/dist/conversations/attachments.controller.js +96 -0
  121. package/dist/conversations/attachments.service.d.ts +180 -0
  122. package/dist/conversations/attachments.service.js +578 -0
  123. package/dist/conversations/conversations.controller.d.ts +288 -0
  124. package/dist/conversations/conversations.controller.js +607 -0
  125. package/dist/conversations/conversations.service.d.ts +321 -0
  126. package/dist/conversations/conversations.service.js +754 -0
  127. package/dist/conversations/event-log.d.ts +122 -0
  128. package/dist/conversations/event-log.js +259 -0
  129. package/dist/conversations/events.d.ts +856 -0
  130. package/dist/conversations/events.js +639 -0
  131. package/dist/conversations/files.d.ts +92 -0
  132. package/dist/conversations/files.js +213 -0
  133. package/dist/conversations/outputs-read.d.ts +34 -0
  134. package/dist/conversations/outputs-read.js +37 -0
  135. package/dist/conversations/queue.d.ts +169 -0
  136. package/dist/conversations/queue.js +527 -0
  137. package/dist/conversations/sandbox-status.d.ts +21 -0
  138. package/dist/conversations/sandbox-status.js +24 -0
  139. package/dist/conversations/selection.d.ts +46 -0
  140. package/dist/conversations/selection.js +75 -0
  141. package/dist/conversations/titles.d.ts +93 -0
  142. package/dist/conversations/titles.js +423 -0
  143. package/dist/ctx/bus.d.ts +16 -0
  144. package/dist/ctx/bus.js +101 -0
  145. package/dist/ctx/create-ctx.d.ts +109 -0
  146. package/dist/ctx/create-ctx.js +592 -0
  147. package/dist/ctx/kv.d.ts +4 -0
  148. package/dist/ctx/kv.js +31 -0
  149. package/dist/ctx/lock.d.ts +4 -0
  150. package/dist/ctx/lock.js +39 -0
  151. package/dist/ctx/namespace.d.ts +6 -0
  152. package/dist/ctx/namespace.js +12 -0
  153. package/dist/ctx/queue.d.ts +25 -0
  154. package/dist/ctx/queue.js +151 -0
  155. package/dist/ctx/secrets.d.ts +45 -0
  156. package/dist/ctx/secrets.js +175 -0
  157. package/dist/db/kernel-schema.d.ts +1122 -0
  158. package/dist/db/kernel-schema.js +5 -0
  159. package/dist/db/migrator.d.ts +28 -0
  160. package/dist/db/migrator.js +140 -0
  161. package/dist/db/pool.d.ts +21 -0
  162. package/dist/db/pool.js +28 -0
  163. package/dist/db/sql-statements.d.ts +34 -0
  164. package/dist/db/sql-statements.js +168 -0
  165. package/dist/errors.d.ts +211 -0
  166. package/dist/errors.js +213 -0
  167. package/dist/evals/budget.d.ts +61 -0
  168. package/dist/evals/budget.js +181 -0
  169. package/dist/evals/diff.d.ts +55 -0
  170. package/dist/evals/diff.js +133 -0
  171. package/dist/evals/judge.d.ts +53 -0
  172. package/dist/evals/judge.js +149 -0
  173. package/dist/evals/plan.d.ts +47 -0
  174. package/dist/evals/plan.js +77 -0
  175. package/dist/evals/reconstruct.d.ts +137 -0
  176. package/dist/evals/reconstruct.js +322 -0
  177. package/dist/evals/recorded-tools.d.ts +54 -0
  178. package/dist/evals/recorded-tools.js +167 -0
  179. package/dist/evals/run-replay.d.ts +86 -0
  180. package/dist/evals/run-replay.js +502 -0
  181. package/dist/evals/runs.controller.d.ts +100 -0
  182. package/dist/evals/runs.controller.js +199 -0
  183. package/dist/evals/runs.service.d.ts +275 -0
  184. package/dist/evals/runs.service.js +836 -0
  185. package/dist/evals/sets.controller.d.ts +61 -0
  186. package/dist/evals/sets.controller.js +183 -0
  187. package/dist/evals/sets.service.d.ts +128 -0
  188. package/dist/evals/sets.service.js +558 -0
  189. package/dist/evals/snapshot.d.ts +76 -0
  190. package/dist/evals/snapshot.js +207 -0
  191. package/dist/evals/step-replay.d.ts +113 -0
  192. package/dist/evals/step-replay.js +181 -0
  193. package/dist/evals/trajectory.controller.d.ts +41 -0
  194. package/dist/evals/trajectory.controller.js +88 -0
  195. package/dist/evals/trajectory.d.ts +144 -0
  196. package/dist/evals/trajectory.js +338 -0
  197. package/dist/evals/trajectory.service.d.ts +108 -0
  198. package/dist/evals/trajectory.service.js +323 -0
  199. package/dist/evals/worker.d.ts +101 -0
  200. package/dist/evals/worker.js +790 -0
  201. package/dist/extensions/admin-ops.d.ts +105 -0
  202. package/dist/extensions/admin-ops.js +353 -0
  203. package/dist/extensions/admin-view.d.ts +79 -0
  204. package/dist/extensions/admin-view.js +327 -0
  205. package/dist/extensions/approvals.d.ts +119 -0
  206. package/dist/extensions/approvals.js +205 -0
  207. package/dist/extensions/blobs.d.ts +45 -0
  208. package/dist/extensions/blobs.js +160 -0
  209. package/dist/extensions/bootstrap.d.ts +176 -0
  210. package/dist/extensions/bootstrap.js +535 -0
  211. package/dist/extensions/cli.d.ts +61 -0
  212. package/dist/extensions/cli.js +165 -0
  213. package/dist/extensions/config-json-schema.d.ts +9 -0
  214. package/dist/extensions/config-json-schema.js +73 -0
  215. package/dist/extensions/deployment-config.d.ts +138 -0
  216. package/dist/extensions/deployment-config.js +394 -0
  217. package/dist/extensions/enabled.d.ts +31 -0
  218. package/dist/extensions/enabled.js +55 -0
  219. package/dist/extensions/hooks.d.ts +161 -0
  220. package/dist/extensions/hooks.js +425 -0
  221. package/dist/extensions/host.d.ts +577 -0
  222. package/dist/extensions/host.js +2071 -0
  223. package/dist/extensions/inflight.d.ts +69 -0
  224. package/dist/extensions/inflight.js +277 -0
  225. package/dist/extensions/installs.d.ts +166 -0
  226. package/dist/extensions/installs.js +253 -0
  227. package/dist/extensions/instance.d.ts +167 -0
  228. package/dist/extensions/instance.js +329 -0
  229. package/dist/extensions/ledger.d.ts +30 -0
  230. package/dist/extensions/ledger.js +50 -0
  231. package/dist/extensions/lifecycle.d.ts +80 -0
  232. package/dist/extensions/lifecycle.js +152 -0
  233. package/dist/extensions/migrate-lock.d.ts +51 -0
  234. package/dist/extensions/migrate-lock.js +100 -0
  235. package/dist/extensions/org-config.controller.d.ts +24 -0
  236. package/dist/extensions/org-config.controller.js +59 -0
  237. package/dist/extensions/org-config.d.ts +169 -0
  238. package/dist/extensions/org-config.js +520 -0
  239. package/dist/extensions/package.d.ts +115 -0
  240. package/dist/extensions/package.js +246 -0
  241. package/dist/extensions/permission-set.d.ts +117 -0
  242. package/dist/extensions/permission-set.js +184 -0
  243. package/dist/extensions/permission-view.d.ts +194 -0
  244. package/dist/extensions/permission-view.js +284 -0
  245. package/dist/extensions/registry.d.ts +24 -0
  246. package/dist/extensions/registry.js +56 -0
  247. package/dist/extensions/resolve.d.ts +12 -0
  248. package/dist/extensions/resolve.js +71 -0
  249. package/dist/extensions/sources.d.ts +69 -0
  250. package/dist/extensions/sources.js +120 -0
  251. package/dist/extensions/sync.d.ts +40 -0
  252. package/dist/extensions/sync.js +242 -0
  253. package/dist/files/previewers.controller.d.ts +46 -0
  254. package/dist/files/previewers.controller.js +105 -0
  255. package/dist/files/previewers.d.ts +100 -0
  256. package/dist/files/previewers.js +250 -0
  257. package/dist/http/api.module.d.ts +114 -0
  258. package/dist/http/api.module.js +448 -0
  259. package/dist/http/app.d.ts +77 -0
  260. package/dist/http/app.js +220 -0
  261. package/dist/http/error.filter.d.ts +21 -0
  262. package/dist/http/error.filter.js +85 -0
  263. package/dist/http/gateway.module.d.ts +15 -0
  264. package/dist/http/gateway.module.js +13 -0
  265. package/dist/http/health.controller.d.ts +26 -0
  266. package/dist/http/health.controller.js +79 -0
  267. package/dist/http/nest-logger.d.ts +20 -0
  268. package/dist/http/nest-logger.js +28 -0
  269. package/dist/http/plugin-dispatch.d.ts +68 -0
  270. package/dist/http/plugin-dispatch.js +200 -0
  271. package/dist/http/tokens.d.ts +131 -0
  272. package/dist/http/tokens.js +131 -0
  273. package/dist/http/zod.pipe.d.ts +8 -0
  274. package/dist/http/zod.pipe.js +14 -0
  275. package/dist/identity/accounts.d.ts +93 -0
  276. package/dist/identity/accounts.js +367 -0
  277. package/dist/identity/api-tokens.d.ts +73 -0
  278. package/dist/identity/api-tokens.js +197 -0
  279. package/dist/identity/audit.d.ts +23 -0
  280. package/dist/identity/audit.js +41 -0
  281. package/dist/identity/auth.controller.d.ts +40 -0
  282. package/dist/identity/auth.controller.js +130 -0
  283. package/dist/identity/auth.guard.d.ts +41 -0
  284. package/dist/identity/auth.guard.js +162 -0
  285. package/dist/identity/binding.d.ts +47 -0
  286. package/dist/identity/binding.js +84 -0
  287. package/dist/identity/cookies.d.ts +16 -0
  288. package/dist/identity/cookies.js +32 -0
  289. package/dist/identity/credential-check.d.ts +17 -0
  290. package/dist/identity/credential-check.js +24 -0
  291. package/dist/identity/enterprise-admin.d.ts +14 -0
  292. package/dist/identity/enterprise-admin.js +27 -0
  293. package/dist/identity/external.d.ts +106 -0
  294. package/dist/identity/external.js +337 -0
  295. package/dist/identity/hash.d.ts +2 -0
  296. package/dist/identity/hash.js +3 -0
  297. package/dist/identity/login.d.ts +6 -0
  298. package/dist/identity/login.js +20 -0
  299. package/dist/identity/me.controller.d.ts +61 -0
  300. package/dist/identity/me.controller.js +159 -0
  301. package/dist/identity/passwords.d.ts +6 -0
  302. package/dist/identity/passwords.js +37 -0
  303. package/dist/identity/principal.d.ts +56 -0
  304. package/dist/identity/principal.js +38 -0
  305. package/dist/identity/rate-limit.d.ts +55 -0
  306. package/dist/identity/rate-limit.js +121 -0
  307. package/dist/identity/sessions.d.ts +84 -0
  308. package/dist/identity/sessions.js +306 -0
  309. package/dist/identity/settings.d.ts +121 -0
  310. package/dist/identity/settings.js +248 -0
  311. package/dist/identity/sso.controller.d.ts +59 -0
  312. package/dist/identity/sso.controller.js +192 -0
  313. package/dist/identity/sso.d.ts +83 -0
  314. package/dist/identity/sso.js +202 -0
  315. package/dist/inbox/inbox.service.d.ts +43 -0
  316. package/dist/inbox/inbox.service.js +57 -0
  317. package/dist/index.d.ts +69 -0
  318. package/dist/index.js +67 -0
  319. package/dist/kernel.d.ts +259 -0
  320. package/dist/kernel.js +1326 -0
  321. package/dist/log-config.d.ts +9 -0
  322. package/dist/log-config.js +27 -0
  323. package/dist/log-context.d.ts +32 -0
  324. package/dist/log-context.js +40 -0
  325. package/dist/log-loki.d.ts +34 -0
  326. package/dist/log-loki.js +145 -0
  327. package/dist/log.d.ts +79 -0
  328. package/dist/log.js +303 -0
  329. package/dist/models/prices.d.ts +53 -0
  330. package/dist/models/prices.js +121 -0
  331. package/dist/models/purposes.controller.d.ts +19 -0
  332. package/dist/models/purposes.controller.js +56 -0
  333. package/dist/models/purposes.d.ts +140 -0
  334. package/dist/models/purposes.js +267 -0
  335. package/dist/net/proxy.d.ts +44 -0
  336. package/dist/net/proxy.js +162 -0
  337. package/dist/net/public-address.d.ts +60 -0
  338. package/dist/net/public-address.js +272 -0
  339. package/dist/net/safe-fetch.d.ts +38 -0
  340. package/dist/net/safe-fetch.js +140 -0
  341. package/dist/orgs/directory-sync.controller.d.ts +14 -0
  342. package/dist/orgs/directory-sync.controller.js +40 -0
  343. package/dist/orgs/directory-sync.d.ts +166 -0
  344. package/dist/orgs/directory-sync.js +558 -0
  345. package/dist/orgs/invitations.controller.d.ts +58 -0
  346. package/dist/orgs/invitations.controller.js +137 -0
  347. package/dist/orgs/invitations.service.d.ts +80 -0
  348. package/dist/orgs/invitations.service.js +217 -0
  349. package/dist/orgs/members.service.d.ts +63 -0
  350. package/dist/orgs/members.service.js +234 -0
  351. package/dist/orgs/offboarding.d.ts +141 -0
  352. package/dist/orgs/offboarding.js +398 -0
  353. package/dist/orgs/org-lock.d.ts +32 -0
  354. package/dist/orgs/org-lock.js +46 -0
  355. package/dist/orgs/org.guard.d.ts +35 -0
  356. package/dist/orgs/org.guard.js +95 -0
  357. package/dist/orgs/orgs.controller.d.ts +78 -0
  358. package/dist/orgs/orgs.controller.js +192 -0
  359. package/dist/orgs/orgs.service.d.ts +61 -0
  360. package/dist/orgs/orgs.service.js +209 -0
  361. package/dist/orgs/roles.d.ts +13 -0
  362. package/dist/orgs/roles.js +58 -0
  363. package/dist/orgs/settings.controller.d.ts +20 -0
  364. package/dist/orgs/settings.controller.js +53 -0
  365. package/dist/orgs/settings.service.d.ts +120 -0
  366. package/dist/orgs/settings.service.js +178 -0
  367. package/dist/packages/npm.d.ts +50 -0
  368. package/dist/packages/npm.js +218 -0
  369. package/dist/packages/provenance.d.ts +47 -0
  370. package/dist/packages/provenance.js +147 -0
  371. package/dist/packages/tgz.d.ts +76 -0
  372. package/dist/packages/tgz.js +225 -0
  373. package/dist/runtime/abort.d.ts +32 -0
  374. package/dist/runtime/abort.js +71 -0
  375. package/dist/runtime/checkpointer.d.ts +32 -0
  376. package/dist/runtime/checkpointer.js +243 -0
  377. package/dist/runtime/context/build.d.ts +254 -0
  378. package/dist/runtime/context/build.js +693 -0
  379. package/dist/runtime/context/claude-tools.d.ts +11 -0
  380. package/dist/runtime/context/claude-tools.js +24 -0
  381. package/dist/runtime/context/compaction.d.ts +26 -0
  382. package/dist/runtime/context/compaction.js +195 -0
  383. package/dist/runtime/context/pairing.d.ts +11 -0
  384. package/dist/runtime/context/pairing.js +82 -0
  385. package/dist/runtime/context/reminders.d.ts +5 -0
  386. package/dist/runtime/context/reminders.js +11 -0
  387. package/dist/runtime/context/skills.d.ts +105 -0
  388. package/dist/runtime/context/skills.js +283 -0
  389. package/dist/runtime/context/tokens.d.ts +19 -0
  390. package/dist/runtime/context/tokens.js +104 -0
  391. package/dist/runtime/conversation-lock.d.ts +40 -0
  392. package/dist/runtime/conversation-lock.js +97 -0
  393. package/dist/runtime/hook-calls.d.ts +84 -0
  394. package/dist/runtime/hook-calls.js +136 -0
  395. package/dist/runtime/langgraph-loop.d.ts +52 -0
  396. package/dist/runtime/langgraph-loop.js +247 -0
  397. package/dist/runtime/loop.d.ts +93 -0
  398. package/dist/runtime/loop.js +7 -0
  399. package/dist/runtime/maintenance.d.ts +80 -0
  400. package/dist/runtime/maintenance.js +148 -0
  401. package/dist/runtime/media-cache.d.ts +21 -0
  402. package/dist/runtime/media-cache.js +51 -0
  403. package/dist/runtime/model-call.d.ts +176 -0
  404. package/dist/runtime/model-call.js +1049 -0
  405. package/dist/runtime/providers.d.ts +86 -0
  406. package/dist/runtime/providers.js +224 -0
  407. package/dist/runtime/quota.d.ts +29 -0
  408. package/dist/runtime/quota.js +70 -0
  409. package/dist/runtime/run-finish.d.ts +45 -0
  410. package/dist/runtime/run-finish.js +69 -0
  411. package/dist/runtime/run-queue.d.ts +121 -0
  412. package/dist/runtime/run-queue.js +493 -0
  413. package/dist/runtime/run-worker.d.ts +138 -0
  414. package/dist/runtime/run-worker.js +806 -0
  415. package/dist/runtime/stream.controller.d.ts +34 -0
  416. package/dist/runtime/stream.controller.js +122 -0
  417. package/dist/runtime/stream.d.ts +63 -0
  418. package/dist/runtime/stream.js +262 -0
  419. package/dist/runtime/subagents/child.d.ts +80 -0
  420. package/dist/runtime/subagents/child.js +225 -0
  421. package/dist/runtime/subagents/definition.d.ts +103 -0
  422. package/dist/runtime/subagents/definition.js +392 -0
  423. package/dist/runtime/subagents/dispatch.d.ts +69 -0
  424. package/dist/runtime/subagents/dispatch.js +502 -0
  425. package/dist/runtime/subagents/limits.d.ts +23 -0
  426. package/dist/runtime/subagents/limits.js +34 -0
  427. package/dist/runtime/subagents/org-agents.d.ts +50 -0
  428. package/dist/runtime/subagents/org-agents.js +70 -0
  429. package/dist/runtime/subagents/preamble.d.ts +7 -0
  430. package/dist/runtime/subagents/preamble.js +11 -0
  431. package/dist/runtime/subagents/progress.d.ts +26 -0
  432. package/dist/runtime/subagents/progress.js +36 -0
  433. package/dist/runtime/subagents/roster.d.ts +55 -0
  434. package/dist/runtime/subagents/roster.js +111 -0
  435. package/dist/runtime/subagents/skill-fork.d.ts +123 -0
  436. package/dist/runtime/subagents/skill-fork.js +259 -0
  437. package/dist/runtime/usage-report.d.ts +163 -0
  438. package/dist/runtime/usage-report.js +551 -0
  439. package/dist/runtime/waits.d.ts +233 -0
  440. package/dist/runtime/waits.js +883 -0
  441. package/dist/sandbox/leases.d.ts +169 -0
  442. package/dist/sandbox/leases.js +515 -0
  443. package/dist/sandbox/lifecycle.d.ts +21 -0
  444. package/dist/sandbox/lifecycle.js +68 -0
  445. package/dist/sandbox/sandbox.service.d.ts +249 -0
  446. package/dist/sandbox/sandbox.service.js +908 -0
  447. package/dist/skills/catalog.d.ts +149 -0
  448. package/dist/skills/catalog.js +405 -0
  449. package/dist/skills/changes.controller.d.ts +56 -0
  450. package/dist/skills/changes.controller.js +136 -0
  451. package/dist/skills/changes.service.d.ts +175 -0
  452. package/dist/skills/changes.service.js +805 -0
  453. package/dist/skills/invocable.controller.d.ts +23 -0
  454. package/dist/skills/invocable.controller.js +50 -0
  455. package/dist/skills/invocation-keys.d.ts +23 -0
  456. package/dist/skills/invocation-keys.js +44 -0
  457. package/dist/skills/invoke.d.ts +135 -0
  458. package/dist/skills/invoke.js +286 -0
  459. package/dist/skills/package.d.ts +87 -0
  460. package/dist/skills/package.js +227 -0
  461. package/dist/skills/skills.controller.d.ts +40 -0
  462. package/dist/skills/skills.controller.js +110 -0
  463. package/dist/skills/skills.service.d.ts +192 -0
  464. package/dist/skills/skills.service.js +567 -0
  465. package/dist/storage/prefix.d.ts +36 -0
  466. package/dist/storage/prefix.js +104 -0
  467. package/dist/storage/purge.d.ts +27 -0
  468. package/dist/storage/purge.js +75 -0
  469. package/dist/storage/storage.service.d.ts +44 -0
  470. package/dist/storage/storage.service.js +145 -0
  471. package/dist/text.d.ts +13 -0
  472. package/dist/text.js +13 -0
  473. package/dist/tools/approvals.controller.d.ts +53 -0
  474. package/dist/tools/approvals.controller.js +128 -0
  475. package/dist/tools/approvals.service.d.ts +172 -0
  476. package/dist/tools/approvals.service.js +568 -0
  477. package/dist/tools/breaker.d.ts +28 -0
  478. package/dist/tools/breaker.js +72 -0
  479. package/dist/tools/builtins/agent.d.ts +81 -0
  480. package/dist/tools/builtins/agent.js +223 -0
  481. package/dist/tools/builtins/ask-user.d.ts +28 -0
  482. package/dist/tools/builtins/ask-user.js +139 -0
  483. package/dist/tools/builtins/read-tool-output.d.ts +18 -0
  484. package/dist/tools/builtins/read-tool-output.js +108 -0
  485. package/dist/tools/builtins/sandbox/bash.d.ts +16 -0
  486. package/dist/tools/builtins/sandbox/bash.js +137 -0
  487. package/dist/tools/builtins/sandbox/common.d.ts +116 -0
  488. package/dist/tools/builtins/sandbox/common.js +306 -0
  489. package/dist/tools/builtins/sandbox/deliver.d.ts +5 -0
  490. package/dist/tools/builtins/sandbox/deliver.js +180 -0
  491. package/dist/tools/builtins/sandbox/edit.d.ts +4 -0
  492. package/dist/tools/builtins/sandbox/edit.js +98 -0
  493. package/dist/tools/builtins/sandbox/glob-match.d.ts +25 -0
  494. package/dist/tools/builtins/sandbox/glob-match.js +240 -0
  495. package/dist/tools/builtins/sandbox/glob.d.ts +4 -0
  496. package/dist/tools/builtins/sandbox/glob.js +104 -0
  497. package/dist/tools/builtins/sandbox/grep.d.ts +9 -0
  498. package/dist/tools/builtins/sandbox/grep.js +133 -0
  499. package/dist/tools/builtins/sandbox/index.d.ts +12 -0
  500. package/dist/tools/builtins/sandbox/index.js +27 -0
  501. package/dist/tools/builtins/sandbox/names.d.ts +7 -0
  502. package/dist/tools/builtins/sandbox/names.js +20 -0
  503. package/dist/tools/builtins/sandbox/read.d.ts +8 -0
  504. package/dist/tools/builtins/sandbox/read.js +121 -0
  505. package/dist/tools/builtins/sandbox/write.d.ts +4 -0
  506. package/dist/tools/builtins/sandbox/write.js +88 -0
  507. package/dist/tools/builtins/skill-load.d.ts +53 -0
  508. package/dist/tools/builtins/skill-load.js +446 -0
  509. package/dist/tools/builtins/skill-write.d.ts +59 -0
  510. package/dist/tools/builtins/skill-write.js +244 -0
  511. package/dist/tools/builtins/tool-search.d.ts +31 -0
  512. package/dist/tools/builtins/tool-search.js +171 -0
  513. package/dist/tools/connector-content.controller.d.ts +34 -0
  514. package/dist/tools/connector-content.controller.js +78 -0
  515. package/dist/tools/connector-content.d.ts +104 -0
  516. package/dist/tools/connector-content.js +267 -0
  517. package/dist/tools/connectors.controller.d.ts +210 -0
  518. package/dist/tools/connectors.controller.js +234 -0
  519. package/dist/tools/connectors.service.d.ts +303 -0
  520. package/dist/tools/connectors.service.js +1102 -0
  521. package/dist/tools/credentials.d.ts +139 -0
  522. package/dist/tools/credentials.js +351 -0
  523. package/dist/tools/drift.d.ts +141 -0
  524. package/dist/tools/drift.js +539 -0
  525. package/dist/tools/elicitation.d.ts +81 -0
  526. package/dist/tools/elicitation.js +266 -0
  527. package/dist/tools/endpoint.d.ts +18 -0
  528. package/dist/tools/endpoint.js +115 -0
  529. package/dist/tools/info.d.ts +67 -0
  530. package/dist/tools/info.js +113 -0
  531. package/dist/tools/naming.d.ts +27 -0
  532. package/dist/tools/naming.js +47 -0
  533. package/dist/tools/oauth/discovery.d.ts +65 -0
  534. package/dist/tools/oauth/discovery.js +307 -0
  535. package/dist/tools/oauth/flow.d.ts +63 -0
  536. package/dist/tools/oauth/flow.js +237 -0
  537. package/dist/tools/oauth/oauth.controller.d.ts +31 -0
  538. package/dist/tools/oauth/oauth.controller.js +95 -0
  539. package/dist/tools/oauth/tokens.d.ts +111 -0
  540. package/dist/tools/oauth/tokens.js +258 -0
  541. package/dist/tools/outputs.d.ts +61 -0
  542. package/dist/tools/outputs.js +156 -0
  543. package/dist/tools/pipeline.d.ts +322 -0
  544. package/dist/tools/pipeline.js +1236 -0
  545. package/dist/tools/policy.d.ts +60 -0
  546. package/dist/tools/policy.js +236 -0
  547. package/dist/tools/questions.controller.d.ts +24 -0
  548. package/dist/tools/questions.controller.js +58 -0
  549. package/dist/tools/questions.service.d.ts +103 -0
  550. package/dist/tools/questions.service.js +295 -0
  551. package/dist/tools/redact.d.ts +20 -0
  552. package/dist/tools/redact.js +60 -0
  553. package/dist/tools/registry.d.ts +246 -0
  554. package/dist/tools/registry.js +481 -0
  555. package/dist/tools/resources.d.ts +16 -0
  556. package/dist/tools/resources.js +76 -0
  557. package/dist/tools/specs.d.ts +14 -0
  558. package/dist/tools/specs.js +31 -0
  559. package/dist/tools/validate.d.ts +20 -0
  560. package/dist/tools/validate.js +183 -0
  561. package/dist/tools/view.d.ts +43 -0
  562. package/dist/tools/view.js +225 -0
  563. package/dist/ui/embed-token.d.ts +31 -0
  564. package/dist/ui/embed-token.js +132 -0
  565. package/dist/ui/registry.d.ts +58 -0
  566. package/dist/ui/registry.js +90 -0
  567. package/dist/ui/ui-stream.d.ts +37 -0
  568. package/dist/ui/ui-stream.js +162 -0
  569. package/dist/ui/views.controller.d.ts +81 -0
  570. package/dist/ui/views.controller.js +261 -0
  571. package/dist/ui/views.d.ts +155 -0
  572. package/dist/ui/views.js +306 -0
  573. package/dist/version.d.ts +5 -0
  574. package/dist/version.js +6 -0
  575. package/package.json +69 -3
  576. package/src/migrations/0001_init.sql +8 -0
  577. package/src/migrations/0002_secrets.sql +8 -0
  578. package/src/migrations/0003_identity.sql +117 -0
  579. package/src/migrations/0004_plugin_org_config_nullable.sql +3 -0
  580. package/src/migrations/0005_agents.sql +27 -0
  581. package/src/migrations/0006_conversations.sql +37 -0
  582. package/src/migrations/0007_checkpoints.sql +26 -0
  583. package/src/migrations/0008_runs_trigger_seq.sql +3 -0
  584. package/src/migrations/0009_runs_active_unique.sql +4 -0
  585. package/src/migrations/0010_tools.sql +125 -0
  586. package/src/migrations/0011_approvals_spec.sql +10 -0
  587. package/src/migrations/0012_run_waits.sql +24 -0
  588. package/src/migrations/0013_app_tools_source.sql +4 -0
  589. package/src/migrations/0014_tool_specs.sql +8 -0
  590. package/src/migrations/0015_tool_drift.sql +20 -0
  591. package/src/migrations/0016_plugin_permissions.sql +11 -0
  592. package/src/migrations/0017_identity_sources.sql +6 -0
  593. package/src/migrations/0018_channels.sql +74 -0
  594. package/src/migrations/0019_identity_trust.sql +17 -0
  595. package/src/migrations/0020_channel_card_lookup.sql +10 -0
  596. package/src/migrations/0021_org_storage_purge.sql +9 -0
  597. package/src/migrations/0022_attachments.sql +29 -0
  598. package/src/migrations/0023_sandboxes.sql +37 -0
  599. package/src/migrations/0024_skills.sql +46 -0
  600. package/src/migrations/0025_skills_catalog_idx.sql +6 -0
  601. package/src/migrations/0026_skill_changes.sql +34 -0
  602. package/src/migrations/0027_model_prices.sql +18 -0
  603. package/src/migrations/0028_evals.sql +105 -0
  604. package/src/migrations/0029_model_purposes.sql +18 -0
  605. package/src/migrations/0030_plugin_installs.sql +73 -0
  606. package/src/migrations/0031_plugin_snapshot_source.sql +6 -0
  607. package/src/migrations/0032_plugin_deployment_configs.sql +10 -0
  608. package/src/migrations/0033_rename_apps_to_connectors.sql +25 -0
  609. package/src/migrations/0034_directory.sql +49 -0
  610. package/src/migrations/0035_catalogs.sql +40 -0
  611. package/src/migrations/0036_app_import_staging.sql +17 -0
  612. package/src/migrations/0037_directory_installs.sql +92 -0
  613. package/src/migrations/0038_channel_inbound_mode.sql +6 -0
  614. package/src/migrations/0039_channel_card_pending.sql +8 -0
  615. package/src/migrations/0040_usage_report_sweep.sql +10 -0
  616. package/src/migrations/0041_rename_plugins_to_extensions.sql +38 -0
  617. package/src/migrations/0042_rename_directory_to_catalog.sql +80 -0
  618. package/src/migrations/0043_rename_app_installs_to_plugin_installs.sql +20 -0
  619. package/src/migrations/0044_catalog_one_kind.sql +90 -0
  620. package/src/migrations/0045_plugin_dependencies.sql +9 -0
  621. package/src/migrations/0046_git_sources.sql +25 -0
  622. package/src/migrations/0047_marketplace_entry_heads.sql +7 -0
  623. package/src/migrations/0048_marketplace_entry_trees.sql +8 -0
  624. package/src/migrations/0049_plugin_upgrades.sql +37 -0
  625. package/src/migrations/0050_bootstrap.sql +53 -0
  626. package/src/migrations/0051_claude_compat.sql +26 -0
  627. package/src/migrations/0052_conversations_account_agent_idx.sql +5 -0
  628. package/src/migrations/0053_subagents.sql +73 -0
  629. package/src/migrations/0054_subagent_wait_expiry.sql +7 -0
  630. package/src/migrations/0055_subagent_parent_indexes.sql +10 -0
  631. package/src/migrations/0056_approval_reason.sql +3 -0
  632. package/src/migrations/0057_conversation_titles.sql +11 -0
  633. package/src/migrations/0058_conversation_selection.sql +4 -0
  634. package/src/migrations/0059_conversation_queue.sql +22 -0
  635. package/src/migrations/0060_file_previewers.sql +10 -0
  636. package/src/migrations/0061_queue_selection.sql +5 -0
  637. package/src/migrations/0062_queued_skill.sql +8 -0
@@ -0,0 +1,908 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { isSandboxGoneError, } from "@capybara-hq/plugin-sdk";
3
+ import { sql, } from "kysely";
4
+ import { DEFAULT_MAX_COMMAND_SECONDS, DEFAULT_MAX_RUN_SECONDS, } from "../config/schema.js";
5
+ import { ApiError } from "../errors.js";
6
+ import { readSandboxLimitOverride } from "../identity/settings.js";
7
+ import { effectiveSandboxEgress, effectiveSandboxLimits, readOrgSettings, } from "../orgs/settings.service.js";
8
+ import { canonicalJson } from "../tools/registry.js";
9
+ import { SANDBOX_LOCK_TTL_MS, SandboxLeases, SandboxLockLostError, sandboxBusy, } from "./leases.js";
10
+ // Moved to leases.ts (phase C); re-exported for existing importers
11
+ export { SANDBOX_BUSY_PREFIX, SANDBOX_LOCK_PREFIX, SANDBOX_LOCK_TTL_MS, SandboxLockLostError, sandboxBusy, } from "./leases.js";
12
+ /** Margin (ms) of a holder record's TTL beyond the longest command: by then the in-sandbox limit has ended its commands. */
13
+ const HOLDER_TTL_MARGIN_MS = 10 * 60_000;
14
+ /** How long the kernel's own read-modify-write of one file waits for another holder's (ms, spec §10.4). */
15
+ const PATH_LEASE_WAIT_MS = 30_000;
16
+ /** 维护扫描里单个提供方操作(休眠 / 销毁)的超时。 */
17
+ const SWEEP_OP_TIMEOUT_MS = 120_000;
18
+ /** How long the suspension sweep holds the intent on a sandbox past `maxRunningMinutes` (ms). */
19
+ const SWEEP_INTENT_WAIT_MS = 1000;
20
+ /** 每次扫描最多处理的沙箱数,与同时进行的提供方操作数。 */
21
+ const SWEEP_BATCH = 200;
22
+ const SWEEP_CONCURRENCY = 4;
23
+ /** destroyFor 等锁的上限:持有者在跑命令时最多等这么久。 */
24
+ const DESTROY_FOR_WAIT_MS = 10 * 60_000;
25
+ /** 维护里一次提供方操作的 signal:超时或锁丢失时中止。 */
26
+ const opSignal = (lease) => AbortSignal.any([lease.signal, AbortSignal.timeout(SWEEP_OP_TIMEOUT_MS)]);
27
+ /**
28
+ * The row still records the instance `gone` (same handle and same `created_at`). Handles compare
29
+ * canonically: a jsonb round trip reorders a multi-key handle's keys (final review m9).
30
+ */
31
+ export const isInstance = (row, gone) => gone !== null &&
32
+ row.handle !== null &&
33
+ canonicalJson(row.handle) === canonicalJson(gone.handle) &&
34
+ row.created_at.getTime() === gone.createdAt;
35
+ /**
36
+ * Concurrent commands a provider gets (spec §10.7): `max` when it declares both `concurrentExec`
37
+ * and `killTagged`, else 1 (one call at a time, the behavior before phase C).
38
+ */
39
+ export function sandboxConcurrency(impl, max) {
40
+ return impl.concurrentExec === true && typeof impl.killTagged === "function" ? max : 1;
41
+ }
42
+ /**
43
+ * `impl` with every `exec` labelled with the holder's tag; `before` runs first (it refreshes the
44
+ * holder's record, so a callback that runs several long commands in a row stays recorded).
45
+ */
46
+ function tagged(impl, tag, before) {
47
+ const wrapped = Object.create(impl);
48
+ Object.defineProperty(wrapped, "exec", {
49
+ value: async (h, args) => {
50
+ await before();
51
+ return impl.exec(h, { ...args, tag });
52
+ },
53
+ });
54
+ return wrapped;
55
+ }
56
+ /**
57
+ * 启动时要提醒运维的沙箱配置问题(英文日志):
58
+ * - `runtime.maxRunSeconds` 小于 `sandbox.limits.maxCommandSeconds`:长命令会被运行时限截断(Ruling M20);
59
+ * - `sandbox.limits.egress` 为 open、但当前提供方插件没打开 `allowOpenEgress`:每次创建沙箱都会被拒。
60
+ * 两个时限都是默认值(900 < 7200,已知的取舍)时只记 info;运维改过其中一个却仍不一致时才 warn。
61
+ * - the provider declares only one of `concurrentExec` and `killTagged`: it is used one call at a
62
+ * time (spec §10.7).
63
+ */
64
+ export function sandboxConfigWarnings(cfg, providerPlugin, impl) {
65
+ const out = [];
66
+ if (impl && (impl.concurrentExec === true) !== (typeof impl.killTagged === "function")) {
67
+ out.push({
68
+ level: "warn",
69
+ message: `the sandbox plugin ${providerPlugin ?? ""} declares only one of concurrentExec and killTagged; concurrent commands need both, so its sandboxes run one command at a time`,
70
+ });
71
+ }
72
+ if (cfg.sandbox.limits.egress === "open" && providerPlugin !== undefined) {
73
+ const pc = cfg.extensionConfig[providerPlugin];
74
+ if (pc?.allowOpenEgress !== true) {
75
+ out.push({
76
+ level: "warn",
77
+ message: `sandbox.limits.egress is "open" but extensionConfig."${providerPlugin}".allowOpenEgress is not true: the ${providerPlugin} plugin refuses to create sandboxes with open egress; set allowOpenEgress to true (trusted environments only) or use "allowlist" or "none"`,
78
+ });
79
+ }
80
+ }
81
+ const run = cfg.runtime.maxRunSeconds;
82
+ const cmd = cfg.sandbox.limits.maxCommandSeconds;
83
+ if (run < cmd) {
84
+ const defaults = run === DEFAULT_MAX_RUN_SECONDS && cmd === DEFAULT_MAX_COMMAND_SECONDS;
85
+ out.push({
86
+ level: defaults ? "info" : "warn",
87
+ message: `runtime.maxRunSeconds (${run}) is less than sandbox.limits.maxCommandSeconds (${cmd}): sandbox commands are limited to the time left in the run, so commands longer than about ${run} seconds cannot finish; raise runtime.maxRunSeconds to at least ${cmd} if agents need long commands`,
88
+ });
89
+ }
90
+ return out;
91
+ }
92
+ /** 没有沙箱提供方时的错误(503)。 */
93
+ export const sandboxUnavailable = () => new ApiError("sandbox.unavailable", "Sandbox is not available in this deployment; install a sandbox plugin");
94
+ const abortReason = (signal) => signal.reason ?? new DOMException("This operation was aborted", "AbortError");
95
+ function sleep(ms, signal) {
96
+ return new Promise((resolve, reject) => {
97
+ if (signal.aborted) {
98
+ reject(abortReason(signal));
99
+ return;
100
+ }
101
+ const onAbort = () => {
102
+ clearTimeout(t);
103
+ reject(abortReason(signal));
104
+ };
105
+ const t = setTimeout(() => {
106
+ signal.removeEventListener("abort", onAbort);
107
+ resolve();
108
+ }, ms);
109
+ signal.addEventListener("abort", onAbort, { once: true });
110
+ });
111
+ }
112
+ /** 按 `limit` 个一组并发执行。 */
113
+ async function inPool(items, limit, fn) {
114
+ let next = 0;
115
+ const worker = async () => {
116
+ while (next < items.length) {
117
+ const x = items[next++];
118
+ await fn(x);
119
+ }
120
+ };
121
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
122
+ }
123
+ /**
124
+ * 内核的沙箱服务(M5 T7):在 `sandbox@1` 提供方之上,每个对话一个沙箱。
125
+ *
126
+ * - **懒创建**:第一次 {@link withSandbox} 才创建;状态在 PG 表 `sandboxes`。
127
+ * - **Leases** (phase C, spec §10, {@link SandboxLeases}): using a sandbox recorded `running` with
128
+ * the current provider takes a **shared** lease, so up to `sandbox.maxConcurrentCommands` holders
129
+ * (commands of parallel sub-agents) run at once ({@link sandboxConcurrency}: 1 for a provider
130
+ * without `concurrentExec` + `killTagged`). Every lifecycle change (create, resume, rebuild,
131
+ * suspend, destroy) takes the **exclusive** lease, alone; a caller that needs one on the use path
132
+ * sets the intent so new shared leases wait for it, and downgrades to a shared lease to run its
133
+ * own command once the sandbox is running. Waiting is capped at `sandbox.lockWaitSeconds`
134
+ * (`sandbox.busy`); two replicas using it for the first time at once still build only one.
135
+ * - **防护**:exclusive holders read `generation` and compare-and-increment it with every write of
136
+ * status / handle ({@link SandboxLockLostError}); every acquisition (shared ones too) bumps it
137
+ * first, so a stale exclusive holder's later writes fail. Shared holders write nothing else but
138
+ * `last_used_at` (monotonic). A lost
139
+ * lease aborts the signal passed to `fn` and to maintenance provider calls.
140
+ * - **休眠与销毁**由维护队列定期调用 {@link suspendIdle} / {@link destroyInactive};它们只试一次 exclusive
141
+ * lease and skip a sandbox with any holder; a sandbox past `maxRunningMinutes` gets the intent for
142
+ * a short window (about a second) so the suspension can get in when its commands end.
143
+ * - **透明恢复**:已休眠的在下次使用时恢复;恢复返回 `gone`、已销毁、记录的提供方不是当前提供方、
144
+ * 或 `fn` 抛 `SandboxGoneError`(重试一次)时重建空沙箱,`recreated` 为 true(工具层告诉模型「之前的文件已不在」)。
145
+ * Several holders hitting `gone` together rebuild once: the others find the new instance.
146
+ * - **Takeover** (spec §10.5): every holder records itself in `capk:sandbox-holders:<conv>` (again
147
+ * before each `exec`) and its commands carry its lease token as `tag`. An acquirer that finds a
148
+ * record without a live lease (a crashed holder) kills its commands: `killTagged` on the shared
149
+ * path whenever the provider has it (else `killAll` only while it is the one shared holder),
150
+ * `killAll` under the exclusive lease (no live sibling can exist). The single marker
151
+ * `capk:sandbox-busy:<conv>` of kernels before phase C sends the next use down the exclusive
152
+ * path, which `killAll`s; holders of this kernel write it as `v2` for older replicas (§10.8),
153
+ * which this kernel ignores. A sandbox just resumed from `suspended` is `killAll`ed too. Failures
154
+ * are only warned (the in-sandbox time limit is the backstop).
155
+ * - **Instance identity**: a holder that hits `gone` names the instance by its handle and the
156
+ * row's `created_at` (set by the fenced write after each `create`); the handle alone is the same
157
+ * after a rebuild on providers whose handle is the instance name (sandbox-docker).
158
+ *
159
+ * 换提供方会泄漏旧实例:新提供方销毁不了旧提供方的实例,只有旧提供方仍加载时才能清理(记在 `prev_*` 列)。
160
+ * 没有提供方时 {@link available} 为 false,使用时抛 `sandbox.unavailable`(503)。本身无进程内状态,可多副本共用。
161
+ */
162
+ export class SandboxService {
163
+ d;
164
+ leases;
165
+ pollMs;
166
+ constructor(d) {
167
+ this.d = d;
168
+ this.pollMs = d.lockPollMs ?? 100;
169
+ this.leases =
170
+ d.leases ??
171
+ new SandboxLeases({
172
+ redis: d.redis,
173
+ log: d.log,
174
+ ttlMs: d.lockTtlMs ?? SANDBOX_LOCK_TTL_MS,
175
+ pollMs: this.pollMs,
176
+ });
177
+ }
178
+ /** 是否有沙箱提供方。 */
179
+ get available() {
180
+ return this.d.provider() !== undefined;
181
+ }
182
+ /** Concurrent holders the provider gets ({@link sandboxConcurrency} with the current config). */
183
+ effectiveCap(p) {
184
+ return sandboxConcurrency(p.impl, this.d.config().sandbox.maxConcurrentCommands);
185
+ }
186
+ /**
187
+ * 该对话沙箱的对外状态(只读,不拿锁):`running` / `suspended` 与最近使用时间。
188
+ * 还没有、已销毁、或停在 `creating`(正在创建,或创建失败等下次使用重建)的都返回 null(界面显示「未创建」)。
189
+ */
190
+ async status(orgId, conversationId) {
191
+ const row = await this.d.db
192
+ .selectFrom("sandboxes")
193
+ .select(["status", "last_used_at"])
194
+ .where("conversation_id", "=", conversationId)
195
+ .where("org_id", "=", orgId)
196
+ .executeTakeFirst();
197
+ if (!row || (row.status !== "running" && row.status !== "suspended"))
198
+ return null;
199
+ return { status: row.status, lastUsedAt: row.last_used_at };
200
+ }
201
+ /**
202
+ * A short lease on one path of the conversation's sandbox around the kernel's own
203
+ * read-modify-write of that file (spec §10.4); waits up to 30 s, then throws
204
+ * `SandboxPathBusyError`. Its `signal` aborts when a renewal finds it lost.
205
+ */
206
+ pathLease(conversationId, path, signal) {
207
+ return this.leases.pathLease(conversationId, path, signal, PATH_LEASE_WAIT_MS);
208
+ }
209
+ /**
210
+ * 在该对话(the sandbox conversation)的沙箱上执行 `fn`: take a lease → make sure the sandbox runs
211
+ * (create / resume / rebuild, under the exclusive lease) → `fn` under a shared lease → update
212
+ * `last_used_at` → release.
213
+ * - 等待超过 `sandbox.lockWaitSeconds` 抛 `sandbox.busy`(409);`signal` 中止等待与创建 / 恢复。
214
+ * - `fn` 的第四个参数是 `signal` 与「lease 已丢失」的合并:执行命令时用它,lease 丢失就中止。
215
+ * - The provider passed to `fn` labels every `exec` with this holder's tag.
216
+ * - `fn` 抛 `SandboxGoneError`(实例已不在)时重建(or take the instance another holder rebuilt)、
217
+ * 以 `recreated: true` 再调用一次;再次 gone 则抛出。其余错误原样抛出。
218
+ * - `holderConversationId` (default `conversationId`): the agent conversation using it, a
219
+ * sub-agent's own for a child. When that holder last used another instance (another holder
220
+ * rebuilt it meanwhile), `info.recreated` is true, so it learns its files are gone (final
221
+ * review m5).
222
+ */
223
+ async withSandbox(ctx, fn, signal) {
224
+ const p = this.require();
225
+ const holder = ctx.holderConversationId ?? ctx.conversationId;
226
+ let e = await this.seenBy(ctx.conversationId, holder, await this.enter(ctx, p, signal, null));
227
+ try {
228
+ try {
229
+ return await this.runAs(e, p, fn, signal);
230
+ }
231
+ catch (err) {
232
+ if (!isSandboxGoneError(err))
233
+ throw err;
234
+ this.d.log.info("sandbox instance is gone during use; recreating", {
235
+ conversationId: ctx.conversationId,
236
+ });
237
+ const gone = { handle: e.handle, createdAt: e.createdAt };
238
+ const prev = e;
239
+ await this.leave(ctx.conversationId, prev);
240
+ e = await this.seenBy(ctx.conversationId, holder, await this.enter(ctx, p, signal, gone));
241
+ return await this.runAs({ ...e, info: { fresh: true, recreated: true } }, p, fn, signal);
242
+ }
243
+ }
244
+ finally {
245
+ await this.leave(ctx.conversationId, e);
246
+ }
247
+ }
248
+ /**
249
+ * Records that `holder` uses the instance of `e` (`capk:sandbox-seen:<conv>:<holder>`, its
250
+ * `created_at`, with a TTL of the inactivity destruction plus a day) and marks `e` recreated
251
+ * when the holder last used another instance. Best effort: a Redis error only logs.
252
+ */
253
+ async seenBy(conversationId, holder, e) {
254
+ try {
255
+ const ttl = (this.d.config().sandbox.limits.destroyAfterInactiveDays + 1) * 86_400;
256
+ const prev = await this.d.redis.set(`capk:sandbox-seen:${conversationId}:${holder}`, String(e.createdAt), "EX", ttl, "GET");
257
+ if (prev !== null && prev !== String(e.createdAt) && !e.info.recreated) {
258
+ return { ...e, info: { fresh: e.info.fresh, recreated: true } };
259
+ }
260
+ }
261
+ catch (err) {
262
+ this.d.log.warn("cannot record the sandbox instance a holder uses", {
263
+ conversationId,
264
+ holder,
265
+ err,
266
+ });
267
+ }
268
+ return e;
269
+ }
270
+ /** Runs `fn` as the holder `e`: its tag on every exec, its lease's signal. */
271
+ runAs(e, p, fn, signal) {
272
+ const sig = AbortSignal.any([signal, e.lease.signal]);
273
+ const conv = e.lease.conversationId;
274
+ const impl = tagged(p.impl, e.lease.token, () => this.register(conv, e.lease.token));
275
+ return fn(e.handle, impl, e.info, sig);
276
+ }
277
+ /**
278
+ * Becomes a holder: loops until it has a shared lease on a running instance, or `sandbox.busy`.
279
+ * A sandbox recorded `running` (with the current provider, not the `gone` instance, and no legacy
280
+ * busy marker) is used under a shared lease; otherwise the exclusive lease (with intent) makes it
281
+ * run and is downgraded. One token serves both, so the waiter passes its own intent.
282
+ */
283
+ async enter(ctx, p, signal, gone) {
284
+ const conv = ctx.conversationId;
285
+ const cap = this.effectiveCap(p);
286
+ const deadline = Date.now() + this.d.config().sandbox.lockWaitSeconds * 1000;
287
+ const token = randomUUID();
288
+ let intent = false;
289
+ try {
290
+ for (;;) {
291
+ if (signal.aborted)
292
+ throw abortReason(signal);
293
+ const row = await this.readRow(ctx);
294
+ if (this.usable(row, p, gone) && !(await this.leases.legacyMarker(conv))) {
295
+ const lease = await this.leases.tryShared(conv, cap, token);
296
+ if (lease) {
297
+ if (intent)
298
+ await this.clearIntent(conv, token);
299
+ intent = false;
300
+ const e = await this.asShared(ctx, p, lease, gone, signal);
301
+ if (e)
302
+ return e;
303
+ }
304
+ }
305
+ else {
306
+ const lease = await this.leases.tryExclusive(conv, { token, intent: true });
307
+ intent = lease === null;
308
+ if (lease)
309
+ return await this.asExclusive(ctx, p, lease, gone, signal);
310
+ }
311
+ if (Date.now() + this.pollMs > deadline)
312
+ throw sandboxBusy();
313
+ await sleep(this.pollMs + Math.floor(Math.random() * this.pollMs), signal);
314
+ }
315
+ }
316
+ catch (err) {
317
+ if (intent)
318
+ await this.clearIntent(conv, token);
319
+ throw err;
320
+ }
321
+ }
322
+ async clearIntent(conv, token) {
323
+ await this.leases
324
+ .clearIntent(conv, token)
325
+ .catch((err) => this.d.log.warn("cannot clear sandbox intent", { conversationId: conv, err }));
326
+ }
327
+ async readRow(ctx) {
328
+ const row = await this.d.db
329
+ .selectFrom("sandboxes")
330
+ .selectAll()
331
+ .where("conversation_id", "=", ctx.conversationId)
332
+ .executeTakeFirst();
333
+ if (row && row.org_id !== ctx.orgId) {
334
+ throw new ApiError("forbidden", `The sandbox of conversation ${ctx.conversationId} does not belong to organization ${ctx.orgId}`);
335
+ }
336
+ return row;
337
+ }
338
+ /** Recorded running with the current provider, and not the instance found gone. */
339
+ usable(row, p, gone) {
340
+ return (row !== undefined &&
341
+ row.status === "running" &&
342
+ row.provider === p.plugin &&
343
+ row.handle !== null &&
344
+ !isInstance(row, gone));
345
+ }
346
+ /**
347
+ * Under a just-taken shared lease: confirms the row is still usable (else releases and returns
348
+ * null), records the holder and kills crashed holders' commands.
349
+ */
350
+ async asShared(ctx, p, lease, gone, signal) {
351
+ const conv = ctx.conversationId;
352
+ try {
353
+ const row = await this.readRow(ctx);
354
+ if (!this.usable(row, p, gone)) {
355
+ await lease.release();
356
+ return null;
357
+ }
358
+ // Bump the fence as every acquisition did before phase C: an exclusive holder whose lease
359
+ // expired unnoticed (a stalled sweep) then fails its next write instead of suspending or
360
+ // destroying the instance under this holder. Shared holders never compare it.
361
+ await this.d.db
362
+ .updateTable("sandboxes")
363
+ .set((eb) => ({ generation: eb("generation", "+", 1) }))
364
+ .where("conversation_id", "=", conv)
365
+ .execute();
366
+ await this.register(conv, lease.token);
367
+ // a shared caller never claims an older kernel's busy marker (it holds no exclusive lease)
368
+ const dead = await this.claim(conv);
369
+ if (dead.tags.length > 0) {
370
+ const sig = AbortSignal.any([signal, lease.signal]);
371
+ const impl = p.impl;
372
+ if (impl.killTagged) {
373
+ // by tag whenever the provider can, even with cap 1: while a hot change of
374
+ // maxConcurrentCommands spreads, a replica on a larger cap may have let a sibling in
375
+ await this.kill(conv, "takeover", async () => impl.killTagged?.(row.handle, dead.tags, sig), sig);
376
+ }
377
+ else if ((await this.leases.sharedCount(conv)) <= 1) {
378
+ // cap 1 and this holder is alone: killing everything cannot hit a sibling
379
+ await this.killLeftovers(conv, row.handle, p, sig, "takeover");
380
+ }
381
+ else {
382
+ this.d.log.warn("cannot kill leftover sandbox commands: the provider has no killTagged and another holder is live; the in-sandbox time limit ends them", { conversationId: conv });
383
+ }
384
+ }
385
+ return {
386
+ lease,
387
+ handle: row.handle,
388
+ createdAt: row.created_at.getTime(),
389
+ info: { fresh: false, recreated: false },
390
+ };
391
+ }
392
+ catch (err) {
393
+ await this.unregister(conv, lease.token);
394
+ await lease.release();
395
+ throw err;
396
+ }
397
+ }
398
+ /**
399
+ * Under a just-taken exclusive lease: makes the sandbox run (create / resume / rebuild when the
400
+ * recorded instance is the one found gone), kills leftovers, then downgrades to a shared lease.
401
+ */
402
+ async asExclusive(ctx, p, lease, gone, signal) {
403
+ const conv = ctx.conversationId;
404
+ const sig = AbortSignal.any([signal, lease.signal]);
405
+ try {
406
+ const s = await this.ensure(ctx, p, lease, sig, gone);
407
+ const dead = await this.claim(conv, lease.token);
408
+ if (!s.info.fresh && (s.resumed || dead.legacyMarker || dead.tags.length > 0)) {
409
+ await this.killLeftovers(conv, s.handle, p, sig, s.resumed && !dead.legacyMarker && dead.tags.length === 0 ? "resume" : "takeover");
410
+ }
411
+ const shared = await lease.downgrade();
412
+ await this.register(conv, shared.token);
413
+ return { lease: shared, handle: s.handle, createdAt: s.createdAt, info: s.info };
414
+ }
415
+ catch (err) {
416
+ await this.unregister(conv, lease.token);
417
+ // releases the lease in whatever kind it has now (also after a downgrade)
418
+ await lease.release();
419
+ throw err;
420
+ }
421
+ }
422
+ /** Ends a holder: unregisters it before releasing its lease, then moves `last_used_at` forward. */
423
+ async leave(conv, e) {
424
+ await this.unregister(conv, e.lease.token);
425
+ await e.lease.release();
426
+ await this.d.db
427
+ .updateTable("sandboxes")
428
+ .set({ last_used_at: sql `GREATEST(last_used_at, now())` })
429
+ .where("conversation_id", "=", conv)
430
+ .where("status", "=", "running")
431
+ .execute()
432
+ .catch((err) => this.d.log.warn("cannot update sandbox last_used_at", { conversationId: conv, err }));
433
+ }
434
+ async register(conv, token) {
435
+ const ttl = this.d.config().sandbox.limits.maxCommandSeconds * 1000 + HOLDER_TTL_MARGIN_MS;
436
+ await this.leases
437
+ .registerHolder(conv, token, ttl)
438
+ .catch((err) => this.d.log.warn("cannot record sandbox holder", { conversationId: conv, err }));
439
+ }
440
+ async unregister(conv, token) {
441
+ await this.leases
442
+ .unregisterHolder(conv, token)
443
+ .catch((err) => this.d.log.warn("cannot clear sandbox holder record", { conversationId: conv, err }));
444
+ }
445
+ /**
446
+ * Crashed holders' tags (claimed), and an older kernel's busy marker when `exclusiveToken` is
447
+ * the caller's exclusive lease; a Redis error counts as none.
448
+ */
449
+ async claim(conv, exclusiveToken) {
450
+ try {
451
+ return await this.leases.claimDeadHolders(conv, exclusiveToken);
452
+ }
453
+ catch (err) {
454
+ this.d.log.warn("cannot read sandbox holder records", { conversationId: conv, err });
455
+ return { tags: [], legacyMarker: false };
456
+ }
457
+ }
458
+ /** Runs one kill call: gone and abort are thrown, other failures only warned. */
459
+ async kill(conversationId, why, call, signal) {
460
+ try {
461
+ await call();
462
+ if (why === "takeover") {
463
+ this.d.log.info("sandbox taken over from a holder that did not finish; leftover commands killed", { conversationId });
464
+ }
465
+ }
466
+ catch (err) {
467
+ if (signal?.aborted || isSandboxGoneError(err))
468
+ throw err;
469
+ this.d.log.warn("cannot kill leftover sandbox commands", { conversationId, err });
470
+ }
471
+ }
472
+ /**
473
+ * 休眠:空闲超过 `suspendAfterIdleMinutes`、或连续运行超过 `maxRunningMinutes` 的运行中沙箱。
474
+ * Skips a sandbox with any holder; one past `maxRunningMinutes` gets the intent for one poll
475
+ * window (about a second) first. 返回本次休眠的个数。由维护队列每分钟调用。
476
+ */
477
+ async suspendIdle(now) {
478
+ const p = this.d.provider();
479
+ if (!p)
480
+ return 0;
481
+ const due = this.suspendDue(now, p.plugin);
482
+ const runningBefore = new Date(now.getTime() - this.d.config().sandbox.limits.maxRunningMinutes * 60_000);
483
+ const rows = await this.candidates(due);
484
+ let n = 0;
485
+ await inPool(rows, SWEEP_CONCURRENCY, async (c) => {
486
+ const overdue = c.running_since !== null && c.running_since < runningBefore;
487
+ const done = await this.tryLocked(c.conversation_id, async (lease) => {
488
+ const id = c.conversation_id;
489
+ const row = await this.d.db
490
+ .selectFrom("sandboxes")
491
+ .selectAll()
492
+ .where("conversation_id", "=", id)
493
+ .where(due)
494
+ .executeTakeFirst();
495
+ if (!row?.handle)
496
+ return false;
497
+ const fence = { gen: row.generation };
498
+ await p.impl.suspend(row.handle, opSignal(lease));
499
+ if (!(await lease.renew()))
500
+ throw new SandboxLockLostError(id);
501
+ await this.fenced(lease, id, fence, {
502
+ status: "suspended",
503
+ suspended_at: sql `now()`,
504
+ running_since: null,
505
+ sweep_failed_at: null,
506
+ });
507
+ this.d.log.info("sandbox suspended", { conversationId: id });
508
+ return true;
509
+ }, overdue);
510
+ if (done)
511
+ n++;
512
+ });
513
+ return n;
514
+ }
515
+ /**
516
+ * 销毁:超过 `destroyAfterInactiveDays` 没用过的、所属组织已删除的、对话已不存在的沙箱
517
+ * (对话已不存在的,销毁后删掉记录)。有 holder 的跳过。返回本次销毁的个数。由维护队列每小时调用。
518
+ */
519
+ async destroyInactive(now) {
520
+ const p = this.d.provider();
521
+ if (!p)
522
+ return 0;
523
+ const due = this.destroyDue(now);
524
+ const rows = await this.candidates(due);
525
+ let n = 0;
526
+ await inPool(rows, SWEEP_CONCURRENCY, async ({ conversation_id: id }) => {
527
+ const done = await this.tryLocked(id, async (lease) => {
528
+ const row = await this.d.db
529
+ .selectFrom("sandboxes")
530
+ .selectAll()
531
+ .where("conversation_id", "=", id)
532
+ .where(due)
533
+ .executeTakeFirst();
534
+ if (!row)
535
+ return false;
536
+ await this.destroyRow(row, p, lease);
537
+ return true;
538
+ });
539
+ if (done)
540
+ n++;
541
+ });
542
+ // 早先已销毁、对话后来才删除、且没有遗留旧实例的记录
543
+ await this.d.db
544
+ .deleteFrom("sandboxes")
545
+ .where("status", "=", "destroyed")
546
+ .where("prev_handle", "is", null)
547
+ .where(({ not, exists, selectFrom }) => not(exists(selectFrom("conversations")
548
+ .select("conversations.id")
549
+ .whereRef("conversations.id", "=", "sandboxes.conversation_id"))))
550
+ .execute();
551
+ return n;
552
+ }
553
+ /**
554
+ * 立即销毁该对话的沙箱(对话删除、离职处理时调用);没有沙箱或已销毁时什么也不做。
555
+ * Waits for the exclusive lease (with intent, so running commands finish and new ones wait) up
556
+ * to 10 minutes. 没有提供方时只记 warn。
557
+ */
558
+ async destroyFor(conversationId) {
559
+ const row = await this.d.db
560
+ .selectFrom("sandboxes")
561
+ .select("status")
562
+ .where("conversation_id", "=", conversationId)
563
+ .executeTakeFirst();
564
+ if (!row || row.status === "destroyed")
565
+ return;
566
+ const p = this.d.provider();
567
+ if (!p) {
568
+ this.d.log.warn("cannot destroy sandbox: no sandbox provider", { conversationId });
569
+ return;
570
+ }
571
+ const lease = await this.leases.acquireExclusive(conversationId, AbortSignal.timeout(DESTROY_FOR_WAIT_MS), undefined, { intent: true });
572
+ try {
573
+ const current = await this.d.db
574
+ .selectFrom("sandboxes")
575
+ .selectAll()
576
+ .where("conversation_id", "=", conversationId)
577
+ .where("status", "<>", "destroyed")
578
+ .executeTakeFirst();
579
+ if (current)
580
+ await this.destroyRow(current, p, lease);
581
+ }
582
+ finally {
583
+ await lease.release();
584
+ }
585
+ }
586
+ /**
587
+ * 接手前任:杀掉沙箱里遗留的全部命令(killAll)。实例已不在(SandboxGoneError)与中止照常抛出(交给 withSandbox 重建 /
588
+ * 结束);其余失败只记 warn。提供方没有 `killAll` 时只记 info。
589
+ */
590
+ async killLeftovers(conversationId, handle, p, signal, why) {
591
+ const killAll = p.impl.killAll;
592
+ if (!killAll) {
593
+ if (why === "takeover") {
594
+ this.d.log.info("sandbox taken over from a holder that did not finish; provider cannot kill its commands", { conversationId });
595
+ }
596
+ return;
597
+ }
598
+ await this.kill(conversationId, why, () => killAll.call(p.impl, handle, signal), signal);
599
+ }
600
+ require() {
601
+ const p = this.d.provider();
602
+ if (!p)
603
+ throw sandboxUnavailable();
604
+ return p;
605
+ }
606
+ /**
607
+ * 防护写入:lease 仍在、且记录的 generation 仍是锁内读到的值时才写(并加一);否则抛 {@link SandboxLockLostError}。
608
+ */
609
+ async fenced(lease, id, fence, set) {
610
+ if (lease.signal.aborted)
611
+ throw new SandboxLockLostError(id);
612
+ const r = await this.d.db
613
+ .updateTable("sandboxes")
614
+ .set({ ...set, generation: fence.gen + 1 })
615
+ .where("conversation_id", "=", id)
616
+ .where("generation", "=", fence.gen)
617
+ .returning("created_at")
618
+ .executeTakeFirst();
619
+ if (!r)
620
+ throw new SandboxLockLostError(id, "generation");
621
+ fence.gen++;
622
+ return r.created_at;
623
+ }
624
+ /** 扫描的候选:上次扫描失败的排在后面,其余按最久未用在前。 */
625
+ async candidates(due) {
626
+ return await this.d.db
627
+ .selectFrom("sandboxes")
628
+ .select(["conversation_id", "running_since"])
629
+ .where(due)
630
+ .orderBy(sql `sweep_failed_at ASC NULLS FIRST`)
631
+ .orderBy("last_used_at")
632
+ .limit(SWEEP_BATCH)
633
+ .execute();
634
+ }
635
+ /**
636
+ * One try at the exclusive lease: null → false; else runs `fn`. With `overdue` (past
637
+ * `maxRunningMinutes`) a busy sandbox gets the intent for about a second, so new commands wait
638
+ * briefly and the sweep gets in if the running ones end. Failures are warned, recorded in
639
+ * `sweep_failed_at` (sorted last next time) and return false.
640
+ */
641
+ async tryLocked(conversationId, fn, overdue = false) {
642
+ let lease;
643
+ try {
644
+ lease = await this.leases.tryExclusive(conversationId);
645
+ if (!lease && overdue) {
646
+ lease = await this.leases
647
+ .acquireExclusive(conversationId, AbortSignal.timeout(SWEEP_OP_TIMEOUT_MS), Math.max(this.pollMs * 2, SWEEP_INTENT_WAIT_MS), { intent: true })
648
+ .catch((err) => {
649
+ if (err instanceof ApiError && err.code === "sandbox.busy")
650
+ return null;
651
+ throw err;
652
+ });
653
+ }
654
+ }
655
+ catch (err) {
656
+ this.d.log.warn("cannot take sandbox lock", { conversationId, err });
657
+ return false;
658
+ }
659
+ if (!lease)
660
+ return false;
661
+ try {
662
+ return await fn(lease);
663
+ }
664
+ catch (err) {
665
+ this.d.log.warn("sandbox maintenance failed; will retry", { conversationId, err });
666
+ await this.d.db
667
+ .updateTable("sandboxes")
668
+ .set({ sweep_failed_at: sql `now()` })
669
+ .where("conversation_id", "=", conversationId)
670
+ .execute()
671
+ .catch(() => { });
672
+ return false;
673
+ }
674
+ finally {
675
+ await lease.release();
676
+ }
677
+ }
678
+ /**
679
+ * Under the exclusive lease: make the conversation's sandbox run. `gone`: the instance a holder
680
+ * found gone; when it is still the recorded one it is rebuilt, when another holder already
681
+ * rebuilt it the new one is used.
682
+ */
683
+ async ensure(ctx, p, held, signal, gone) {
684
+ const id = ctx.conversationId;
685
+ await this.d.db
686
+ .insertInto("sandboxes")
687
+ .values({ conversation_id: id, org_id: ctx.orgId, provider: p.plugin, status: "creating" })
688
+ .onConflict((oc) => oc.column("conversation_id").doNothing())
689
+ .execute();
690
+ const row = await this.d.db
691
+ .selectFrom("sandboxes")
692
+ .selectAll()
693
+ .where("conversation_id", "=", id)
694
+ .executeTakeFirstOrThrow();
695
+ if (row.org_id !== ctx.orgId) {
696
+ throw new ApiError("forbidden", `The sandbox of conversation ${id} does not belong to organization ${ctx.orgId}`);
697
+ }
698
+ const fence = { gen: row.generation };
699
+ // 每次拿到锁先防护地加一:锁已过期、却还在跑的旧持有者(如卡住的扫描)之后的写入一定失败
700
+ await this.fenced(held, id, fence, {});
701
+ const handle = row.handle;
702
+ const createdAt = row.created_at.getTime();
703
+ if (handle && row.provider === p.plugin && isInstance(row, gone) && row.status === "running") {
704
+ const created = await this.create(ctx, p, held, fence, signal, {});
705
+ return { ...created, info: { fresh: true, recreated: true }, fence };
706
+ }
707
+ if (handle && row.provider === p.plugin) {
708
+ if (row.status === "running") {
709
+ return { handle, createdAt, info: { fresh: false, recreated: false }, fence };
710
+ }
711
+ if (row.status === "suspended") {
712
+ const r = await p.impl.resume(handle, signal);
713
+ if (r === "resumed") {
714
+ await this.fenced(held, id, fence, {
715
+ status: "running",
716
+ running_since: sql `now()`,
717
+ suspended_at: null,
718
+ sweep_failed_at: null,
719
+ });
720
+ return {
721
+ handle,
722
+ createdAt,
723
+ info: { fresh: false, recreated: false },
724
+ fence,
725
+ resumed: true,
726
+ };
727
+ }
728
+ this.d.log.info("sandbox is gone at the provider; recreating", { conversationId: id });
729
+ }
730
+ }
731
+ // 换了提供方且旧实例还没销毁:交给旧提供方(仍加载时)销毁,否则记进 prev_*
732
+ let prev = {};
733
+ if (handle && row.provider !== p.plugin && row.status !== "destroyed") {
734
+ const kept = await this.retire(id, row.provider, handle, signal);
735
+ if (kept) {
736
+ if (row.prev_handle) {
737
+ this.d.log.warn("sandbox instance of an earlier provider is leaked", {
738
+ conversationId: id,
739
+ provider: row.prev_provider,
740
+ });
741
+ }
742
+ prev = { prev_provider: row.provider, prev_handle: JSON.stringify(handle) };
743
+ }
744
+ }
745
+ const created = await this.create(ctx, p, held, fence, signal, prev);
746
+ // had_instance 只会置 true:之前成功创建过(哪怕后来重建失败)都算重建
747
+ return { ...created, info: { fresh: true, recreated: row.had_instance }, fence };
748
+ }
749
+ /**
750
+ * 在锁内创建:先防护地重置为 creating,再调提供方,最后防护地写入句柄。
751
+ * 写句柄时发现锁已丢失:尽力销毁刚建好的实例,再抛 {@link SandboxLockLostError}。
752
+ */
753
+ async create(ctx, p, held, fence, signal, extra) {
754
+ const id = ctx.conversationId;
755
+ await this.fenced(held, id, fence, {
756
+ org_id: ctx.orgId,
757
+ provider: p.plugin,
758
+ handle: null,
759
+ status: "creating",
760
+ last_used_at: sql `now()`,
761
+ running_since: null,
762
+ suspended_at: null,
763
+ destroyed_at: null,
764
+ sweep_failed_at: null,
765
+ ...extra,
766
+ });
767
+ const cfg = this.d.config().sandbox;
768
+ // 出站按组织设置收窄(Ruling M19);只在创建时生效,改设置只影响之后新建的沙箱
769
+ const egress = effectiveSandboxEgress(cfg.limits, (await readOrgSettings(this.d.db, ctx.orgId)).sandboxEgress);
770
+ // cpu / 内存按企业管理员的组织覆盖(C19 / Ruling M16),同样只在创建时生效
771
+ const sized = effectiveSandboxLimits({ ...cfg.limits, ...egress }, await readSandboxLimitOverride(this.d.db, ctx.orgId));
772
+ const limits = {
773
+ cpu: sized.cpu,
774
+ memoryMb: sized.memoryMb,
775
+ diskMb: sized.diskMb,
776
+ egress: sized.egress,
777
+ allowHosts: sized.allowHosts,
778
+ };
779
+ const handle = await p.impl.create({ name: `cap-${id}`, image: cfg.image, limits, signal });
780
+ let createdAt;
781
+ try {
782
+ createdAt = await this.fenced(held, id, fence, {
783
+ status: "running",
784
+ handle: JSON.stringify(handle),
785
+ had_instance: true,
786
+ created_at: sql `now()`,
787
+ running_since: sql `now()`,
788
+ last_used_at: sql `now()`,
789
+ });
790
+ }
791
+ catch (e) {
792
+ // 锁已丢失、没能记下句柄:尽力销毁刚建好的实例,免得泄漏。
793
+ // 提供方按名字(cap-<对话>)复用实例时(Ruling M10),这个 destroy 可能打到新持有者刚恢复 / 创建的同一个实例。
794
+ // 这能自愈:新持有者下一次提供方调用抛 SandboxGoneError(或 resume 返回 gone),withSandbox 原地重建并重试。
795
+ await p.impl.destroy(handle, AbortSignal.timeout(SWEEP_OP_TIMEOUT_MS)).catch((err) => this.d.log.warn("cannot destroy sandbox created after the lock was lost", {
796
+ conversationId: id,
797
+ err,
798
+ }));
799
+ throw e;
800
+ }
801
+ this.d.log.info("sandbox created", { conversationId: id, provider: p.plugin });
802
+ return { handle, createdAt: createdAt.getTime() };
803
+ }
804
+ /**
805
+ * 用旧提供方销毁别的提供方创建的实例:旧提供方仍加载且销毁成功 → false;否则 warn 并返回 true(需要留着记录)。
806
+ */
807
+ async retire(id, provider, handle, signal) {
808
+ const old = this.d.lookup?.(provider);
809
+ if (old) {
810
+ try {
811
+ await old.destroy(handle, AbortSignal.any([signal, AbortSignal.timeout(SWEEP_OP_TIMEOUT_MS)]));
812
+ return false;
813
+ }
814
+ catch (err) {
815
+ this.d.log.warn("cannot destroy sandbox instance of the previous provider", {
816
+ conversationId: id,
817
+ provider,
818
+ err,
819
+ });
820
+ return true;
821
+ }
822
+ }
823
+ this.d.log.warn("sandbox was created by another provider that is not loaded; its instance may leak", { conversationId: id, provider });
824
+ return true;
825
+ }
826
+ /** 在锁内:销毁实例(与遗留的旧提供方实例);对话已不存在的删掉记录,否则标记 destroyed。 */
827
+ async destroyRow(row, p, held) {
828
+ const id = row.conversation_id;
829
+ const fence = { gen: row.generation };
830
+ const signal = opSignal(held);
831
+ let prev = {};
832
+ if (row.handle && row.provider === p.plugin) {
833
+ await p.impl.destroy(row.handle, signal);
834
+ }
835
+ else if (row.handle && row.status !== "destroyed") {
836
+ if (await this.retire(id, row.provider, row.handle, signal)) {
837
+ prev = { prev_provider: row.provider, prev_handle: JSON.stringify(row.handle) };
838
+ }
839
+ }
840
+ else if (!row.handle && row.provider === p.plugin && p.impl.destroyByName) {
841
+ // 创建中途退出:提供方那边可能已有实例,只能按名字清理
842
+ await p.impl.destroyByName(`cap-${id}`, signal);
843
+ }
844
+ if (row.prev_handle && row.prev_provider && !("prev_handle" in prev)) {
845
+ if (!(await this.retire(id, row.prev_provider, row.prev_handle, signal))) {
846
+ prev = { prev_provider: null, prev_handle: null };
847
+ }
848
+ }
849
+ if (!(await held.renew()))
850
+ throw new SandboxLockLostError(id);
851
+ const conversation = await this.d.db
852
+ .selectFrom("conversations")
853
+ .select("id")
854
+ .where("id", "=", id)
855
+ .executeTakeFirst();
856
+ const keepRow = conversation !== undefined || (prev.prev_handle ?? row.prev_handle) != null;
857
+ if (keepRow) {
858
+ await this.fenced(held, id, fence, {
859
+ status: "destroyed",
860
+ destroyed_at: sql `now()`,
861
+ running_since: null,
862
+ sweep_failed_at: null,
863
+ ...prev,
864
+ });
865
+ }
866
+ else {
867
+ const r = await this.d.db
868
+ .deleteFrom("sandboxes")
869
+ .where("conversation_id", "=", id)
870
+ .where("generation", "=", fence.gen)
871
+ .executeTakeFirst();
872
+ if (Number(r.numDeletedRows) !== 1)
873
+ throw new SandboxLockLostError(id, "generation");
874
+ }
875
+ this.d.log.info("sandbox destroyed", { conversationId: id });
876
+ }
877
+ /** 该休眠的:本提供方创建、运行中,且空闲到期或连续运行超时。 */
878
+ suspendDue(now, plugin) {
879
+ const lim = this.d.config().sandbox.limits;
880
+ const idleBefore = new Date(now.getTime() - lim.suspendAfterIdleMinutes * 60_000);
881
+ const runningBefore = new Date(now.getTime() - lim.maxRunningMinutes * 60_000);
882
+ return (eb) => eb.and([
883
+ eb("status", "=", "running"),
884
+ eb("provider", "=", plugin),
885
+ eb.or([eb("last_used_at", "<", idleBefore), eb("running_since", "<", runningBefore)]),
886
+ ]);
887
+ }
888
+ /** 该销毁的:未销毁,且不活跃到期、组织已删除或对话已不存在。 */
889
+ destroyDue(now) {
890
+ const days = this.d.config().sandbox.limits.destroyAfterInactiveDays;
891
+ const before = new Date(now.getTime() - days * 24 * 3_600_000);
892
+ return (eb) => eb.and([
893
+ eb("status", "<>", "destroyed"),
894
+ eb.or([
895
+ eb("last_used_at", "<", before),
896
+ eb.exists(eb
897
+ .selectFrom("orgs")
898
+ .select("orgs.id")
899
+ .whereRef("orgs.id", "=", "sandboxes.org_id")
900
+ .where("orgs.deleted_at", "is not", null)),
901
+ eb.not(eb.exists(eb
902
+ .selectFrom("conversations")
903
+ .select("conversations.id")
904
+ .whereRef("conversations.id", "=", "sandboxes.conversation_id"))),
905
+ ]),
906
+ ]);
907
+ }
908
+ }