@capybara-hq/kernel 0.1.0-rc.22 → 0.1.0-rc.24

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 (234) hide show
  1. package/bootstrap.json +25 -25
  2. package/dist/admin/admin.controller.d.ts +3 -3
  3. package/dist/admin/admin.controller.js +6 -2
  4. package/dist/admin/admin.module.d.ts +2 -1
  5. package/dist/admin/admin.module.js +7 -3
  6. package/dist/admin/admin.schemas.d.ts +13 -0
  7. package/dist/admin/outbox-dead.controller.d.ts +23 -0
  8. package/dist/admin/outbox-dead.controller.js +62 -0
  9. package/dist/admin/outbox-dead.schemas.d.ts +8 -0
  10. package/dist/admin/outbox-dead.schemas.js +18 -0
  11. package/dist/admin/outbox-dead.service.d.ts +66 -0
  12. package/dist/admin/outbox-dead.service.js +179 -0
  13. package/dist/admin/overview.service.d.ts +40 -5
  14. package/dist/admin/overview.service.js +41 -7
  15. package/dist/agents/from-template.js +1 -1
  16. package/dist/background/finish.d.ts +5 -2
  17. package/dist/background/finish.js +14 -3
  18. package/dist/background/launch.d.ts +44 -5
  19. package/dist/background/launch.js +92 -9
  20. package/dist/background/session.d.ts +14 -2
  21. package/dist/background/session.js +26 -4
  22. package/dist/background/status.d.ts +1 -1
  23. package/dist/background/status.js +3 -3
  24. package/dist/background/stop.d.ts +0 -2
  25. package/dist/background/stop.js +1 -21
  26. package/dist/background/store.d.ts +8 -0
  27. package/dist/background/store.js +24 -0
  28. package/dist/catalog/admin-catalog.controller.d.ts +21 -2
  29. package/dist/catalog/admin-catalog.controller.js +45 -6
  30. package/dist/catalog/admin-catalog.schemas.d.ts +5 -0
  31. package/dist/catalog/admin-catalog.schemas.js +8 -0
  32. package/dist/catalog/catalog-content.d.ts +5 -0
  33. package/dist/catalog/catalog-content.js +1 -0
  34. package/dist/catalog/catalog.module.d.ts +2 -1
  35. package/dist/catalog/catalog.module.js +5 -1
  36. package/dist/catalog/marketplaces.service.d.ts +1 -1
  37. package/dist/catalog/platform-import-update.d.ts +50 -0
  38. package/dist/catalog/platform-import-update.js +116 -0
  39. package/dist/catalog/plugin-import.d.ts +36 -1
  40. package/dist/catalog/plugin-import.js +23 -1
  41. package/dist/catalog/plugin-upgrade.d.ts +18 -0
  42. package/dist/catalog/plugin-upgrade.js +19 -0
  43. package/dist/catalog/requests.service.js +2 -2
  44. package/dist/channels/channels.module.d.ts +5 -2
  45. package/dist/channels/channels.module.js +9 -2
  46. package/dist/channels/deliver-job.d.ts +25 -0
  47. package/dist/channels/deliver-job.js +25 -0
  48. package/dist/channels/inbound.d.ts +1 -1
  49. package/dist/channels/inbound.js +2 -15
  50. package/dist/channels/jobs.d.ts +28 -18
  51. package/dist/channels/jobs.js +63 -26
  52. package/dist/config/schema.d.ts +3 -6
  53. package/dist/config/schema.js +20 -27
  54. package/dist/conversations/agent-tree.d.ts +16 -0
  55. package/dist/conversations/agent-tree.js +36 -0
  56. package/dist/conversations/conversations.service.d.ts +44 -14
  57. package/dist/conversations/conversations.service.js +52 -38
  58. package/dist/conversations/event-log.d.ts +7 -10
  59. package/dist/conversations/event-log.js +24 -15
  60. package/dist/conversations/events.js +2 -1
  61. package/dist/conversations/queue.service.d.ts +8 -3
  62. package/dist/conversations/queue.service.js +37 -13
  63. package/dist/conversations/titles.d.ts +21 -8
  64. package/dist/conversations/titles.js +42 -15
  65. package/dist/db/kernel-schema.d.ts +26 -8
  66. package/dist/devices/devices.service.d.ts +12 -3
  67. package/dist/devices/devices.service.js +30 -9
  68. package/dist/errors.d.ts +15 -0
  69. package/dist/errors.js +15 -0
  70. package/dist/evals/run-replay.js +14 -8
  71. package/dist/extensions/extensions.module.js +3 -0
  72. package/dist/extensions/lifecycle.d.ts +6 -1
  73. package/dist/extensions/lifecycle.js +9 -2
  74. package/dist/extensions/usage-subscribers.d.ts +43 -0
  75. package/dist/extensions/usage-subscribers.js +108 -0
  76. package/dist/http/api.module.js +2 -0
  77. package/dist/identity/settings.d.ts +53 -3
  78. package/dist/identity/settings.js +92 -9
  79. package/dist/index.d.ts +1 -0
  80. package/dist/index.js +1 -0
  81. package/dist/infra/cli/kernel-cli.module.js +5 -0
  82. package/dist/infra/config/kernel-config.service.d.ts +7 -0
  83. package/dist/infra/config/kernel-config.service.js +20 -0
  84. package/dist/infra/events/kernel-events.d.ts +9 -35
  85. package/dist/infra/outbox/outbox-relay.module.d.ts +6 -0
  86. package/dist/infra/outbox/outbox-relay.module.js +18 -0
  87. package/dist/infra/outbox/outbox.dispatcher.d.ts +103 -0
  88. package/dist/infra/outbox/outbox.dispatcher.js +310 -0
  89. package/dist/infra/outbox/outbox.module.d.ts +24 -0
  90. package/dist/infra/outbox/outbox.module.js +50 -0
  91. package/dist/infra/outbox/outbox.relay.d.ts +31 -0
  92. package/dist/infra/outbox/outbox.relay.js +76 -0
  93. package/dist/infra/outbox/outbox.service.d.ts +51 -0
  94. package/dist/infra/outbox/outbox.service.js +131 -0
  95. package/dist/infra/outbox/registry.d.ts +120 -0
  96. package/dist/infra/outbox/registry.js +108 -0
  97. package/dist/infra/queue/queue-names.d.ts +8 -0
  98. package/dist/infra/queue/queue-names.js +16 -0
  99. package/dist/kernel.module.js +3 -0
  100. package/dist/local-devices/device-policy.service.d.ts +4 -0
  101. package/dist/local-devices/device-policy.service.js +61 -3
  102. package/dist/local-devices/local-device-gateway.d.ts +4 -2
  103. package/dist/local-devices/local-device-gateway.js +39 -14
  104. package/dist/local-devices/machines.d.ts +25 -1
  105. package/dist/local-devices/machines.js +91 -15
  106. package/dist/local-devices/policy-summary.d.ts +44 -1
  107. package/dist/local-devices/policy-summary.js +69 -0
  108. package/dist/models/embeddings.d.ts +9 -9
  109. package/dist/models/embeddings.js +11 -18
  110. package/dist/models/models.module.js +3 -0
  111. package/dist/orgs/orgs.service.js +7 -0
  112. package/dist/orgs/settings.controller.js +2 -2
  113. package/dist/orgs/settings.service.d.ts +11 -5
  114. package/dist/orgs/settings.service.js +28 -14
  115. package/dist/overrides.d.ts +20 -3
  116. package/dist/overrides.js +10 -2
  117. package/dist/platform/exclusions.js +5 -0
  118. package/dist/platform/ops/admin.ops.js +50 -9
  119. package/dist/platform/ops/catalog-admin.ops.d.ts +3 -1
  120. package/dist/platform/ops/catalog-admin.ops.js +32 -4
  121. package/dist/platform/ops/org-settings.ops.js +3 -3
  122. package/dist/platform/ops/orgs.ops.js +1 -0
  123. package/dist/platform/ops/outbox.ops.d.ts +21 -0
  124. package/dist/platform/ops/outbox.ops.js +125 -0
  125. package/dist/platform/ops/platform.ops.js +1 -1
  126. package/dist/platform/ops/schedules.ops.js +40 -1
  127. package/dist/platform/platform-ops.module.d.ts +2 -1
  128. package/dist/platform/platform-ops.module.js +3 -0
  129. package/dist/platform/route-schemas.js +3 -0
  130. package/dist/roles/worker.module.js +9 -4
  131. package/dist/runtime/langgraph-loop.js +13 -5
  132. package/dist/runtime/limits.d.ts +15 -0
  133. package/dist/runtime/limits.js +25 -0
  134. package/dist/runtime/loop.d.ts +8 -3
  135. package/dist/runtime/loop.js +2 -1
  136. package/dist/runtime/maintenance.d.ts +4 -2
  137. package/dist/runtime/maintenance.js +17 -2
  138. package/dist/runtime/model-call.d.ts +14 -16
  139. package/dist/runtime/model-call.js +14 -24
  140. package/dist/runtime/queue-lost-jobs.d.ts +24 -0
  141. package/dist/runtime/queue-lost-jobs.js +51 -0
  142. package/dist/runtime/run-control.d.ts +42 -5
  143. package/dist/runtime/run-control.js +66 -12
  144. package/dist/runtime/run-finish.d.ts +39 -12
  145. package/dist/runtime/run-finish.js +97 -14
  146. package/dist/runtime/run-jobs.d.ts +95 -13
  147. package/dist/runtime/run-jobs.js +199 -28
  148. package/dist/runtime/run-orphan-check.job.d.ts +142 -0
  149. package/dist/runtime/run-orphan-check.job.js +339 -0
  150. package/dist/runtime/run-processors.d.ts +16 -9
  151. package/dist/runtime/run-processors.js +1 -1
  152. package/dist/runtime/run-queue.d.ts +64 -55
  153. package/dist/runtime/run-queue.js +147 -284
  154. package/dist/runtime/run-steps.d.ts +1 -2
  155. package/dist/runtime/run-steps.js +8 -11
  156. package/dist/runtime/run-worker.d.ts +6 -17
  157. package/dist/runtime/run-worker.js +32 -34
  158. package/dist/runtime/runtime.module.d.ts +2 -3
  159. package/dist/runtime/runtime.module.js +5 -6
  160. package/dist/runtime/step-log.d.ts +16 -4
  161. package/dist/runtime/step-log.js +26 -6
  162. package/dist/runtime/stream.controller.d.ts +2 -4
  163. package/dist/runtime/stream.controller.js +5 -11
  164. package/dist/runtime/subagents/child.js +1 -1
  165. package/dist/runtime/subagents/definition.d.ts +2 -1
  166. package/dist/runtime/subagents/definition.js +4 -2
  167. package/dist/runtime/subagents/deliveries.d.ts +6 -3
  168. package/dist/runtime/subagents/deliveries.js +16 -7
  169. package/dist/runtime/subagents/dispatch.d.ts +12 -12
  170. package/dist/runtime/subagents/dispatch.js +13 -38
  171. package/dist/runtime/subagents/messages.d.ts +5 -1
  172. package/dist/runtime/subagents/messages.js +11 -3
  173. package/dist/runtime/subagents/skill-fork.d.ts +3 -3
  174. package/dist/runtime/subagents/skill-fork.js +5 -5
  175. package/dist/runtime/subagents/subagents.service.js +1 -1
  176. package/dist/runtime/subagents/tree-jobs.d.ts +61 -0
  177. package/dist/runtime/subagents/tree-jobs.js +86 -0
  178. package/dist/runtime/usage-report-jobs.d.ts +23 -48
  179. package/dist/runtime/usage-report-jobs.js +67 -119
  180. package/dist/runtime/usage-report.d.ts +28 -77
  181. package/dist/runtime/usage-report.js +35 -213
  182. package/dist/runtime/waits.d.ts +48 -198
  183. package/dist/runtime/waits.js +103 -805
  184. package/dist/sandbox/platform-tiers.d.ts +86 -0
  185. package/dist/sandbox/platform-tiers.js +97 -0
  186. package/dist/sandbox/sandbox.service.d.ts +18 -5
  187. package/dist/sandbox/sandbox.service.js +75 -20
  188. package/dist/sandbox/tiers.d.ts +14 -2
  189. package/dist/sandbox/tiers.js +8 -0
  190. package/dist/schedules/calendar.d.ts +134 -0
  191. package/dist/schedules/calendar.js +410 -0
  192. package/dist/schedules/fire.d.ts +1 -3
  193. package/dist/schedules/fire.js +5 -10
  194. package/dist/schedules/run-tracker.d.ts +0 -5
  195. package/dist/schedules/run-tracker.js +0 -13
  196. package/dist/schedules/scan.job.js +15 -20
  197. package/dist/schedules/schedule-math.d.ts +42 -0
  198. package/dist/schedules/schedule-math.js +52 -0
  199. package/dist/schedules/scheduled-tasks.service.d.ts +16 -0
  200. package/dist/schedules/scheduled-tasks.service.js +53 -0
  201. package/dist/schedules/schedules.controller.d.ts +5 -1
  202. package/dist/schedules/schedules.controller.js +15 -1
  203. package/dist/schedules/schedules.schemas.d.ts +35 -0
  204. package/dist/schedules/schedules.schemas.js +27 -0
  205. package/dist/schedules/store.d.ts +5 -0
  206. package/dist/schedules/store.js +20 -0
  207. package/dist/tools/approvals.service.d.ts +26 -7
  208. package/dist/tools/approvals.service.js +88 -10
  209. package/dist/tools/builtin-names.d.ts +5 -3
  210. package/dist/tools/builtin-names.js +12 -4
  211. package/dist/tools/builtins/builtin-tools.d.ts +1 -2
  212. package/dist/tools/builtins/builtin-tools.js +21 -17
  213. package/dist/tools/builtins/sandbox/bash.js +9 -3
  214. package/dist/tools/builtins/sandbox/common.d.ts +18 -1
  215. package/dist/tools/builtins/sandbox/common.js +14 -1
  216. package/dist/tools/builtins/sandbox/index.d.ts +2 -1
  217. package/dist/tools/builtins/sandbox/index.js +4 -1
  218. package/dist/tools/builtins/sandbox/task-list.d.ts +12 -0
  219. package/dist/tools/builtins/sandbox/task-list.js +130 -0
  220. package/dist/tools/builtins/sandbox/task-output.d.ts +8 -1
  221. package/dist/tools/builtins/sandbox/task-output.js +21 -3
  222. package/dist/tools/builtins/web/extract.d.ts +2 -4
  223. package/dist/tools/builtins/web/extract.js +2 -5
  224. package/dist/tools/pipeline.d.ts +1 -1
  225. package/dist/tools/pipeline.js +1 -1
  226. package/dist/tools/questions.service.d.ts +15 -6
  227. package/dist/tools/questions.service.js +20 -8
  228. package/dist/tools/registry.d.ts +4 -2
  229. package/dist/tools/registry.js +4 -3
  230. package/dist/tools/tools.module.js +4 -3
  231. package/package.json +3 -3
  232. package/src/migrations/0118_device_policy_ops.sql +6 -0
  233. package/src/migrations/0119_outbox.sql +22 -0
  234. package/src/migrations/0120_drop_usage_report_sweep.sql +7 -0
