@capybara-hq/kernel 0.1.0-rc.8 → 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/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 -0
- package/dist/platform/ops/environments.ops.js +21 -3
- 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 +5 -2
- package/src/migrations/0093_connected_environments.sql +37 -0
- package/src/migrations/0094_environment_paths_idx.sql +7 -0
|
@@ -8,7 +8,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
-
import { isFileSystemGoneError, isSandboxGoneError, } 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;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ToolError } from "@capybara-hq/plugin-sdk";
|
|
1
|
+
import { DEFAULT_SANDBOX_ROOTS, ToolError, } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import { backgroundMaxSeconds, LAUNCH_EXEC_TIMEOUT_MS, launchScript, newTaskId, startedText, taskOutputPath, taskSidPath, } from "../../../background/launch.js";
|
|
3
3
|
import { KILL_SESSION_TIMEOUT_MS, killSession, SID_RE } from "../../../background/session.js";
|
|
4
4
|
import { clip, display, inSandbox, mapSandboxError, noSandboxError, runBudgetNote, runTimeLeftMs, sandboxBaseTimeoutMs, sandboxConversationOf, unavailableWhenNoSandbox, withNotices, } from "./common.js";
|
|
@@ -14,14 +14,14 @@ const SPEC = {
|
|
|
14
14
|
name: "Bash",
|
|
15
15
|
// Display only (the tool row; not in the fingerprint, never sent to the model).
|
|
16
16
|
keyArgs: ["command"],
|
|
17
|
-
description: "Run a shell command (bash) in this conversation's sandbox, an isolated
|
|
17
|
+
description: "Run a shell command (bash) in this conversation's sandbox, an isolated machine. " +
|
|
18
18
|
"Use it to run programs and scripts, install packages, process files and inspect the " +
|
|
19
|
-
"filesystem. The working directory is
|
|
19
|
+
"filesystem. The working directory is the workspace directory; files persist between calls in the same " +
|
|
20
20
|
"conversation, but background processes may not: when the sandbox is suspended after a period " +
|
|
21
|
-
"without use they can be stopped and
|
|
22
|
-
"Files the user attached to the conversation are copied to
|
|
23
|
-
"(the result names the paths), " +
|
|
24
|
-
"and skill files
|
|
21
|
+
"without use they can be stopped and the temporary files directory cleared, so keep results in " +
|
|
22
|
+
"files under the workspace directory. Files the user attached to the conversation are copied to " +
|
|
23
|
+
"attachments/<conversation>/ in the workspace directory (the result names the paths), " +
|
|
24
|
+
"and skill files to the skills directory (read-only). Each call runs in a fresh shell (no state such as " +
|
|
25
25
|
"cd or environment variables carries over). The result has stdout, stderr and the exit code; " +
|
|
26
26
|
"long output is saved and can be paged with ReadToolOutput. A command that runs longer than " +
|
|
27
27
|
"timeout milliseconds is stopped. Prefer Read, Write, Edit, Glob and " +
|
|
@@ -32,13 +32,13 @@ const SPEC = {
|
|
|
32
32
|
"You can use the run_in_background parameter to run the command in the background, which " +
|
|
33
33
|
"allows you to continue working while the command runs, e.g. a dev server, a long build or a " +
|
|
34
34
|
"test suite. The call returns at once with a task id and the command's output goes to a file " +
|
|
35
|
-
"under
|
|
35
|
+
"under .capybara/tasks in the workspace directory. Read the output with TaskOutput (or Read) and stop the " +
|
|
36
36
|
"task with TaskStop; you will be told when it finishes. You do not need to use '&' at the end " +
|
|
37
37
|
"of the command when using this parameter. In the background, timeout defaults to the " +
|
|
38
38
|
"deployment's background maximum (7200000 by default). A background task ends when its " +
|
|
39
39
|
"command exits, and processes it left running are stopped. A background task is lost if the " +
|
|
40
40
|
"sandbox is suspended (it is suspended after it has been running for a while, " +
|
|
41
|
-
"sandbox.limits.maxRunningMinutes), so keep its results in files under
|
|
41
|
+
"sandbox.limits.maxRunningMinutes), so keep its results in files under the workspace directory.",
|
|
42
42
|
inputSchema: {
|
|
43
43
|
type: "object",
|
|
44
44
|
properties: {
|
|
@@ -71,9 +71,9 @@ const SPEC = {
|
|
|
71
71
|
const msFor = (input, maxSeconds) => Math.min(input.timeout ?? BASH_DEFAULT_TIMEOUT_SECONDS * 1000, maxSeconds * 1000);
|
|
72
72
|
/**
|
|
73
73
|
* 命令结果 → 给模型的文本。`budgetClamped`:运行的时限把超时收紧到了 `seconds`(Ruling M20),超时说明改为
|
|
74
|
-
* 指向 `runtime.maxRunSeconds`,而不是让模型加大 timeout
|
|
74
|
+
* 指向 `runtime.maxRunSeconds`,而不是让模型加大 timeout。`workspace`: the instance's workspace root.
|
|
75
75
|
*/
|
|
76
|
-
export function formatExec(r, seconds, budgetClamped = false) {
|
|
76
|
+
export function formatExec(r, seconds, budgetClamped = false, workspace = DEFAULT_SANDBOX_ROOTS.workspace) {
|
|
77
77
|
const parts = [];
|
|
78
78
|
if (r.stdout !== "")
|
|
79
79
|
parts.push(r.stdout.replace(/\n$/, ""));
|
|
@@ -82,10 +82,10 @@ export function formatExec(r, seconds, budgetClamped = false) {
|
|
|
82
82
|
if (r.truncated)
|
|
83
83
|
parts.push(`[Output truncated: only the first ${MAX_OUTPUT_BYTES} bytes were kept.]`);
|
|
84
84
|
if (r.timedOut && budgetClamped) {
|
|
85
|
-
parts.push(`[timedOut: the command did not finish within ${seconds}s and was stopped. ${runBudgetNote(seconds)} The sandbox is still usable; split the work into shorter steps that save progress to files under
|
|
85
|
+
parts.push(`[timedOut: the command did not finish within ${seconds}s and was stopped. ${runBudgetNote(seconds)} The sandbox is still usable; split the work into shorter steps that save progress to files under ${workspace}, or run long jobs in the background and check on them in a later run.]`);
|
|
86
86
|
}
|
|
87
87
|
else if (r.timedOut) {
|
|
88
|
-
parts.push(`[timedOut: the command did not finish within ${seconds}s and was stopped. The sandbox is still usable; pass a larger timeout in milliseconds (up to the deployment maximum), or split the work into shorter steps that save progress to files under
|
|
88
|
+
parts.push(`[timedOut: the command did not finish within ${seconds}s and was stopped. The sandbox is still usable; pass a larger timeout in milliseconds (up to the deployment maximum), or split the work into shorter steps that save progress to files under ${workspace}.]`);
|
|
89
89
|
}
|
|
90
90
|
else if (r.exitCode === null) {
|
|
91
91
|
parts.push("[The command was stopped before it finished; its effects may be partial. Check the sandbox state before retrying.]");
|
|
@@ -136,8 +136,6 @@ async function runInBackground(deps, ctx, input) {
|
|
|
136
136
|
if (ctx.runId === undefined) {
|
|
137
137
|
throw new ToolError("Background tasks can only be started from an agent run; call Bash without run_in_background.");
|
|
138
138
|
}
|
|
139
|
-
if (!sandbox.available)
|
|
140
|
-
throw noSandboxError();
|
|
141
139
|
const currentInstance = sandbox.currentInstance.bind(sandbox);
|
|
142
140
|
const cfg = deps.config.get();
|
|
143
141
|
const maxMs = backgroundMaxSeconds(cfg) * 1000;
|
|
@@ -152,6 +150,8 @@ async function runInBackground(deps, ctx, input) {
|
|
|
152
150
|
catch (e) {
|
|
153
151
|
throw mapSandboxError(e);
|
|
154
152
|
}
|
|
153
|
+
if (!(await sandbox.availableFor(environmentId)))
|
|
154
|
+
throw noSandboxError();
|
|
155
155
|
const maxTasks = cfg.sandbox.background.maxTasks;
|
|
156
156
|
const id = newTaskId();
|
|
157
157
|
const description = input.description ?? clip(input.command, 120);
|
|
@@ -185,22 +185,26 @@ async function runInBackground(deps, ctx, input) {
|
|
|
185
185
|
await deps.eventLog.publishChanged(c, "background");
|
|
186
186
|
}
|
|
187
187
|
};
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
// the instance's workspace once the call entered it (the default until then)
|
|
189
|
+
let workspace = DEFAULT_SANDBOX_ROOTS.workspace;
|
|
190
|
+
const isLarge = Math.ceil(Buffer.byteLength(input.command, "utf8") / 3) * 4 > LARGE_COMMAND_BASE64_BYTES;
|
|
191
|
+
const largeText = () => isLarge
|
|
192
|
+
? ` A very large command (over about 96 KB after base64) cannot be started in the background; write it to a script under ${workspace} and run that instead.`
|
|
190
193
|
: "";
|
|
191
194
|
// set once the row has its sid: from then on the task runs and is tracked, whatever follows
|
|
192
195
|
let started = false;
|
|
193
196
|
let launch;
|
|
194
197
|
let notices;
|
|
195
198
|
try {
|
|
196
|
-
({ value: launch, notices } = await inSandbox(deps, ctx, async ({ h, p, signal }) => {
|
|
199
|
+
({ value: launch, notices } = await inSandbox(deps, ctx, async ({ h, roots, p, signal }) => {
|
|
200
|
+
workspace = roots.workspace;
|
|
197
201
|
// right after the launch a process may fork while it is killed: killSession repeats
|
|
198
202
|
const kill = (sid) => killSession(p, h, sid);
|
|
199
203
|
// the session id the launch wrote before printing it: the launch may have ended (aborted,
|
|
200
204
|
// timed out, thrown) after starting the command but before its output came back
|
|
201
205
|
const sidFile = async () => {
|
|
202
206
|
const f = await p
|
|
203
|
-
.readFile(h,
|
|
207
|
+
.readFile(h, `${roots.workspace}/${taskSidPath(id)}`, {
|
|
204
208
|
maxBytes: 64,
|
|
205
209
|
signal: AbortSignal.timeout(KILL_SESSION_TIMEOUT_MS),
|
|
206
210
|
})
|
|
@@ -212,7 +216,7 @@ async function runInBackground(deps, ctx, input) {
|
|
|
212
216
|
try {
|
|
213
217
|
r = await p.exec(h, {
|
|
214
218
|
command: launchScript(id, input.command, seconds),
|
|
215
|
-
cwd:
|
|
219
|
+
cwd: roots.workspace,
|
|
216
220
|
timeoutMs: LAUNCH_EXEC_TIMEOUT_MS,
|
|
217
221
|
maxOutputBytes: LAUNCH_MAX_OUTPUT_BYTES,
|
|
218
222
|
signal,
|
|
@@ -240,6 +244,7 @@ async function runInBackground(deps, ctx, input) {
|
|
|
240
244
|
const marked = await background.markStarted(id, {
|
|
241
245
|
sid,
|
|
242
246
|
sandboxCreatedAt: instance.createdAt,
|
|
247
|
+
outputPath: `${roots.workspace}/${taskOutputPath(id)}`,
|
|
243
248
|
});
|
|
244
249
|
if (!marked)
|
|
245
250
|
throw new Error("its task record was finished meanwhile");
|
|
@@ -275,15 +280,15 @@ async function runInBackground(deps, ctx, input) {
|
|
|
275
280
|
const why = e instanceof Error ? e.message : String(e);
|
|
276
281
|
await background.failLaunch(id, `launch failed: ${why}`).catch(() => undefined);
|
|
277
282
|
await announce();
|
|
278
|
-
if (
|
|
283
|
+
if (!isLarge || e instanceof ToolError)
|
|
279
284
|
throw e;
|
|
280
|
-
throw new ToolError(`The background task was not started: ${clip(why, 500).replace(/\.$/, "")}. Check the sandbox, or call Bash without run_in_background.${
|
|
285
|
+
throw new ToolError(`The background task was not started: ${clip(why, 500).replace(/\.$/, "")}. Check the sandbox, or call Bash without run_in_background.${largeText()}`);
|
|
281
286
|
}
|
|
282
287
|
}
|
|
283
288
|
if (launch.kind === "not_started") {
|
|
284
289
|
await background.failLaunch(id, launch.why);
|
|
285
290
|
await announce();
|
|
286
|
-
throw new ToolError(`The background task was not started: ${launch.why}. Check the sandbox, or call Bash without run_in_background.${
|
|
291
|
+
throw new ToolError(`The background task was not started: ${launch.why}. Check the sandbox, or call Bash without run_in_background.${largeText()}`);
|
|
287
292
|
}
|
|
288
293
|
if (launch.kind === "stopped") {
|
|
289
294
|
await announce();
|
|
@@ -303,7 +308,12 @@ async function runInBackground(deps, ctx, input) {
|
|
|
303
308
|
: "";
|
|
304
309
|
const shown = clip(input.command, 120);
|
|
305
310
|
return {
|
|
306
|
-
content: [
|
|
311
|
+
content: [
|
|
312
|
+
{
|
|
313
|
+
type: "text",
|
|
314
|
+
text: withNotices(notices, startedText(id, description, workspace) + lowered),
|
|
315
|
+
},
|
|
316
|
+
],
|
|
307
317
|
isError: false,
|
|
308
318
|
view: { kind: "command", description, exitCode: null, timedOut: false },
|
|
309
319
|
display: display(`后台运行命令:${shown}`, `Started in the background: ${shown}`),
|
|
@@ -338,12 +348,14 @@ export function sandboxBashTool(deps) {
|
|
|
338
348
|
// The run's deadline makes the command time out itself first (Ruling M20): counted after the
|
|
339
349
|
// sandbox is obtained, so the time spent waiting for the lock and resuming is deducted
|
|
340
350
|
let ms = askedMs;
|
|
341
|
-
|
|
351
|
+
let workspace = DEFAULT_SANDBOX_ROOTS.workspace;
|
|
352
|
+
const { value: r, notices } = await inSandbox(deps, ctx, ({ h, roots, p, signal }) => {
|
|
342
353
|
const left = Math.floor(runTimeLeftMs(ctx.runDeadline) / 1000) * 1000;
|
|
343
354
|
ms = Math.max(1000, Math.min(askedMs, left));
|
|
355
|
+
workspace = roots.workspace;
|
|
344
356
|
return p.exec(h, {
|
|
345
357
|
command,
|
|
346
|
-
cwd:
|
|
358
|
+
cwd: roots.workspace,
|
|
347
359
|
timeoutMs: ms,
|
|
348
360
|
maxOutputBytes: MAX_OUTPUT_BYTES,
|
|
349
361
|
signal,
|
|
@@ -355,7 +367,7 @@ export function sandboxBashTool(deps) {
|
|
|
355
367
|
const lowered = requested !== undefined && requested > maxMs
|
|
356
368
|
? `\n[timeout ${requested} ms is over the deployment maximum; it was lowered to ${maxMs} ms.]`
|
|
357
369
|
: "";
|
|
358
|
-
const text = formatExec(r, seconds, clamped) + lowered;
|
|
370
|
+
const text = formatExec(r, seconds, clamped, workspace) + lowered;
|
|
359
371
|
return {
|
|
360
372
|
content: [{ type: "text", text: withNotices(notices, text) }],
|
|
361
373
|
isError: r.timedOut || r.exitCode === null,
|