@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.
- package/dist/admin/admin.controller.d.ts +3 -3
- package/dist/admin/admin.controller.js +6 -2
- package/dist/admin/admin.schemas.d.ts +13 -0
- package/dist/agents/admin-agents.d.ts +16 -3
- package/dist/agents/admin-agents.js +65 -7
- package/dist/agents/agents.schemas.d.ts +24 -6
- package/dist/agents/agents.service.d.ts +4 -3
- package/dist/agents/agents.service.js +39 -26
- package/dist/agents/definition.d.ts +44 -6
- package/dist/agents/definition.js +86 -18
- package/dist/agents/from-template.d.ts +6 -3
- package/dist/agents/from-template.js +57 -44
- package/dist/agents/org-templates.js +97 -41
- package/dist/agents/templates.d.ts +6 -0
- package/dist/agents/templates.js +15 -1
- package/dist/background/launch.d.ts +43 -5
- package/dist/background/launch.js +88 -8
- package/dist/background/session.d.ts +14 -2
- package/dist/background/session.js +26 -4
- package/dist/background/status.d.ts +1 -1
- package/dist/background/status.js +3 -3
- package/dist/background/stop.d.ts +0 -2
- package/dist/background/stop.js +1 -21
- package/dist/catalog/admin-catalog.controller.d.ts +2 -4
- package/dist/catalog/admin-catalog.controller.js +2 -6
- package/dist/catalog/catalog-content.d.ts +5 -0
- package/dist/catalog/catalog-content.js +1 -0
- package/dist/catalog/catalog.module.js +3 -0
- package/dist/catalog/claude/report.d.ts +9 -2
- package/dist/catalog/claude/report.js +1 -0
- package/dist/catalog/claude/translate.js +1 -1
- package/dist/catalog/component-promotion.js +1 -2
- package/dist/catalog/component-state.d.ts +13 -1
- package/dist/catalog/component-state.js +14 -5
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/marketplaces.service.js +2 -1
- package/dist/catalog/org-catalog.controller.d.ts +13 -3
- package/dist/catalog/org-catalog.controller.js +26 -4
- package/dist/catalog/org-catalog.schemas.d.ts +13 -0
- package/dist/catalog/org-catalog.schemas.js +34 -0
- package/dist/catalog/org-plugin-uploads.controller.js +0 -1
- package/dist/catalog/org-plugin-uploads.d.ts +0 -1
- package/dist/catalog/org-plugin-uploads.js +0 -3
- package/dist/catalog/org-plugin-uploads.schemas.d.ts +0 -1
- package/dist/catalog/org-plugin-uploads.schemas.js +0 -2
- package/dist/catalog/platform-components.d.ts +0 -1
- package/dist/catalog/platform-components.js +3 -8
- package/dist/catalog/plugin-import.d.ts +0 -8
- package/dist/catalog/plugin-import.js +4 -35
- package/dist/catalog/plugin-install.d.ts +2 -2
- package/dist/catalog/plugin-install.js +2 -2
- package/dist/catalog/plugin-package.d.ts +7 -0
- package/dist/catalog/plugin-package.js +27 -1
- package/dist/catalog/plugin-upgrade.d.ts +5 -6
- package/dist/catalog/plugin-upgrade.js +5 -23
- package/dist/catalog/requests.service.d.ts +66 -10
- package/dist/catalog/requests.service.js +196 -15
- package/dist/channels/actions.d.ts +2 -0
- package/dist/channels/actions.js +19 -4
- package/dist/channels/cards.d.ts +16 -2
- package/dist/channels/cards.js +40 -7
- package/dist/channels/channels.module.js +3 -0
- package/dist/channels/outbound.d.ts +22 -1
- package/dist/channels/outbound.js +116 -7
- package/dist/channels/phrases.d.ts +25 -0
- package/dist/channels/phrases.js +19 -0
- package/dist/channels/threads.d.ts +12 -0
- package/dist/channels/threads.js +34 -0
- package/dist/config/schema.d.ts +23 -1
- package/dist/config/schema.js +107 -5
- package/dist/conversations/conversations.service.d.ts +42 -9
- package/dist/conversations/conversations.service.js +57 -25
- package/dist/conversations/events.d.ts +106 -11
- package/dist/conversations/events.js +121 -18
- package/dist/conversations/message-context.d.ts +6 -3
- package/dist/conversations/message-context.js +14 -10
- package/dist/conversations/queue.d.ts +15 -2
- package/dist/conversations/queue.js +22 -3
- package/dist/conversations/queue.service.d.ts +12 -6
- package/dist/conversations/queue.service.js +36 -13
- package/dist/db/kernel-schema.d.ts +105 -3
- package/dist/devices/conversation-devices.d.ts +8 -1
- package/dist/devices/conversation-devices.js +8 -0
- package/dist/devices/devices.controller.d.ts +21 -4
- package/dist/devices/devices.controller.js +46 -8
- package/dist/devices/devices.schemas.d.ts +9 -1
- package/dist/devices/devices.schemas.js +11 -3
- package/dist/devices/devices.service.d.ts +176 -4
- package/dist/devices/devices.service.js +347 -6
- package/dist/devices/dto.d.ts +56 -3
- package/dist/devices/dto.js +51 -1
- package/dist/devices/public-suffix.d.ts +6 -0
- package/dist/devices/public-suffix.js +79 -0
- package/dist/errors.d.ts +50 -5
- package/dist/errors.js +50 -5
- package/dist/evals/reconstruct.d.ts +18 -13
- package/dist/evals/reconstruct.js +50 -36
- package/dist/evals/recorded-tools.d.ts +7 -7
- package/dist/evals/recorded-tools.js +4 -3
- package/dist/evals/trajectory.d.ts +6 -1
- package/dist/evals/trajectory.js +7 -0
- package/dist/extensions/package.js +2 -2
- package/dist/http/api.module.js +5 -0
- package/dist/identity/settings.d.ts +53 -3
- package/dist/identity/settings.js +92 -9
- package/dist/inbox/inbox.module.js +2 -1
- package/dist/inbox/inbox.service.d.ts +19 -4
- package/dist/inbox/inbox.service.js +14 -3
- package/dist/infra/config/kernel-config.service.d.ts +7 -0
- package/dist/infra/config/kernel-config.service.js +31 -0
- package/dist/infra/events/kernel-events.d.ts +2 -1
- package/dist/local-devices/device-policy.service.d.ts +65 -0
- package/dist/local-devices/device-policy.service.js +217 -0
- package/dist/local-devices/local-device-gateway.d.ts +4 -2
- package/dist/local-devices/local-device-gateway.js +39 -11
- package/dist/local-devices/local-device-provider.d.ts +40 -3
- package/dist/local-devices/local-device-provider.js +109 -7
- package/dist/local-devices/local-devices.module.d.ts +2 -1
- package/dist/local-devices/local-devices.module.js +19 -3
- package/dist/local-devices/machines.d.ts +30 -3
- package/dist/local-devices/machines.js +79 -3
- package/dist/local-devices/network-grants.d.ts +104 -0
- package/dist/local-devices/network-grants.js +288 -0
- package/dist/local-devices/policy-summary.d.ts +20 -0
- package/dist/local-devices/policy-summary.js +63 -0
- package/dist/orgs/orgs.module.js +4 -10
- package/dist/orgs/roles.d.ts +1 -1
- package/dist/orgs/roles.js +4 -0
- package/dist/orgs/settings.controller.js +4 -3
- package/dist/orgs/settings.service.d.ts +172 -14
- package/dist/orgs/settings.service.js +229 -27
- package/dist/platform/exclusions.js +11 -1
- package/dist/platform/operation.js +4 -0
- package/dist/platform/ops/admin-items.ops.js +1 -1
- package/dist/platform/ops/admin.ops.js +32 -7
- package/dist/platform/ops/agent-skills.ops.js +2 -2
- package/dist/platform/ops/agent-templates.ops.js +2 -2
- package/dist/platform/ops/agents.ops.js +11 -15
- package/dist/platform/ops/devices.ops.js +3 -1
- package/dist/platform/ops/inbox.ops.js +19 -0
- package/dist/platform/ops/org-settings.ops.js +15 -6
- package/dist/platform/ops/orgs.ops.js +1 -0
- package/dist/platform/ops/platform.ops.d.ts +9 -1
- package/dist/platform/ops/platform.ops.js +34 -12
- package/dist/platform/ops/requests.ops.js +3 -2
- package/dist/platform/ops/schedules.ops.d.ts +21 -0
- package/dist/platform/ops/schedules.ops.js +470 -0
- package/dist/platform/ops/skills.ops.js +3 -3
- package/dist/platform/platform-ops.module.d.ts +2 -1
- package/dist/platform/platform-ops.module.js +5 -0
- package/dist/platform/route-schemas.js +3 -0
- package/dist/roles/worker.module.js +3 -0
- package/dist/runtime/context/build.js +13 -4
- package/dist/runtime/context/compaction.js +6 -3
- package/dist/runtime/context/device-paths.d.ts +43 -21
- package/dist/runtime/context/device-paths.js +104 -21
- package/dist/runtime/context/run-context.d.ts +9 -0
- package/dist/runtime/context/run-context.js +8 -0
- package/dist/runtime/context/skills.d.ts +14 -1
- package/dist/runtime/context/skills.js +36 -5
- package/dist/runtime/maintenance.d.ts +4 -2
- package/dist/runtime/maintenance.js +16 -5
- package/dist/runtime/run-finish.d.ts +6 -1
- package/dist/runtime/run-finish.js +16 -2
- package/dist/runtime/run-queue.js +21 -2
- package/dist/runtime/run-worker.js +14 -33
- package/dist/runtime/runtime.module.js +3 -0
- package/dist/runtime/subagents/definition.d.ts +32 -14
- package/dist/runtime/subagents/definition.js +106 -109
- package/dist/runtime/subagents/dispatch.js +4 -3
- package/dist/runtime/subagents/preamble.d.ts +2 -1
- package/dist/runtime/subagents/preamble.js +2 -1
- package/dist/runtime/waits.js +14 -4
- package/dist/sandbox/platform-tiers.d.ts +86 -0
- package/dist/sandbox/platform-tiers.js +97 -0
- package/dist/sandbox/sandbox.service.d.ts +121 -11
- package/dist/sandbox/sandbox.service.js +351 -30
- package/dist/sandbox/tiers.d.ts +98 -0
- package/dist/sandbox/tiers.js +104 -0
- package/dist/schedules/effective-settings.d.ts +16 -0
- package/dist/schedules/effective-settings.js +17 -0
- package/dist/schedules/errors.d.ts +8 -0
- package/dist/schedules/errors.js +8 -0
- package/dist/schedules/fire.d.ts +118 -0
- package/dist/schedules/fire.js +625 -0
- package/dist/schedules/grant-rules.d.ts +94 -0
- package/dist/schedules/grant-rules.js +126 -0
- package/dist/schedules/job-ids.d.ts +32 -0
- package/dist/schedules/job-ids.js +27 -0
- package/dist/schedules/pause.d.ts +9 -0
- package/dist/schedules/pause.js +33 -0
- package/dist/schedules/run-tracker.d.ts +81 -0
- package/dist/schedules/run-tracker.js +333 -0
- package/dist/schedules/scan.job.d.ts +64 -0
- package/dist/schedules/scan.job.js +303 -0
- package/dist/schedules/schedule-math.d.ts +31 -0
- package/dist/schedules/schedule-math.js +223 -0
- package/dist/schedules/scheduled-tasks.service.d.ts +351 -0
- package/dist/schedules/scheduled-tasks.service.js +1193 -0
- package/dist/schedules/schedules.controller.d.ts +68 -0
- package/dist/schedules/schedules.controller.js +242 -0
- package/dist/schedules/schedules.module.d.ts +16 -0
- package/dist/schedules/schedules.module.js +65 -0
- package/dist/schedules/schedules.schemas.d.ts +316 -0
- package/dist/schedules/schedules.schemas.js +186 -0
- package/dist/schedules/store.d.ts +81 -0
- package/dist/schedules/store.js +390 -0
- package/dist/schedules/store.module.d.ts +6 -0
- package/dist/schedules/store.module.js +21 -0
- package/dist/schedules/types.d.ts +108 -0
- package/dist/schedules/types.js +1 -0
- package/dist/schedules/unattended.d.ts +117 -0
- package/dist/schedules/unattended.js +228 -0
- package/dist/setup/oauth-token-probe.js +8 -4
- package/dist/skills/agent-skills.service.d.ts +4 -8
- package/dist/skills/agent-skills.service.js +18 -53
- package/dist/skills/catalog.d.ts +65 -62
- package/dist/skills/catalog.js +109 -206
- package/dist/skills/conversation-catalog.d.ts +3 -3
- package/dist/skills/conversation-catalog.js +5 -12
- package/dist/skills/invoke.d.ts +2 -2
- package/dist/skills/skills.controller.d.ts +3 -1
- package/dist/skills/skills.service.d.ts +38 -37
- package/dist/skills/skills.service.js +168 -107
- package/dist/skills/usable.d.ts +57 -8
- package/dist/skills/usable.js +172 -38
- package/dist/tools/approvals.controller.d.ts +4 -2
- package/dist/tools/approvals.controller.js +17 -2
- package/dist/tools/approvals.schemas.d.ts +1 -0
- package/dist/tools/approvals.schemas.js +3 -1
- package/dist/tools/approvals.service.d.ts +182 -7
- package/dist/tools/approvals.service.js +597 -13
- package/dist/tools/builtin-names.d.ts +25 -4
- package/dist/tools/builtin-names.js +45 -4
- package/dist/tools/builtins/builtin-tools.d.ts +8 -0
- package/dist/tools/builtins/builtin-tools.js +23 -2
- package/dist/tools/builtins/sandbox/bash.js +16 -7
- package/dist/tools/builtins/sandbox/common.d.ts +1 -1
- package/dist/tools/builtins/sandbox/common.js +33 -10
- package/dist/tools/builtins/sandbox/edit.js +7 -3
- package/dist/tools/builtins/sandbox/file-state.d.ts +31 -1
- package/dist/tools/builtins/sandbox/file-state.js +33 -3
- package/dist/tools/builtins/sandbox/write.js +8 -4
- package/dist/tools/builtins/schedules.d.ts +107 -0
- package/dist/tools/builtins/schedules.js +789 -0
- package/dist/tools/elicitation.d.ts +2 -0
- package/dist/tools/elicitation.js +1 -1
- package/dist/tools/fixed-ask.d.ts +7 -0
- package/dist/tools/fixed-ask.js +9 -0
- package/dist/tools/network-asks.d.ts +109 -0
- package/dist/tools/network-asks.js +221 -0
- package/dist/tools/pipeline.d.ts +53 -6
- package/dist/tools/pipeline.js +202 -13
- package/dist/tools/policy.d.ts +21 -8
- package/dist/tools/policy.js +63 -9
- package/dist/tools/registry.d.ts +10 -1
- package/dist/tools/tools.module.js +12 -0
- package/package.json +2 -2
- package/src/migrations/0113_scheduled_tasks.sql +108 -0
- package/src/migrations/0114_network_asks.sql +23 -0
- package/src/migrations/0115_network_requests.sql +28 -0
- package/src/migrations/0116_skills_default_on.sql +37 -0
- 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 {
|
|
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.
|
|
86
|
-
* instance
|
|
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.
|
|
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?.
|
|
98
|
+
if (pc?.openEgress === "off") {
|
|
95
99
|
out.push({
|
|
96
100
|
level: "warn",
|
|
97
|
-
message: `sandbox.
|
|
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
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
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
|
|
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
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
1166
|
-
// instances created afterwards
|
|
1167
|
-
const limits = await this.
|
|
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;
|