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

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 (242) hide show
  1. package/dist/agents/admin-agents.d.ts +16 -3
  2. package/dist/agents/admin-agents.js +65 -7
  3. package/dist/agents/agents.schemas.d.ts +24 -6
  4. package/dist/agents/agents.service.d.ts +4 -3
  5. package/dist/agents/agents.service.js +39 -26
  6. package/dist/agents/definition.d.ts +44 -6
  7. package/dist/agents/definition.js +86 -18
  8. package/dist/agents/from-template.d.ts +6 -3
  9. package/dist/agents/from-template.js +57 -44
  10. package/dist/agents/org-templates.js +97 -41
  11. package/dist/agents/templates.d.ts +6 -0
  12. package/dist/agents/templates.js +15 -1
  13. package/dist/catalog/admin-catalog.controller.d.ts +2 -4
  14. package/dist/catalog/admin-catalog.controller.js +2 -6
  15. package/dist/catalog/catalog.module.js +3 -0
  16. package/dist/catalog/claude/report.d.ts +9 -2
  17. package/dist/catalog/claude/report.js +1 -0
  18. package/dist/catalog/claude/translate.js +1 -1
  19. package/dist/catalog/component-promotion.js +1 -2
  20. package/dist/catalog/component-state.d.ts +13 -1
  21. package/dist/catalog/component-state.js +14 -5
  22. package/dist/catalog/marketplaces.service.d.ts +1 -1
  23. package/dist/catalog/marketplaces.service.js +2 -1
  24. package/dist/catalog/org-catalog.controller.d.ts +13 -3
  25. package/dist/catalog/org-catalog.controller.js +26 -4
  26. package/dist/catalog/org-catalog.schemas.d.ts +13 -0
  27. package/dist/catalog/org-catalog.schemas.js +34 -0
  28. package/dist/catalog/org-plugin-uploads.controller.js +0 -1
  29. package/dist/catalog/org-plugin-uploads.d.ts +0 -1
  30. package/dist/catalog/org-plugin-uploads.js +0 -3
  31. package/dist/catalog/org-plugin-uploads.schemas.d.ts +0 -1
  32. package/dist/catalog/org-plugin-uploads.schemas.js +0 -2
  33. package/dist/catalog/platform-components.d.ts +0 -1
  34. package/dist/catalog/platform-components.js +3 -8
  35. package/dist/catalog/plugin-import.d.ts +0 -8
  36. package/dist/catalog/plugin-import.js +4 -35
  37. package/dist/catalog/plugin-install.d.ts +2 -2
  38. package/dist/catalog/plugin-install.js +2 -2
  39. package/dist/catalog/plugin-package.d.ts +7 -0
  40. package/dist/catalog/plugin-package.js +27 -1
  41. package/dist/catalog/plugin-upgrade.d.ts +5 -6
  42. package/dist/catalog/plugin-upgrade.js +5 -23
  43. package/dist/catalog/requests.service.d.ts +66 -10
  44. package/dist/catalog/requests.service.js +196 -15
  45. package/dist/channels/actions.d.ts +2 -0
  46. package/dist/channels/actions.js +19 -4
  47. package/dist/channels/cards.d.ts +16 -2
  48. package/dist/channels/cards.js +40 -7
  49. package/dist/channels/channels.module.js +3 -0
  50. package/dist/channels/outbound.d.ts +22 -1
  51. package/dist/channels/outbound.js +116 -7
  52. package/dist/channels/phrases.d.ts +25 -0
  53. package/dist/channels/phrases.js +19 -0
  54. package/dist/channels/threads.d.ts +12 -0
  55. package/dist/channels/threads.js +34 -0
  56. package/dist/config/schema.d.ts +23 -1
  57. package/dist/config/schema.js +105 -5
  58. package/dist/conversations/conversations.service.d.ts +42 -9
  59. package/dist/conversations/conversations.service.js +57 -25
  60. package/dist/conversations/events.d.ts +106 -11
  61. package/dist/conversations/events.js +121 -18
  62. package/dist/conversations/message-context.d.ts +6 -3
  63. package/dist/conversations/message-context.js +14 -10
  64. package/dist/conversations/queue.d.ts +15 -2
  65. package/dist/conversations/queue.js +22 -3
  66. package/dist/conversations/queue.service.d.ts +12 -6
  67. package/dist/conversations/queue.service.js +36 -13
  68. package/dist/db/kernel-schema.d.ts +105 -3
  69. package/dist/devices/conversation-devices.d.ts +8 -1
  70. package/dist/devices/conversation-devices.js +8 -0
  71. package/dist/devices/devices.controller.d.ts +21 -4
  72. package/dist/devices/devices.controller.js +46 -8
  73. package/dist/devices/devices.schemas.d.ts +9 -1
  74. package/dist/devices/devices.schemas.js +11 -3
  75. package/dist/devices/devices.service.d.ts +176 -4
  76. package/dist/devices/devices.service.js +341 -6
  77. package/dist/devices/dto.d.ts +56 -3
  78. package/dist/devices/dto.js +51 -1
  79. package/dist/devices/public-suffix.d.ts +6 -0
  80. package/dist/devices/public-suffix.js +79 -0
  81. package/dist/errors.d.ts +46 -5
  82. package/dist/errors.js +46 -5
  83. package/dist/evals/reconstruct.d.ts +18 -13
  84. package/dist/evals/reconstruct.js +50 -36
  85. package/dist/evals/recorded-tools.d.ts +7 -7
  86. package/dist/evals/recorded-tools.js +4 -3
  87. package/dist/evals/trajectory.d.ts +6 -1
  88. package/dist/evals/trajectory.js +7 -0
  89. package/dist/extensions/package.js +2 -2
  90. package/dist/http/api.module.js +5 -0
  91. package/dist/inbox/inbox.module.js +2 -1
  92. package/dist/inbox/inbox.service.d.ts +19 -4
  93. package/dist/inbox/inbox.service.js +14 -3
  94. package/dist/infra/config/kernel-config.service.js +11 -0
  95. package/dist/infra/events/kernel-events.d.ts +2 -1
  96. package/dist/local-devices/device-policy.service.d.ts +65 -0
  97. package/dist/local-devices/device-policy.service.js +217 -0
  98. package/dist/local-devices/local-device-gateway.js +4 -1
  99. package/dist/local-devices/local-device-provider.d.ts +40 -3
  100. package/dist/local-devices/local-device-provider.js +109 -7
  101. package/dist/local-devices/local-devices.module.d.ts +2 -1
  102. package/dist/local-devices/local-devices.module.js +19 -3
  103. package/dist/local-devices/machines.d.ts +14 -3
  104. package/dist/local-devices/machines.js +41 -3
  105. package/dist/local-devices/network-grants.d.ts +104 -0
  106. package/dist/local-devices/network-grants.js +288 -0
  107. package/dist/local-devices/policy-summary.d.ts +20 -0
  108. package/dist/local-devices/policy-summary.js +63 -0
  109. package/dist/orgs/orgs.module.js +4 -10
  110. package/dist/orgs/roles.d.ts +1 -1
  111. package/dist/orgs/roles.js +4 -0
  112. package/dist/orgs/settings.controller.js +2 -1
  113. package/dist/orgs/settings.service.d.ts +164 -12
  114. package/dist/orgs/settings.service.js +212 -24
  115. package/dist/platform/exclusions.js +11 -1
  116. package/dist/platform/operation.js +4 -0
  117. package/dist/platform/ops/admin-items.ops.js +1 -1
  118. package/dist/platform/ops/agent-skills.ops.js +2 -2
  119. package/dist/platform/ops/agent-templates.ops.js +2 -2
  120. package/dist/platform/ops/agents.ops.js +11 -15
  121. package/dist/platform/ops/devices.ops.js +3 -1
  122. package/dist/platform/ops/inbox.ops.js +19 -0
  123. package/dist/platform/ops/org-settings.ops.js +14 -5
  124. package/dist/platform/ops/platform.ops.d.ts +9 -1
  125. package/dist/platform/ops/platform.ops.js +34 -12
  126. package/dist/platform/ops/requests.ops.js +3 -2
  127. package/dist/platform/ops/schedules.ops.d.ts +21 -0
  128. package/dist/platform/ops/schedules.ops.js +470 -0
  129. package/dist/platform/ops/skills.ops.js +3 -3
  130. package/dist/platform/platform-ops.module.d.ts +2 -1
  131. package/dist/platform/platform-ops.module.js +5 -0
  132. package/dist/platform/route-schemas.js +3 -0
  133. package/dist/roles/worker.module.js +3 -0
  134. package/dist/runtime/context/build.js +13 -4
  135. package/dist/runtime/context/compaction.js +6 -3
  136. package/dist/runtime/context/device-paths.d.ts +43 -21
  137. package/dist/runtime/context/device-paths.js +104 -21
  138. package/dist/runtime/context/run-context.d.ts +9 -0
  139. package/dist/runtime/context/run-context.js +8 -0
  140. package/dist/runtime/context/skills.d.ts +14 -1
  141. package/dist/runtime/context/skills.js +36 -5
  142. package/dist/runtime/maintenance.d.ts +4 -2
  143. package/dist/runtime/maintenance.js +16 -5
  144. package/dist/runtime/run-finish.d.ts +6 -1
  145. package/dist/runtime/run-finish.js +16 -2
  146. package/dist/runtime/run-queue.js +21 -2
  147. package/dist/runtime/run-worker.js +14 -33
  148. package/dist/runtime/runtime.module.js +3 -0
  149. package/dist/runtime/subagents/definition.d.ts +32 -14
  150. package/dist/runtime/subagents/definition.js +106 -109
  151. package/dist/runtime/subagents/dispatch.js +4 -3
  152. package/dist/runtime/subagents/preamble.d.ts +2 -1
  153. package/dist/runtime/subagents/preamble.js +2 -1
  154. package/dist/runtime/waits.js +14 -4
  155. package/dist/sandbox/sandbox.service.d.ts +120 -10
  156. package/dist/sandbox/sandbox.service.js +335 -28
  157. package/dist/sandbox/tiers.d.ts +86 -0
  158. package/dist/sandbox/tiers.js +96 -0
  159. package/dist/schedules/effective-settings.d.ts +16 -0
  160. package/dist/schedules/effective-settings.js +17 -0
  161. package/dist/schedules/errors.d.ts +8 -0
  162. package/dist/schedules/errors.js +8 -0
  163. package/dist/schedules/fire.d.ts +118 -0
  164. package/dist/schedules/fire.js +625 -0
  165. package/dist/schedules/grant-rules.d.ts +94 -0
  166. package/dist/schedules/grant-rules.js +126 -0
  167. package/dist/schedules/job-ids.d.ts +32 -0
  168. package/dist/schedules/job-ids.js +27 -0
  169. package/dist/schedules/pause.d.ts +9 -0
  170. package/dist/schedules/pause.js +33 -0
  171. package/dist/schedules/run-tracker.d.ts +81 -0
  172. package/dist/schedules/run-tracker.js +333 -0
  173. package/dist/schedules/scan.job.d.ts +64 -0
  174. package/dist/schedules/scan.job.js +303 -0
  175. package/dist/schedules/schedule-math.d.ts +31 -0
  176. package/dist/schedules/schedule-math.js +223 -0
  177. package/dist/schedules/scheduled-tasks.service.d.ts +351 -0
  178. package/dist/schedules/scheduled-tasks.service.js +1193 -0
  179. package/dist/schedules/schedules.controller.d.ts +68 -0
  180. package/dist/schedules/schedules.controller.js +242 -0
  181. package/dist/schedules/schedules.module.d.ts +16 -0
  182. package/dist/schedules/schedules.module.js +65 -0
  183. package/dist/schedules/schedules.schemas.d.ts +316 -0
  184. package/dist/schedules/schedules.schemas.js +186 -0
  185. package/dist/schedules/store.d.ts +81 -0
  186. package/dist/schedules/store.js +390 -0
  187. package/dist/schedules/store.module.d.ts +6 -0
  188. package/dist/schedules/store.module.js +21 -0
  189. package/dist/schedules/types.d.ts +108 -0
  190. package/dist/schedules/types.js +1 -0
  191. package/dist/schedules/unattended.d.ts +117 -0
  192. package/dist/schedules/unattended.js +228 -0
  193. package/dist/setup/oauth-token-probe.js +8 -4
  194. package/dist/skills/agent-skills.service.d.ts +4 -8
  195. package/dist/skills/agent-skills.service.js +18 -53
  196. package/dist/skills/catalog.d.ts +65 -62
  197. package/dist/skills/catalog.js +109 -206
  198. package/dist/skills/conversation-catalog.d.ts +3 -3
  199. package/dist/skills/conversation-catalog.js +5 -12
  200. package/dist/skills/invoke.d.ts +2 -2
  201. package/dist/skills/skills.controller.d.ts +3 -1
  202. package/dist/skills/skills.service.d.ts +38 -37
  203. package/dist/skills/skills.service.js +168 -107
  204. package/dist/skills/usable.d.ts +57 -8
  205. package/dist/skills/usable.js +172 -38
  206. package/dist/tools/approvals.controller.d.ts +4 -2
  207. package/dist/tools/approvals.controller.js +17 -2
  208. package/dist/tools/approvals.schemas.d.ts +1 -0
  209. package/dist/tools/approvals.schemas.js +3 -1
  210. package/dist/tools/approvals.service.d.ts +182 -7
  211. package/dist/tools/approvals.service.js +597 -13
  212. package/dist/tools/builtin-names.d.ts +25 -4
  213. package/dist/tools/builtin-names.js +45 -4
  214. package/dist/tools/builtins/builtin-tools.d.ts +8 -0
  215. package/dist/tools/builtins/builtin-tools.js +23 -2
  216. package/dist/tools/builtins/sandbox/bash.js +16 -7
  217. package/dist/tools/builtins/sandbox/common.d.ts +1 -1
  218. package/dist/tools/builtins/sandbox/common.js +33 -10
  219. package/dist/tools/builtins/sandbox/edit.js +7 -3
  220. package/dist/tools/builtins/sandbox/file-state.d.ts +31 -1
  221. package/dist/tools/builtins/sandbox/file-state.js +33 -3
  222. package/dist/tools/builtins/sandbox/write.js +8 -4
  223. package/dist/tools/builtins/schedules.d.ts +107 -0
  224. package/dist/tools/builtins/schedules.js +789 -0
  225. package/dist/tools/elicitation.d.ts +2 -0
  226. package/dist/tools/elicitation.js +1 -1
  227. package/dist/tools/fixed-ask.d.ts +7 -0
  228. package/dist/tools/fixed-ask.js +9 -0
  229. package/dist/tools/network-asks.d.ts +109 -0
  230. package/dist/tools/network-asks.js +221 -0
  231. package/dist/tools/pipeline.d.ts +53 -6
  232. package/dist/tools/pipeline.js +202 -13
  233. package/dist/tools/policy.d.ts +21 -8
  234. package/dist/tools/policy.js +63 -9
  235. package/dist/tools/registry.d.ts +10 -1
  236. package/dist/tools/tools.module.js +12 -0
  237. package/package.json +2 -2
  238. package/src/migrations/0113_scheduled_tasks.sql +108 -0
  239. package/src/migrations/0114_network_asks.sql +23 -0
  240. package/src/migrations/0115_network_requests.sql +28 -0
  241. package/src/migrations/0116_skills_default_on.sql +37 -0
  242. package/src/migrations/0117_sandbox_tiers.sql +11 -0
