@capybara-hq/kernel 0.1.0-rc.3 → 0.1.0-rc.4

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 (226) hide show
  1. package/dist/admin/admin.controller.d.ts +9 -4
  2. package/dist/admin/admin.controller.js +13 -4
  3. package/dist/admin/extensions.controller.d.ts +8 -1
  4. package/dist/admin/extensions.controller.js +22 -2
  5. package/dist/agents/agent-lookup.d.ts +3 -0
  6. package/dist/agents/agent-lookup.js +10 -0
  7. package/dist/agents/agent-profile.service.d.ts +29 -0
  8. package/dist/agents/agent-profile.service.js +117 -0
  9. package/dist/agents/agents.controller.d.ts +93 -3
  10. package/dist/agents/agents.controller.js +114 -4
  11. package/dist/agents/agents.module.js +6 -2
  12. package/dist/agents/agents.service.d.ts +13 -5
  13. package/dist/agents/agents.service.js +30 -7
  14. package/dist/agents/definition.d.ts +4 -0
  15. package/dist/agents/definition.js +10 -0
  16. package/dist/agents/instruction-files.d.ts +60 -0
  17. package/dist/agents/instruction-files.js +111 -0
  18. package/dist/agents/models.controller.d.ts +9 -3
  19. package/dist/agents/models.controller.js +19 -4
  20. package/dist/agents/profile.d.ts +96 -0
  21. package/dist/agents/profile.js +51 -0
  22. package/dist/agents/readiness.controller.d.ts +26 -0
  23. package/dist/agents/readiness.controller.js +56 -0
  24. package/dist/agents/templates.js +1 -1
  25. package/dist/background/background.controller.d.ts +38 -0
  26. package/dist/background/background.controller.js +87 -0
  27. package/dist/background/background.module.d.ts +10 -0
  28. package/dist/background/background.module.js +32 -0
  29. package/dist/background/finish.d.ts +43 -0
  30. package/dist/background/finish.js +94 -0
  31. package/dist/background/launch-lock.d.ts +14 -0
  32. package/dist/background/launch-lock.js +39 -0
  33. package/dist/background/launch.d.ts +36 -0
  34. package/dist/background/launch.js +70 -0
  35. package/dist/background/poll.job.d.ts +53 -0
  36. package/dist/background/poll.job.js +182 -0
  37. package/dist/background/render.d.ts +23 -0
  38. package/dist/background/render.js +48 -0
  39. package/dist/background/session.d.ts +37 -0
  40. package/dist/background/session.js +66 -0
  41. package/dist/background/status.d.ts +35 -0
  42. package/dist/background/status.js +57 -0
  43. package/dist/background/stop.d.ts +62 -0
  44. package/dist/background/stop.js +225 -0
  45. package/dist/background/store.d.ts +150 -0
  46. package/dist/background/store.js +351 -0
  47. package/dist/background/store.module.d.ts +6 -0
  48. package/dist/background/store.module.js +22 -0
  49. package/dist/background/tasks.service.d.ts +67 -0
  50. package/dist/background/tasks.service.js +186 -0
  51. package/dist/background/wake-rules.d.ts +57 -0
  52. package/dist/background/wake-rules.js +83 -0
  53. package/dist/background/wake.d.ts +28 -0
  54. package/dist/background/wake.js +57 -0
  55. package/dist/catalog/claude/agents.js +2 -0
  56. package/dist/catalog/marketplaces.service.d.ts +1 -1
  57. package/dist/catalog/plugin-import.js +1 -1
  58. package/dist/catalog/plugin-upgrade.js +1 -1
  59. package/dist/channels/cards.js +1 -1
  60. package/dist/channels/connections.service.d.ts +16 -1
  61. package/dist/channels/connections.service.js +38 -4
  62. package/dist/channels/threads.d.ts +5 -0
  63. package/dist/channels/threads.js +8 -0
  64. package/dist/config/schema.d.ts +18 -0
  65. package/dist/config/schema.js +102 -0
  66. package/dist/conversations/access.js +2 -0
  67. package/dist/conversations/attachments.service.d.ts +30 -0
  68. package/dist/conversations/attachments.service.js +130 -5
  69. package/dist/conversations/conversations.controller.d.ts +36 -5
  70. package/dist/conversations/conversations.controller.js +62 -12
  71. package/dist/conversations/conversations.module.js +3 -0
  72. package/dist/conversations/conversations.service.d.ts +44 -2
  73. package/dist/conversations/conversations.service.js +140 -12
  74. package/dist/conversations/event-log.d.ts +1 -1
  75. package/dist/conversations/event-log.js +10 -1
  76. package/dist/conversations/events.d.ts +58 -0
  77. package/dist/conversations/events.js +57 -2
  78. package/dist/conversations/queue.d.ts +3 -1
  79. package/dist/conversations/queue.js +13 -2
  80. package/dist/conversations/queue.service.js +3 -1
  81. package/dist/conversations/titles.d.ts +7 -0
  82. package/dist/conversations/titles.js +15 -0
  83. package/dist/db/kernel-schema.d.ts +58 -2
  84. package/dist/db/pool.d.ts +5 -0
  85. package/dist/db/pool.js +3 -0
  86. package/dist/errors.d.ts +8 -0
  87. package/dist/errors.js +8 -0
  88. package/dist/evals/run-replay.d.ts +5 -2
  89. package/dist/evals/run-replay.js +30 -7
  90. package/dist/evals/step-replay.js +12 -6
  91. package/dist/evals/trajectory.js +1 -1
  92. package/dist/extensions/admin-ops.js +2 -0
  93. package/dist/extensions/admin-view.d.ts +3 -1
  94. package/dist/extensions/admin-view.js +10 -2
  95. package/dist/extensions/blobs.js +1 -0
  96. package/dist/extensions/config-issues.d.ts +13 -1
  97. package/dist/extensions/config-issues.js +22 -5
  98. package/dist/extensions/config-json-schema.d.ts +43 -7
  99. package/dist/extensions/config-json-schema.js +124 -58
  100. package/dist/extensions/deployment-config-store.d.ts +6 -0
  101. package/dist/extensions/deployment-config.d.ts +50 -3
  102. package/dist/extensions/deployment-config.js +125 -28
  103. package/dist/extensions/display-name.d.ts +22 -0
  104. package/dist/extensions/display-name.js +77 -0
  105. package/dist/extensions/extensions.module.js +6 -0
  106. package/dist/extensions/host.d.ts +26 -1
  107. package/dist/extensions/host.js +82 -9
  108. package/dist/extensions/org-config.controller.d.ts +11 -3
  109. package/dist/extensions/org-config.controller.js +14 -6
  110. package/dist/extensions/org-config.d.ts +6 -0
  111. package/dist/extensions/org-config.js +12 -13
  112. package/dist/extensions/package.d.ts +14 -1
  113. package/dist/extensions/package.js +51 -2
  114. package/dist/extensions/permission-view.d.ts +3 -0
  115. package/dist/extensions/point-defaults.controller.d.ts +34 -0
  116. package/dist/extensions/point-defaults.controller.js +71 -0
  117. package/dist/extensions/point-defaults.d.ts +64 -0
  118. package/dist/extensions/point-defaults.js +206 -0
  119. package/dist/extensions/registry.d.ts +1 -1
  120. package/dist/extensions/registry.js +2 -0
  121. package/dist/http/api.module.js +8 -0
  122. package/dist/http/nest-logger.d.ts +6 -5
  123. package/dist/http/nest-logger.js +24 -10
  124. package/dist/identity/auth.controller.js +9 -5
  125. package/dist/identity/cookies.d.ts +20 -5
  126. package/dist/identity/cookies.js +44 -6
  127. package/dist/identity/me.controller.d.ts +5 -1
  128. package/dist/identity/me.controller.js +12 -3
  129. package/dist/identity/sessions.d.ts +12 -0
  130. package/dist/identity/sessions.js +45 -0
  131. package/dist/index.d.ts +1 -1
  132. package/dist/infra/config/kernel-config.service.js +6 -0
  133. package/dist/infra/database/database.module.js +1 -0
  134. package/dist/infra/events/kernel-events.d.ts +8 -0
  135. package/dist/infra/lifecycle/process-guards.js +4 -0
  136. package/dist/infra/logging/logging.module.d.ts +4 -2
  137. package/dist/infra/logging/logging.module.js +24 -6
  138. package/dist/infra/queue/redis-connection.d.ts +6 -0
  139. package/dist/infra/queue/redis-connection.js +7 -0
  140. package/dist/infra/redis/redis.module.js +6 -2
  141. package/dist/infra/tokens.d.ts +6 -1
  142. package/dist/infra/tokens.js +6 -1
  143. package/dist/log.d.ts +13 -1
  144. package/dist/log.js +93 -17
  145. package/dist/models/purposes.d.ts +23 -0
  146. package/dist/models/purposes.js +31 -10
  147. package/dist/orgs/offboarding.d.ts +3 -1
  148. package/dist/orgs/offboarding.js +26 -16
  149. package/dist/orgs/settings.service.d.ts +5 -0
  150. package/dist/orgs/settings.service.js +13 -2
  151. package/dist/roles/worker.module.js +6 -0
  152. package/dist/runtime/context/build.d.ts +9 -7
  153. package/dist/runtime/context/build.js +27 -19
  154. package/dist/runtime/context/claude-tools.d.ts +2 -1
  155. package/dist/runtime/context/claude-tools.js +5 -4
  156. package/dist/runtime/context/compaction.js +4 -0
  157. package/dist/runtime/langgraph-loop.js +3 -2
  158. package/dist/runtime/loop.d.ts +4 -3
  159. package/dist/runtime/maintenance.d.ts +4 -2
  160. package/dist/runtime/maintenance.js +17 -5
  161. package/dist/runtime/model-call.d.ts +17 -1
  162. package/dist/runtime/model-call.js +3 -2
  163. package/dist/runtime/providers.d.ts +6 -0
  164. package/dist/runtime/providers.js +8 -0
  165. package/dist/runtime/run-queue.d.ts +25 -2
  166. package/dist/runtime/run-queue.js +129 -33
  167. package/dist/runtime/run-worker.js +16 -3
  168. package/dist/runtime/runtime.module.js +8 -3
  169. package/dist/runtime/step-log.d.ts +6 -0
  170. package/dist/runtime/step-log.js +16 -2
  171. package/dist/runtime/subagents/definition.js +3 -1
  172. package/dist/runtime/usage-report.d.ts +2 -1
  173. package/dist/runtime/usage-report.js +2 -1
  174. package/dist/runtime/waits.d.ts +8 -2
  175. package/dist/runtime/waits.js +37 -11
  176. package/dist/sandbox/sandbox.module.js +6 -1
  177. package/dist/sandbox/sandbox.service.d.ts +61 -3
  178. package/dist/sandbox/sandbox.service.js +141 -7
  179. package/dist/tools/approvals.service.js +2 -0
  180. package/dist/tools/builtin-names.d.ts +20 -2
  181. package/dist/tools/builtin-names.js +29 -2
  182. package/dist/tools/builtins/builtin-tools.d.ts +15 -1
  183. package/dist/tools/builtins/builtin-tools.js +34 -2
  184. package/dist/tools/builtins/sandbox/bash.d.ts +1 -1
  185. package/dist/tools/builtins/sandbox/bash.js +230 -3
  186. package/dist/tools/builtins/sandbox/common.d.ts +27 -4
  187. package/dist/tools/builtins/sandbox/common.js +7 -3
  188. package/dist/tools/builtins/sandbox/deliver.js +1 -1
  189. package/dist/tools/builtins/sandbox/index.d.ts +2 -0
  190. package/dist/tools/builtins/sandbox/index.js +6 -0
  191. package/dist/tools/builtins/sandbox/task-output.d.ts +63 -0
  192. package/dist/tools/builtins/sandbox/task-output.js +304 -0
  193. package/dist/tools/builtins/sandbox/task-stop.d.ts +10 -0
  194. package/dist/tools/builtins/sandbox/task-stop.js +112 -0
  195. package/dist/tools/builtins/web/errors.d.ts +8 -0
  196. package/dist/tools/builtins/web/errors.js +53 -0
  197. package/dist/tools/builtins/web/extract.d.ts +62 -0
  198. package/dist/tools/builtins/web/extract.js +267 -0
  199. package/dist/tools/builtins/web/fetch.d.ts +55 -0
  200. package/dist/tools/builtins/web/fetch.js +174 -0
  201. package/dist/tools/builtins/web/index.d.ts +13 -0
  202. package/dist/tools/builtins/web/index.js +12 -0
  203. package/dist/tools/builtins/web/page-cache.d.ts +25 -0
  204. package/dist/tools/builtins/web/page-cache.js +77 -0
  205. package/dist/tools/builtins/web/search.d.ts +25 -0
  206. package/dist/tools/builtins/web/search.js +209 -0
  207. package/dist/tools/builtins/web/url.d.ts +17 -0
  208. package/dist/tools/builtins/web/url.js +43 -0
  209. package/dist/tools/oauth/flow-paths.d.ts +1 -1
  210. package/dist/tools/oauth/flow-paths.js +1 -1
  211. package/dist/tools/pipeline.js +15 -0
  212. package/dist/tools/registry.d.ts +22 -3
  213. package/dist/tools/registry.js +26 -11
  214. package/dist/tools/tools.module.js +12 -0
  215. package/dist/tools/validate.js +1 -1
  216. package/package.json +3 -3
  217. package/src/migrations/0064_account_last_org.sql +7 -0
  218. package/src/migrations/0065_conversation_archive.sql +8 -0
  219. package/src/migrations/0066_extension_display_name.sql +4 -0
  220. package/src/migrations/0067_agent_profiles.sql +20 -0
  221. package/src/migrations/0068_agent_avatar_uploads.sql +7 -0
  222. package/src/migrations/0069_attachments_agent_idx.sql +3 -0
  223. package/src/migrations/0070_background_tasks.sql +37 -0
  224. package/src/migrations/0071_background_tasks_reserve.sql +6 -0
  225. package/src/migrations/0072_background_tasks_stopping.sql +5 -0
  226. package/src/migrations/0073_org_extension_point_defaults.sql +11 -0