@@ -20,7 +20,7 @@ import { KERNEL_BUS, KERNEL_LOGGER } from "../infra/tokens.js";
20
20
  import { LOCAL_PROVIDER } from "../sandbox/providers.js";
21
21
  import { deviceNotFound } from "../sandbox/sandbox.service.js";
22
22
  import { deviceRevokedTopic } from "./keys.js";
23
- import { deviceFeaturesOf, isolationOf, localPolicyOf } from "./policy-summary.js";
23
+ import { deviceFeaturesOf, isolationOf, localPolicyDiff, localPolicyOf, opExplains, pendingPolicyOpsOf, } from "./policy-summary.js";
24
24
  import { LocalDeviceRelay } from "./relay.js";
25
25
  /** The machine credential's prefix (`cap_dev_` plus 32 random bytes, base64url). */
26
26
  export const MACHINE_CREDENTIAL_PREFIX = "cap_dev_";
@@ -162,7 +162,9 @@ let MachinesService = class MachinesService {
162
162
  * alone, read from `rawPolicy` (the frame's raw `policy`; {@link isolationOf}: anything but
163
163
  * `sandbox` is `none`), so it never hides that commands run without a sandbox. A reported
164
164
  * isolation that differs from the stored one (none stored counts as `sandbox`) is audited
165
- * `device.isolation_reported` (sandbox dev tiers spec §10.4), once per change.
165
+ * `device.isolation_reported` (sandbox dev tiers spec §10.4), once per change; a change of what
166
+ * the owner sets, made on the machine while it was away, is audited `device.policy_changed`
167
+ * ({@link reportedPolicy}).
166
168
  */
167
169
  async recordHello(machineId, hello, rawPolicy = hello.policy) {
168
170
  const self = await this.db
@@ -177,6 +179,7 @@ let MachinesService = class MachinesService {
177
179
  "roots",
178
180
  "created_at",
179
181
  "local_policy",
182
+ "pending_policy_ops",
180
183
  ])
181
184
  .where("id", "=", machineId)
182
185
  .where("revoked_at", "is", null)
@@ -207,19 +210,7 @@ let MachinesService = class MachinesService {
207
210
  .execute();
208
211
  }
209
212
  }
