@capybara-hq/kernel 0.1.0-rc.7 → 0.1.0-rc.9
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/background/finish.js +2 -1
- package/dist/background/launch.d.ts +13 -7
- package/dist/background/launch.js +15 -8
- package/dist/background/poll.job.d.ts +1 -1
- package/dist/background/poll.job.js +8 -3
- package/dist/background/session.d.ts +2 -2
- package/dist/background/session.js +3 -2
- package/dist/background/status.d.ts +1 -1
- package/dist/background/status.js +1 -1
- package/dist/background/stop.js +4 -3
- package/dist/background/store.d.ts +6 -1
- package/dist/background/store.js +5 -1
- package/dist/background/tasks.service.d.ts +1 -1
- package/dist/background/tasks.service.js +4 -4
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/config/docs.js +2 -0
- package/dist/config/schema.d.ts +7 -0
- package/dist/config/schema.js +45 -0
- package/dist/conversations/event-log.d.ts +4 -3
- package/dist/conversations/event-log.js +4 -3
- package/dist/conversations/events.d.ts +9 -0
- package/dist/conversations/events.js +14 -0
- package/dist/db/kernel-schema.d.ts +26 -1
- package/dist/devices/device-auth.controller.d.ts +42 -0
- package/dist/devices/device-auth.controller.js +116 -0
- package/dist/devices/device-auth.d.ts +119 -0
- package/dist/devices/device-auth.js +403 -0
- package/dist/devices/device-auth.module.d.ts +7 -0
- package/dist/devices/device-auth.module.js +25 -0
- package/dist/devices/device-gateway.d.ts +128 -0
- package/dist/devices/device-gateway.js +530 -0
- package/dist/devices/device-provider.d.ts +89 -0
- package/dist/devices/device-provider.js +151 -0
- package/dist/devices/device-routes.d.ts +36 -0
- package/dist/devices/device-routes.js +90 -0
- package/dist/devices/devices.module.d.ts +16 -0
- package/dist/devices/devices.module.js +44 -0
- package/dist/devices/dto.d.ts +66 -0
- package/dist/devices/dto.js +44 -0
- package/dist/devices/keys.d.ts +25 -0
- package/dist/devices/keys.js +25 -0
- package/dist/devices/machines.d.ts +106 -0
- package/dist/devices/machines.js +301 -0
- package/dist/devices/relay.d.ts +100 -0
- package/dist/devices/relay.js +373 -0
- package/dist/devices/targets.d.ts +28 -0
- package/dist/devices/targets.js +73 -0
- package/dist/environments/access.d.ts +11 -6
- package/dist/environments/access.js +23 -8
- package/dist/environments/conversation-environments.d.ts +20 -6
- package/dist/environments/conversation-environments.js +70 -9
- package/dist/environments/environments.controller.d.ts +4 -1
- package/dist/environments/environments.controller.js +15 -1
- package/dist/environments/environments.module.js +3 -1
- package/dist/environments/environments.service.d.ts +54 -7
- package/dist/environments/environments.service.js +104 -20
- package/dist/environments/legacy-cleanup.job.d.ts +1 -1
- package/dist/environments/legacy-cleanup.job.js +4 -4
- package/dist/errors.d.ts +24 -0
- package/dist/errors.js +24 -0
- package/dist/evals/recorded-tools.d.ts +9 -0
- package/dist/evals/recorded-tools.js +27 -0
- package/dist/http/api.module.js +4 -0
- package/dist/http/gateway-routes.d.ts +7 -3
- package/dist/http/gateway-routes.js +6 -3
- package/dist/identity/me.controller.d.ts +1 -3
- package/dist/identity/me.controller.js +1 -9
- package/dist/identity/me.schemas.d.ts +14 -0
- package/dist/identity/me.schemas.js +17 -0
- package/dist/infra/config/kernel-config.service.js +2 -0
- package/dist/infra/throttler/named-throttlers.d.ts +5 -0
- package/dist/infra/throttler/named-throttlers.js +38 -0
- package/dist/kernel.module.js +3 -0
- package/dist/lifecycle/kernel-lifecycle.js +5 -0
- package/dist/platform/exclusions.js +33 -10
- package/dist/platform/operation.js +1 -1
- package/dist/platform/ops/environments.ops.js +21 -3
- package/dist/platform/ops/platform.ops.d.ts +7 -2
- package/dist/platform/ops/platform.ops.js +50 -3
- package/dist/platform/route-schemas.js +3 -0
- package/dist/roles/gateway-role.d.ts +14 -3
- package/dist/roles/gateway-role.js +25 -5
- package/dist/roles/gateway.module.d.ts +2 -1
- package/dist/roles/gateway.module.js +6 -3
- package/dist/runtime/context/build.d.ts +2 -0
- package/dist/runtime/context/build.js +14 -0
- package/dist/runtime/context/compaction.js +1 -0
- package/dist/runtime/context/environment-paths.d.ts +40 -0
- package/dist/runtime/context/environment-paths.js +48 -0
- package/dist/runtime/model-call.js +2 -0
- package/dist/sandbox/providers.d.ts +29 -7
- package/dist/sandbox/providers.js +43 -8
- package/dist/sandbox/sandbox.service.d.ts +70 -18
- package/dist/sandbox/sandbox.service.js +176 -59
- package/dist/tools/builtins/sandbox/bash.d.ts +2 -2
- package/dist/tools/builtins/sandbox/bash.js +39 -27
- package/dist/tools/builtins/sandbox/common.d.ts +60 -25
- package/dist/tools/builtins/sandbox/common.js +121 -30
- package/dist/tools/builtins/sandbox/deliver.js +9 -6
- package/dist/tools/builtins/sandbox/edit.js +40 -37
- package/dist/tools/builtins/sandbox/glob.js +13 -8
- package/dist/tools/builtins/sandbox/grep.js +10 -6
- package/dist/tools/builtins/sandbox/index.d.ts +6 -4
- package/dist/tools/builtins/sandbox/index.js +6 -4
- package/dist/tools/builtins/sandbox/read.js +9 -6
- package/dist/tools/builtins/sandbox/task-output.d.ts +7 -3
- package/dist/tools/builtins/sandbox/task-output.js +14 -10
- package/dist/tools/builtins/sandbox/task-stop.js +3 -3
- package/dist/tools/builtins/sandbox/write.js +33 -30
- package/dist/tools/builtins/skill-load.d.ts +8 -5
- package/dist/tools/builtins/skill-load.js +21 -15
- package/dist/tools/builtins/skill-write.js +2 -2
- package/dist/tools/pipeline.js +2 -0
- package/dist/tools/registry.d.ts +21 -4
- package/dist/tools/registry.js +10 -2
- package/package.json +6 -3
- package/src/migrations/0093_connected_environments.sql +37 -0
- package/src/migrations/0094_environment_paths_idx.sql +7 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ExecResult, type SandboxActor, type SandboxHandle, type SandboxLimits, type SandboxProvider } from "@capybara-hq/plugin-sdk";
|
|
1
|
+
import { type ExecResult, type SandboxActor, type SandboxHandle, type SandboxLimits, type SandboxProvider, type SandboxRoots } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import type { Redis } from "ioredis";
|
|
3
3
|
import { type Selectable } from "kysely";
|
|
4
4
|
import { BackgroundTaskStore } from "../background/store.js";
|
|
@@ -21,6 +21,11 @@ export interface SandboxUseInfo {
|
|
|
21
21
|
* new instance on the kept files, so earlier processes are gone.
|
|
22
22
|
*/
|
|
23
23
|
restarted: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The instance's roots ({@link rootsOf} of its handle): every model-visible path is built from
|
|
26
|
+
* them (environments spec §13.1; a connected machine's real paths).
|
|
27
|
+
*/
|
|
28
|
+
roots: SandboxRoots;
|
|
24
29
|
}
|
|
25
30
|
/** Who uses the sandbox in {@link SandboxService.withSandbox}. */
|
|
26
31
|
export interface SandboxUseContext {
|
|
@@ -78,8 +83,12 @@ export type IfRunningResult<T> = {
|
|
|
78
83
|
export declare const environmentResourceName: (environmentId: string) => string;
|
|
79
84
|
/** The environment is missing, deleted, or not in the caller's org (404; ids cannot be probed). */
|
|
80
85
|
export declare const environmentNotFound: () => ApiError;
|
|
81
|
-
/**
|
|
82
|
-
|
|
86
|
+
/**
|
|
87
|
+
* The environment's file system is gone at the provider (409); never replaced silently. On a
|
|
88
|
+
* connected machine (`device`, details `[{ source: "device" }]`) the directory is the machine
|
|
89
|
+
* owner's: `capybara-device logout`, then `login` into the environment, recreates it (ruling R24).
|
|
90
|
+
*/
|
|
91
|
+
export declare const environmentFilesLost: (device?: boolean) => ApiError;
|
|
83
92
|
/**
|
|
84
93
|
* The row still records the instance `gone` (same handle and same `created_at`). Handles compare
|
|
85
94
|
* canonically: a jsonb round trip reorders a multi-key handle's keys (final review m9).
|
|
@@ -146,18 +155,25 @@ export declare const sandboxUnavailable: () => ApiError;
|
|
|
146
155
|
* row's `created_at` (set by the fenced write after each `create`); the handle alone is the same
|
|
147
156
|
* after a rebuild on providers whose handle is the instance name (sandbox-docker).
|
|
148
157
|
*
|
|
158
|
+
* - **Provider per environment** (environments spec §6): each call resolves the provider from
|
|
159
|
+
* the environment's `environments.provider` ({@link providerForEnvironment}): a `device`
|
|
160
|
+
* environment runs on the kernel-internal `@device` provider (never the cloud one), a cloud
|
|
161
|
+
* environment on the loaded `sandbox@2` extension. A `@device` instance is never suspended or
|
|
162
|
+
* destroyed for inactivity; a deleted `@device` environment (or one of a deleted org) is
|
|
163
|
+
* destroyed by the sweep like any other.
|
|
164
|
+
*
|
|
149
165
|
* Files kept by another provider than the current one make the environment unusable
|
|
150
166
|
* (`environment.unavailable`) until that provider is back; deleting it still works while that
|
|
151
|
-
* provider is loaded ({@link SandboxProviders.
|
|
152
|
-
* uses throw `sandbox.unavailable` (503). No in-process state:
|
|
153
|
-
* Redis.
|
|
167
|
+
* provider is loaded ({@link SandboxProviders.forName}). Without the environment's provider
|
|
168
|
+
* {@link availableFor} is false and uses throw `sandbox.unavailable` (503). No in-process state:
|
|
169
|
+
* replicas share it through PG and Redis.
|
|
154
170
|
*/
|
|
155
171
|
export declare class SandboxService {
|
|
156
172
|
private readonly db;
|
|
157
173
|
/** The record of the instance each holder last used (`capk:sandbox-seen:…`). */
|
|
158
174
|
private readonly redis;
|
|
159
175
|
private readonly leases;
|
|
160
|
-
/** The
|
|
176
|
+
/** The providers (read at every call: providers are ready after the extension load). */
|
|
161
177
|
private readonly providers;
|
|
162
178
|
private readonly config;
|
|
163
179
|
private readonly log;
|
|
@@ -166,14 +182,28 @@ export declare class SandboxService {
|
|
|
166
182
|
constructor(db: KernelDb,
|
|
167
183
|
/** The record of the instance each holder last used (`capk:sandbox-seen:…`). */
|
|
168
184
|
redis: Pick<Redis, "set">, leases: SandboxLeases,
|
|
169
|
-
/** The
|
|
170
|
-
providers: Pick<SandboxProviders, "
|
|
185
|
+
/** The providers (read at every call: providers are ready after the extension load). */
|
|
186
|
+
providers: Pick<SandboxProviders, "cloud" | "forName">, config: Pick<KernelConfigService, "get">, log: Pick<KernelLogger, "info" | "warn">,
|
|
171
187
|
/** Background tasks lost while their process may live on (killed after a resume). */
|
|
172
188
|
background: Pick<BackgroundTaskStore, "pendingKills" | "clearKill">);
|
|
173
189
|
/** Poll interval while waiting for a lease (the leases' own). */
|
|
174
190
|
private get pollMs();
|
|
175
|
-
/**
|
|
176
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Whether there is a cloud sandbox provider (a loaded `sandbox@2` extension). For gates that have
|
|
193
|
+
* no environment yet (tool listing, skill files); a call on an environment asks
|
|
194
|
+
* {@link availableFor}.
|
|
195
|
+
*/
|
|
196
|
+
get cloudAvailable(): boolean;
|
|
197
|
+
/** Whether the environment's provider is there (`@device` registered, or a cloud provider). */
|
|
198
|
+
availableFor(environmentId: string): Promise<boolean>;
|
|
199
|
+
/**
|
|
200
|
+
* The provider the environment works with, read from its row ({@link providerForEnvironment});
|
|
201
|
+
* a missing row (or one of another org, with `orgId`) reads as a cloud environment, whose use
|
|
202
|
+
* then fails as not found.
|
|
203
|
+
*/
|
|
204
|
+
private providerOf;
|
|
205
|
+
/** {@link providerOf}, or `sandbox.unavailable` (503) without it. */
|
|
206
|
+
private require;
|
|
177
207
|
/** Concurrent holders of one environment (`sandbox.maxConcurrentCommands`, read at every use). */
|
|
178
208
|
private get cap();
|
|
179
209
|
/**
|
|
@@ -233,11 +263,20 @@ export declare class SandboxService {
|
|
|
233
263
|
* - When `fn` throws `SandboxGoneError` (the instance is gone) the instance is rebuilt on the
|
|
234
264
|
* kept files (or the one another holder rebuilt is taken) and `fn` runs once more with
|
|
235
265
|
* `restarted: true`; a second `gone` is thrown. Other errors are thrown as they are.
|
|
236
|
-
* - Lost files throw `environment.files_lost` (409)
|
|
266
|
+
* - Lost files throw `environment.files_lost` (409), also when `fn` throws
|
|
267
|
+
* `FileSystemGoneError` (the files are then recorded lost, ruling R24).
|
|
237
268
|
* - When `ctx.holderConversationId` last used another instance (another holder rebuilt it
|
|
238
269
|
* meanwhile), `info.restarted` is true, so it learns its processes are gone (final review m5).
|
|
239
270
|
*/
|
|
240
271
|
withSandbox<T>(ctx: SandboxUseContext, fn: (h: SandboxHandle, p: HeldProvider, info: SandboxUseInfo, signal: AbortSignal) => Promise<T>, signal: AbortSignal): Promise<T>;
|
|
272
|
+
/**
|
|
273
|
+
* Ruling R24: an operation on the held instance `e` found its file system gone. Records
|
|
274
|
+
* `fs_status = 'lost'` while the row still records that instance on its file system (the
|
|
275
|
+
* instance handle stays, so a reset still destroys it), moving the fence on, and returns
|
|
276
|
+
* `environment.files_lost` to throw. Uses then fail until the files are reset (a connected
|
|
277
|
+
* machine: until a new login, which resets the row). A failed write only logs.
|
|
278
|
+
*/
|
|
279
|
+
private filesGone;
|
|
241
280
|
/**
|
|
242
281
|
* Records that `holder` uses the instance of `e` (`capk:sandbox-seen:<env>:<holder>` =
|
|
243
282
|
* `<created_at>:<file system id>`, with a TTL of the inactivity destruction plus a day). When the
|
|
@@ -258,7 +297,10 @@ export declare class SandboxService {
|
|
|
258
297
|
private readRow;
|
|
259
298
|
/** The instance row belongs to another org: reported as not found, so ids cannot be probed. */
|
|
260
299
|
private foreign;
|
|
261
|
-
/**
|
|
300
|
+
/**
|
|
301
|
+
* Recorded running with the current provider on files not lost (lost ones go through the
|
|
302
|
+
* exclusive path, which refuses them), and not the instance found gone.
|
|
303
|
+
*/
|
|
262
304
|
private usable;
|
|
263
305
|
/**
|
|
264
306
|
* Under a just-taken shared lease: confirms the row is still `accept`ed (else releases and
|
|
@@ -295,15 +337,19 @@ export declare class SandboxService {
|
|
|
295
337
|
* (the instance only; the file system is kept and the next use builds a new instance on it); an
|
|
296
338
|
* environment that was deleted, or whose org was deleted, loses its instance and its file system
|
|
297
339
|
* and its row is deleted (as {@link destroyEnvironment} does). Environments with any holder are
|
|
298
|
-
* skipped.
|
|
340
|
+
* skipped. Inactivity never destroys a `@device` instance; a gone `@device` environment is
|
|
341
|
+
* destroyed like any other while `@device` is registered. Returns how many it destroyed. Called
|
|
342
|
+
* hourly by the `sandbox-destroy-inactive` cron.
|
|
299
343
|
*/
|
|
300
344
|
destroyInactive(now: Date): Promise<number>;
|
|
301
345
|
/**
|
|
302
346
|
* Destroys the environment's instance, then its file system, then deletes its instance row
|
|
303
347
|
* (environment deletion, offboarding); nothing to do without a row. Waits for the exclusive lease
|
|
304
|
-
* (with intent, so running commands finish and new ones wait) up to 10 minutes. Without
|
|
305
|
-
* provider it
|
|
306
|
-
*
|
|
348
|
+
* (with intent, so running commands finish and new ones wait) up to 10 minutes. Without the
|
|
349
|
+
* environment's provider it throws and the row stays, marked `sweep_failed_at` (the job is
|
|
350
|
+
* retried, and the destruction sweep finishes a deleted environment once the provider is back).
|
|
351
|
+
* Files kept by another provider that is not loaded cannot be destroyed: it throws and the row
|
|
352
|
+
* stays.
|
|
307
353
|
*/
|
|
308
354
|
destroyEnvironment(environmentId: string): Promise<void>;
|
|
309
355
|
/**
|
|
@@ -329,7 +375,6 @@ export declare class SandboxService {
|
|
|
329
375
|
* and leaves the flags of this instance's tasks for the next resume.
|
|
330
376
|
*/
|
|
331
377
|
private killPendingTasks;
|
|
332
|
-
private require;
|
|
333
378
|
/**
|
|
334
379
|
* A fenced write: only while the lease holds and the row's generation is still the one read
|
|
335
380
|
* under it (and incremented); otherwise {@link SandboxLockLostError}.
|
|
@@ -390,6 +435,13 @@ export declare class SandboxService {
|
|
|
390
435
|
private destroyRow;
|
|
391
436
|
/** Due for suspension: made by this provider, running, and idle or running for too long. */
|
|
392
437
|
private suspendDue;
|
|
438
|
+
/** The provider of an instance row: an internal one (`@device`) by name, else the cloud one. */
|
|
439
|
+
private rowProvider;
|
|
440
|
+
/**
|
|
441
|
+
* A row the idle sweeps may touch: not on a kernel-internal provider (`@device` instances live on
|
|
442
|
+
* people's machines and are never suspended or destroyed for inactivity).
|
|
443
|
+
*/
|
|
444
|
+
private cloudRow;
|
|
393
445
|
/** The environment was deleted, or its org was: the instance and the files go. */
|
|
394
446
|
private environmentGone;
|
|
395
447
|
/** Due for instance destruction: an instance (or a create in progress) unused for too long. */
|
|
@@ -8,7 +8,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
-
import { isFileSystemGoneError, isSandboxGoneError, } from "@capybara-hq/plugin-sdk";
|
|
11
|
+
import { isFileSystemGoneError, isSandboxGoneError, rootsOf, } from "@capybara-hq/plugin-sdk";
|
|
12
12
|
import { Inject, Injectable } from "@nestjs/common";
|
|
13
13
|
import { sql, } from "kysely";
|
|
14
14
|
import { BackgroundTaskStore } from "../background/store.js";
|
|
@@ -21,7 +21,7 @@ import { KERNEL_LOGGER, REDIS } from "../infra/tokens.js";
|
|
|
21
21
|
import { effectiveSandboxEgress, effectiveSandboxLimits, readOrgSettings, } from "../orgs/settings.service.js";
|
|
22
22
|
import { canonicalJson } from "../tools/canonical-json.js";
|
|
23
23
|
import { SandboxLeases, SandboxLockLostError, sandboxBusy, } from "./leases.js";
|
|
24
|
-
import { SandboxProviders } from "./providers.js";
|
|
24
|
+
import { DEVICE_PROVIDER, providerForEnvironment, SandboxProviders, } from "./providers.js";
|
|
25
25
|
// Moved to leases.ts (phase C); re-exported for existing importers
|
|
26
26
|
export { SANDBOX_LOCK_PREFIX, SANDBOX_LOCK_TTL_MS, SandboxLockLostError, sandboxBusy, } from "./leases.js";
|
|
27
27
|
/** Margin (ms) of a holder record's TTL beyond the longest command: by then the in-sandbox limit has ended its commands. */
|
|
@@ -47,8 +47,14 @@ const opSignal = (lease) => AbortSignal.any([lease.signal, AbortSignal.timeout(S
|
|
|
47
47
|
export const environmentResourceName = (environmentId) => `cap-env-${environmentId}`;
|
|
48
48
|
/** The environment is missing, deleted, or not in the caller's org (404; ids cannot be probed). */
|
|
49
49
|
export const environmentNotFound = () => new ApiError("environment.not_found", "This environment does not exist or was deleted.");
|
|
50
|
-
/**
|
|
51
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The environment's file system is gone at the provider (409); never replaced silently. On a
|
|
52
|
+
* connected machine (`device`, details `[{ source: "device" }]`) the directory is the machine
|
|
53
|
+
* owner's: `capybara-device logout`, then `login` into the environment, recreates it (ruling R24).
|
|
54
|
+
*/
|
|
55
|
+
export const environmentFilesLost = (device = false) => device
|
|
56
|
+
? new ApiError("environment.files_lost", "The environment's directory is gone from its connected machine; run capybara-device logout, then capybara-device login into this environment, on that machine to recreate it.", [{ source: "device" }])
|
|
57
|
+
: new ApiError("environment.files_lost", "The environment's files are gone; reset its files from the environments page to start with an empty workspace.");
|
|
52
58
|
/**
|
|
53
59
|
* The row still records the instance `gone` (same handle and same `created_at`). Handles compare
|
|
54
60
|
* canonically: a jsonb round trip reorders a multi-key handle's keys (final review m9).
|
|
@@ -185,11 +191,18 @@ async function inPool(items, limit, fn) {
|
|
|
185
191
|
* row's `created_at` (set by the fenced write after each `create`); the handle alone is the same
|
|
186
192
|
* after a rebuild on providers whose handle is the instance name (sandbox-docker).
|
|
187
193
|
*
|
|
194
|
+
* - **Provider per environment** (environments spec §6): each call resolves the provider from
|
|
195
|
+
* the environment's `environments.provider` ({@link providerForEnvironment}): a `device`
|
|
196
|
+
* environment runs on the kernel-internal `@device` provider (never the cloud one), a cloud
|
|
197
|
+
* environment on the loaded `sandbox@2` extension. A `@device` instance is never suspended or
|
|
198
|
+
* destroyed for inactivity; a deleted `@device` environment (or one of a deleted org) is
|
|
199
|
+
* destroyed by the sweep like any other.
|
|
200
|
+
*
|
|
188
201
|
* Files kept by another provider than the current one make the environment unusable
|
|
189
202
|
* (`environment.unavailable`) until that provider is back; deleting it still works while that
|
|
190
|
-
* provider is loaded ({@link SandboxProviders.
|
|
191
|
-
* uses throw `sandbox.unavailable` (503). No in-process state:
|
|
192
|
-
* Redis.
|
|
203
|
+
* provider is loaded ({@link SandboxProviders.forName}). Without the environment's provider
|
|
204
|
+
* {@link availableFor} is false and uses throw `sandbox.unavailable` (503). No in-process state:
|
|
205
|
+
* replicas share it through PG and Redis.
|
|
193
206
|
*/
|
|
194
207
|
let SandboxService = class SandboxService {
|
|
195
208
|
db;
|
|
@@ -202,7 +215,7 @@ let SandboxService = class SandboxService {
|
|
|
202
215
|
constructor(db,
|
|
203
216
|
/** The record of the instance each holder last used (`capk:sandbox-seen:…`). */
|
|
204
217
|
redis, leases,
|
|
205
|
-
/** The
|
|
218
|
+
/** The providers (read at every call: providers are ready after the extension load). */
|
|
206
219
|
providers, config, log,
|
|
207
220
|
/** Background tasks lost while their process may live on (killed after a resume). */
|
|
208
221
|
background) {
|
|
@@ -218,9 +231,38 @@ let SandboxService = class SandboxService {
|
|
|
218
231
|
get pollMs() {
|
|
219
232
|
return this.leases.pollMs;
|
|
220
233
|
}
|
|
221
|
-
/**
|
|
222
|
-
|
|
223
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Whether there is a cloud sandbox provider (a loaded `sandbox@2` extension). For gates that have
|
|
236
|
+
* no environment yet (tool listing, skill files); a call on an environment asks
|
|
237
|
+
* {@link availableFor}.
|
|
238
|
+
*/
|
|
239
|
+
get cloudAvailable() {
|
|
240
|
+
return this.providers.cloud() !== undefined;
|
|
241
|
+
}
|
|
242
|
+
/** Whether the environment's provider is there (`@device` registered, or a cloud provider). */
|
|
243
|
+
async availableFor(environmentId) {
|
|
244
|
+
return (await this.providerOf(environmentId)) !== undefined;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The provider the environment works with, read from its row ({@link providerForEnvironment});
|
|
248
|
+
* a missing row (or one of another org, with `orgId`) reads as a cloud environment, whose use
|
|
249
|
+
* then fails as not found.
|
|
250
|
+
*/
|
|
251
|
+
async providerOf(environmentId, orgId) {
|
|
252
|
+
const row = await this.db
|
|
253
|
+
.selectFrom("environments")
|
|
254
|
+
.select("provider")
|
|
255
|
+
.where("id", "=", environmentId)
|
|
256
|
+
.$if(orgId !== undefined, (q) => q.where("org_id", "=", orgId))
|
|
257
|
+
.executeTakeFirst();
|
|
258
|
+
return providerForEnvironment(this.providers, row?.provider ?? null);
|
|
259
|
+
}
|
|
260
|
+
/** {@link providerOf}, or `sandbox.unavailable` (503) without it. */
|
|
261
|
+
async require(environmentId, orgId) {
|
|
262
|
+
const p = await this.providerOf(environmentId, orgId);
|
|
263
|
+
if (!p)
|
|
264
|
+
throw sandboxUnavailable();
|
|
265
|
+
return p;
|
|
224
266
|
}
|
|
225
267
|
/** Concurrent holders of one environment (`sandbox.maxConcurrentCommands`, read at every use). */
|
|
226
268
|
get cap() {
|
|
@@ -290,7 +332,7 @@ let SandboxService = class SandboxService {
|
|
|
290
332
|
* too) are thrown as they are.
|
|
291
333
|
*/
|
|
292
334
|
async ifRunning(environmentId, instance, fn, signal = new AbortController().signal) {
|
|
293
|
-
const p = this.require();
|
|
335
|
+
const p = await this.require(environmentId);
|
|
294
336
|
const env = environmentId;
|
|
295
337
|
const deadline = Date.now() + this.config.get().sandbox.lockWaitSeconds * 1000;
|
|
296
338
|
const token = randomUUID();
|
|
@@ -319,7 +361,14 @@ let SandboxService = class SandboxService {
|
|
|
319
361
|
try {
|
|
320
362
|
const impl = tagged(p.impl, lease.token, undefined, () => this.register(env, lease.token));
|
|
321
363
|
const sig = AbortSignal.any([signal, lease.signal]);
|
|
322
|
-
|
|
364
|
+
try {
|
|
365
|
+
return { kind: "ran", value: await fn(impl, e.handle, sig) };
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
if (isFileSystemGoneError(err))
|
|
369
|
+
throw await this.filesGone(env, p, e);
|
|
370
|
+
throw err;
|
|
371
|
+
}
|
|
323
372
|
}
|
|
324
373
|
finally {
|
|
325
374
|
await this.leave(env, e);
|
|
@@ -353,12 +402,13 @@ let SandboxService = class SandboxService {
|
|
|
353
402
|
* - When `fn` throws `SandboxGoneError` (the instance is gone) the instance is rebuilt on the
|
|
354
403
|
* kept files (or the one another holder rebuilt is taken) and `fn` runs once more with
|
|
355
404
|
* `restarted: true`; a second `gone` is thrown. Other errors are thrown as they are.
|
|
356
|
-
* - Lost files throw `environment.files_lost` (409)
|
|
405
|
+
* - Lost files throw `environment.files_lost` (409), also when `fn` throws
|
|
406
|
+
* `FileSystemGoneError` (the files are then recorded lost, ruling R24).
|
|
357
407
|
* - When `ctx.holderConversationId` last used another instance (another holder rebuilt it
|
|
358
408
|
* meanwhile), `info.restarted` is true, so it learns its processes are gone (final review m5).
|
|
359
409
|
*/
|
|
360
410
|
async withSandbox(ctx, fn, signal) {
|
|
361
|
-
const p = this.require();
|
|
411
|
+
const p = await this.require(ctx.environmentId, ctx.orgId);
|
|
362
412
|
const env = ctx.environmentId;
|
|
363
413
|
const holder = ctx.holderConversationId;
|
|
364
414
|
let e = await this.seenBy(env, holder, await this.enter(ctx, p, signal, null));
|
|
@@ -367,6 +417,8 @@ let SandboxService = class SandboxService {
|
|
|
367
417
|
return await this.runAs(e, ctx, p, fn, signal);
|
|
368
418
|
}
|
|
369
419
|
catch (err) {
|
|
420
|
+
if (isFileSystemGoneError(err))
|
|
421
|
+
throw await this.filesGone(env, p, e);
|
|
370
422
|
if (!isSandboxGoneError(err))
|
|
371
423
|
throw err;
|
|
372
424
|
this.log.info("sandbox instance is gone during use; recreating", { environmentId: env });
|
|
@@ -374,13 +426,47 @@ let SandboxService = class SandboxService {
|
|
|
374
426
|
const prev = e;
|
|
375
427
|
await this.leave(env, prev);
|
|
376
428
|
e = await this.seenBy(env, holder, await this.enter(ctx, p, signal, gone));
|
|
377
|
-
|
|
429
|
+
try {
|
|
430
|
+
return await this.runAs({ ...e, info: { fresh: true, restarted: true } }, ctx, p, fn, signal);
|
|
431
|
+
}
|
|
432
|
+
catch (err2) {
|
|
433
|
+
if (isFileSystemGoneError(err2))
|
|
434
|
+
throw await this.filesGone(env, p, e);
|
|
435
|
+
throw err2;
|
|
436
|
+
}
|
|
378
437
|
}
|
|
379
438
|
}
|
|
380
439
|
finally {
|
|
381
440
|
await this.leave(env, e);
|
|
382
441
|
}
|
|
383
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* Ruling R24: an operation on the held instance `e` found its file system gone. Records
|
|
445
|
+
* `fs_status = 'lost'` while the row still records that instance on its file system (the
|
|
446
|
+
* instance handle stays, so a reset still destroys it), moving the fence on, and returns
|
|
447
|
+
* `environment.files_lost` to throw. Uses then fail until the files are reset (a connected
|
|
448
|
+
* machine: until a new login, which resets the row). A failed write only logs.
|
|
449
|
+
*/
|
|
450
|
+
async filesGone(env, p, e) {
|
|
451
|
+
this.log.warn("environment file system is gone at the provider; its files are lost", {
|
|
452
|
+
environmentId: env,
|
|
453
|
+
provider: p.plugin,
|
|
454
|
+
});
|
|
455
|
+
try {
|
|
456
|
+
await this.db
|
|
457
|
+
.updateTable("environment_instances")
|
|
458
|
+
.set((eb) => ({ fs_status: "lost", generation: eb("generation", "+", 1) }))
|
|
459
|
+
.where("environment_id", "=", env)
|
|
460
|
+
.where("fs_status", "=", "ready")
|
|
461
|
+
// `createdAt` is the row's value read back in milliseconds
|
|
462
|
+
.where(sql `date_trunc('milliseconds', created_at)`, "=", new Date(e.createdAt))
|
|
463
|
+
.execute();
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
this.log.warn("cannot record lost environment files", { environmentId: env, err });
|
|
467
|
+
}
|
|
468
|
+
return environmentFilesLost(p.plugin === DEVICE_PROVIDER);
|
|
469
|
+
}
|
|
384
470
|
/**
|
|
385
471
|
* Records that `holder` uses the instance of `e` (`capk:sandbox-seen:<env>:<holder>` =
|
|
386
472
|
* `<created_at>:<file system id>`, with a TTL of the inactivity destruction plus a day). When the
|
|
@@ -415,7 +501,7 @@ let SandboxService = class SandboxService {
|
|
|
415
501
|
const sig = AbortSignal.any([signal, e.lease.signal]);
|
|
416
502
|
const env = ctx.environmentId;
|
|
417
503
|
const impl = tagged(p.impl, e.lease.token, ctx.actor, () => this.register(env, e.lease.token));
|
|
418
|
-
return fn(e.handle, impl, e.info, sig);
|
|
504
|
+
return fn(e.handle, impl, { ...e.info, roots: rootsOf(e.handle) }, sig);
|
|
419
505
|
}
|
|
420
506
|
/**
|
|
421
507
|
* Becomes a holder: loops until it has a shared lease on a running instance, or `sandbox.busy`.
|
|
@@ -480,10 +566,14 @@ let SandboxService = class SandboxService {
|
|
|
480
566
|
foreign(_ctx) {
|
|
481
567
|
return environmentNotFound();
|
|
482
568
|
}
|
|
483
|
-
/**
|
|
569
|
+
/**
|
|
570
|
+
* Recorded running with the current provider on files not lost (lost ones go through the
|
|
571
|
+
* exclusive path, which refuses them), and not the instance found gone.
|
|
572
|
+
*/
|
|
484
573
|
usable(row, p, gone) {
|
|
485
574
|
return (row !== undefined &&
|
|
486
575
|
row.status === "running" &&
|
|
576
|
+
row.fs_status !== "lost" &&
|
|
487
577
|
row.provider === p.plugin &&
|
|
488
578
|
row.handle !== null &&
|
|
489
579
|
!isInstance(row, gone));
|
|
@@ -624,7 +714,8 @@ let SandboxService = class SandboxService {
|
|
|
624
714
|
* many it suspended. Called every minute by the `sandbox-suspend-idle` cron.
|
|
625
715
|
*/
|
|
626
716
|
async suspendIdle(now) {
|
|
627
|
-
|
|
717
|
+
// cloud only: suspendDue keeps to this provider's rows, so `@device` is never suspended here
|
|
718
|
+
const p = this.providers.cloud();
|
|
628
719
|
if (!p)
|
|
629
720
|
return 0;
|
|
630
721
|
const due = this.suspendDue(now, p.plugin);
|
|
@@ -666,31 +757,45 @@ let SandboxService = class SandboxService {
|
|
|
666
757
|
* (the instance only; the file system is kept and the next use builds a new instance on it); an
|
|
667
758
|
* environment that was deleted, or whose org was deleted, loses its instance and its file system
|
|
668
759
|
* and its row is deleted (as {@link destroyEnvironment} does). Environments with any holder are
|
|
669
|
-
* skipped.
|
|
760
|
+
* skipped. Inactivity never destroys a `@device` instance; a gone `@device` environment is
|
|
761
|
+
* destroyed like any other while `@device` is registered. Returns how many it destroyed. Called
|
|
762
|
+
* hourly by the `sandbox-destroy-inactive` cron.
|
|
670
763
|
*/
|
|
671
764
|
async destroyInactive(now) {
|
|
672
|
-
const p = this.providers.
|
|
673
|
-
|
|
674
|
-
return 0;
|
|
765
|
+
const p = this.providers.cloud();
|
|
766
|
+
const device = this.providers.forName(DEVICE_PROVIDER) !== undefined;
|
|
675
767
|
let n = 0;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
768
|
+
// Gone environments (deleted, or of a deleted org) on every provider that is here, `@device`
|
|
769
|
+
// too (its destroy calls never need the machine online); rows whose provider is missing wait.
|
|
770
|
+
const here = (eb) => eb.or([
|
|
771
|
+
...(p ? [this.cloudRow(eb)] : []),
|
|
772
|
+
...(device ? [eb("provider", "=", DEVICE_PROVIDER)] : []),
|
|
773
|
+
]);
|
|
774
|
+
const gone = (eb) => eb.and([here(eb), this.environmentGone(eb)]);
|
|
775
|
+
if (p || device) {
|
|
776
|
+
await inPool(await this.candidates(gone), SWEEP_CONCURRENCY, async ({ environment_id: id }) => {
|
|
777
|
+
const done = await this.tryLocked(id, async (lease) => {
|
|
778
|
+
const row = await this.db
|
|
779
|
+
.selectFrom("environment_instances")
|
|
780
|
+
.selectAll()
|
|
781
|
+
.where("environment_id", "=", id)
|
|
782
|
+
.where(gone)
|
|
783
|
+
.executeTakeFirst();
|
|
784
|
+
const owner = row && this.rowProvider(row);
|
|
785
|
+
if (!row || !owner)
|
|
786
|
+
return false;
|
|
787
|
+
await this.destroyRow(row, owner, lease, "environment");
|
|
788
|
+
return true;
|
|
789
|
+
});
|
|
790
|
+
if (done)
|
|
791
|
+
n++;
|
|
689
792
|
});
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
793
|
+
}
|
|
794
|
+
// inactivity destroys cloud instances only: `@device` instances are never swept
|
|
795
|
+
if (!p)
|
|
796
|
+
return n;
|
|
797
|
+
const due = this.instanceDue(now);
|
|
798
|
+
const idle = (eb) => eb.and([this.cloudRow(eb), due(eb)]);
|
|
694
799
|
await inPool(await this.candidates(idle), SWEEP_CONCURRENCY, async ({ environment_id: id }) => {
|
|
695
800
|
const done = await this.tryLocked(id, async (lease) => {
|
|
696
801
|
const row = await this.db
|
|
@@ -712,9 +817,11 @@ let SandboxService = class SandboxService {
|
|
|
712
817
|
/**
|
|
713
818
|
* Destroys the environment's instance, then its file system, then deletes its instance row
|
|
714
819
|
* (environment deletion, offboarding); nothing to do without a row. Waits for the exclusive lease
|
|
715
|
-
* (with intent, so running commands finish and new ones wait) up to 10 minutes. Without
|
|
716
|
-
* provider it
|
|
717
|
-
*
|
|
820
|
+
* (with intent, so running commands finish and new ones wait) up to 10 minutes. Without the
|
|
821
|
+
* environment's provider it throws and the row stays, marked `sweep_failed_at` (the job is
|
|
822
|
+
* retried, and the destruction sweep finishes a deleted environment once the provider is back).
|
|
823
|
+
* Files kept by another provider that is not loaded cannot be destroyed: it throws and the row
|
|
824
|
+
* stays.
|
|
718
825
|
*/
|
|
719
826
|
async destroyEnvironment(environmentId) {
|
|
720
827
|
const row = await this.db
|
|
@@ -724,10 +831,15 @@ let SandboxService = class SandboxService {
|
|
|
724
831
|
.executeTakeFirst();
|
|
725
832
|
if (!row)
|
|
726
833
|
return;
|
|
727
|
-
const p = this.
|
|
834
|
+
const p = await this.providerOf(environmentId);
|
|
728
835
|
if (!p) {
|
|
729
|
-
this.
|
|
730
|
-
|
|
836
|
+
await this.db
|
|
837
|
+
.updateTable("environment_instances")
|
|
838
|
+
.set({ sweep_failed_at: sql `now()` })
|
|
839
|
+
.where("environment_id", "=", environmentId)
|
|
840
|
+
.execute();
|
|
841
|
+
throw new Error(`cannot destroy environment ${environmentId}: its sandbox provider is not there; ` +
|
|
842
|
+
"the destruction sweep retries it");
|
|
731
843
|
}
|
|
732
844
|
const lease = await this.leases.acquireExclusive(environmentId, AbortSignal.timeout(EXCLUSIVE_WAIT_MS), undefined, { intent: true });
|
|
733
845
|
try {
|
|
@@ -750,7 +862,7 @@ let SandboxService = class SandboxService {
|
|
|
750
862
|
* creates a new one. Nothing to do without a row. Throws `sandbox.unavailable` without a provider.
|
|
751
863
|
*/
|
|
752
864
|
async resetFileSystem(environmentId) {
|
|
753
|
-
const p = this.require();
|
|
865
|
+
const p = await this.require(environmentId);
|
|
754
866
|
const lease = await this.leases.acquireExclusive(environmentId, AbortSignal.timeout(EXCLUSIVE_WAIT_MS), undefined, { intent: true });
|
|
755
867
|
try {
|
|
756
868
|
const row = await this.db
|
|
@@ -839,12 +951,6 @@ let SandboxService = class SandboxService {
|
|
|
839
951
|
}
|
|
840
952
|
await this.background.clearKill(cleared.map((r) => r.id));
|
|
841
953
|
}
|
|
842
|
-
require() {
|
|
843
|
-
const p = this.providers.current();
|
|
844
|
-
if (!p)
|
|
845
|
-
throw sandboxUnavailable();
|
|
846
|
-
return p;
|
|
847
|
-
}
|
|
848
954
|
/**
|
|
849
955
|
* A fenced write: only while the lease holds and the row's generation is still the one read
|
|
850
956
|
* under it (and incremented); otherwise {@link SandboxLockLostError}.
|
|
@@ -968,7 +1074,7 @@ let SandboxService = class SandboxService {
|
|
|
968
1074
|
// kept running (a stalled sweep) then fails its next write.
|
|
969
1075
|
await this.fenced(held, id, fence, {});
|
|
970
1076
|
if (row.fs_status === "lost")
|
|
971
|
-
throw environmentFilesLost();
|
|
1077
|
+
throw environmentFilesLost(p.plugin === DEVICE_PROVIDER);
|
|
972
1078
|
if (row.provider !== null &&
|
|
973
1079
|
row.provider !== p.plugin &&
|
|
974
1080
|
(row.fs_status !== "none" || row.handle !== null)) {
|
|
@@ -1080,7 +1186,7 @@ let SandboxService = class SandboxService {
|
|
|
1080
1186
|
provider: p.plugin,
|
|
1081
1187
|
});
|
|
1082
1188
|
await this.fenced(held, id, fence, { fs_status: "lost", status: "none" });
|
|
1083
|
-
throw environmentFilesLost();
|
|
1189
|
+
throw environmentFilesLost(p.plugin === DEVICE_PROVIDER);
|
|
1084
1190
|
}
|
|
1085
1191
|
let createdAt;
|
|
1086
1192
|
try {
|
|
@@ -1115,7 +1221,7 @@ let SandboxService = class SandboxService {
|
|
|
1115
1221
|
* usable; an abort is thrown.
|
|
1116
1222
|
*/
|
|
1117
1223
|
async dropUnrecordedFiles(id, provider, signal) {
|
|
1118
|
-
const old = this.providers.
|
|
1224
|
+
const old = this.providers.forName(provider)?.impl;
|
|
1119
1225
|
if (!old) {
|
|
1120
1226
|
this.log.warn("environment file system may have been made by a provider that is not loaded; it may leak", { environmentId: id, provider });
|
|
1121
1227
|
return;
|
|
@@ -1138,7 +1244,7 @@ let SandboxService = class SandboxService {
|
|
|
1138
1244
|
* false; otherwise warns and returns true (the record must be kept).
|
|
1139
1245
|
*/
|
|
1140
1246
|
async retire(id, provider, handle, signal) {
|
|
1141
|
-
const old = this.providers.
|
|
1247
|
+
const old = this.providers.forName(provider)?.impl;
|
|
1142
1248
|
if (old) {
|
|
1143
1249
|
try {
|
|
1144
1250
|
await old.destroy(handle, AbortSignal.any([signal, AbortSignal.timeout(SWEEP_OP_TIMEOUT_MS)]));
|
|
@@ -1175,7 +1281,7 @@ let SandboxService = class SandboxService {
|
|
|
1175
1281
|
}
|
|
1176
1282
|
else if (!row.handle && row.status === "creating") {
|
|
1177
1283
|
// the provider may have built it already: only the name finds it
|
|
1178
|
-
const impl = owner === p.plugin ? p.impl : this.providers.
|
|
1284
|
+
const impl = owner === p.plugin ? p.impl : this.providers.forName(owner)?.impl;
|
|
1179
1285
|
await impl?.destroyByName(environmentResourceName(id), signal);
|
|
1180
1286
|
}
|
|
1181
1287
|
if (row.prev_handle && row.prev_provider && !("prev_handle" in prev)) {
|
|
@@ -1194,7 +1300,7 @@ let SandboxService = class SandboxService {
|
|
|
1194
1300
|
async destroyFiles(row, p, signal, lenient = false) {
|
|
1195
1301
|
const id = row.environment_id;
|
|
1196
1302
|
const owner = row.provider ?? p.plugin;
|
|
1197
|
-
const impl = owner === p.plugin ? p.impl : this.providers.
|
|
1303
|
+
const impl = owner === p.plugin ? p.impl : this.providers.forName(owner)?.impl;
|
|
1198
1304
|
if (!impl) {
|
|
1199
1305
|
if (row.fs_handle === null)
|
|
1200
1306
|
return;
|
|
@@ -1256,6 +1362,17 @@ let SandboxService = class SandboxService {
|
|
|
1256
1362
|
eb.or([eb("last_used_at", "<", idleBefore), eb("running_since", "<", runningBefore)]),
|
|
1257
1363
|
]);
|
|
1258
1364
|
}
|
|
1365
|
+
/** The provider of an instance row: an internal one (`@device`) by name, else the cloud one. */
|
|
1366
|
+
rowProvider(row) {
|
|
1367
|
+
return providerForEnvironment(this.providers, row.provider);
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* A row the idle sweeps may touch: not on a kernel-internal provider (`@device` instances live on
|
|
1371
|
+
* people's machines and are never suspended or destroyed for inactivity).
|
|
1372
|
+
*/
|
|
1373
|
+
cloudRow(eb) {
|
|
1374
|
+
return eb.or([eb("provider", "is", null), eb("provider", "not like", "@%")]);
|
|
1375
|
+
}
|
|
1259
1376
|
/** The environment was deleted, or its org was: the instance and the files go. */
|
|
1260
1377
|
environmentGone(eb) {
|
|
1261
1378
|
return eb.or([
|
|
@@ -9,8 +9,8 @@ export declare const BASH_DESCRIPTION_MAX_CHARS = 120;
|
|
|
9
9
|
export declare const BASH_SPILL_CHARS = 30000;
|
|
10
10
|
/**
|
|
11
11
|
* 命令结果 → 给模型的文本。`budgetClamped`:运行的时限把超时收紧到了 `seconds`(Ruling M20),超时说明改为
|
|
12
|
-
* 指向 `runtime.maxRunSeconds`,而不是让模型加大 timeout
|
|
12
|
+
* 指向 `runtime.maxRunSeconds`,而不是让模型加大 timeout。`workspace`: the instance's workspace root.
|
|
13
13
|
*/
|
|
14
|
-
export declare function formatExec(r: ExecResult, seconds: number, budgetClamped?: boolean): string;
|
|
14
|
+
export declare function formatExec(r: ExecResult, seconds: number, budgetClamped?: boolean, workspace?: string): string;
|
|
15
15
|
/** 内置工具 `Bash`:在本对话的沙箱里执行命令。 */
|
|
16
16
|
export declare function sandboxBashTool(deps: SandboxToolDeps): BuiltinTool;
|