@@ -8,7 +8,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
10
  import { randomUUID } from "node:crypto";
11
- import { isFileSystemGoneError, isSandboxGoneError, rootsOf, } from "@capybara-hq/plugin-sdk";
11
+ import { compareTiers, isFileSystemGoneError, isSandboxGoneError, rootsOf, } from "@capybara-hq/plugin-sdk";
12
12
  import { Inject, Injectable } from "@nestjs/common";
13
13
  import { sql, } from "kysely";
14
14
  import { BackgroundTaskStore } from "../background/store.js";
@@ -18,10 +18,12 @@ import { readSandboxLimitOverride } from "../identity/settings.js";
18
18
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
19
19
  import { KernelDb } from "../infra/database/kernel-db.js";
20
20
  import { KERNEL_LOGGER, REDIS } from "../infra/tokens.js";
21
- import { effectiveSandboxEgress, effectiveSandboxLimits, readOrgSettings, } from "../orgs/settings.service.js";
21
+ import { effectiveSandboxLimits, readOrgSettings } from "../orgs/settings.service.js";
22
+ import { appendMissingGrant, TIER_OPEN_GRANT_KEY, UNATTENDED_OPEN_TIER_ERROR, unattendedTierOf, } from "../schedules/unattended.js";
22
23
  import { canonicalJson } from "../tools/canonical-json.js";