210
- const summary = localPolicyOf(hello.policy);
211
- const isolation = summary?.isolation ?? isolationOf(rawPolicy);
212
- const policy = summary ?? (isolation === null ? null : { isolation });
213
- const before = isolationOf(self.local_policy) ?? "sandbox";
214
- if (isolation !== null && isolation !== before) {
215
- await audit(this.db.db, {
216
- actorAccountId: self.account_id,
217
- orgId: self.org_id,
218
- action: "device.isolation_reported",
219
- target: self.device_id,
220
- detail: { machineId, isolation, previous: before },
221
- });
222
- }
213
+ const policy = await this.reportedPolicy(machineId, self, hello.policy, rawPolicy);
223
214
  await this.db
224
215
  .updateTable("device_machines")
225
216
  .set({
@@ -237,6 +228,88 @@ let MachinesService = class MachinesService {
237
228
  .where("revoked_at", "is", null)
238
229
  .execute();
239
230
  }
231
+ /**
232
+ * Stores the local policy summary a connected program pushed after its hello (a `policy` frame:
233
+ * its policy file changed on the machine, its localhost proxy guard flipped, a new tier
234
+ * ceiling), read and audited as the hello's ({@link recordHello}): an unreadable summary keeps
235
+ * its isolation alone (`rawPolicy`); a change the owner made on the machine is audited
236
+ * `device.policy_changed` (`via: "device"`), else a changed isolation `device.isolation_reported`.
237
+ * The next sandbox call then records a new `device_paths` event when the facts the model is told
238
+ * changed. A revoked machine is left alone.
239
+ */
240
+ async recordPolicy(machineId, summary, rawPolicy = summary) {
241
+ const self = await this.db
242
+ .selectFrom("device_machines")
243
+ .select(["device_id", "org_id", "account_id", "local_policy", "pending_policy_ops"])
244
+ .where("id", "=", machineId)
245
+ .where("revoked_at", "is", null)
246
+ .forUpdate()
247
+ .executeTakeFirst();
248
+ if (self === undefined)
249
+ return;
250
+ const policy = await this.reportedPolicy(machineId, self, summary, rawPolicy);
251
+ await this.db
252
+ .updateTable("device_machines")
253
+ .set({ local_policy: jsonOrNull(policy), last_seen_at: sql `now()` })
254
+ .where("id", "=", machineId)
255
+ .where("revoked_at", "is", null)
256
+ .execute();
257
+ }
258
+ /**
259
+ * The summary to store for one a program reported (hello or `policy` frame): the parsed one,
260
+ * else its raw isolation alone, else null. When both the stored and the reported summary are
261
+ * readable and what the owner sets changed ({@link localPolicyDiff}) by no `policy` op the
262
+ * platform sent ({@link opExplains}: its answer may come after the machine's own report), the
263
+ * owner changed it on the machine (`capybara-device policy ...`): audited
264
+ * `device.policy_changed` with `via: "device"`, the isolation with it, so the device page lists
265
+ * it beside the platform's changes; one an op explains is not audited here (the op's answer
266
+ * is). Otherwise a reported isolation that differs from the stored one (none stored counts as
267
+ * `sandbox`) is audited `device.isolation_reported`. A summary reported again is no change. Runs in the caller's transaction, with the machine's row locked.
268
+ */
269
+ async reportedPolicy(machineId, self, reported, rawPolicy) {
270
+ const summary = localPolicyOf(reported);
271
+ const prev = localPolicyOf(self.local_policy);
272
+ const diff = summary !== null && prev !== null ? localPolicyDiff(prev, summary) : null;
273
+ if (diff !== null) {
274
+ const now = Date.now();
275
+ const byOp = pendingPolicyOpsOf(self.pending_policy_ops).some((op) => op.until > now && opExplains(op, diff));
276
+ // the platform's op: its answer lists it (the isolation with it)
277
+ if (byOp)
278
+ return summary;
279
+ await audit(this.db.db, {
280
+ actorAccountId: self.account_id,
281
+ orgId: self.org_id,
282
+ action: "device.policy_changed",
283
+ target: self.device_id,
284
+ detail: {
285
+ add: diff.add,
286
+ remove: diff.remove,
287
+ ...(diff.tier !== undefined && { tier: diff.tier.to, tierFrom: diff.tier.from }),
288
+ ...(diff.localhost !== undefined && { localhost: diff.localhost }),
289
+ ...(diff.isolation !== undefined && { isolation: diff.isolation }),
290
+ ...(diff.remoteGrants !== undefined && { remoteGrants: diff.remoteGrants }),
291
+ by: self.account_id,
292
+ via: "device",
293
+ machineId,
294
+ approvalId: null,
295
+ confirmed: true,
296
+ },
297
+ });
298
+ return summary;
299
+ }
300
+ const isolation = summary?.isolation ?? isolationOf(rawPolicy);
301
+ const before = isolationOf(self.local_policy) ?? "sandbox";
302
+ if (isolation !== null && isolation !== before) {
303
+ await audit(this.db.db, {
304
+ actorAccountId: self.account_id,
305
+ orgId: self.org_id,
306
+ action: "device.isolation_reported",
307
+ target: self.device_id,
308
+ detail: { machineId, isolation, previous: before },
309
+ });
310
+ }
311
+ return summary ?? (isolation === null ? null : { isolation });
312
+ }
240
313
  /** Marks the machine seen (a ping); writes at most once every 60 seconds. */
241
314
  async touch(machineId) {
242
315
  await this.db
@@ -385,6 +458,9 @@ __decorate([
385
458
  __decorate([
386
459
  Transactional()
387
460
  ], MachinesService.prototype, "recordHello", null);
461
+ __decorate([
462
+ Transactional()
463
+ ], MachinesService.prototype, "recordPolicy", null);
388
464
  __decorate([
389
465
  Transactional()
390
466
  ], MachinesService.prototype, "revoke", null);
@@ -1,4 +1,4 @@
1
- import { type DeviceFeature, type DeviceLocalPolicySummary } from "@capybara-hq/plugin-sdk";
1
+ import { type DeviceFeature, type DeviceLocalPolicySummary, type SandboxTier } from "@capybara-hq/plugin-sdk";
2
2
  /**
3
3
  * The known features in `v` (a hello's `features` or the stored column), in a fixed order and
4
4
  * deduped; anything else (unknown names, a non-array) is dropped.
@@ -18,3 +18,46 @@ export declare function localPolicyOf(v: unknown): DeviceLocalPolicySummary | nu
18
18
  * there is none.
19
19
  */
20
20
  export declare function isolationOf(v: unknown): "sandbox" | "none" | null;
21
+ /**
22
+ * What changed between two local policy summaries, in the owner's terms (the device page's change
23
+ * list, sandbox dev tiers spec §10.4): hosts added to and removed from the allowlist, the chosen
24
+ * tier (`null`: follows the org's default), the localhost access, the isolation and whether the
25
+ * machine accepts the platform's changes. Facts the owner does not set (the effective tier, unix
26
+ * sockets) are left out. Each field is present only when it changed.
27
+ */
28
+ export interface LocalPolicyDiff {
29
+ add: string[];
30
+ remove: string[];
31
+ tier?: {
32
+ from: SandboxTier | null;
33
+ to: SandboxTier | null;
34
+ };
35
+ localhost?: DeviceLocalPolicySummary["localhost"];
36
+ isolation?: DeviceLocalPolicySummary["isolation"];
37
+ remoteGrants?: DeviceLocalPolicySummary["remoteGrants"];
38
+ }
39
+ /** The {@link LocalPolicyDiff} from `prev` to `next`; null when nothing the owner sets changed. */
40
+ export declare function localPolicyDiff(prev: DeviceLocalPolicySummary, next: DeviceLocalPolicySummary): LocalPolicyDiff | null;
41
+ /**
42
+ * A `policy` op the platform sent to a machine whose answer may not be recorded yet (kept on the
43
+ * machine's row, `pending_policy_ops`): the change asked for, and until when (epoch ms) a summary
44
+ * the machine reports by itself may still be that op's.
45
+ */
46
+ export interface PendingPolicyOp {
47
+ id: string;
48
+ add: string[];
49
+ remove: string[];
50
+ /** Present when the op set the tier (null: follow the org's default). */
51
+ tier?: SandboxTier | null;
52
+ isolation?: "sandbox";
53
+ until: number;
54
+ }
55
+ /** The pending ops stored on a machine row (malformed entries dropped). */
56
+ export declare function pendingPolicyOpsOf(v: unknown): PendingPolicyOp[];
57
+ /**
58
+ * Whether `diff` is what the platform's `op` did (its answer, or the machine's own report of it
59
+ * before the answer was recorded): hosts within the op's, the tier the op set, and only what a
60
+ * tier change moves with it (the isolation back to `sandbox`, the localhost access). The device
61
+ * page then lists the change once, as the platform's.
62
+ */
63
+ export declare function opExplains(op: PendingPolicyOp, diff: LocalPolicyDiff): boolean;
@@ -61,3 +61,72 @@ export function isolationOf(v) {
61
61
  }
62
62
  return "none";
63
63
  }
64
+ /** The {@link LocalPolicyDiff} from `prev` to `next`; null when nothing the owner sets changed. */
65
+ export function localPolicyDiff(prev, next) {
66
+ const before = new Set(prev.allowedDomains);
67
+ const after = new Set(next.allowedDomains);
68
+ const d = {
69
+ add: [...after].filter((h) => !before.has(h)).sort(),
70
+ remove: [...before].filter((h) => !after.has(h)).sort(),
71
+ };
72
+ const fromTier = prev.tier ?? null;
73
+ const toTier = next.tier ?? null;
74
+ if (fromTier !== toTier)
75
+ d.tier = { from: fromTier, to: toTier };
76
+ if (prev.localhost !== next.localhost)
77
+ d.localhost = next.localhost;
78
+ if (prev.isolation !== next.isolation)
79
+ d.isolation = next.isolation;
80
+ if (prev.remoteGrants !== next.remoteGrants)
81
+ d.remoteGrants = next.remoteGrants;
82
+ const changed = d.add.length > 0 ||
83
+ d.remove.length > 0 ||
84
+ d.tier !== undefined ||
85
+ d.localhost !== undefined ||
86
+ d.isolation !== undefined ||
87
+ d.remoteGrants !== undefined;
88
+ return changed ? d : null;
89
+ }
90
+ /** The pending ops stored on a machine row (malformed entries dropped). */
91
+ export function pendingPolicyOpsOf(v) {
92
+ if (!Array.isArray(v))
93
+ return [];
94
+ const hosts = (x) => Array.isArray(x) ? x.filter((h) => typeof h === "string") : [];
95
+ const out = [];
96
+ for (const e of v) {
97
+ if (typeof e !== "object" || e === null)
98
+ continue;
99
+ const o = e;
100
+ if (typeof o.id !== "string" || typeof o.until !== "number")
101
+ continue;
102
+ out.push({
103
+ id: o.id,
104
+ add: hosts(o.add),
105
+ remove: hosts(o.remove),
106
+ ...("tier" in o && { tier: isSandboxTier(o.tier) ? o.tier : null }),
107
+ ...(o.isolation === "sandbox" && { isolation: "sandbox" }),
108
+ until: o.until,
109
+ });
110
+ }
111
+ return out;
112
+ }
113
+ /**
114
+ * Whether `diff` is what the platform's `op` did (its answer, or the machine's own report of it
115
+ * before the answer was recorded): hosts within the op's, the tier the op set, and only what a
116
+ * tier change moves with it (the isolation back to `sandbox`, the localhost access). The device
117
+ * page then lists the change once, as the platform's.
118
+ */
119
+ export function opExplains(op, diff) {
120
+ if (!diff.add.every((h) => op.add.includes(h)))
121
+ return false;
122
+ if (!diff.remove.every((h) => op.remove.includes(h)))
123
+ return false;
124
+ if (diff.tier !== undefined && (op.tier === undefined || diff.tier.to !== op.tier))
125
+ return false;
126
+ if (diff.isolation !== undefined && op.isolation === undefined && op.tier === undefined) {
127
+ return false;
128
+ }
129
+ if (diff.localhost !== undefined && op.tier === undefined)
130
+ return false;
131
+ return diff.remoteGrants === undefined;
132
+ }
@@ -2,9 +2,9 @@ import { type ExtensionEmbedRequest, type ExtensionEmbedResult, type ExtensionMo
2
2
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
3
3
  import { KernelDb } from "../infra/database/kernel-db.js";
4
4
  import { TransactionHost } from "../infra/database/transaction-host.js";
5
- import { KernelEvents } from "../infra/events/kernel-events.js";
6
5
  import { type HookRunner } from "../runtime/hook-calls.js";
7
6
  import { ProviderRegistry } from "../runtime/providers.js";
7
+ import { UsageReportJobs } from "../runtime/usage-report-jobs.js";
8
8
  import { ModelPurposesService } from "./purposes.js";
9
9
  /** An extension's `permissions.models` (what it declared, as approved in its manifest). */
10
10
  export type DeclaredModelPurposes = readonly NonNullable<ExtensionPermissions["models"]>[number][];
@@ -29,9 +29,9 @@ export declare const EMBED_MAX_INPUTS = 2048;
29
29
  * limited by `models.embedTimeoutMs` (`ModelError("timeout")`), checks each result ({@link validateEmbedResult}: a
30
30
  * wrong count, dimension or a non-finite number is a `ModelError` of kind `server`), and
31
31
  * concatenates the vectors; no retries (an extension retries itself if it wants to);
32
- * - writes one `model_usage` row (the sum of the batches' input tokens) and announces it after the
33
- * commit (`model_usage.recorded`: the usage reporter's `ModelUsageJobs` enqueues its
34
- * `usage.report` delivery; the usage sweep catches up a lost one). A call that fails after the
32
+ * - writes one `model_usage` row (the sum of the batches' input tokens) and, in the same
33
+ * transaction, owes its `usage.report` delivery while some plugin subscribes (`UsageReportJobs`,
34
+ * transactional outbox spec §8.1). A call that fails after the
35
35
  * provider answered some batches (an invalid result, a later batch failing) still records their
36
36
  * input tokens before the error reaches the extension; one that fails before any answer records
37
37
  * nothing.
@@ -42,14 +42,14 @@ export declare class ModelEmbeddings {
42
42
  readonly txHost: TransactionHost;
43
43
  private readonly purposes;
44
44
  private readonly providers;
45
- /** `model_usage.recorded`: the usage reporter enqueues the row's delivery. */
46
- private readonly events;
45
+ /** The row's `usage.report` delivery, owed with it. */
46
+ private readonly usage;
47
47
  private readonly log;
48
48
  /** `models.embedTimeoutMs` (hot). */
49
49
  private readonly config;
50
50
  constructor(kdb: KernelDb, txHost: TransactionHost, purposes: Pick<ModelPurposesService, "resolve">, providers: Pick<ProviderRegistry, "resolve">,
51
- /** `model_usage.recorded`: the usage reporter enqueues the row's delivery. */
52
- events: Pick<KernelEvents, "emit">, log: Pick<Logger, "warn">,
51
+ /** The row's `usage.report` delivery, owed with it. */
52
+ usage: Pick<UsageReportJobs, "oweModelUsage">, log: Pick<Logger, "warn">,
53
53
  /** `models.embedTimeoutMs` (hot). */
54
54
  config: Pick<KernelConfigService, "get">);
55
55
  /** Refuses a purpose `plugin` did not declare with kind `embedding`, or an org it is off in. */
@@ -77,6 +77,6 @@ export declare class ModelEmbeddings {
77
77
  orgId: string;
78
78
  purpose: string;
79
79
  }): Promise<ExtensionModelTarget | null>;
80
- /** One `model_usage` row; its `usage.report` delivery is enqueued after the commit. */
80
+ /** One `model_usage` row with its owed `usage.report` delivery. */
81
81
  private record;
82
82
  }
@@ -13,15 +13,14 @@ import { ApiError } from "../errors.js";
13
13
  import { isExtensionEnabled } from "../extensions/enabled.js";
14
14
  import { audit } from "../identity/audit.js";
15
15
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
16
- import { afterCommit } from "../infra/database/after-commit.js";
17
16
  import { KernelDb } from "../infra/database/kernel-db.js";
18
17
  import { TransactionHost } from "../infra/database/transaction-host.js";
19
18
  import { Transactional } from "../infra/database/transactional.decorator.js";
20
- import { KernelEvents } from "../infra/events/kernel-events.js";
21
19
  import { KERNEL_LOGGER } from "../infra/tokens.js";
22
20
  import { embeddingDeniedAudit } from "../runtime/hook-calls.js";
23
21
  import { modelDeniedText } from "../runtime/model-call.js";
24
22
  import { ProviderRegistry } from "../runtime/providers.js";
23
+ import { UsageReportJobs } from "../runtime/usage-report-jobs.js";
25
24
  import { ModelPurposesService } from "./purposes.js";
26
25
  /** Most texts one `ctx.models.embed` call may carry (the kernel splits them by `maxBatch`). */
27
26
  export const EMBED_MAX_INPUTS = 2048;
@@ -44,9 +43,9 @@ export const EMBED_MAX_INPUTS = 2048;
44
43
  * limited by `models.embedTimeoutMs` (`ModelError("timeout")`), checks each result ({@link validateEmbedResult}: a
45
44
  * wrong count, dimension or a non-finite number is a `ModelError` of kind `server`), and
46
45
  * concatenates the vectors; no retries (an extension retries itself if it wants to);
47
- * - writes one `model_usage` row (the sum of the batches' input tokens) and announces it after the
48
- * commit (`model_usage.recorded`: the usage reporter's `ModelUsageJobs` enqueues its
49
- * `usage.report` delivery; the usage sweep catches up a lost one). A call that fails after the
46
+ * - writes one `model_usage` row (the sum of the batches' input tokens) and, in the same
47
+ * transaction, owes its `usage.report` delivery while some plugin subscribes (`UsageReportJobs`,
48
+ * transactional outbox spec §8.1). A call that fails after the
50
49
  * provider answered some batches (an invalid result, a later batch failing) still records their
51
50
  * input tokens before the error reaches the extension; one that fails before any answer records
52
51
  * nothing.
@@ -57,19 +56,19 @@ let ModelEmbeddings = class ModelEmbeddings {
57
56
  txHost;
58
57
  purposes;
59
58
  providers;
60
- events;
59
+ usage;
61
60
  log;
62
61
  config;
63
62
  constructor(kdb, txHost, purposes, providers,
64
- /** `model_usage.recorded`: the usage reporter enqueues the row's delivery. */
65
- events, log,
63
+ /** The row's `usage.report` delivery, owed with it. */
64
+ usage, log,
66
65
  /** `models.embedTimeoutMs` (hot). */
67
66
  config) {
68
67
  this.kdb = kdb;
69
68
  this.txHost = txHost;
70
69
  this.purposes = purposes;
71
70
  this.providers = providers;
72
- this.events = events;
71
+ this.usage = usage;
73
72
  this.log = log;
74
73
  this.config = config;
75
74
  }
@@ -243,7 +242,7 @@ let ModelEmbeddings = class ModelEmbeddings {
243
242
  throw e;
244
243
  }
245
244
  }
246
- /** One `model_usage` row; its `usage.report` delivery is enqueued after the commit. */
245
+ /** One `model_usage` row with its owed `usage.report` delivery. */
247
246
  async record(plugin, orgId, purpose, model, inputTokens) {
248
247
  const row = await this.kdb.db
249
248
  .insertInto("model_usage")
@@ -257,13 +256,7 @@ let ModelEmbeddings = class ModelEmbeddings {
257
256
  })
258
257
  .returning("id")
259
258
  .executeTakeFirstOrThrow();
260
- // fire and forget: the extension's call never waits on the enqueue (PG, Redis); a lost one is
261
- // caught up by the usage sweep
262
- afterCommit(() => {
263
- void this.events
264
- .emit("model_usage.recorded", { id: row.id })
265
- .catch((err) => this.log.warn("model usage event failed", { usageId: row.id, err }));
266
- });
259
+ await this.usage.oweModelUsage(row.id, orgId);
267
260
  }
268
261
  };
269
262
  __decorate([
@@ -275,7 +268,7 @@ ModelEmbeddings = __decorate([
275
268
  __param(1, Inject(TransactionHost)),
276
269
  __param(2, Inject(ModelPurposesService)),
277
270
  __param(3, Inject(ProviderRegistry)),
278
- __param(4, Inject(KernelEvents)),
271
+ __param(4, Inject(UsageReportJobs)),
279
272
  __param(5, Inject(KERNEL_LOGGER)),
280
273
  __param(6, Inject(KernelConfigService))
281
274
  ], ModelEmbeddings);
@@ -9,6 +9,7 @@ import { ExtensionRegistryModule } from "../extensions/registry.js";
9
9
  import { IdentityModule } from "../identity/identity.module.js";
10
10
  import { OrgLockModule } from "../orgs/org-lock.js";
11
11
  import { ProvidersModule } from "../runtime/providers.js";
12
+ import { UsageReportJobsModule } from "../runtime/usage-report-jobs.js";
12
13
  import { ModelEmbeddings } from "./embeddings.js";
13
14
  import { ModelEnablementModule } from "./enablement.js";
14
15
  import { PricesService } from "./prices.js";
@@ -28,6 +29,8 @@ ModelsModule = __decorate([
28
29
  OrgLockModule,
29
30
  ProvidersModule,
30
31
  ModelEnablementModule,
32
+ // an embed call's `usage.report` delivery, owed with its `model_usage` row (no queue)
33
+ UsageReportJobsModule,
31
34
  ],
32
35
  providers: [PricesService, ModelPurposesService, ModelEmbeddings],
33
36
  exports: [PricesService, ModelPurposesService, ModelEnablementModule, ModelEmbeddings],
@@ -235,6 +235,13 @@ let OrgsService = class OrgsService {
235
235
  .set({ deleted_at: sql `now()` })
236
236
  .where("id", "=", orgId)
237
237
  .execute();
238
+ // the org's owed jobs go with it; dead letters stay for the overview, and the deletion's own
239
+ // cleanup work carries no org_id (transactional outbox spec §9)
240
+ await this.db
241
+ .deleteFrom("outbox")
242
+ .where("org_id", "=", orgId)
243
+ .where("dead_at", "is", null)
244
+ .execute();
238
245
  // after the org is marked deleted, so it counts as no user of what it shared (P3-18)
239
246
  await this.ownedItems.onOrgDeleted(orgId, actor.id, {
240
247
  ...(actorTokenId !== undefined && { actorTokenId }),
@@ -23,7 +23,7 @@ let OrgSettingsController = class OrgSettingsController {
23
23
  async get(org) {
24
24
  return {
25
25
  settings: await this.settings.get(org.orgId),
26
- deployment: this.settings.deployment(),
26
+ deployment: await this.settings.deployment(),
27
27
  sandboxLimits: await this.settings.sandboxLimits(org.orgId),
28
28
  };
29
29
  }
@@ -32,7 +32,7 @@ let OrgSettingsController = class OrgSettingsController {
32
32
  return {
33
33
  // an API token may narrow the sandbox, never widen it (sandbox dev tiers spec §2)
34
34
  settings: await this.settings.update(org.orgId, body, p.account.id, patTokenId(p), undefined, { onlyNarrows: patTokenId(p) !== undefined }),
35
- deployment: this.settings.deployment(),
35
+ deployment: await this.settings.deployment(),
36
36
  sandboxLimits: await this.settings.sandboxLimits(org.orgId),
37
37
  };
38
38
  }
@@ -5,6 +5,7 @@ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
5
5
  import { KernelDb } from "../infra/database/kernel-db.js";
6
6
  import { TransactionHost } from "../infra/database/transaction-host.js";
7
7
  import type { KernelLogger } from "../log.js";
8
+ import { type PlatformTiers } from "../sandbox/platform-tiers.js";
8
9
  import { type EffectiveSandboxPolicy } from "../sandbox/tiers.js";
9
10
  import { OrgLock } from "./org-lock.js";
10
11
  /**
@@ -296,13 +297,15 @@ export declare class OrgSettingsService {
296
297
  config: Pick<KernelConfigService, "get">, log?: KernelLogger | undefined);
297
298
  /** Parses stored settings, warning when an invalid `sandboxTier` narrows to `{ max: "strict" }`. */
298
299
  private parseStored;
300
+ /** The platform's effective sandbox tiers (the startup config over the admin console's choice). */
301
+ platformTiers(): Promise<PlatformTiers>;
299
302
  /** 部署默认值(设置页展示「跟随部署」时的实际取值)。 */
300
- deployment(): OrgSettingsDeployment;
303
+ deployment(): Promise<OrgSettingsDeployment>;
301
304
  /** 组织实际生效的沙箱 cpu / 内存(只读;由超级管理员在部署设置里覆盖)。 */
302
305
  sandboxLimits(orgId: string): Promise<OrgSandboxLimits>;
303
306
  get(orgId: string): Promise<OrgSettings>;
304
307
  /** The refusals of {@link update} that need no read of the org (the Platform tool's precheck). */
305
- check(patch: OrgSettingsPatch): void;
308
+ check(patch: OrgSettingsPatch): Promise<void>;
306
309
  update(orgId: string, patch: OrgSettingsPatch, actorAccountId: string, actorTokenId?: string,
307
310
  /** Merged into the audit detail (the Platform tool's `via: "agent"`). */
308
311
  auditDetail?: Record<string, unknown>,
@@ -313,8 +316,11 @@ export declare class OrgSettingsService {
313
316
  opts?: {
314
317
  onlyNarrows?: boolean;
315
318
  }): Promise<OrgSettings>;
316
- /** The effective sandbox policy for the org's settings and a device's own tier (null = none). */
317
- sandboxPolicy(settings: OrgSettings, deviceTier: SandboxTier | null): EffectiveSandboxPolicy;
319
+ /**
320
+ * The effective sandbox policy for the org's settings and a device's own tier (null = none);
321
+ * `tiers`: the platform's tiers when the caller already read them.
322
+ */
323
+ sandboxPolicy(settings: OrgSettings, deviceTier: SandboxTier | null, tiers?: PlatformTiers): Promise<EffectiveSandboxPolicy>;
318
324
  /**
319
325
  * Refuses a patch that widens the org's sandbox (sandbox dev tiers spec §2, §7.2: only a person
320
326
  * widens it, never the Platform tool or an API token):
@@ -327,7 +333,7 @@ export declare class OrgSettingsService {
327
333
  * - `sandboxEgress`: the base list grows: from none to anything else, or to hosts (the
328
334
  * deployment's when the org gives none) not all in the current list.
329
335
  */
330
- assertOnlyNarrows(before: OrgSettings, patch: OrgSettingsPatch): void;
336
+ assertOnlyNarrows(before: OrgSettings, patch: OrgSettingsPatch): Promise<void>;
331
337
  private updateIn;
332
338
  }
333
339
  /**
@@ -18,6 +18,7 @@ import { KernelDb } from "../infra/database/kernel-db.js";
18
18
  import { TransactionHost } from "../infra/database/transaction-host.js";
19
19
  import { Transactional } from "../infra/database/transactional.decorator.js";
20
20
  import { KERNEL_LOGGER } from "../infra/tokens.js";
21
+ import { readPlatformTiers } from "../sandbox/platform-tiers.js";
21
22
  import { effectiveSandboxPolicy } from "../sandbox/tiers.js";
22
23
  import { assertThemeOnIn } from "../themes/theme-state.js";
23
24
  import { IanaTimeZone } from "../time-zone.js";
@@ -255,9 +256,14 @@ let OrgSettingsService = class OrgSettingsService {
255
256
  }
256
257
  return parseOrgSettings(raw);
257
258
  }
259
+ /** The platform's effective sandbox tiers (the startup config over the admin console's choice). */
260
+ platformTiers() {
261
+ return readPlatformTiers(this.db.db, this.config, this.log);
262
+ }
258
263
  /** 部署默认值(设置页展示「跟随部署」时的实际取值)。 */
259
- deployment() {
260
- const { limits: lim, tiers } = this.config.get().sandbox;
264
+ async deployment() {
265
+ const lim = this.config.get().sandbox.limits;
266
+ const tiers = await this.platformTiers();
261
267
  return {
262
268
  sandboxEgress: { mode: lim.egress, allowHosts: [...lim.allowHosts] },
263
269
  sandboxTiers: {
@@ -290,14 +296,14 @@ let OrgSettingsService = class OrgSettingsService {
290
296
  return this.parseStored(orgId, row.settings);
291
297
  }
292
298
  /** The refusals of {@link update} that need no read of the org (the Platform tool's precheck). */
293
- check(patch) {
299
+ async check(patch) {
294
300
  if (patch.sandboxEgress?.mode === "allowlist" &&
295
- this.deployment().sandboxEgress.mode === "none") {
301
+ this.config.get().sandbox.limits.egress === "none") {
296
302
  throw new ApiError("org.sandbox_egress_not_allowed", "This deployment disables sandbox network access (sandbox.limits.egress is none); an organization can only set sandboxEgress to none");
297
303
  }
298
304
  const t = patch.sandboxTier;
299
305
  if (t != null) {
300
- const platform = this.config.get().sandbox.tiers;
306
+ const platform = await this.platformTiers();
301
307
  if (t.max !== undefined && !tierAtMost(t.max, platform.max)) {
302
308
  throw sandboxTierNotAllowed(`The platform allows sandbox tiers up to ${platform.max}; sandboxTier.max cannot be ${t.max}`);
303
309
  }
@@ -318,12 +324,19 @@ let OrgSettingsService = class OrgSettingsService {
318
324
  * change that widens (see {@link assertOnlyNarrows}); only a person in a session widens.
319
325
  */
320
326
  opts) {
321
- this.check(patch);
327
+ await this.check(patch);
322
328
  return this.updateIn(orgId, patch, actorAccountId, actorTokenId, auditDetail, opts);
323
329
  }
324
- /** The effective sandbox policy for the org's settings and a device's own tier (null = none). */
325
- sandboxPolicy(settings, deviceTier) {
326
- return effectiveSandboxPolicy(this.config.get().sandbox, settings, deviceTier);
330
+ /**
331
+ * The effective sandbox policy for the org's settings and a device's own tier (null = none);
332
+ * `tiers`: the platform's tiers when the caller already read them.
333
+ */
334
+ async sandboxPolicy(settings, deviceTier, tiers) {
335
+ const deployment = {
336
+ limits: this.config.get().sandbox.limits,
337
+ tiers: tiers ?? (await this.platformTiers()),
338
+ };
339
+ return effectiveSandboxPolicy(deployment, settings, deviceTier);
327
340
  }
328
341
  /**
329
342
  * Refuses a patch that widens the org's sandbox (sandbox dev tiers spec §2, §7.2: only a person
@@ -337,12 +350,13 @@ let OrgSettingsService = class OrgSettingsService {
337
350
  * - `sandboxEgress`: the base list grows: from none to anything else, or to hosts (the
338
351
  * deployment's when the org gives none) not all in the current list.
339
352
  */
340
- assertOnlyNarrows(before, patch) {
353
+ async assertOnlyNarrows(before, patch) {
341
354
  if (patch.sandboxTier !== undefined) {
342
355
  const next = patch.sandboxTier ?? undefined;
343
356
  const prev = before.sandboxTier;
344
- const b = this.sandboxPolicy(before, null);
345
- const a = this.sandboxPolicy({ ...before, sandboxTier: next }, null);
357
+ const tiers = await this.platformTiers();
358
+ const b = await this.sandboxPolicy(before, null, tiers);
359
+ const a = await this.sandboxPolicy({ ...before, sandboxTier: next }, null, tiers);
346
360
  // a cleared default follows the platform, capped by the ceiling: `max` bounds it then
347
361
  const nextDefault = next?.default === undefined && next?.max !== undefined ? next.max : next?.default;
348
362
  if (!tierAtMost(a.ceiling, b.ceiling) ||
@@ -381,10 +395,10 @@ let OrgSettingsService = class OrgSettingsService {
381
395
  const stored = asObject(row.settings);
382
396
  const before = this.parseStored(orgId, stored);
383
397
  // again under the org lock: a hot config reload may have lowered the platform since
384
- this.check(patch);
398
+ await this.check(patch);
385
399
  // under the org lock, so the comparison is with the value the write replaces
386
400
  if (opts?.onlyNarrows)
387
- this.assertOnlyNarrows(before, patch);
401
+ await this.assertOnlyNarrows(before, patch);
388
402
  const changes = Object.fromEntries(Object.entries(patch)
389
403
  .filter(([, v]) => v !== undefined)
390
404
  // null clears sandboxEgress / wakeOnBackgroundTask: follow the deployment default again