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

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