@@ -11,6 +11,7 @@ import { getQueueToken } from "@nestjs/bullmq";
11
11
  import { Inject, Injectable } from "@nestjs/common";
12
12
  import { DelayedError } from "bullmq";
13
13
  import { sql } from "kysely";
14
+ import { BackgroundTaskStopper } from "../background/stop.js";
14
15
  import { EventLog } from "../conversations/event-log.js";
15
16
  import { KernelDb } from "../infra/database/kernel-db.js";
16
17
  import { TransactionHost } from "../infra/database/transaction-host.js";
@@ -70,6 +71,7 @@ let Waits = class Waits {
70
71
  abort;
71
72
  timeouts;
72
73
  log;
74
+ background;
73
75
  txHost;
74
76
  /** Drops the `run.orphaned` subscription (the events may outlive this instance). */
75
77
  unsubscribe;
@@ -88,7 +90,9 @@ let Waits = class Waits {
88
90
  */
89
91
  abort,
90
92
  /** The maintenance queue: wait timeout jobs (`MaintenanceProcessor` consumes them). */
91
- timeouts, events, log, txHost) {
93
+ timeouts, events, log,
94
+ /** Stops a finished sub-agent run's background tasks ({@link childFinished}). */
95
+ background, txHost) {
92
96
  this.kdb = kdb;
93
97
  this.eventLog = eventLog;
94
98
  this.approvals = approvals;
@@ -100,6 +104,7 @@ let Waits = class Waits {
100
104
  this.abort = abort;
101
105
  this.timeouts = timeouts;
102
106
  this.log = log;
107
+ this.background = background;
103
108
  this.txHost = txHost;
104
109
  // an orphaned parent run (marked failed by `ensureRun`) stops its children at once; the
105
110
  // sweep's `sweepChildrenOfFinished` is the backstop
@@ -193,9 +198,17 @@ let Waits = class Waits {
193
198
  const row = waitingFor.kind === "approval"
194
199
  ? await this.db
195
200
  .selectFrom("approvals")
196
- .select(["tool_key", "expires_at"])
201
+ .select(["tool_key", "expires_at", "requested_for", "approver_role"])
197
202
  .where("id", "=", waitingFor.id)
198
203
  .executeTakeFirst()
204
+ .then((r) => r && {
205
+ tool_key: r.tool_key,
206
+ expires_at: r.expires_at,
207
+ approver: {
208
+ requestedFor: r.requested_for,
209
+ approverRole: r.approver_role,
210
+ },
211
+ })
199
212
  : await this.db
200
213
  .selectFrom("questions")
201
214
  .select([sql `spec->>'toolKey'`.as("tool_key"), "expires_at"])
@@ -213,7 +226,10 @@ let Waits = class Waits {
213
226
  childRunId: runId,
214
227
  kind: waitingFor.kind,
215
228
  ...(waitingFor.kind === "approval"
216
- ? { approvalId: waitingFor.id }
229
+ ? {
230
+ approvalId: waitingFor.id,
231
+ ...row.approver,
232
+ }
217
233
  : { questionId: waitingFor.id }),
218
234
  toolKey: row.tool_key,
219
235
  expiresAt: new Date(row.expires_at).toISOString(),
@@ -925,7 +941,8 @@ let Waits = class Waits {
925
941
  * the conditional resume lets exactly one succeed, the last to see all its siblings terminal. A
926
942
  * run that is not a dispatched child, or not terminal yet, is left alone. A crash in between is
927
943
  * repaired by the wait sweep and by the parent's tools node, which writes a missing
928
- * `subagent_finished` itself.
944
+ * `subagent_finished` itself. The running background tasks the terminal child run started are
945
+ * then stopped (not those of a later follow-up run of the same child).
929
946
  */
930
947
  async childFinished(childRunId) {
931
948
  const row = await dispatchOfChildRun(this.db, childRunId);
@@ -938,13 +955,21 @@ let Waits = class Waits {
938
955
  .executeTakeFirst();
939
956
  if (!run || !TERMINAL_STATUSES.has(run.status))
940
957
  return;
941
- const r = await childResult(this.eventLog, row);
942
- if (!r) {
943
- this.log.warn("sub-agent run is terminal without a terminal event", { childRunId });
944
- return;
958
+ try {
959
+ const r = await childResult(this.eventLog, row);
960
+ if (!r) {
961
+ this.log.warn("sub-agent run is terminal without a terminal event", { childRunId });
962
+ return;
963
+ }
964
+ await this.eventLog.append(row.parentConversationId, [finishedEvent(row, r.finished)]);
965
+ await this.resume(row.parentRunId, childRunId);
966
+ }
967
+ finally {
968
+ // whichever way the child run ended (its worker, a stop while waiting, an orphan repair):
969
+ // nobody can read or stop the background tasks it started through its model any more
970
+ // (background Bash spec §4.3). Never throws.
971
+ await this.background.stopSubagentTasks(row.childConversationId, childRunId);
945
972
  }
946
- await this.eventLog.append(row.parentConversationId, [finishedEvent(row, r.finished)]);
947
- await this.resume(row.parentRunId, childRunId);
948
973
  }
949
974
  /**
950
975
  * Stops the active children of a parent run (sub-agents spec §12), or only those dispatched in
@@ -1020,7 +1045,8 @@ Waits = __decorate([
1020
1045
  __param(9, Inject(getQueueToken(MAINTENANCE_QUEUE_NAME))),
1021
1046
  __param(10, Inject(KernelEvents)),
1022
1047
  __param(11, Inject(KERNEL_LOGGER)),
1023
- __param(12, Inject(TransactionHost))
1048
+ __param(12, Inject(BackgroundTaskStopper)),
1049
+ __param(13, Inject(TransactionHost))
1024
1050
  ], Waits);
1025
1051
  export { Waits };
1026
1052
  /**
@@ -5,6 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { Module } from "@nestjs/common";
8
+ import { BackgroundStoreModule } from "../background/store.module.js";
8
9
  import { ExtensionRegistryModule } from "../extensions/registry.js";
9
10
  import { QueueModule } from "../infra/queue/queue.module.js";
10
11
  import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
@@ -22,7 +23,11 @@ let SandboxModule = class SandboxModule {
22
23
  };
23
24
  SandboxModule = __decorate([
24
25
  Module({
25
- imports: [ExtensionRegistryModule, QueueModule.registerQueue(MAINTENANCE_QUEUE_NAME)],
26
+ imports: [
27
+ ExtensionRegistryModule,
28
+ BackgroundStoreModule,
29
+ QueueModule.registerQueue(MAINTENANCE_QUEUE_NAME),
30
+ ],
26
31
  providers: [SandboxProviders, SandboxLeases, SandboxService, SandboxJobs],
27
32
  exports: [SandboxService, SandboxJobs],
28
33
  })
@@ -1,6 +1,7 @@
1
1
  import { type SandboxHandle, type SandboxProvider } from "@capybara-hq/plugin-sdk";
2
2
  import type { Redis } from "ioredis";
3
3
  import { type Selectable } from "kysely";
4
+ import { BackgroundTaskStore } from "../background/store.js";
4
5
  import { type KernelConfig } from "../config/schema.js";
5
6
  import type { SandboxesTable } from "../db/kernel-schema.js";
6
7
  import { ApiError } from "../errors.js";
@@ -28,6 +29,28 @@ interface Instance {
28
29
  handle: SandboxHandle;
29
30
  createdAt: number;
30
31
  }
32
+ /**
33
+ * The provider {@link SandboxService.ifRunning} passes to its callback: every `exec` carries the
34
+ * holder's tag, like the one `withSandbox` passes.
35
+ */
36
+ export type TaggedProvider = SandboxProvider;
37
+ /** What {@link SandboxService.ifRunning} did. */
38
+ export type IfRunningResult<T> = {
39
+ kind: "ran";
40
+ value: T;
41
+ }
42
+ /**
43
+ * No sandbox, or the instance is not running: `status` is the sandbox row's (`suspended`,
44
+ * `creating`, `destroyed`, or `running` recorded with another provider), `gone` without a row.
45
+ */
46
+ | {
47
+ kind: "not_running";
48
+ status: SandboxesTable["status"] | "gone";
49
+ }
50
+ /** The sandbox now records another instance (a rebuild). */
51
+ | {
52
+ kind: "other_instance";
53
+ };
31
54
  /**
32
55
  * The row still records the instance `gone` (same handle and same `created_at`). Handles compare
33
56
  * canonically: a jsonb round trip reorders a multi-key handle's keys (final review m9).
@@ -100,11 +123,15 @@ export declare class SandboxService {
100
123
  private readonly providers;
101
124
  private readonly config;
102
125
  private readonly log;
126
+ /** Background tasks lost while their process may live on (killed after a resume). */
127
+ private readonly background;
103
128
  constructor(db: KernelDb,
104
129
  /** The record of the instance each holder last used (`capk:sandbox-seen:…`). */
105
130
  redis: Pick<Redis, "set">, leases: SandboxLeases,
106
131
  /** The `sandbox@1` provider (read at every call: providers are ready after the extension load). */
107
- providers: Pick<SandboxProviders, "current" | "lookup">, config: Pick<KernelConfigService, "get">, log: Pick<KernelLogger, "info" | "warn">);
132
+ providers: Pick<SandboxProviders, "current" | "lookup">, config: Pick<KernelConfigService, "get">, log: Pick<KernelLogger, "info" | "warn">,
133
+ /** Background tasks lost while their process may live on (killed after a resume). */
134
+ background: Pick<BackgroundTaskStore, "pendingKills" | "clearKill">);
108
135
  /** Poll interval while waiting for a lease (the leases' own). */
109
136
  private get pollMs();
110
137
  /** 是否有沙箱提供方。 */
@@ -119,6 +146,29 @@ export declare class SandboxService {
119
146
  status: "running" | "suspended";
120
147
  lastUsedAt: Date;
121
148
  } | null>;
149
+ /**
150
+ * The instance the conversation's sandbox records now (its `created_at`), while it has one that
151
+ * runs or is suspended; null otherwise. Read without a lease: a caller inside `withSandbox`
152
+ * reads the instance its own lease keeps.
153
+ */
154
+ currentInstance(conversationId: string): Promise<{
155
+ createdAt: Date;
156
+ } | null>;
157
+ /**
158
+ * Runs `fn` on the conversation's sandbox only while it runs the instance created at
159
+ * `instance.createdAt` (background tasks, spec 2026-11-05-background-bash-design §4.3). It never
160
+ * creates, resumes or rebuilds a sandbox: a sandbox that is missing, suspended, being created or
161
+ * destroyed (or recorded with another provider) is `not_running`, one that records another
162
+ * instance is `other_instance`. Otherwise `fn` runs under a shared lease, as a `withSandbox`
163
+ * holder does (its tag on every exec, crashed holders' commands killed), and `last_used_at`
164
+ * moves forward when it ends. Waits for the lease up to `sandbox.lockWaitSeconds`
165
+ * (`sandbox.busy`); `signal` (optional) ends the wait with its abort reason. The signal passed
166
+ * to `fn` aborts with `signal` or when the lease is lost. Errors of `fn` (`SandboxGoneError`
167
+ * too) are thrown as they are.
168
+ */
169
+ ifRunning<T>(conversationId: string, instance: {
170
+ createdAt: Date;
171
+ }, fn: (p: TaggedProvider, h: SandboxHandle, signal: AbortSignal) => Promise<T>, signal?: AbortSignal): Promise<IfRunningResult<T>>;
122
172
  /**
123
173
  * A short lease on one path of the conversation's sandbox around the kernel's own
124
174
  * read-modify-write of that file (spec §10.4); waits up to 30 s, then throws
@@ -164,8 +214,8 @@ export declare class SandboxService {
164
214
  /** Recorded running with the current provider, and not the instance found gone. */
165
215
  private usable;
166
216
  /**
167
- * Under a just-taken shared lease: confirms the row is still usable (else releases and returns
168
- * null), records the holder and kills crashed holders' commands.
217
+ * Under a just-taken shared lease: confirms the row is still `accept`ed (else releases and
218
+ * returns null), records the holder and kills crashed holders' commands.
169
219
  */
170
220
  private asShared;
171
221
  /**
@@ -206,6 +256,14 @@ export declare class SandboxService {
206
256
  * 结束);其余失败只记 warn。提供方没有 `killAll` 时只记 info。
207
257
  */
208
258
  private killLeftovers;
259
+ /**
260
+ * Under the exclusive lease, right after a resume or a (re)build: kills the sessions of the
261
+ * background tasks marked lost while their process may live on (`kill_pending`), in one exec of
262
+ * `pkill -KILL -s <sid>`, then clears their flags. Tasks of another instance have no process
263
+ * here and are only cleared. A gone instance and an abort are thrown; another failure is warned
264
+ * and leaves the flags of this instance's tasks for the next resume.
265
+ */
266
+ private killPendingTasks;
209
267
  private require;
210
268
  /**
211
269
  * 防护写入:lease 仍在、且记录的 generation 仍是锁内读到的值时才写(并加一);否则抛 {@link SandboxLockLostError}。
@@ -11,6 +11,7 @@ import { randomUUID } from "node:crypto";
11
11
  import { isSandboxGoneError, } from "@capybara-hq/plugin-sdk";
12
12
  import { Inject, Injectable } from "@nestjs/common";
13
13
  import { sql, } from "kysely";
14
+ import { BackgroundTaskStore } from "../background/store.js";
14
15
  import { DEFAULT_MAX_COMMAND_SECONDS, DEFAULT_MAX_RUN_SECONDS, } from "../config/schema.js";
15
16
  import { ApiError } from "../errors.js";
16
17
  import { readSandboxLimitOverride } from "../identity/settings.js";
@@ -36,6 +37,10 @@ const SWEEP_BATCH = 200;
36
37
  const SWEEP_CONCURRENCY = 4;
37
38
  /** destroyFor 等锁的上限:持有者在跑命令时最多等这么久。 */
38
39
  const DESTROY_FOR_WAIT_MS = 10 * 60_000;
40
+ /** The time limit of the exec that kills lost background tasks after a resume (ms). */
41
+ const PENDING_KILL_TIMEOUT_MS = 30_000;
42
+ /** A background task's session id as `echo $!` printed it; anything else is never put in a command. */
43
+ const SID_RE = /^[1-9][0-9]{0,9}$/;
39
44
  /** 维护里一次提供方操作的 signal:超时或锁丢失时中止。 */
40
45
  const opSignal = (lease) => AbortSignal.any([lease.signal, AbortSignal.timeout(SWEEP_OP_TIMEOUT_MS)]);
41
46
  /**
@@ -180,17 +185,21 @@ let SandboxService = class SandboxService {
180
185
  providers;
181
186
  config;
182
187
  log;
188
+ background;
183
189
  constructor(db,
184
190
  /** The record of the instance each holder last used (`capk:sandbox-seen:…`). */
185
191
  redis, leases,
186
192
  /** The `sandbox@1` provider (read at every call: providers are ready after the extension load). */
187
- providers, config, log) {
193
+ providers, config, log,
194
+ /** Background tasks lost while their process may live on (killed after a resume). */
195
+ background) {
188
196
  this.db = db;
189
197
  this.redis = redis;
190
198
  this.leases = leases;
191
199
  this.providers = providers;
192
200
  this.config = config;
193
201
  this.log = log;
202
+ this.background = background;
194
203
  }
195
204
  /** Poll interval while waiting for a lease (the leases' own). */
196
205
  get pollMs() {
@@ -219,6 +228,82 @@ let SandboxService = class SandboxService {
219
228
  return null;
220
229
  return { status: row.status, lastUsedAt: row.last_used_at };
221
230
  }
231
+ /**
232
+ * The instance the conversation's sandbox records now (its `created_at`), while it has one that
233
+ * runs or is suspended; null otherwise. Read without a lease: a caller inside `withSandbox`
234
+ * reads the instance its own lease keeps.
235
+ */
236
+ async currentInstance(conversationId) {
237
+ const row = await this.db
238
+ .selectFrom("sandboxes")
239
+ .select(["status", "handle", "created_at"])
240
+ .where("conversation_id", "=", conversationId)
241
+ .executeTakeFirst();
242
+ if (!row || row.handle === null)
243
+ return null;
244
+ if (row.status !== "running" && row.status !== "suspended")
245
+ return null;
246
+ return { createdAt: row.created_at };
247
+ }
248
+ /**
249
+ * Runs `fn` on the conversation's sandbox only while it runs the instance created at
250
+ * `instance.createdAt` (background tasks, spec 2026-11-05-background-bash-design §4.3). It never
251
+ * creates, resumes or rebuilds a sandbox: a sandbox that is missing, suspended, being created or
252
+ * destroyed (or recorded with another provider) is `not_running`, one that records another
253
+ * instance is `other_instance`. Otherwise `fn` runs under a shared lease, as a `withSandbox`
254
+ * holder does (its tag on every exec, crashed holders' commands killed), and `last_used_at`
255
+ * moves forward when it ends. Waits for the lease up to `sandbox.lockWaitSeconds`
256
+ * (`sandbox.busy`); `signal` (optional) ends the wait with its abort reason. The signal passed
257
+ * to `fn` aborts with `signal` or when the lease is lost. Errors of `fn` (`SandboxGoneError`
258
+ * too) are thrown as they are.
259
+ */
260
+ async ifRunning(conversationId, instance, fn, signal = new AbortController().signal) {
261
+ const p = this.require();
262
+ const conv = conversationId;
263
+ const cap = this.effectiveCap(p);
264
+ const deadline = Date.now() + this.config.get().sandbox.lockWaitSeconds * 1000;
265
+ const token = randomUUID();
266
+ for (;;) {
267
+ if (signal.aborted)
268
+ throw abortReason(signal);
269
+ const row = await this.db
270
+ .selectFrom("sandboxes")
271
+ .selectAll()
272
+ .where("conversation_id", "=", conv)
273
+ .executeTakeFirst();
274
+ if (!row)
275
+ return { kind: "not_running", status: "gone" };
276
+ if (row.created_at.getTime() !== instance.createdAt.getTime()) {
277
+ return { kind: "other_instance" };
278
+ }
279
+ if (!this.usable(row, p, null))
280
+ return { kind: "not_running", status: row.status };
281
+ // the instance this check saw: its handle plus `created_at` (never `generation`, which
282
+ // every lease bumps)
283
+ const want = { handle: row.handle, createdAt: row.created_at.getTime() };
284
+ if (!(await this.leases.legacyMarker(conv))) {
285
+ const lease = await this.leases.tryShared(conv, cap, token);
286
+ if (lease) {
287
+ const e = await this.asShared({ orgId: row.org_id, conversationId: conv }, p, lease, (r) => this.usable(r, p, null) && isInstance(r, want), signal);
288
+ if (e) {
289
+ try {
290
+ const impl = tagged(p.impl, lease.token, () => this.register(conv, lease.token));
291
+ const sig = AbortSignal.any([signal, lease.signal]);
292
+ return { kind: "ran", value: await fn(impl, e.handle, sig) };
293
+ }
294
+ finally {
295
+ await this.leave(conv, e);
296
+ }
297
+ }
298
+ // the row changed between the check and the lease: classify it again
299
+ continue;
300
+ }
301
+ }
302
+ if (Date.now() + this.pollMs > deadline)
303
+ throw sandboxBusy();
304
+ await sleep(this.pollMs + Math.floor(Math.random() * this.pollMs), signal);
305
+ }
306
+ }
222
307
  /**
223
308
  * A short lease on one path of the conversation's sandbox around the kernel's own
224
309
  * read-modify-write of that file (spec §10.4); waits up to 30 s, then throws
@@ -318,7 +403,7 @@ let SandboxService = class SandboxService {
318
403
  if (intent)
319
404
  await this.clearIntent(conv, token);
320
405
  intent = false;
321
- const e = await this.asShared(ctx, p, lease, gone, signal);
406
+ const e = await this.asShared(ctx, p, lease, (r) => this.usable(r, p, gone), signal);
322
407
  if (e)
323
408
  return e;
324
409
  }
@@ -365,14 +450,14 @@ let SandboxService = class SandboxService {
365
450
  !isInstance(row, gone));
366
451
  }
367
452
  /**
368
- * Under a just-taken shared lease: confirms the row is still usable (else releases and returns
369
- * null), records the holder and kills crashed holders' commands.
453
+ * Under a just-taken shared lease: confirms the row is still `accept`ed (else releases and
454
+ * returns null), records the holder and kills crashed holders' commands.
370
455
  */
371
- async asShared(ctx, p, lease, gone, signal) {
456
+ async asShared(ctx, p, lease, accept, signal) {
372
457
  const conv = ctx.conversationId;
373
458
  try {
374
459
  const row = await this.readRow(ctx);
375
- if (!this.usable(row, p, gone)) {
460
+ if (!accept(row)) {
376
461
  await lease.release();
377
462
  return null;
378
463
  }
@@ -429,6 +514,9 @@ let SandboxService = class SandboxService {
429
514
  if (!s.info.fresh && (s.resumed || dead.legacyMarker || dead.tags.length > 0)) {
430
515
  await this.killLeftovers(conv, s.handle, p, sig, s.resumed && !dead.legacyMarker && dead.tags.length === 0 ? "resume" : "takeover");
431
516
  }
517
+ if (s.resumed || s.info.fresh) {
518
+ await this.killPendingTasks(conv, s.handle, s.createdAt, p, lease.token, sig);
519
+ }
432
520
  const shared = await lease.downgrade();
433
521
  await this.register(conv, shared.token);
434
522
  return { lease: shared, handle: s.handle, createdAt: s.createdAt, info: s.info };
@@ -618,6 +706,51 @@ let SandboxService = class SandboxService {
618
706
  }
619
707
  await this.kill(conversationId, why, () => killAll.call(p.impl, handle, signal), signal);
620
708
  }
709
+ /**
710
+ * Under the exclusive lease, right after a resume or a (re)build: kills the sessions of the
711
+ * background tasks marked lost while their process may live on (`kill_pending`), in one exec of
712
+ * `pkill -KILL -s <sid>`, then clears their flags. Tasks of another instance have no process
713
+ * here and are only cleared. A gone instance and an abort are thrown; another failure is warned
714
+ * and leaves the flags of this instance's tasks for the next resume.
715
+ */
716
+ async killPendingTasks(conversationId, handle, createdAt, p, tag, signal) {
717
+ const rows = await this.background.pendingKills(conversationId);
718
+ if (rows.length === 0)
719
+ return;
720
+ const mine = rows.filter((r) => r.sandboxCreatedAt?.getTime() === createdAt && r.sid !== null && SID_RE.test(r.sid));
721
+ let cleared = rows;
722
+ if (mine.length > 0) {
723
+ // pkill exits 1 when the session has no process left: only a higher status is a failure
724
+ const command = `rc=0; for sid in ${mine.map((r) => r.sid).join(" ")}; do pkill -KILL -s "$sid"; c=$?; [ "$c" -le 1 ] || rc=$c; done; exit $rc`;
725
+ try {
726
+ const r = await p.impl.exec(handle, {
727
+ command,
728
+ timeoutMs: PENDING_KILL_TIMEOUT_MS,
729
+ maxOutputBytes: 4096,
730
+ signal,
731
+ tag,
732
+ });
733
+ if (r.exitCode !== 0) {
734
+ throw new Error(`pkill exited with ${r.exitCode === null ? "no status" : r.exitCode}: ${r.stderr.slice(0, 500)}`);
735
+ }
736
+ this.log.info("lost background tasks killed after a sandbox resume", {
737
+ conversationId,
738
+ tasks: mine.map((t) => t.id),
739
+ });
740
+ }
741
+ catch (err) {
742
+ if (signal.aborted || isSandboxGoneError(err))
743
+ throw err;
744
+ this.log.warn("cannot kill lost background tasks after a sandbox resume", {
745
+ conversationId,
746
+ err,
747
+ });
748
+ const kept = new Set(mine.map((r) => r.id));
749
+ cleared = rows.filter((r) => !kept.has(r.id));
750
+ }
751
+ }
752
+ await this.background.clearKill(cleared.map((r) => r.id));
753
+ }
621
754
  require() {
622
755
  const p = this.providers.current();
623
756
  if (!p)
@@ -934,6 +1067,7 @@ SandboxService = __decorate([
934
1067
  __param(2, Inject(SandboxLeases)),
935
1068
  __param(3, Inject(SandboxProviders)),
936
1069
  __param(4, Inject(KernelConfigService)),
937
- __param(5, Inject(KERNEL_LOGGER))
1070
+ __param(5, Inject(KERNEL_LOGGER)),
1071
+ __param(6, Inject(BackgroundTaskStore))
938
1072
  ], SandboxService);
939
1073
  export { SandboxService };
@@ -151,6 +151,8 @@ let ApprovalsService = class ApprovalsService {
151
151
  toolKey: rec.toolKey,
152
152
  expiresAt: rec.expiresAt,
153
153
  ...(input.policy !== undefined && { policy: input.policy }),
154
+ requestedFor: rec.requestedFor,
155
+ approverRole: rec.approverRole,
154
156
  },
155
157
  },
156
158
  ]);
@@ -21,11 +21,29 @@ export declare const BUILTIN: {
21
21
  readonly ReadToolOutput: "ReadToolOutput";
22
22
  readonly ToolSearch: "ToolSearch";
23
23
  readonly Agent: "Agent";
24
+ readonly TaskOutput: "TaskOutput";
25
+ readonly TaskStop: "TaskStop";
26
+ readonly WebSearch: "WebSearch";
27
+ readonly WebFetch: "WebFetch";
24
28
  };
25
29
  export type BuiltinName = (typeof BUILTIN)[keyof typeof BUILTIN];
26
- /** The builtin tools that run in the conversation's sandbox. */
30
+ /**
31
+ * The builtin tools that run in the conversation's sandbox. `TaskOutput` and `TaskStop` read and
32
+ * stop the background tasks `Bash` started (background Bash spec §2.2, §2.3).
33
+ */
27
34
  export declare const SANDBOX_TOOL_NAMES: readonly BuiltinName[];
28
- /** Sandbox tools that need approval when the org setting `sandboxToolPolicy` is `ask` (not the read-only ones). */
35
+ /** The builtin tools that reach the web through the org's web-search / web-fetch providers. */
36
+ export declare const WEB_TOOL_NAMES: readonly BuiltinName[];
37
+ /**
38
+ * The tools offered with `Bash` whenever it is offered (background Bash: a model that can start a
39
+ * background task can read and stop it). `ToolRegistry` adds them; an explicit ref is harmless.
40
+ */
41
+ export declare const BASH_COMPANION_TOOLS: readonly BuiltinName[];
42
+ /**
43
+ * Sandbox tools that need approval when the org setting `sandboxToolPolicy` is `ask` (not the
44
+ * read-only ones). Not `TaskOutput` or `TaskStop`: they only read or stop a task whose launch
45
+ * already went through `Bash`'s policy (background Bash spec §4.5).
46
+ */
29
47
  export declare const SANDBOX_ASK_TOOL_NAMES: ReadonlySet<string>;
30
48
  /**
31
49
  * Every builtin name. Saving an agent checks builtin refs against this list, not against what is
@@ -19,8 +19,15 @@ export const BUILTIN = {
19
19
  ReadToolOutput: "ReadToolOutput",
20
20
  ToolSearch: "ToolSearch",
21
21
  Agent: "Agent",
22
+ TaskOutput: "TaskOutput",
23
+ TaskStop: "TaskStop",
24
+ WebSearch: "WebSearch",
25
+ WebFetch: "WebFetch",
22
26
  };
23
- /** The builtin tools that run in the conversation's sandbox. */
27
+ /**
28
+ * The builtin tools that run in the conversation's sandbox. `TaskOutput` and `TaskStop` read and
29
+ * stop the background tasks `Bash` started (background Bash spec §2.2, §2.3).
30
+ */
24
31
  export const SANDBOX_TOOL_NAMES = [
25
32
  BUILTIN.Bash,
26
33
  BUILTIN.Read,
@@ -29,8 +36,21 @@ export const SANDBOX_TOOL_NAMES = [
29
36
  BUILTIN.Glob,
30
37
  BUILTIN.Grep,
31
38
  BUILTIN.Deliver,
39
+ BUILTIN.TaskOutput,
40
+ BUILTIN.TaskStop,
32
41
  ];
33
- /** Sandbox tools that need approval when the org setting `sandboxToolPolicy` is `ask` (not the read-only ones). */
42
+ /** The builtin tools that reach the web through the org's web-search / web-fetch providers. */
43
+ export const WEB_TOOL_NAMES = [BUILTIN.WebSearch, BUILTIN.WebFetch];
44
+ /**
45
+ * The tools offered with `Bash` whenever it is offered (background Bash: a model that can start a
46
+ * background task can read and stop it). `ToolRegistry` adds them; an explicit ref is harmless.
47
+ */
48
+ export const BASH_COMPANION_TOOLS = [BUILTIN.TaskOutput, BUILTIN.TaskStop];
49
+ /**
50
+ * Sandbox tools that need approval when the org setting `sandboxToolPolicy` is `ask` (not the
51
+ * read-only ones). Not `TaskOutput` or `TaskStop`: they only read or stop a task whose launch
52
+ * already went through `Bash`'s policy (background Bash spec §4.5).
53
+ */
34
54
  export const SANDBOX_ASK_TOOL_NAMES = new Set([
35
55
  BUILTIN.Bash,
36
56
  BUILTIN.Write,
@@ -51,6 +71,7 @@ export const BUILTIN_TOOL_NAMES = [
51
71
  BUILTIN.SkillWrite,
52
72
  ...SANDBOX_TOOL_NAMES,
53
73
  BUILTIN.Agent,
74
+ ...WEB_TOOL_NAMES,
54
75
  ];
55
76
  /** The builtin that loads a skill (added by the registry when the log has a skills catalog). */
56
77
  export const SKILL_LOAD = BUILTIN.Skill;
@@ -99,11 +120,15 @@ export const BUILTIN_TOOL_KINDS = {
99
120
  [BUILTIN.Glob]: "search",
100
121
  [BUILTIN.Grep]: "search",
101
122
  [BUILTIN.Deliver]: "tool",
123
+ [BUILTIN.TaskOutput]: "read",
124
+ [BUILTIN.TaskStop]: "command",
102
125
  [BUILTIN.ToolSearch]: "meta",
103
126
  [BUILTIN.Skill]: "meta",
104
127
  [BUILTIN.SkillWrite]: "meta",
105
128
  [BUILTIN.ReadToolOutput]: "meta",
106
129
  [BUILTIN.AskUserQuestion]: "meta",
130
+ [BUILTIN.WebSearch]: "search",
131
+ [BUILTIN.WebFetch]: "read",
107
132
  };
108
133
  /**
109
134
  * Input fields left out when matching a replayed call with a recorded one (full-run replay), per
@@ -112,5 +137,7 @@ export const BUILTIN_TOOL_KINDS = {
112
137
  * with one (or a different wording) still match.
113
138
  */
114
139
  export const MATCH_IGNORED_FIELDS = {
140
+ // `run_in_background` is matched on purpose: it changes what the call did (a launch that
141
+ // returns at once instead of the command's output; background Bash spec §4.4)
115
142
  [BUILTIN.Bash]: ["description"],
116
143
  };
@@ -1,12 +1,22 @@
1
+ import type { Logger } from "@capybara-hq/plugin-sdk";
2
+ import { BackgroundTaskFinisher } from "../../background/finish.js";
3
+ import { BackgroundTaskStopper } from "../../background/stop.js";
4
+ import { BackgroundTaskStore } from "../../background/store.js";
1
5
  import { AttachmentsService } from "../../conversations/attachments.service.js";
2
6
  import { EventLog } from "../../conversations/event-log.js";
7
+ import { ExtensionPointDefaults } from "../../extensions/point-defaults.js";
3
8
  import { KernelConfigService } from "../../infra/config/kernel-config.service.js";
4
9
  import { KernelDb } from "../../infra/database/kernel-db.js";
10
+ import { ModelPurposesService } from "../../models/purposes.js";
11
+ import type { HookRunner } from "../../runtime/hook-calls.js";
12
+ import { StepLog } from "../../runtime/step-log.js";
13
+ import { UsageReporter } from "../../runtime/usage-report.js";
5
14
  import { SandboxService } from "../../sandbox/sandbox.service.js";
6
15
  import { SkillChangesService } from "../../skills/changes.service.js";
7
16
  import { StorageService } from "../../storage/storage.service.js";
8
17
  import { ToolOutputs } from "../outputs.js";
9
18
  import type { BuiltinTool } from "../registry.js";
19
+ import { WebPages } from "./web/index.js";
10
20
  /**
11
21
  * The kernel's builtin tools, built once from the injected services (their specs: names,
12
22
  * descriptions and schemas, are the factories' own and unchanged). `ToolSearch` and `Skill`
@@ -15,7 +25,11 @@ import type { BuiltinTool } from "../registry.js";
15
25
  */
16
26
  export declare class BuiltinTools {
17
27
  private readonly tools;
18
- constructor(db: KernelDb, config: KernelConfigService, outputs: ToolOutputs, storage: StorageService, skillChanges: SkillChangesService, attachments: AttachmentsService, eventLog: EventLog, sandbox: SandboxService);
28
+ constructor(db: KernelDb, config: KernelConfigService, outputs: ToolOutputs, storage: StorageService, skillChanges: SkillChangesService, attachments: AttachmentsService, eventLog: EventLog, sandbox: SandboxService, points: ExtensionPointDefaults, webPages: WebPages,
29
+ /** The WebFetch extraction call: the `web_fetch` purpose, its hooks, usage and denials. */
30
+ purposes: ModelPurposesService, host: {
31
+ readonly hooks?: HookRunner;
32
+ } | undefined, usageReport: UsageReporter, stepLog: StepLog, log: Logger, background: BackgroundTaskStore, finisher: BackgroundTaskFinisher, stopper: BackgroundTaskStopper);
19
33
  /** The builtin tools, in a fixed order. */
20
34
  list(): readonly BuiltinTool[];
21
35
  }