23
24
  import { SandboxLeases, SandboxLockLostError, sandboxBusy, } from "./leases.js";
24
25
  import { LOCAL_PROVIDER, providerForDevice, SandboxProviders, } from "./providers.js";
26
+ import { appliedOf, effectiveSandboxPolicy, instanceLimitsJson, limitsForTier, readInstanceLimits, sameApplied, } from "./tiers.js";
25
27
  // Moved to leases.ts (phase C); re-exported for existing importers
26
28
  export { SANDBOX_LOCK_PREFIX, SANDBOX_LOCK_TTL_MS, SandboxLockLostError, sandboxBusy, } from "./leases.js";
27
29
  /** Margin (ms) of a holder record's TTL beyond the longest command: by then the in-sandbox limit has ended its commands. */
@@ -82,19 +84,20 @@ function tagged(impl, tag, actor, before) {
82
84
  * Sandbox config problems to tell operators at startup (English log lines):
83
85
  * - `runtime.maxRunSeconds` below `sandbox.limits.maxCommandSeconds`: long commands are cut by the
84
86
  * run time limit (Ruling M20);
85
- * - `sandbox.limits.egress` is open but the current provider plugin has no `allowOpenEgress`: every
86
- * instance creation is refused.
87
+ * - `sandbox.tiers.max` is open but the current provider plugin sets `openEgress: "off"`: every
88
+ * creation of an open-tier instance is refused.
87
89
  * With both time limits at their defaults (900 < 7200, a known trade-off) it is only info; warn
88
90
  * when an operator changed one and they still disagree.
89
91
  */
90
92
  export function sandboxConfigWarnings(cfg, providerPlugin) {
91
93
  const out = [];
92
- if (cfg.sandbox.limits.egress === "open" && providerPlugin !== undefined) {
94
+ if (cfg.sandbox.tiers.max === "open" && providerPlugin !== undefined) {
95
+ // absent means the plugin's default ("proxy"), which creates open instances
93
96
  const pc = cfg.extensionConfig[providerPlugin];
94
- if (pc?.allowOpenEgress !== true) {
97
+ if (pc?.openEgress === "off") {
95
98
  out.push({
96
99
  level: "warn",
97
- 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"`,
100
+ message: `sandbox.tiers.max is "open" but extensionConfig."${providerPlugin}".openEgress is "off": the ${providerPlugin} plugin refuses to create cloud devices in the open tier; set openEgress to "proxy" (or "direct" for trusted deployments only), or lower sandbox.tiers.max to "dev"`,
98
101
  });
99
102
  }
100
103
  }
@@ -269,23 +272,125 @@ let SandboxService = class SandboxService {
269
272
  return this.config.get().sandbox.maxConcurrentCommands;
270
273
  }
271
274
  /**
272
- * The org's effective sandbox limits (spec §8.4): the deployment's `sandbox.limits`, egress
273
- * narrowed by the org setting `sandboxEgress` (Ruling M19), cpu / memory per the super admin's
274
- * org override (C19 / Ruling M16). Instances are created with them; phase 1 devices use
275
- * them unchanged.
275
+ * The effective sandbox policy of a device (sandbox dev tiers spec §4.2): the platform's and
276
+ * the org's tiers and network with the device's own tier (`devices.tier`, read fresh; a local
277
+ * device and a missing one have none). Without `deviceId`: a device that follows the org's
278
+ * default.
276
279
  */
277
- async orgLimits(orgId) {
280
+ async devicePolicy(device) {
278
281
  const cfg = this.config.get().sandbox;
279
- const egress = effectiveSandboxEgress(cfg.limits, (await readOrgSettings(this.db.db, orgId)).sandboxEgress);
280
- const sized = effectiveSandboxLimits({ ...cfg.limits, ...egress }, await readSandboxLimitOverride(this.db.db, orgId));
282
+ const [settings, tier] = await Promise.all([
283
+ readOrgSettings(this.db.db, device.orgId),
284
+ device.deviceId === undefined ? null : this.deviceTier(device.orgId, device.deviceId),
285
+ ]);
286
+ return effectiveSandboxPolicy(cfg, settings, tier);
287
+ }
288
+ /** The device's own tier (`devices.tier`); null when it follows the org or is not there. */
289
+ async deviceTier(orgId, deviceId) {
290
+ const row = await this.db
291
+ .selectFrom("devices")
292
+ .select("tier")
293
+ .where("id", "=", deviceId)
294
+ .where("org_id", "=", orgId)
295
+ .executeTakeFirst();
296
+ return row?.tier ?? null;
297
+ }
298
+ /**
299
+ * The device's effective sandbox limits (spec §8.4; sandbox dev tiers spec §4.2): the
300
+ * deployment's `sandbox.limits`, cpu / memory per the super admin's org override (C19 / Ruling
301
+ * M16), and the network of the effective tier ({@link devicePolicy}): egress narrowed by the
302
+ * org setting `sandboxEgress` (Ruling M19), widened by the tier. `opts.policy`: a policy the
303
+ * caller already read; `opts.tier`: limits for that tier instead (a local device's ceiling, an
304
+ * unattended run's cap). Cloud instances are created with them.
305
+ */
306
+ async deviceLimits(device, opts = {}) {
307
+ const cfg = this.config.get().sandbox;
308
+ const [policy, override] = await Promise.all([
309
+ opts.policy ?? this.devicePolicy(device),
310
+ readSandboxLimitOverride(this.db.db, device.orgId),
311
+ ]);
312
+ const tier = opts.tier ?? policy.tier;
313
+ const net = limitsForTier(policy, tier);
314
+ const sized = effectiveSandboxLimits({ ...cfg.limits, ...net, tier, devHosts: policy.devHosts }, override);
281
315
  return {
282
316
  cpu: sized.cpu,
283
317
  memoryMb: sized.memoryMb,
284
318
  diskMb: sized.diskMb,
285
319
  egress: sized.egress,
286
320
  allowHosts: sized.allowHosts,
321
+ tier: sized.tier,
322
+ devHosts: [...policy.devHosts],
323
+ };
324
+ }
325
+ /**
326
+ * The limits of the org's ceiling (its highest tier, {@link deviceLimits} at `ceiling`): what a
327
+ * local device's exec may reach at most, so what a network grant or approval is checked
328
+ * against.
329
+ */
330
+ async ceilingLimits(orgId) {
331
+ const policy = await this.devicePolicy({ orgId });
332
+ return await this.deviceLimits({ orgId }, { policy, tier: policy.ceiling });
333
+ }
334
+ /**
335
+ * The limits state of a device's instance for its page (sandbox dev tiers spec §7.1, §11.1),
336
+ * read without a lease: `applied` is what the instance or file system was made with or updated
337
+ * to (null: unknown, or no instance yet), `effective` what it should run with now.
338
+ * `pendingNextUse`: a cloud instance exists and differs; its next use applies the new limits
339
+ * (files kept). `pendingFilesReset`: the provider fixes limits at its file system
340
+ * (`limitsApplyAt: "fileSystem"`) and cannot `update` them, so only a file reset applies them.
341
+ * A local device (its machine applies the tier at each command) is never pending, and its
342
+ * `applied` is null. `opts.policy`: the device's effective policy when the caller already read
343
+ * it ({@link devicePolicy}).
344
+ */
345
+ async limitsState(device, opts = {}) {
346
+ const [policy, row, p] = await Promise.all([
347
+ opts.policy ?? this.devicePolicy(device),
348
+ this.db
349
+ .selectFrom("device_instances")
350
+ .select(["provider", "status", "handle", "fs_status", "instance_limits"])
351
+ .where("device_id", "=", device.deviceId)
352
+ .where("org_id", "=", device.orgId)
353
+ .executeTakeFirst(),
354
+ this.providerOf(device.deviceId, device.orgId),
355
+ ]);
356
+ const effective = { tier: policy.tier, egress: policy.egress };
357
+ if (p === undefined || p.plugin === LOCAL_PROVIDER || row === undefined) {
358
+ return { applied: null, effective, pendingNextUse: false, pendingFilesReset: false };
359
+ }
360
+ const rec = readInstanceLimits(row.instance_limits);
361
+ const differs = row.provider === p.plugin && !sameApplied(rec.applied, effective);
362
+ const fsBound = differs && row.fs_status === "ready" && this.fsBound(p, rec, effective);
363
+ const hasInstance = row.handle !== null && (row.status === "running" || row.status === "suspended");
364
+ return {
365
+ applied: rec.applied,
366
+ effective,
367
+ pendingNextUse: differs && !fsBound && hasInstance,
368
+ pendingFilesReset: fsBound,
287
369
  };
288
370
  }
371
+ /**
372
+ * Whether a provider whose limits are fixed at its file system cannot apply `effective`
373
+ * without a file reset: it has no `update`, or its `update` already answered `recreate` for it.
374
+ */
375
+ fsBound(p, rec, effective) {
376
+ if (p.impl.limitsApplyAt !== "fileSystem")
377
+ return false;
378
+ return p.impl.update === undefined || sameApplied(rec.unapplied, effective);
379
+ }
380
+ /**
381
+ * Whether the running instance of `row` must get `want` before use (spec §7.1): a cloud
382
+ * instance whose recorded tier or egress mode differs, unless only a file reset could apply it
383
+ * ({@link fsBound}). `want` null: a local device (the tier travels with each command).
384
+ */
385
+ mustApply(row, p, want) {
386
+ if (want === null)
387
+ return false;
388
+ const rec = readInstanceLimits(row.instance_limits);
389
+ const target = appliedOf(want);
390
+ if (sameApplied(rec.applied, target))
391
+ return false;
392
+ return !this.fsBound(p, rec, target);
393
+ }
289
394
  /**
290
395
  * The device's instance state as shown to people (read only, no lease): `running` /
291
396
  * `suspended` and the last use. No instance, a destroyed one, or one stuck in `creating` (being
@@ -411,7 +516,10 @@ let SandboxService = class SandboxService {
411
516
  const p = await this.require(ctx.deviceId, ctx.orgId);
412
517
  const env = ctx.deviceId;
413
518
  const holder = ctx.holderConversationId;
414
- let e = await this.seenBy(env, holder, await this.enter(ctx, p, signal, null));
519
+ // decides the shared fast path only (read again before each shared lease is used): the
520
+ // exclusive path reads it again under the lease
521
+ const want = () => this.wantedLimits(ctx, p);
522
+ let e = await this.seenBy(env, holder, await this.enter(ctx, p, signal, null, want));
415
523
  try {
416
524
  try {
417
525
  return await this.runAs(e, ctx, p, fn, signal);
@@ -425,7 +533,7 @@ let SandboxService = class SandboxService {
425
533
  const gone = { handle: e.handle, createdAt: e.createdAt };
426
534
  const prev = e;
427
535
  await this.leave(env, prev);
428
- e = await this.seenBy(env, holder, await this.enter(ctx, p, signal, gone));
536
+ e = await this.seenBy(env, holder, await this.enter(ctx, p, signal, gone, want));
429
537
  // a reset seen on the first try still has to be told (it covers the restart)
430
538
  const info = prev.info.filesReset
431
539
  ? { fresh: true, restarted: false, filesReset: true }
@@ -444,6 +552,58 @@ let SandboxService = class SandboxService {
444
552
  await this.leave(env, e);
445
553
  }
446
554
  }
555
+ /**
556
+ * The limits a cloud device's instance must run with for this use (sandbox dev tiers spec
557
+ * §7.1), read fresh; null for a local device (its machine gets the tier with each command).
558
+ * `row`: the instance row read under the exclusive lease (read here when absent).
559
+ *
560
+ * A scheduled run without a person on a cloud device that may run in the open tier, without the
561
+ * task's `tier:open` grant, is refused (`device.tier_not_granted`, recorded in the task run's
562
+ * `missing_grants`): the tier belongs to the shared instance, so it is not switched under the
563
+ * device's attended sessions (spec §11.4). The tier it may run in is the effective one, or for a
564
+ * provider whose limits are fixed at its file system the higher of that and the recorded one
565
+ * (a NULL record there counts as open: nothing says the file system is narrower).
566
+ */
567
+ async wantedLimits(ctx, p, row) {
568
+ if (p.plugin === LOCAL_PROVIDER)
569
+ return null;
570
+ const policy = await this.devicePolicy(ctx);
571
+ const runId = ctx.actor.runId;
572
+ if (runId !== undefined && (await this.mayRunOpen(ctx, p, policy.tier, row))) {
573
+ const u = await unattendedTierOf(this.db.db, runId, ctx.deviceId);
574
+ if (u !== null && !u.openGranted) {
575
+ await appendMissingGrant(this.db.db, u.taskRunId, TIER_OPEN_GRANT_KEY).catch((err) => this.log.warn("cannot record a missing task grant", { taskRunId: u.taskRunId, err }));
576
+ this.log.warn("scheduled run refused on a cloud device in the open tier", {
577
+ deviceId: ctx.deviceId,
578
+ taskRunId: u.taskRunId,
579
+ });
580
+ throw new ApiError("device.tier_not_granted", UNATTENDED_OPEN_TIER_ERROR);
581
+ }
582
+ }
583
+ return await this.deviceLimits(ctx, { policy });
584
+ }
585
+ /**
586
+ * Whether the device's instance may run in the open tier during this use (see
587
+ * {@link wantedLimits}): the effective tier is open, or the provider fixes limits at its file
588
+ * system and the recorded tier is open or unknown.
589
+ */
590
+ async mayRunOpen(ctx, p, tier, row) {
591
+ if (tier === "open")
592
+ return true;
593
+ if (p.impl.limitsApplyAt !== "fileSystem")
594
+ return false;
595
+ const r = row ??
596
+ (await this.db
597
+ .selectFrom("device_instances")
598
+ .selectAll()
599
+ .where("device_id", "=", ctx.deviceId)
600
+ .where("org_id", "=", ctx.orgId)
601
+ .executeTakeFirst());
602
+ if (r === undefined || r.fs_status !== "ready" || r.provider !== p.plugin)
603
+ return false;
604
+ const applied = readInstanceLimits(r.instance_limits).applied;
605
+ return applied === null || compareTiers(applied.tier, "open") >= 0;
606
+ }
447
607
  /**
448
608
  * Ruling R24: an operation on the held instance `e` found its file system gone. Records
449
609
  * `fs_status = 'lost'` while the row still records that instance on its file system (the
@@ -516,25 +676,32 @@ let SandboxService = class SandboxService {
516
676
  * Becomes a holder: loops until it has a shared lease on a running instance, or `sandbox.busy`.
517
677
  * An instance recorded `running` (with the current provider, not the `gone` instance) is used
518
678
  * under a shared lease; otherwise the exclusive lease (with intent) makes it run and is
519
- * downgraded. One token serves both, so the waiter passes its own intent.
679
+ * downgraded. One token serves both, so the waiter passes its own intent. `wantNow` reads the
680
+ * limits this use wants ({@link wantedLimits}), again after each wait, so a tier narrowed
681
+ * during the wait is never used with the old, wider limits.
520
682
  */
521
- async enter(ctx, p, signal, gone) {
683
+ async enter(ctx, p, signal, gone, wantNow) {
522
684
  const env = ctx.deviceId;
523
685
  const deadline = Date.now() + this.config.get().sandbox.lockWaitSeconds * 1000;
524
686
  const token = randomUUID();
525
687
  let intent = false;
688
+ let want = await wantNow();
526
689
  try {
527
- for (;;) {
690
+ for (let waited = false;; waited = true) {
528
691
  if (signal.aborted)
529
692
  throw abortReason(signal);
693
+ if (waited)
694
+ want = await wantNow();
530
695
  const row = await this.readRow(ctx);
531
- if (this.usable(row, p, gone)) {
696
+ // an instance whose tier or egress must change goes through the exclusive lease
697
+ const ready = (r) => this.usable(r, p, gone) && !this.mustApply(r, p, want);
698
+ if (ready(row)) {
532
699
  const lease = await this.leases.tryShared(env, this.cap, token);
533
700
  if (lease) {
534
701
  if (intent)
535
702
  await this.clearIntent(env, token);
536
703
  intent = false;
537
- const e = await this.asShared(ctx, p, lease, (r) => this.usable(r, p, gone), signal);
704
+ const e = await this.asShared(ctx, p, lease, ready, signal);
538
705
  if (e)
539
706
  return e;
540
707
  }
@@ -898,6 +1065,8 @@ let SandboxService = class SandboxService {
898
1065
  sweep_failed_at: null,
899
1066
  // conversations that used the old files are told they were reset (seenBy)
900
1067
  fs_resets: sql `fs_resets + 1`,
1068
+ // the next file system and instance record their own limits
1069
+ instance_limits: null,
901
1070
  ...prev,
902
1071
  });
903
1072
  this.log.info("device files reset", { deviceId });
@@ -1099,19 +1268,42 @@ let SandboxService = class SandboxService {
1099
1268
  // provider may have made it by name, and nothing else would ever destroy it
1100
1269
  await this.dropUnrecordedFiles(id, row.provider, signal);
1101
1270
  }
1271
+ // read under the lease, after the fence moved: a value read before the wait may be stale
1272
+ // (another replica may have changed the tier meanwhile)
1273
+ const want = await this.wantedLimits(ctx, p, row);
1102
1274
  const handle = row.handle;
1103
1275
  const createdAt = row.created_at.getTime();
1104
1276
  const fsResets = row.fs_resets;
1105
1277
  if (handle && isInstance(row, gone) && row.status === "running") {
1106
- const created = await this.create(ctx, p, held, row, fence, signal);
1278
+ const created = await this.create(ctx, p, held, row, fence, signal, want);
1107
1279
  return { ...created, fsResets, info: { fresh: true, restarted: true }, fence };
1108
1280
  }
1109
1281
  if (handle && row.status === "running") {
1282
+ if (want !== null && this.mustApply(row, p, want)) {
1283
+ const created = await this.applyLimits(ctx, p, held, row, handle, fence, signal, want);
1284
+ if (created)
1285
+ return { ...created, fsResets, info: { fresh: true, restarted: true }, fence };
1286
+ }
1110
1287
  return { handle, createdAt, fsResets, info: { fresh: false, restarted: false }, fence };
1111
1288
  }
1112
1289
  if (handle && row.status === "suspended") {
1113
1290
  const r = await p.impl.resume(handle, signal);
1114
1291
  if (r === "resumed") {
1292
+ // the new limits first: the row says running only once the instance runs with them
1293
+ if (want !== null && this.mustApply(row, p, want)) {
1294
+ let created;
1295
+ try {
1296
+ created = await this.applyLimits(ctx, p, held, row, handle, fence, signal, want);
1297
+ }
1298
+ catch (err) {
1299
+ // the row still says suspended; the next use resumes and kills what is left
1300
+ await this.bestEffort(id, "kill the commands left after a failed limits change", (s) => p.impl.killAll(handle, s));
1301
+ throw err;
1302
+ }
1303
+ if (created) {
1304
+ return { ...created, fsResets, info: { fresh: true, restarted: true }, fence };
1305
+ }
1306
+ }
1115
1307
  await this.fenced(held, id, fence, {
1116
1308
  status: "running",
1117
1309
  running_since: sql `now()`,
@@ -1131,7 +1323,7 @@ let SandboxService = class SandboxService {
1131
1323
  deviceId: id,
1132
1324
  });
1133
1325
  }
1134
- const created = await this.create(ctx, p, held, row, fence, signal);
1326
+ const created = await this.create(ctx, p, held, row, fence, signal, want);
1135
1327
  // had_instance only ever becomes true: any earlier instance (even if a rebuild failed since)
1136
1328
  // makes this one a restart on the kept files
1137
1329
  return {
@@ -1148,7 +1340,7 @@ let SandboxService = class SandboxService {
1148
1340
  * throws `device.files_lost`. When the instance handle cannot be written (lease lost), the
1149
1341
  * new instance is destroyed (best effort) before {@link SandboxLockLostError} is thrown.
1150
1342
  */
1151
- async create(ctx, p, held, row, fence, signal) {
1343
+ async create(ctx, p, held, row, fence, signal, want) {
1152
1344
  const id = ctx.deviceId;
1153
1345
  const name = deviceResourceName(id);
1154
1346
  await this.fenced(held, id, fence, {
@@ -1162,9 +1354,13 @@ let SandboxService = class SandboxService {
1162
1354
  destroyed_at: null,
1163
1355
  sweep_failed_at: null,
1164
1356
  });
1165
- // the org's effective limits apply at creation only, so a changed setting reaches only
1166
- // instances created afterwards
1167
- const limits = await this.orgLimits(ctx.orgId);
1357
+ // the device's effective limits apply at creation (a changed tier or egress mode is applied
1358
+ // on use, see applyLimits; other changes reach only instances created afterwards)
1359
+ const limits = want ?? (await this.deviceLimits(ctx));
1360
+ // the record says what is in force: a provider whose limits are fixed at its file system
1361
+ // records them with the file system, any other with the instance (spec §7.1)
1362
+ const record = instanceLimitsJson(appliedOf(limits));
1363
+ const atFileSystem = p.impl.limitsApplyAt === "fileSystem";
1168
1364
  const cfg = this.config.get().sandbox;
1169
1365
  let fs = row.fs_status === "ready" && (row.provider === null || row.provider === p.plugin)
1170
1366
  ? row.fs_handle
@@ -1177,6 +1373,7 @@ let SandboxService = class SandboxService {
1177
1373
  provider: p.plugin,
1178
1374
  fs_handle: JSON.stringify(fs),
1179
1375
  fs_status: "ready",
1376
+ ...(atFileSystem && { instance_limits: record }),
1180
1377
  });
1181
1378
  this.log.info("device file system created", { deviceId: id, provider: p.plugin });
1182
1379
  }
@@ -1191,8 +1388,13 @@ let SandboxService = class SandboxService {
1191
1388
  });
1192
1389
  }
1193
1390
  catch (err) {
1194
- if (!isFileSystemGoneError(err))
1391
+ if (!isFileSystemGoneError(err)) {
1392
+ if (limits.egress === "open" && !signal.aborted) {
1393
+ // spec §5.3: a provider set to refuse open egress (`openEgress: "off"`) fails here
1394
+ this.log.warn("the sandbox provider did not create an instance in the open tier; check its openEgress setting or lower sandbox.tiers.max", { deviceId: id, provider: p.plugin, err });
1395
+ }
1195
1396
  throw err;
1397
+ }
1196
1398
  this.log.warn("device file system is gone at the provider; its files are lost", {
1197
1399
  deviceId: id,
1198
1400
  provider: p.plugin,
@@ -1209,6 +1411,7 @@ let SandboxService = class SandboxService {
1209
1411
  created_at: sql `now()`,
1210
1412
  running_since: sql `now()`,
1211
1413
  last_used_at: sql `now()`,
1414
+ ...(!atFileSystem && { instance_limits: record }),
1212
1415
  });
1213
1416
  }
1214
1417
  catch (e) {
@@ -1226,6 +1429,110 @@ let SandboxService = class SandboxService {
1226
1429
  this.log.info("sandbox created", { deviceId: id, provider: p.plugin });
1227
1430
  return { handle, createdAt: createdAt.getTime() };
1228
1431
  }
1432
+ /**
1433
+ * Under the exclusive lease, on the running instance `handle` of `row` whose tier or egress
1434
+ * mode differs from `want` (sandbox dev tiers spec §7.1): the provider's `update` when it has
1435
+ * one; `updated` records `want`. Otherwise (`recreate`, no `update`, or the instance found gone
1436
+ * by `update`) the instance is destroyed and created again on the kept files, which records
1437
+ * `want` and is returned (the caller reports a restart). A provider whose limits are fixed at
1438
+ * its file system is not recreated (that would not apply them): `want` is recorded as
1439
+ * `unapplied` and the instance keeps running as is until a file reset.
1440
+ *
1441
+ * An `update` that fails otherwise never records `want`: a provider whose limits apply at
1442
+ * instance create is recreated (warned); after an abort, or for a provider bound to its file
1443
+ * system, the instance is suspended ({@link quarantine}) and the error thrown, so the next use
1444
+ * resumes it and tries again.
1445
+ */
1446
+ async applyLimits(ctx, p, held, row, handle, fence, signal, want) {
1447
+ const id = ctx.deviceId;
1448
+ const target = appliedOf(want);
1449
+ const was = readInstanceLimits(row.instance_limits).applied;
1450
+ const facts = { deviceId: id, provider: p.plugin, from: was, to: target };
1451
+ let gone = false;
1452
+ if (p.impl.update) {
1453
+ try {
1454
+ if ((await p.impl.update(handle, want, signal)) === "updated") {
1455
+ await this.fenced(held, id, fence, { instance_limits: instanceLimitsJson(target) });
1456
+ this.log.info("sandbox instance limits updated", facts);
1457
+ return null;
1458
+ }
1459
+ }
1460
+ catch (err) {
1461
+ if (isSandboxGoneError(err)) {
1462
+ gone = true;
1463
+ }
1464
+ else if (signal.aborted || p.impl.limitsApplyAt === "fileSystem") {
1465
+ // the instance may run with half-applied limits and cannot be recreated here: stop it,
1466
+ // so the next use resumes it and applies them again
1467
+ await this.quarantine(held, id, handle, p, fence);
1468
+ throw err;
1469
+ }
1470
+ else {
1471
+ // a new instance gets the limits at create: recreate rather than trust a failed update
1472
+ this.log.warn("sandbox limits update failed; recreating the instance", {
1473
+ ...facts,
1474
+ err,
1475
+ });
1476
+ }
1477
+ }
1478
+ }
1479
+ if (p.impl.limitsApplyAt === "fileSystem" && !gone) {
1480
+ await this.fenced(held, id, fence, { instance_limits: instanceLimitsJson(was, target) });
1481
+ this.log.info("sandbox limits changed; they apply when the device's files are reset", facts);
1482
+ return null;
1483
+ }
1484
+ this.log.info("sandbox limits changed; recreating the instance on the kept files", facts);
1485
+ if (!gone) {
1486
+ try {
1487
+ await p.impl.destroy(handle, signal);
1488
+ }
1489
+ catch (err) {
1490
+ // `create` replaces an instance of the same name anyway
1491
+ if (signal.aborted)
1492
+ throw err;
1493
+ this.log.warn("cannot destroy the sandbox instance before recreating it", {
1494
+ deviceId: id,
1495
+ err,
1496
+ });
1497
+ }
1498
+ }
1499
+ return await this.create(ctx, p, held, row, fence, signal, want);
1500
+ }
1501
+ /**
1502
+ * After a failed `update` that cannot be repaired by a recreate: best effort, with fresh short
1503
+ * signals (the use's own may be aborted), suspends the instance (or kills its commands) and
1504
+ * records it suspended (fenced: a lost lease leaves the record to the new holder).
1505
+ */
1506
+ async quarantine(held, id, handle, p, fence) {
1507
+ const suspended = await this.bestEffort(id, "suspend after a failed limits update", (s) => p.impl.suspend(handle, s));
1508
+ if (!suspended) {
1509
+ await this.bestEffort(id, "kill commands after a failed limits update", (s) => p.impl.killAll(handle, s));
1510
+ }
1511
+ try {
1512
+ await this.fenced(held, id, fence, {
1513
+ status: "suspended",
1514
+ suspended_at: sql `now()`,
1515
+ running_since: null,
1516
+ });
1517
+ }
1518
+ catch (err) {
1519
+ this.log.warn("cannot record the sandbox suspended after a failed limits update", {
1520
+ deviceId: id,
1521
+ err,
1522
+ });
1523
+ }
1524
+ }
1525
+ /** Runs `call` with its own short signal; a failure is warned and returns false. */
1526
+ async bestEffort(deviceId, what, call) {
1527
+ try {
1528
+ await call(AbortSignal.timeout(PENDING_KILL_TIMEOUT_MS));
1529
+ return true;
1530
+ }
1531
+ catch (err) {
1532
+ this.log.warn(`cannot ${what}`, { deviceId, err });
1533
+ return false;
1534
+ }
1535
+ }
1229
1536
  /**
1230
1537
  * Destroys the file system another provider may have made under the device's name without
1231
1538
  * the kernel recording it (`destroyFileSystemByName`; not there is success). That provider not
@@ -0,0 +1,86 @@
1
+ import { type SandboxTier } from "@capybara-hq/plugin-sdk";
2
+ import type { KernelConfig } from "../config/schema.js";
3
+ import type { OrgSettings } from "../orgs/settings.service.js";
4
+ /**
5
+ * Sandbox tiers (sandbox dev tiers spec §4): who may reach which tier, and the network each tier
6
+ * gets. One function, {@link effectiveSandboxPolicy}, combines the platform's ceiling and default
7
+ * (`sandbox.tiers`), the org's (`sandboxTier`) and the device's own choice.
8
+ */
9
+ /** The deployment config the policy reads (`sandbox.limits` network fields and `sandbox.tiers`). */
10
+ export interface DeploymentSandboxPolicy {
11
+ limits: Pick<KernelConfig["sandbox"]["limits"], "egress" | "allowHosts" | "devHosts">;
12
+ tiers: KernelConfig["sandbox"]["tiers"];
13
+ }
14
+ /** The org settings the policy reads. */
15
+ export type OrgSandboxSettings = Pick<OrgSettings, "sandboxEgress" | "sandboxTier">;
16
+ /**
17
+ * The org's base list `L` (spec §3): `none` when the org's or the platform's egress is `none`,
18
+ * else an allowlist of the org's hosts (the platform's `allowHosts` when the org gives none).
19
+ * Network requests and approvals add to this list; tiers widen it (§4.2).
20
+ */
21
+ export interface BaseSandboxEgress {
22
+ egress: "none" | "allowlist";
23
+ allowHosts: string[];
24
+ }
25
+ /** The network of one tier: what a cloud instance is created with, or a local exec's ceiling. */
26
+ export interface SandboxNetwork {
27
+ egress: "none" | "allowlist" | "open";
28
+ allowHosts: string[];
29
+ }
30
+ /** The effective sandbox policy of one device (or of a new device, with no tier of its own). */
31
+ export interface EffectiveSandboxPolicy extends SandboxNetwork {
32
+ /** min(device tier ?? the org's default, the org's ceiling, the platform's ceiling). */
33
+ tier: SandboxTier;
34
+ /** min(org max, platform max): the highest tier a device of the org may reach. */
35
+ ceiling: SandboxTier;
36
+ /** The org's default tier (the platform's when the org sets none), at most {@link ceiling}. */
37
+ defaultTier: SandboxTier;
38
+ /** Which ceiling cut the chosen tier (the device's, else the org's default), null when none did. */
39
+ cappedBy: "org" | "platform" | null;
40
+ /** The base list `L` the tiers widen. */
41
+ base: BaseSandboxEgress;
42
+ /** The dev hosts `D` (`sandbox.limits.devHosts`). */
43
+ devHosts: string[];
44
+ /** Whether a local device may run unsandboxed: platform and org allow it and the ceiling is open. */
45
+ allowUnsandboxed: boolean;
46
+ }
47
+ /** The org's base list `L`: see {@link BaseSandboxEgress}. */
48
+ export declare function baseSandboxEgress(deployment: Pick<DeploymentSandboxPolicy["limits"], "egress" | "allowHosts">, org: OrgSettings["sandboxEgress"]): BaseSandboxEgress;
49
+ /**
50
+ * The network of `tier` over the base list (spec §4.2): `none` stays `none` in every tier; an
51
+ * allowlist is `L` in strict, `L ∪ D` in dev and open egress in open.
52
+ */
53
+ export declare function networkForTier(base: BaseSandboxEgress, devHosts: readonly string[], tier: SandboxTier): SandboxNetwork;
54
+ /** The network of `tier` under `policy` (e.g. the ceiling's for a local device's exec args). */
55
+ export declare function limitsForTier(policy: EffectiveSandboxPolicy, tier: SandboxTier): SandboxNetwork;
56
+ /**
57
+ * The effective sandbox policy (spec §4.2). `deviceTier` is the device's own choice (null =
58
+ * follow the org's default). The org's ceiling is its `sandboxTier.max` (absent = the platform's)
59
+ * cut to the platform's; its default is `sandboxTier.default ?? sandbox.tiers.default`, cut to the
60
+ * ceiling. Stored values above the platform (the platform lowered its max later) are cut, never
61
+ * honoured.
62
+ */
63
+ export declare function effectiveSandboxPolicy(deployment: DeploymentSandboxPolicy, org: OrgSandboxSettings, deviceTier: SandboxTier | null): EffectiveSandboxPolicy;
64
+ /**
65
+ * What decides whether a running cloud instance must change (spec §7.1): its tier and egress
66
+ * mode only. `allowHosts`, cpu, memory and disk reach instances created afterwards.
67
+ */
68
+ export interface AppliedLimits {
69
+ tier: SandboxTier;
70
+ egress: SandboxNetwork["egress"];
71
+ }
72
+ /** The {@link AppliedLimits} of `limits`. */
73
+ export declare const appliedOf: (limits: AppliedLimits) => AppliedLimits;
74
+ /** Whether `a` and `b` are the same tier and egress mode (null, unknown, is never the same). */
75
+ export declare const sameApplied: (a: AppliedLimits | null | undefined, b: AppliedLimits) => boolean;
76
+ /**
77
+ * `device_instances.instance_limits` read leniently: `applied` is what the instance runs with
78
+ * (null: unknown, e.g. an instance made before migration 0117, which differs from every effective
79
+ * value); `unapplied` is a newer value the provider could not apply without a file reset.
80
+ */
81
+ export declare function readInstanceLimits(v: unknown): {
82
+ applied: AppliedLimits | null;
83
+ unapplied: AppliedLimits | null;
84
+ };
85
+ /** The JSON written to `device_instances.instance_limits`. */
86
+ export declare function instanceLimitsJson(applied: AppliedLimits | null, unapplied?: AppliedLimits): string | null;