@openclaw/openshell-sandbox 2026.8.1-beta.2 → 2026.8.1
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/README.md +24 -1
- package/dist/index.js +473 -300
- package/openclaw.plugin.json +4 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Official NVIDIA OpenShell sandbox backend for OpenClaw.
|
|
4
4
|
|
|
5
|
-
This plugin lets OpenClaw use OpenShell-managed
|
|
5
|
+
This plugin lets OpenClaw use OpenShell-managed local or remote sandboxes with
|
|
6
|
+
SSH command execution. Choose `mirror` mode for a synchronized local workspace
|
|
7
|
+
or `remote` mode for a remote-canonical workspace.
|
|
8
|
+
|
|
9
|
+
Mirror operations sharing a workspace run sequentially so concurrent agent
|
|
10
|
+
turns cannot overwrite one another. Outbound attachments resolve against the
|
|
11
|
+
configured remote workspace, which defaults to `/sandbox`.
|
|
6
12
|
|
|
7
13
|
Configuring an OpenShell workspace requires OpenShell `v0.0.88` or newer. The
|
|
8
14
|
plugin supports OpenShell control-plane workspaces through
|
|
@@ -22,6 +28,23 @@ Restart the Gateway after installing or updating the plugin.
|
|
|
22
28
|
|
|
23
29
|
## Configure
|
|
24
30
|
|
|
31
|
+
Install and configure the OpenShell CLI before enabling the backend. As the same
|
|
32
|
+
operating system user that runs the OpenClaw Gateway, verify:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
openshell --version
|
|
36
|
+
openshell gateway list
|
|
37
|
+
openshell sandbox list
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Set `agents.defaults.sandbox.backend` to `"openshell"`, enable
|
|
41
|
+
`plugins.entries.openshell`, and restart the OpenClaw Gateway. OpenShell
|
|
42
|
+
settings belong under `plugins.entries.openshell.config`.
|
|
43
|
+
|
|
44
|
+
The optional `policy` setting must be the path to a readable OpenShell policy
|
|
45
|
+
YAML file on the Gateway host; it is not a policy name or ID. Use an absolute
|
|
46
|
+
path to avoid resolving it relative to an agent workspace.
|
|
47
|
+
|
|
25
48
|
Use the OpenShell docs for credentials, workspace mirroring, runtime selection, and troubleshooting:
|
|
26
49
|
|
|
27
50
|
- https://docs.openclaw.ai/gateway/openshell
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
-
import { buildRemoteWorkdirValidationCommand, buildValidatedExecRemoteCommand, createRemoteShellSandboxFsBridge, createSshSandboxSessionFromConfigText, createWritableRenameTargetResolver, disposeSshSandboxSession, registerSandboxBackend, resolvePreferredOpenClawTmpDir, runPluginCommandWithTimeout, runSshSandboxCommand, sanitizeEnvVars, shellEscape, withTempWorkspace } from "openclaw/plugin-sdk/sandbox";
|
|
2
|
+
import { buildRemoteCommand, buildRemoteWorkdirValidationCommand, buildValidatedExecRemoteCommand, createRemoteShellSandboxFsBridge, createSshSandboxSessionFromConfigText, createWritableRenameTargetResolver, disposeSshSandboxSession, prepareSshSandboxExec, registerSandboxBackend, resolvePreferredOpenClawTmpDir, runPluginCommandWithTimeout, runSshSandboxCommand, sanitizeEnvVars, shellEscape, withTempWorkspace } from "openclaw/plugin-sdk/sandbox";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import fsPromises from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { createDeferred, formatPluginConfigIssue, mapPluginConfigIssues } from "openclaw/plugin-sdk/extension-shared";
|
|
7
|
+
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
|
|
8
|
+
import { FsSafeError, canonicalPathFromExistingAncestor, extractErrorCode, movePathWithCopyFallback } from "openclaw/plugin-sdk/security-runtime";
|
|
6
9
|
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
7
10
|
import { buildPluginConfigSchema } from "openclaw/plugin-sdk/core";
|
|
8
|
-
import { formatPluginConfigIssue, mapPluginConfigIssues } from "openclaw/plugin-sdk/extension-shared";
|
|
9
11
|
import { MAX_TIMER_TIMEOUT_SECONDS } from "openclaw/plugin-sdk/number-runtime";
|
|
10
12
|
import { z } from "zod";
|
|
11
|
-
import { root } from "openclaw/plugin-sdk/file-access-runtime";
|
|
12
|
-
import { FsSafeError, isPathInside, movePathWithCopyFallback } from "openclaw/plugin-sdk/security-runtime";
|
|
13
|
+
import { isPathInside, root } from "openclaw/plugin-sdk/file-access-runtime";
|
|
13
14
|
import pLimit from "p-limit";
|
|
14
15
|
//#region extensions/openshell/src/cli.ts
|
|
15
16
|
function buildOpenShellBaseArgv(config) {
|
|
@@ -19,9 +20,6 @@ function buildOpenShellBaseArgv(config) {
|
|
|
19
20
|
if (config.workspace) argv.push("--workspace", config.workspace);
|
|
20
21
|
return argv;
|
|
21
22
|
}
|
|
22
|
-
function buildRemoteCommand(argv) {
|
|
23
|
-
return argv.map((entry) => shellEscape(entry)).join(" ");
|
|
24
|
-
}
|
|
25
23
|
function applyGatewayEndpointToSshConfig(params) {
|
|
26
24
|
const endpoint = params.gatewayEndpoint?.trim();
|
|
27
25
|
if (!endpoint) return params.configText;
|
|
@@ -75,6 +73,7 @@ function normalizeProviders(value) {
|
|
|
75
73
|
return providers;
|
|
76
74
|
}
|
|
77
75
|
const nonEmptyTrimmedString = (message) => z.string({ error: message }).trim().min(1, { error: message });
|
|
76
|
+
const openShellManagedRemotePath = (fieldName) => nonEmptyTrimmedString(`${fieldName} must be a non-empty string`).regex(/^\/(?:sandbox|agent)(?:\/|$)/, { error: (issue) => String(issue.input).startsWith("/") ? `OpenShell ${fieldName} must stay under /sandbox or /agent` : `OpenShell ${fieldName} must be absolute` }).refine((value) => isManagedOpenShellRemotePath(path.posix.normalize(value)), { error: `OpenShell ${fieldName} must stay under /sandbox or /agent` });
|
|
78
77
|
const openShellWorkspaceName = z.string({ error: "workspace must be a valid OpenShell workspace name" }).trim().min(1, { error: "workspace must be a valid OpenShell workspace name" }).max(19, { error: "workspace must be at most 19 characters" }).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { error: "workspace must contain lowercase alphanumeric characters or single hyphens and must not start or end with a hyphen" });
|
|
79
78
|
const OpenShellPluginConfigSchema = z.strictObject({
|
|
80
79
|
mode: z.enum(["mirror", "remote"], { error: "mode must be one of mirror, remote" }).optional(),
|
|
@@ -87,8 +86,8 @@ const OpenShellPluginConfigSchema = z.strictObject({
|
|
|
87
86
|
providers: z.array(z.string({ error: "providers must be an array of strings" }).trim().min(1, { error: "providers must be an array of strings" }), { error: "providers must be an array of strings" }).optional(),
|
|
88
87
|
gpu: z.boolean({ error: "gpu must be a boolean" }).optional(),
|
|
89
88
|
autoProviders: z.boolean({ error: "autoProviders must be a boolean" }).optional(),
|
|
90
|
-
remoteWorkspaceDir:
|
|
91
|
-
remoteAgentWorkspaceDir:
|
|
89
|
+
remoteWorkspaceDir: openShellManagedRemotePath("remoteWorkspaceDir").optional(),
|
|
90
|
+
remoteAgentWorkspaceDir: openShellManagedRemotePath("remoteAgentWorkspaceDir").optional(),
|
|
92
91
|
timeoutSeconds: z.number({ error: `timeoutSeconds must be a number between 1 and ${MAX_TIMER_TIMEOUT_SECONDS}` }).min(1, { error: "timeoutSeconds must be a number >= 1" }).max(MAX_TIMER_TIMEOUT_SECONDS, { error: `timeoutSeconds must be a number <= ${MAX_TIMER_TIMEOUT_SECONDS}` }).optional()
|
|
93
92
|
});
|
|
94
93
|
function isManagedOpenShellRemotePath(value) {
|
|
@@ -157,6 +156,24 @@ function resolveOpenShellPluginConfig(value) {
|
|
|
157
156
|
};
|
|
158
157
|
}
|
|
159
158
|
//#endregion
|
|
159
|
+
//#region extensions/openshell/src/workspace-roots.ts
|
|
160
|
+
function remotePathDepth(remote) {
|
|
161
|
+
return remote.split("/").filter(Boolean).length;
|
|
162
|
+
}
|
|
163
|
+
function workspaceRootOwnerOrder(owner) {
|
|
164
|
+
return owner === "workspace" ? 0 : 1;
|
|
165
|
+
}
|
|
166
|
+
function isOpenShellRemotePathInside(root, candidate) {
|
|
167
|
+
const relative = path.posix.relative(root, candidate);
|
|
168
|
+
return relative === "" || relative !== ".." && !relative.startsWith("../") && !path.posix.isAbsolute(relative);
|
|
169
|
+
}
|
|
170
|
+
function orderOpenShellWorkspaceRoots(roots) {
|
|
171
|
+
return roots.toSorted((a, b) => remotePathDepth(a.remote) - remotePathDepth(b.remote) || workspaceRootOwnerOrder(a.owner) - workspaceRootOwnerOrder(b.owner));
|
|
172
|
+
}
|
|
173
|
+
function resolveOpenShellWorkspaceRoot(roots, candidate) {
|
|
174
|
+
return roots.toSorted((a, b) => remotePathDepth(b.remote) - remotePathDepth(a.remote) || workspaceRootOwnerOrder(a.owner) - workspaceRootOwnerOrder(b.owner)).find((root) => isOpenShellRemotePathInside(root.remote, candidate));
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
160
177
|
//#region extensions/openshell/src/fs-bridge.ts
|
|
161
178
|
const MATERIALIZED_SKILLS_CONTAINER_PARTS = [
|
|
162
179
|
".openclaw",
|
|
@@ -352,27 +369,43 @@ var OpenShellFsBridge = class {
|
|
|
352
369
|
});
|
|
353
370
|
if (protectedSkillTarget) return protectedSkillTarget;
|
|
354
371
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
372
|
+
const containerMounts = [{
|
|
373
|
+
remote: workspaceContainerRoot,
|
|
374
|
+
owner: "workspace",
|
|
375
|
+
value: {
|
|
376
|
+
hostRoot: workspaceRoot,
|
|
377
|
+
writable: this.sandbox.workspaceAccess === "rw"
|
|
378
|
+
}
|
|
379
|
+
}, ...hasAgentMount ? [{
|
|
380
|
+
remote: agentContainerRoot,
|
|
381
|
+
owner: "agent",
|
|
382
|
+
value: {
|
|
383
|
+
hostRoot: agentRoot,
|
|
384
|
+
writable: this.sandbox.workspaceAccess === "rw"
|
|
385
|
+
}
|
|
386
|
+
}] : []];
|
|
387
|
+
const resolveContainerTarget = (containerPath) => {
|
|
388
|
+
const containerMount = resolveOpenShellWorkspaceRoot(containerMounts, containerPath);
|
|
389
|
+
if (!containerMount) return;
|
|
390
|
+
const relative = path.posix.relative(containerMount.remote, containerPath) || "";
|
|
391
|
+
const hostPath = relative ? path.resolve(containerMount.value.hostRoot, ...relative.split("/")) : containerMount.value.hostRoot;
|
|
392
|
+
if (!isPathInside(containerMount.value.hostRoot, hostPath)) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${input}`);
|
|
368
393
|
return {
|
|
369
|
-
hostPath
|
|
370
|
-
relativePath: relative ?
|
|
371
|
-
containerPath: relative ? path.posix.join(
|
|
372
|
-
mountHostRoot:
|
|
373
|
-
writable:
|
|
374
|
-
source:
|
|
394
|
+
hostPath,
|
|
395
|
+
relativePath: containerMount.owner === "agent" ? relative ? containerMount.remote + "/" + relative : containerMount.remote : relative,
|
|
396
|
+
containerPath: relative ? path.posix.join(containerMount.remote, relative) : containerMount.remote,
|
|
397
|
+
mountHostRoot: containerMount.value.hostRoot,
|
|
398
|
+
writable: containerMount.value.writable,
|
|
399
|
+
source: containerMount.owner
|
|
375
400
|
};
|
|
401
|
+
};
|
|
402
|
+
const containerCwd = params.cwd?.replace(/\\/g, "/");
|
|
403
|
+
const cwdMount = containerCwd ? resolveOpenShellWorkspaceRoot(containerMounts, containerCwd) : void 0;
|
|
404
|
+
const containerInput = path.posix.isAbsolute(input) ? input : cwdMount || !params.cwd ? path.posix.resolve(containerCwd ?? workspaceContainerRoot, input) : void 0;
|
|
405
|
+
if (containerInput) {
|
|
406
|
+
const target = resolveContainerTarget(containerInput);
|
|
407
|
+
if (target) return target;
|
|
408
|
+
if (cwdMount || containerMounts.some((mount) => input === mount.remote || input.startsWith(`${mount.remote}/`))) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${input}`);
|
|
376
409
|
}
|
|
377
410
|
const cwd = params.cwd ? path.resolve(params.cwd) : workspaceRoot;
|
|
378
411
|
const hostPath = path.isAbsolute(input) ? path.resolve(input) : path.resolve(cwd, input);
|
|
@@ -387,14 +420,7 @@ var OpenShellFsBridge = class {
|
|
|
387
420
|
}
|
|
388
421
|
if (isPathInside(workspaceRoot, hostPath)) {
|
|
389
422
|
const relative = path.relative(workspaceRoot, hostPath).split(path.sep).join(path.posix.sep);
|
|
390
|
-
return
|
|
391
|
-
hostPath,
|
|
392
|
-
relativePath: relative,
|
|
393
|
-
containerPath: relative ? path.posix.join(workspaceContainerRoot, relative) : workspaceContainerRoot,
|
|
394
|
-
mountHostRoot: workspaceRoot,
|
|
395
|
-
writable: this.sandbox.workspaceAccess === "rw",
|
|
396
|
-
source: "workspace"
|
|
397
|
-
};
|
|
423
|
+
return expectResolvedContainerTarget(resolveContainerTarget(path.posix.join(workspaceContainerRoot, relative)), input);
|
|
398
424
|
}
|
|
399
425
|
if (skillsRoot && this.sandbox.workspaceAccess === "rw" && isPathInside(skillsRoot, hostPath)) {
|
|
400
426
|
const relative = path.relative(skillsRoot, hostPath).split(path.sep).join(path.posix.sep);
|
|
@@ -409,18 +435,15 @@ var OpenShellFsBridge = class {
|
|
|
409
435
|
}
|
|
410
436
|
if (hasAgentMount && isPathInside(agentRoot, hostPath)) {
|
|
411
437
|
const relative = path.relative(agentRoot, hostPath).split(path.sep).join(path.posix.sep);
|
|
412
|
-
return
|
|
413
|
-
hostPath,
|
|
414
|
-
relativePath: relative ? `${agentContainerRoot}/${relative}` : agentContainerRoot,
|
|
415
|
-
containerPath: relative ? path.posix.join(agentContainerRoot, relative) : agentContainerRoot,
|
|
416
|
-
mountHostRoot: agentRoot,
|
|
417
|
-
writable: this.sandbox.workspaceAccess === "rw",
|
|
418
|
-
source: "agent"
|
|
419
|
-
};
|
|
438
|
+
return expectResolvedContainerTarget(resolveContainerTarget(path.posix.join(agentContainerRoot, relative)), input);
|
|
420
439
|
}
|
|
421
440
|
throw new Error(`Path escapes sandbox root (${workspaceRoot}): ${params.filePath}`);
|
|
422
441
|
}
|
|
423
442
|
};
|
|
443
|
+
function expectResolvedContainerTarget(target, input) {
|
|
444
|
+
if (!target) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${input}`);
|
|
445
|
+
return target;
|
|
446
|
+
}
|
|
424
447
|
async function mkdirLocalRootPath(params) {
|
|
425
448
|
const relativePath = relativeToRoot(params.target, params.hostPath);
|
|
426
449
|
if (!relativePath) return;
|
|
@@ -585,66 +608,65 @@ function createExcludeMatcher(excludeDirs) {
|
|
|
585
608
|
}
|
|
586
609
|
const runLimitedFs = pLimit(COPY_TREE_FS_CONCURRENCY);
|
|
587
610
|
async function lstatIfExists(targetPath) {
|
|
588
|
-
return await runLimitedFs(
|
|
611
|
+
return await runLimitedFs(fsPromises.lstat, targetPath).catch((error) => {
|
|
612
|
+
if (extractErrorCode(error) === "ENOENT") return null;
|
|
613
|
+
throw error;
|
|
614
|
+
});
|
|
589
615
|
}
|
|
590
|
-
async function
|
|
591
|
-
const stats = await runLimitedFs(async () => await fsPromises.lstat(params.sourcePath));
|
|
592
|
-
if (stats.isSymbolicLink()) return;
|
|
616
|
+
async function reconcileMirrorPath(params) {
|
|
593
617
|
const targetStats = await lstatIfExists(params.targetPath);
|
|
594
|
-
if (
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
618
|
+
if (targetStats && !targetStats.isDirectory() && !targetStats.isFile()) return true;
|
|
619
|
+
const sourceStats = params.sourcePath ? await runLimitedFs(fsPromises.lstat, params.sourcePath) : null;
|
|
620
|
+
const sourceDir = sourceStats?.isDirectory() ? params.sourcePath : void 0;
|
|
621
|
+
const sourceFile = sourceStats?.isFile() ? params.sourcePath : void 0;
|
|
622
|
+
if (!params.replace && !sourceDir && !sourceFile) return false;
|
|
623
|
+
if (sourceDir || targetStats?.isDirectory()) {
|
|
624
|
+
if (targetStats && !targetStats.isDirectory()) await runLimitedFs(fsPromises.rm, params.targetPath, { force: true });
|
|
625
|
+
const preservedEntries = await reconcileMirrorDirectory({
|
|
626
|
+
sourceDir,
|
|
627
|
+
targetDir: params.targetPath,
|
|
628
|
+
replace: params.replace
|
|
629
|
+
});
|
|
630
|
+
if (sourceDir || preservedEntries) return preservedEntries;
|
|
631
|
+
await runLimitedFs(fsPromises.rmdir, params.targetPath);
|
|
632
|
+
} else if (targetStats) await runLimitedFs(fsPromises.rm, params.targetPath, { force: true });
|
|
633
|
+
if (sourceFile) await runLimitedFs(fsPromises.copyFile, sourceFile, params.targetPath);
|
|
634
|
+
return false;
|
|
611
635
|
}
|
|
612
|
-
async function
|
|
636
|
+
async function reconcileMirrorDirectory(params) {
|
|
637
|
+
const { sourceDir } = params;
|
|
613
638
|
const isExcluded = createExcludeMatcher(params.excludeDirs);
|
|
614
|
-
await fsPromises.mkdir
|
|
615
|
-
const
|
|
616
|
-
await
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
if (isExcluded(entry)) continue;
|
|
627
|
-
await copyTreeWithoutSymlinks({
|
|
628
|
-
sourcePath: path.join(params.sourceDir, entry),
|
|
629
|
-
targetPath: path.join(params.targetDir, entry),
|
|
630
|
-
preserveTargetSymlinks: true
|
|
631
|
-
});
|
|
639
|
+
await runLimitedFs(fsPromises.mkdir, params.targetDir, { recursive: true });
|
|
640
|
+
const sourceEntries = new Set(sourceDir ? await runLimitedFs(async () => await fsPromises.readdir(sourceDir)) : []);
|
|
641
|
+
const targetEntries = params.replace ? await runLimitedFs(async () => await fsPromises.readdir(params.targetDir)) : [];
|
|
642
|
+
const results = await Promise.allSettled([.../* @__PURE__ */ new Set([...sourceEntries, ...targetEntries])].filter((entry) => !isExcluded(entry)).map((entry) => reconcileMirrorPath({
|
|
643
|
+
sourcePath: sourceDir && sourceEntries.has(entry) ? path.join(sourceDir, entry) : void 0,
|
|
644
|
+
targetPath: path.join(params.targetDir, entry),
|
|
645
|
+
replace: params.replace
|
|
646
|
+
})));
|
|
647
|
+
let preservedEntries = false;
|
|
648
|
+
for (const result of results) {
|
|
649
|
+
if (result.status === "rejected") throw result.reason;
|
|
650
|
+
preservedEntries ||= result.value;
|
|
632
651
|
}
|
|
652
|
+
return preservedEntries;
|
|
653
|
+
}
|
|
654
|
+
async function replaceDirectoryContents(params) {
|
|
655
|
+
await reconcileMirrorDirectory({
|
|
656
|
+
...params,
|
|
657
|
+
replace: true
|
|
658
|
+
});
|
|
633
659
|
}
|
|
634
660
|
async function stageDirectoryContents(params) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (isExcluded(entry)) continue;
|
|
640
|
-
await copyTreeWithoutSymlinks({
|
|
641
|
-
sourcePath: path.join(params.sourceDir, entry),
|
|
642
|
-
targetPath: path.join(params.targetDir, entry)
|
|
643
|
-
});
|
|
644
|
-
}
|
|
661
|
+
await reconcileMirrorDirectory({
|
|
662
|
+
...params,
|
|
663
|
+
replace: false
|
|
664
|
+
});
|
|
645
665
|
}
|
|
646
666
|
//#endregion
|
|
647
667
|
//#region extensions/openshell/src/backend.ts
|
|
668
|
+
const openShellWorkspaceOperations = new KeyedAsyncQueue();
|
|
669
|
+
let openShellDetachedCreateSupport;
|
|
648
670
|
const MATERIALIZED_SKILLS_REMOTE_PARTS = [".openclaw", "sandbox-skills"];
|
|
649
671
|
function buildOpenShellDirectoryUploadArgs(params) {
|
|
650
672
|
return [
|
|
@@ -653,9 +675,10 @@ function buildOpenShellDirectoryUploadArgs(params) {
|
|
|
653
675
|
"--no-git-ignore",
|
|
654
676
|
params.sandboxName,
|
|
655
677
|
params.localPath,
|
|
656
|
-
normalizeRemotePath(params.remotePath)
|
|
678
|
+
`${normalizeRemotePath(params.remotePath)}/`
|
|
657
679
|
];
|
|
658
680
|
}
|
|
681
|
+
const REMOTE_MANAGED_ROOTS_EMPTY_SCRIPT = "for root in \"$@\"; do if [ -d \"$root\" ] && [ -n \"$(ls -A \"$root\")\" ]; then printf \"1\\n\"; exit 0; fi; done; printf \"0\\n\"";
|
|
659
682
|
const PINNED_REMOTE_PATH_MUTATION_SCRIPT = [
|
|
660
683
|
"set -eu",
|
|
661
684
|
"die() { echo \"$1\" >&2; exit 1; }",
|
|
@@ -761,6 +784,7 @@ const ENSURE_OPEN_SHELL_REMOTE_REAL_DIRECTORY_SCRIPT = [
|
|
|
761
784
|
"set -e",
|
|
762
785
|
"target=\"$1\"",
|
|
763
786
|
"root=\"${2:-$1}\"",
|
|
787
|
+
"replace_blocking=\"${3:-0}\"",
|
|
764
788
|
"case \"$target\" in /*) ;; *) echo \"remote directory must be absolute: $target\" >&2; exit 1 ;; esac",
|
|
765
789
|
"case \"$root\" in /*) ;; *) echo \"remote root must be absolute: $root\" >&2; exit 1 ;; esac",
|
|
766
790
|
"target=\"${target%/}\"",
|
|
@@ -788,9 +812,15 @@ const ENSURE_OPEN_SHELL_REMOTE_REAL_DIRECTORY_SCRIPT = [
|
|
|
788
812
|
" if [ \"$part\" = \"$relative\" ]; then relative=\"\"; else relative=\"${relative#*/}\"; fi",
|
|
789
813
|
" [ -n \"$part\" ] || continue",
|
|
790
814
|
" if [ \"$current\" = \"/\" ]; then next=\"/$part\"; else next=\"$current/$part\"; fi",
|
|
791
|
-
" if [ -L \"$next\" ]; then
|
|
815
|
+
" if [ -L \"$next\" ]; then",
|
|
816
|
+
" if [ \"$replace_blocking\" != \"1\" ]; then echo \"unsafe remote directory symlink: $next\" >&2; exit 1; fi",
|
|
817
|
+
" rm -rf -- \"$next\"",
|
|
818
|
+
" elif [ -e \"$next\" ] && [ ! -d \"$next\" ]; then",
|
|
819
|
+
" if [ \"$replace_blocking\" != \"1\" ]; then echo \"unsafe remote directory component: $next\" >&2; exit 1; fi",
|
|
820
|
+
" rm -rf -- \"$next\"",
|
|
821
|
+
" fi",
|
|
792
822
|
" if [ -e \"$next\" ]; then",
|
|
793
|
-
"
|
|
823
|
+
" [ -d \"$next\" ] || { echo \"unsafe remote directory component: $next\" >&2; exit 1; }",
|
|
794
824
|
" else",
|
|
795
825
|
" mkdir -- \"$next\"",
|
|
796
826
|
" fi",
|
|
@@ -818,20 +848,22 @@ function createOpenShellSandboxBackendManager(params) {
|
|
|
818
848
|
args: [
|
|
819
849
|
"sandbox",
|
|
820
850
|
"get",
|
|
821
|
-
entry.containerName
|
|
851
|
+
entry.containerName,
|
|
852
|
+
"--output",
|
|
853
|
+
"json"
|
|
822
854
|
]
|
|
823
855
|
});
|
|
824
856
|
const configuredSource = execContext.config.from;
|
|
825
857
|
return {
|
|
826
|
-
running: result.code === 0,
|
|
858
|
+
running: result.code === 0 && parseOpenShellSandboxPhase(result.stdout) === "Ready",
|
|
827
859
|
actualConfigLabel: entry.image,
|
|
828
860
|
configLabelMatch: entry.image === configuredSource
|
|
829
861
|
};
|
|
830
862
|
},
|
|
831
|
-
async removeRuntime({ entry }) {
|
|
832
|
-
await runOpenShellCli({
|
|
863
|
+
async removeRuntime({ entry, config }) {
|
|
864
|
+
const result = await runOpenShellCli({
|
|
833
865
|
context: {
|
|
834
|
-
config: params.pluginConfig,
|
|
866
|
+
config: resolveOpenShellPluginConfigFromConfig(config, params.pluginConfig),
|
|
835
867
|
sandboxName: entry.containerName
|
|
836
868
|
},
|
|
837
869
|
args: [
|
|
@@ -840,6 +872,7 @@ function createOpenShellSandboxBackendManager(params) {
|
|
|
840
872
|
entry.containerName
|
|
841
873
|
]
|
|
842
874
|
});
|
|
875
|
+
if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox delete failed");
|
|
843
876
|
}
|
|
844
877
|
};
|
|
845
878
|
}
|
|
@@ -854,69 +887,25 @@ async function createOpenShellSandboxBackend(params) {
|
|
|
854
887
|
config: params.pluginConfig,
|
|
855
888
|
sandboxName
|
|
856
889
|
};
|
|
857
|
-
|
|
890
|
+
return new OpenShellSandboxBackendImpl({
|
|
858
891
|
createParams: params.createParams,
|
|
859
892
|
execContext,
|
|
860
893
|
legacyRuntimeAdopted: resolvedSandboxName.legacyRuntimeAdopted,
|
|
861
894
|
remoteWorkspaceDir: params.pluginConfig.remoteWorkspaceDir,
|
|
862
895
|
remoteAgentWorkspaceDir: params.pluginConfig.remoteAgentWorkspaceDir
|
|
863
|
-
});
|
|
864
|
-
return {
|
|
865
|
-
id: "openshell",
|
|
866
|
-
runtimeId: sandboxName,
|
|
867
|
-
runtimeLabel: sandboxName,
|
|
868
|
-
workdir: params.pluginConfig.remoteWorkspaceDir,
|
|
869
|
-
env: params.createParams.cfg.docker.env,
|
|
870
|
-
mode: params.pluginConfig.mode,
|
|
871
|
-
configLabel: params.pluginConfig.from,
|
|
872
|
-
configLabelKind: "Source",
|
|
873
|
-
workdirValidation: "backend",
|
|
874
|
-
validateWorkdir: async (workdir) => await impl.validateWorkdir(workdir),
|
|
875
|
-
discardPreparedWorkdir: (workdir) => impl.discardPreparedWorkdir(workdir),
|
|
876
|
-
workdirRoots: [params.pluginConfig.remoteWorkspaceDir, params.pluginConfig.remoteAgentWorkspaceDir],
|
|
877
|
-
buildExecSpec: async ({ command, workdir, env, usePty }) => {
|
|
878
|
-
const pending = await impl.prepareExec({
|
|
879
|
-
command,
|
|
880
|
-
workdir,
|
|
881
|
-
env,
|
|
882
|
-
usePty
|
|
883
|
-
});
|
|
884
|
-
return {
|
|
885
|
-
argv: pending.argv,
|
|
886
|
-
env: buildOpenShellSshExecEnv(),
|
|
887
|
-
stdinMode: "pipe-open",
|
|
888
|
-
finalizeToken: pending.token
|
|
889
|
-
};
|
|
890
|
-
},
|
|
891
|
-
finalizeExec: async ({ token }) => {
|
|
892
|
-
await impl.finalizeExec(token);
|
|
893
|
-
},
|
|
894
|
-
runShellCommand: async (command) => await impl.runRemoteShellScript(command),
|
|
895
|
-
createFsBridge: ({ sandbox }) => params.pluginConfig.mode === "remote" ? createRemoteShellSandboxFsBridge({
|
|
896
|
-
sandbox,
|
|
897
|
-
runtime: impl.asHandle()
|
|
898
|
-
}) : createOpenShellFsBridge({
|
|
899
|
-
sandbox,
|
|
900
|
-
backend: impl.asHandle()
|
|
901
|
-
}),
|
|
902
|
-
remoteWorkspaceDir: params.pluginConfig.remoteWorkspaceDir,
|
|
903
|
-
remoteAgentWorkspaceDir: params.pluginConfig.remoteAgentWorkspaceDir,
|
|
904
|
-
runRemoteShellScript: async (command) => await impl.runRemoteShellScript(command),
|
|
905
|
-
mkdirpRemotePath: async (remotePath, signal) => await impl.mkdirpRemotePath(remotePath, signal),
|
|
906
|
-
removeRemotePath: async (remotePath, removeParams) => await impl.removeRemotePath(remotePath, removeParams),
|
|
907
|
-
renameRemotePath: async (fromRemotePath, toRemotePath, signal) => await impl.renameRemotePath(fromRemotePath, toRemotePath, signal),
|
|
908
|
-
syncLocalPathToRemote: async (localPath, remotePath) => await impl.syncLocalPathToRemote(localPath, remotePath)
|
|
909
|
-
};
|
|
896
|
+
}).asHandle();
|
|
910
897
|
}
|
|
911
898
|
var OpenShellSandboxBackendImpl = class {
|
|
912
899
|
constructor(params) {
|
|
913
900
|
this.params = params;
|
|
901
|
+
this.handle = null;
|
|
914
902
|
this.ensurePromise = null;
|
|
915
|
-
this.preparedRemoteWorkspaceForNextExec = null;
|
|
916
903
|
this.remoteSeedPending = false;
|
|
917
904
|
}
|
|
918
905
|
asHandle() {
|
|
919
|
-
return
|
|
906
|
+
if (this.handle) return this.handle;
|
|
907
|
+
const runRemoteShellScript = (command) => this.params.execContext.config.mode === "mirror" ? this.runWorkspaceOperation(() => this.runRemoteShellScript(command), command.signal) : this.runRemoteShellScript(command);
|
|
908
|
+
const handle = {
|
|
920
909
|
id: "openshell",
|
|
921
910
|
runtimeId: this.params.execContext.sandboxName,
|
|
922
911
|
runtimeLabel: this.params.execContext.sandboxName,
|
|
@@ -927,7 +916,6 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
927
916
|
configLabelKind: "Source",
|
|
928
917
|
workdirValidation: "backend",
|
|
929
918
|
validateWorkdir: async (workdir) => await this.validateWorkdir(workdir),
|
|
930
|
-
discardPreparedWorkdir: (workdir) => this.discardPreparedWorkdir(workdir),
|
|
931
919
|
workdirRoots: [this.params.remoteWorkspaceDir, this.params.remoteAgentWorkspaceDir],
|
|
932
920
|
remoteWorkspaceDir: this.params.remoteWorkspaceDir,
|
|
933
921
|
remoteAgentWorkspaceDir: this.params.remoteAgentWorkspaceDir,
|
|
@@ -948,124 +936,189 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
948
936
|
finalizeExec: async ({ token }) => {
|
|
949
937
|
await this.finalizeExec(token);
|
|
950
938
|
},
|
|
951
|
-
runShellCommand:
|
|
939
|
+
runShellCommand: runRemoteShellScript,
|
|
952
940
|
createFsBridge: ({ sandbox }) => this.params.execContext.config.mode === "remote" ? createRemoteShellSandboxFsBridge({
|
|
953
941
|
sandbox,
|
|
954
|
-
runtime:
|
|
955
|
-
}) :
|
|
956
|
-
|
|
957
|
-
backend: this.asHandle()
|
|
958
|
-
}),
|
|
959
|
-
runRemoteShellScript: async (command) => await this.runRemoteShellScript(command),
|
|
960
|
-
mkdirpRemotePath: async (remotePath, signal) => await this.mkdirpRemotePath(remotePath, signal),
|
|
961
|
-
removeRemotePath: async (remotePath, removeParams) => await this.removeRemotePath(remotePath, removeParams),
|
|
962
|
-
renameRemotePath: async (fromRemotePath, toRemotePath, signal) => await this.renameRemotePath(fromRemotePath, toRemotePath, signal),
|
|
963
|
-
syncLocalPathToRemote: async (localPath, remotePath) => await this.syncLocalPathToRemote(localPath, remotePath)
|
|
942
|
+
runtime: handle
|
|
943
|
+
}) : this.createMirrorFsBridge(sandbox),
|
|
944
|
+
runRemoteShellScript
|
|
964
945
|
};
|
|
946
|
+
this.handle = handle;
|
|
947
|
+
return handle;
|
|
948
|
+
}
|
|
949
|
+
createMirrorFsBridge(sandbox) {
|
|
950
|
+
const bridge = createOpenShellFsBridge({
|
|
951
|
+
sandbox,
|
|
952
|
+
backend: {
|
|
953
|
+
remoteAgentWorkspaceDir: this.params.remoteAgentWorkspaceDir,
|
|
954
|
+
mkdirpRemotePath: (remotePath, signal) => this.mkdirpRemotePath(remotePath, signal),
|
|
955
|
+
removeRemotePath: (remotePath, params) => this.removeRemotePath(remotePath, params),
|
|
956
|
+
renameRemotePath: (from, to, signal) => this.renameRemotePath(from, to, signal),
|
|
957
|
+
syncLocalPathToRemote: (localPath, remotePath) => this.syncLocalPathToRemote(localPath, remotePath)
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
return {
|
|
961
|
+
resolvePath: (params) => bridge.resolvePath(params),
|
|
962
|
+
readFile: (params) => this.runWorkspaceOperation(() => bridge.readFile(params), params.signal),
|
|
963
|
+
writeFile: (params) => this.runWorkspaceOperation(() => bridge.writeFile(params), params.signal),
|
|
964
|
+
createFileExclusive: (params) => this.runWorkspaceOperation(() => bridge.createFileExclusive(params), params.signal),
|
|
965
|
+
mkdirp: (params) => this.runWorkspaceOperation(() => bridge.mkdirp(params), params.signal),
|
|
966
|
+
remove: (params) => this.runWorkspaceOperation(() => bridge.remove(params), params.signal),
|
|
967
|
+
rename: (params) => this.runWorkspaceOperation(() => bridge.rename(params), params.signal),
|
|
968
|
+
stat: (params) => this.runWorkspaceOperation(() => bridge.stat(params), params.signal)
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
async runWorkspaceOperation(operation, signal) {
|
|
972
|
+
signal?.throwIfAborted();
|
|
973
|
+
const lease = await this.acquireWorkspaceLease();
|
|
974
|
+
try {
|
|
975
|
+
signal?.throwIfAborted();
|
|
976
|
+
return await operation();
|
|
977
|
+
} finally {
|
|
978
|
+
lease.release();
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
async acquireWorkspaceLease() {
|
|
982
|
+
const { config, sandboxName } = this.params.execContext;
|
|
983
|
+
const keys = [`host:${await canonicalPathFromExistingAncestor(this.params.createParams.workspaceDir)}`, `runtime:${JSON.stringify([
|
|
984
|
+
config.gatewayEndpoint ?? "",
|
|
985
|
+
config.gateway ?? "",
|
|
986
|
+
config.workspace ?? process.env.OPENSHELL_WORKSPACE ?? "",
|
|
987
|
+
sandboxName
|
|
988
|
+
])}`].toSorted();
|
|
989
|
+
const releases = [];
|
|
990
|
+
for (const key of keys) {
|
|
991
|
+
const acquired = createDeferred();
|
|
992
|
+
openShellWorkspaceOperations.enqueue(key, () => new Promise(acquired.resolve));
|
|
993
|
+
releases.push(await acquired.promise);
|
|
994
|
+
}
|
|
995
|
+
return { release: () => {
|
|
996
|
+
for (const release of releases.toReversed()) release();
|
|
997
|
+
} };
|
|
965
998
|
}
|
|
966
999
|
async prepareExec(params) {
|
|
967
1000
|
const remoteWorkdir = params.workdir ?? this.params.remoteWorkspaceDir;
|
|
968
|
-
const preparedWorkspace = this.consumePreparedRemoteWorkspaceForNextExec(remoteWorkdir);
|
|
969
1001
|
const remoteCommand = buildValidatedExecRemoteCommand({
|
|
970
1002
|
command: params.command,
|
|
971
1003
|
workdir: remoteWorkdir,
|
|
972
|
-
env:
|
|
1004
|
+
env: {}
|
|
973
1005
|
});
|
|
974
|
-
|
|
975
|
-
const sshSession = await createOpenShellSshSession({ context: this.params.execContext });
|
|
976
|
-
return {
|
|
977
|
-
argv: [
|
|
978
|
-
"ssh",
|
|
979
|
-
"-F",
|
|
980
|
-
sshSession.configPath,
|
|
981
|
-
...params.usePty ? [
|
|
982
|
-
"-tt",
|
|
983
|
-
"-o",
|
|
984
|
-
"RequestTTY=force",
|
|
985
|
-
"-o",
|
|
986
|
-
"SetEnv=TERM=xterm-256color"
|
|
987
|
-
] : [
|
|
988
|
-
"-T",
|
|
989
|
-
"-o",
|
|
990
|
-
"RequestTTY=no"
|
|
991
|
-
],
|
|
992
|
-
sshSession.host,
|
|
993
|
-
remoteCommand
|
|
994
|
-
],
|
|
995
|
-
token: { sshSession }
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
async validateWorkdir(workdir) {
|
|
999
|
-
const preparedWorkspace = this.prepareRemoteWorkspaceForExec();
|
|
1000
|
-
const reusablePreparation = {
|
|
1001
|
-
workdir,
|
|
1002
|
-
promise: preparedWorkspace
|
|
1003
|
-
};
|
|
1004
|
-
this.preparedRemoteWorkspaceForNextExec = reusablePreparation;
|
|
1006
|
+
const workspaceLease = this.params.execContext.config.mode === "mirror" ? await this.acquireWorkspaceLease() : void 0;
|
|
1005
1007
|
try {
|
|
1006
|
-
await
|
|
1008
|
+
await this.ensureSandboxExists();
|
|
1009
|
+
if (workspaceLease) {
|
|
1010
|
+
await this.syncWorkspaceToRemote();
|
|
1011
|
+
this.remoteSeedPending = false;
|
|
1012
|
+
}
|
|
1007
1013
|
const sshSession = await createOpenShellSshSession({ context: this.params.execContext });
|
|
1008
1014
|
try {
|
|
1009
|
-
const
|
|
1015
|
+
const prepared = await prepareSshSandboxExec({
|
|
1010
1016
|
session: sshSession,
|
|
1011
|
-
remoteCommand
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
}),
|
|
1015
|
-
allowFailure: true
|
|
1017
|
+
remoteCommand,
|
|
1018
|
+
env: params.env,
|
|
1019
|
+
tty: params.usePty
|
|
1016
1020
|
});
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1021
|
+
return {
|
|
1022
|
+
argv: prepared.argv,
|
|
1023
|
+
token: {
|
|
1024
|
+
workspaceLease,
|
|
1025
|
+
cleanup: async () => {
|
|
1026
|
+
try {
|
|
1027
|
+
await prepared.cleanup();
|
|
1028
|
+
} finally {
|
|
1029
|
+
await disposeSshSandboxSession(sshSession);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
} catch (error) {
|
|
1024
1035
|
await disposeSshSandboxSession(sshSession);
|
|
1036
|
+
throw error;
|
|
1025
1037
|
}
|
|
1026
1038
|
} catch (error) {
|
|
1027
|
-
|
|
1039
|
+
workspaceLease?.release();
|
|
1028
1040
|
throw error;
|
|
1029
1041
|
}
|
|
1030
1042
|
}
|
|
1043
|
+
async validateWorkdir(workdir) {
|
|
1044
|
+
if (this.params.execContext.config.mode === "mirror") return await this.runWorkspaceOperation(() => this.validateMirrorWorkdir(workdir));
|
|
1045
|
+
await this.ensureSandboxExists();
|
|
1046
|
+
const sshSession = await createOpenShellSshSession({ context: this.params.execContext });
|
|
1047
|
+
try {
|
|
1048
|
+
const result = await runSshSandboxCommand({
|
|
1049
|
+
session: sshSession,
|
|
1050
|
+
remoteCommand: buildRemoteWorkdirValidationCommand({
|
|
1051
|
+
workdir,
|
|
1052
|
+
root: this.resolveWorkdirValidationRoot(workdir)
|
|
1053
|
+
}),
|
|
1054
|
+
allowFailure: true
|
|
1055
|
+
});
|
|
1056
|
+
return result.code === 0 ? result.stdout.toString("utf8").trim() || null : null;
|
|
1057
|
+
} finally {
|
|
1058
|
+
await disposeSshSandboxSession(sshSession);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1031
1061
|
resolveWorkdirValidationRoot(workdir) {
|
|
1032
1062
|
try {
|
|
1033
1063
|
const normalized = normalizeRemotePath(workdir);
|
|
1034
|
-
return [
|
|
1064
|
+
return resolveOpenShellWorkspaceRoot([{
|
|
1065
|
+
remote: normalizeRemotePath(this.params.remoteWorkspaceDir),
|
|
1066
|
+
owner: "workspace",
|
|
1067
|
+
value: void 0
|
|
1068
|
+
}, {
|
|
1069
|
+
remote: normalizeRemotePath(this.params.remoteAgentWorkspaceDir),
|
|
1070
|
+
owner: "agent",
|
|
1071
|
+
value: void 0
|
|
1072
|
+
}], normalized)?.remote ?? this.params.remoteWorkspaceDir;
|
|
1035
1073
|
} catch {
|
|
1036
1074
|
return this.params.remoteWorkspaceDir;
|
|
1037
1075
|
}
|
|
1038
1076
|
}
|
|
1039
|
-
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
return;
|
|
1077
|
+
async validateMirrorWorkdir(workdir) {
|
|
1078
|
+
const normalized = normalizeRemotePath(workdir);
|
|
1079
|
+
const roots = this.workspaceUploadRoots();
|
|
1080
|
+
if (!roots.some((root) => isOpenShellRemotePathInside(root.remote, normalized))) return null;
|
|
1081
|
+
const { cfg, skillsWorkspaceDir } = this.params.createParams;
|
|
1082
|
+
if (cfg.workspaceAccess === "rw" && skillsWorkspaceDir) roots.push({
|
|
1083
|
+
remote: resolveRemoteMaterializedSkillsWorkspaceDir(this.params.remoteWorkspaceDir),
|
|
1084
|
+
local: skillsWorkspaceDir,
|
|
1085
|
+
owner: "workspace"
|
|
1086
|
+
});
|
|
1087
|
+
let createdAncestor = false;
|
|
1088
|
+
for (const root of roots.toReversed()) {
|
|
1089
|
+
if (!isOpenShellRemotePathInside(root.remote, normalized)) {
|
|
1090
|
+
createdAncestor ||= isOpenShellRemotePathInside(normalized, root.remote);
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
if (createdAncestor) return normalized;
|
|
1094
|
+
const relative = path.posix.relative(root.remote, normalized);
|
|
1095
|
+
if (!relative) return normalized;
|
|
1096
|
+
const parts = relative.split("/");
|
|
1097
|
+
if (DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS.some((excluded) => excluded === normalizeLowercaseStringOrEmpty(parts[0]))) return null;
|
|
1098
|
+
let local = root.local;
|
|
1099
|
+
for (const part of root.local === skillsWorkspaceDir ? ["", ...parts] : parts) {
|
|
1100
|
+
local = path.join(local, part);
|
|
1101
|
+
if (!(await fsPromises.lstat(local).catch(() => null))?.isDirectory()) return null;
|
|
1102
|
+
}
|
|
1103
|
+
return normalized;
|
|
1056
1104
|
}
|
|
1057
|
-
|
|
1105
|
+
return createdAncestor ? normalized : null;
|
|
1058
1106
|
}
|
|
1059
1107
|
async finalizeExec(token) {
|
|
1108
|
+
const workspaceLease = token?.workspaceLease ?? (this.params.execContext.config.mode === "mirror" ? await this.acquireWorkspaceLease() : void 0);
|
|
1060
1109
|
try {
|
|
1061
1110
|
if (this.params.execContext.config.mode === "mirror") await this.syncWorkspaceFromRemote();
|
|
1062
1111
|
} finally {
|
|
1063
|
-
|
|
1112
|
+
try {
|
|
1113
|
+
await token?.cleanup();
|
|
1114
|
+
} finally {
|
|
1115
|
+
workspaceLease?.release();
|
|
1116
|
+
}
|
|
1064
1117
|
}
|
|
1065
1118
|
}
|
|
1066
1119
|
async runRemoteShellScript(params) {
|
|
1067
1120
|
await this.ensureSandboxExists();
|
|
1068
|
-
if (
|
|
1121
|
+
if (this.params.execContext.config.mode === "mirror") await this.maybeSeedRemoteWorkspace();
|
|
1069
1122
|
return await this.runRemoteShellScriptInternal(params);
|
|
1070
1123
|
}
|
|
1071
1124
|
async mkdirpRemotePath(remotePath, signal) {
|
|
@@ -1170,7 +1223,7 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1170
1223
|
"--no-git-ignore",
|
|
1171
1224
|
this.params.execContext.sandboxName,
|
|
1172
1225
|
localPath,
|
|
1173
|
-
|
|
1226
|
+
remotePath
|
|
1174
1227
|
],
|
|
1175
1228
|
cwd: this.params.createParams.workspaceDir
|
|
1176
1229
|
});
|
|
@@ -1185,8 +1238,16 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1185
1238
|
}
|
|
1186
1239
|
resolveRemoteTarget(remotePath) {
|
|
1187
1240
|
const normalized = normalizeRemotePath(remotePath);
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1241
|
+
const root = resolveOpenShellWorkspaceRoot([{
|
|
1242
|
+
remote: normalizeRemotePath(this.params.remoteWorkspaceDir),
|
|
1243
|
+
owner: "workspace",
|
|
1244
|
+
value: void 0
|
|
1245
|
+
}, {
|
|
1246
|
+
remote: normalizeRemotePath(this.params.remoteAgentWorkspaceDir),
|
|
1247
|
+
owner: "agent",
|
|
1248
|
+
value: void 0
|
|
1249
|
+
}], normalized)?.remote;
|
|
1250
|
+
if (root) {
|
|
1190
1251
|
const relativePath = path.posix.relative(root, normalized);
|
|
1191
1252
|
return {
|
|
1192
1253
|
root,
|
|
@@ -1197,7 +1258,11 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1197
1258
|
}
|
|
1198
1259
|
async ensureSandboxExists() {
|
|
1199
1260
|
if (this.ensurePromise) return await this.ensurePromise;
|
|
1200
|
-
|
|
1261
|
+
const initialize = async () => {
|
|
1262
|
+
await this.ensureSandboxExistsInner();
|
|
1263
|
+
if (this.params.execContext.config.mode === "remote" && !await this.maybeSeedRemoteWorkspace()) await this.syncSkillsWorkspaceToRemote();
|
|
1264
|
+
};
|
|
1265
|
+
this.ensurePromise = this.params.execContext.config.mode === "remote" ? this.runWorkspaceOperation(initialize) : initialize();
|
|
1201
1266
|
try {
|
|
1202
1267
|
await this.ensurePromise;
|
|
1203
1268
|
} catch (error) {
|
|
@@ -1221,9 +1286,12 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1221
1286
|
if (!phase) throw this.buildLegacyRuntimeUnavailableError("OpenShell did not report a lifecycle phase for this sandbox.");
|
|
1222
1287
|
if (phase !== "Ready") throw this.buildLegacyRuntimeUnavailableError(`OpenShell reports phase "${phase}".`);
|
|
1223
1288
|
}
|
|
1289
|
+
if (this.params.execContext.config.mode === "remote" && await this.remoteManagedRootsEmpty()) this.remoteSeedPending = true;
|
|
1224
1290
|
return;
|
|
1225
1291
|
}
|
|
1226
1292
|
if (this.params.legacyRuntimeAdopted) throw this.buildLegacyRuntimeUnavailableError(getResult.stderr.trim());
|
|
1293
|
+
if (!/\bsandbox not found\b/iu.test(getResult.stderr)) throw new Error(getResult.stderr.trim() || "openshell sandbox get failed");
|
|
1294
|
+
const detachedCreateSupported = await this.supportsDetachedSandboxCreation();
|
|
1227
1295
|
const createArgs = [
|
|
1228
1296
|
"sandbox",
|
|
1229
1297
|
"create",
|
|
@@ -1235,8 +1303,12 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1235
1303
|
...this.params.execContext.config.gpu ? ["--gpu"] : [],
|
|
1236
1304
|
...this.params.execContext.config.autoProviders ? ["--auto-providers"] : ["--no-auto-providers"],
|
|
1237
1305
|
...this.params.execContext.config.providers.flatMap((provider) => ["--provider", provider]),
|
|
1238
|
-
|
|
1239
|
-
|
|
1306
|
+
...detachedCreateSupported ? [
|
|
1307
|
+
"--detach",
|
|
1308
|
+
"--",
|
|
1309
|
+
"sleep",
|
|
1310
|
+
"infinity"
|
|
1311
|
+
] : ["--", "true"]
|
|
1240
1312
|
];
|
|
1241
1313
|
const createResult = await runOpenShellCli({
|
|
1242
1314
|
context: this.params.execContext,
|
|
@@ -1247,6 +1319,41 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1247
1319
|
if (createResult.code !== 0) throw new Error(createResult.stderr.trim() || "openshell sandbox create failed");
|
|
1248
1320
|
this.remoteSeedPending = true;
|
|
1249
1321
|
}
|
|
1322
|
+
async supportsDetachedSandboxCreation() {
|
|
1323
|
+
const { config } = this.params.execContext;
|
|
1324
|
+
const cliIdentity = JSON.stringify([
|
|
1325
|
+
config.command,
|
|
1326
|
+
config.gatewayEndpoint ?? "",
|
|
1327
|
+
config.gateway ?? "",
|
|
1328
|
+
config.workspace ?? process.env.OPENSHELL_WORKSPACE ?? ""
|
|
1329
|
+
]);
|
|
1330
|
+
let support = openShellDetachedCreateSupport?.key === cliIdentity ? openShellDetachedCreateSupport.promise : void 0;
|
|
1331
|
+
if (!support) {
|
|
1332
|
+
support = (async () => {
|
|
1333
|
+
const result = await runOpenShellCli({
|
|
1334
|
+
context: this.params.execContext,
|
|
1335
|
+
args: [
|
|
1336
|
+
"sandbox",
|
|
1337
|
+
"create",
|
|
1338
|
+
"--help"
|
|
1339
|
+
],
|
|
1340
|
+
cwd: this.params.createParams.workspaceDir
|
|
1341
|
+
});
|
|
1342
|
+
if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox create capability check failed");
|
|
1343
|
+
return /^\s*--detach(?:\s|$)/mu.test(result.stdout);
|
|
1344
|
+
})();
|
|
1345
|
+
openShellDetachedCreateSupport = {
|
|
1346
|
+
key: cliIdentity,
|
|
1347
|
+
promise: support
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
try {
|
|
1351
|
+
return await support;
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
if (openShellDetachedCreateSupport?.promise === support) openShellDetachedCreateSupport = void 0;
|
|
1354
|
+
throw error;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1250
1357
|
async resolveLegacyRuntimePhase() {
|
|
1251
1358
|
const pageSize = 100;
|
|
1252
1359
|
for (let offset = 0;; offset += pageSize) {
|
|
@@ -1279,18 +1386,37 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1279
1386
|
`OpenClaw will not recreate this retired runtime name. Run \`${recreateCommand}\` to migrate this scope to the current naming format.`
|
|
1280
1387
|
].filter(Boolean).join(" "));
|
|
1281
1388
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1389
|
+
workspaceUploadRoots() {
|
|
1390
|
+
const { createParams, remoteWorkspaceDir, remoteAgentWorkspaceDir } = this.params;
|
|
1391
|
+
const roots = [{
|
|
1392
|
+
remote: remoteWorkspaceDir,
|
|
1393
|
+
owner: "workspace",
|
|
1394
|
+
value: createParams.workspaceDir
|
|
1395
|
+
}];
|
|
1396
|
+
if (createParams.cfg.workspaceAccess !== "none" && path.resolve(createParams.agentWorkspaceDir) !== path.resolve(createParams.workspaceDir)) roots.push({
|
|
1397
|
+
remote: remoteAgentWorkspaceDir,
|
|
1398
|
+
owner: "agent",
|
|
1399
|
+
value: createParams.agentWorkspaceDir
|
|
1286
1400
|
});
|
|
1287
|
-
|
|
1288
|
-
|
|
1401
|
+
return orderOpenShellWorkspaceRoots(roots).map(({ remote, owner, value: local }) => ({
|
|
1402
|
+
remote,
|
|
1403
|
+
local,
|
|
1404
|
+
owner
|
|
1405
|
+
}));
|
|
1406
|
+
}
|
|
1407
|
+
async syncWorkspaceToRemote() {
|
|
1408
|
+
const roots = this.workspaceUploadRoots();
|
|
1409
|
+
for (const [index, root] of roots.entries()) {
|
|
1410
|
+
const containingRoot = roots.slice(0, index).toReversed().find((candidate) => isOpenShellRemotePathInside(candidate.remote, root.remote));
|
|
1289
1411
|
await this.runRemoteShellScriptInternal({
|
|
1290
|
-
script:
|
|
1291
|
-
args: [
|
|
1412
|
+
script: `${ENSURE_OPEN_SHELL_REMOTE_REAL_DIRECTORY_SCRIPT}\nfind "$1" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`,
|
|
1413
|
+
args: [
|
|
1414
|
+
root.remote,
|
|
1415
|
+
containingRoot?.remote ?? root.remote,
|
|
1416
|
+
containingRoot ? "1" : "0"
|
|
1417
|
+
]
|
|
1292
1418
|
});
|
|
1293
|
-
await this.uploadPathToRemote(
|
|
1419
|
+
await this.uploadPathToRemote(root.local, root.remote);
|
|
1294
1420
|
}
|
|
1295
1421
|
await this.syncSkillsWorkspaceToRemote();
|
|
1296
1422
|
}
|
|
@@ -1306,40 +1432,62 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1306
1432
|
await this.uploadPathToRemote(this.params.createParams.skillsWorkspaceDir, remoteSkillsWorkspaceDir);
|
|
1307
1433
|
}
|
|
1308
1434
|
async syncWorkspaceFromRemote() {
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
this.params.execContext
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
workspaceDir: this.params.createParams.workspaceDir,
|
|
1328
|
-
tmpDir
|
|
1329
|
-
});
|
|
1330
|
-
try {
|
|
1331
|
-
await replaceDirectoryContents({
|
|
1332
|
-
sourceDir: tmpDir,
|
|
1333
|
-
targetDir: this.params.createParams.workspaceDir,
|
|
1334
|
-
excludeDirs: DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS
|
|
1335
|
-
});
|
|
1336
|
-
} finally {
|
|
1337
|
-
await restoreMaterializedSkillsShadow({
|
|
1338
|
-
workspaceDir: this.params.createParams.workspaceDir,
|
|
1339
|
-
preserved: preservedSandboxSkills
|
|
1435
|
+
const remoteSkillsWorkspaceDir = resolveRemoteMaterializedSkillsWorkspaceDir(this.params.remoteWorkspaceDir);
|
|
1436
|
+
const roots = this.workspaceUploadRoots();
|
|
1437
|
+
for (const [index, root] of roots.entries()) {
|
|
1438
|
+
if (root.owner === "agent") continue;
|
|
1439
|
+
await withTempWorkspace({
|
|
1440
|
+
rootDir: resolveOpenShellTmpRoot(),
|
|
1441
|
+
prefix: "openclaw-openshell-sync-"
|
|
1442
|
+
}, async ({ dir: tmpDir }) => {
|
|
1443
|
+
const result = await runOpenShellCli({
|
|
1444
|
+
context: this.params.execContext,
|
|
1445
|
+
args: [
|
|
1446
|
+
"sandbox",
|
|
1447
|
+
"download",
|
|
1448
|
+
this.params.execContext.sandboxName,
|
|
1449
|
+
root.remote,
|
|
1450
|
+
tmpDir
|
|
1451
|
+
],
|
|
1452
|
+
cwd: this.params.createParams.workspaceDir
|
|
1340
1453
|
});
|
|
1341
|
-
|
|
1342
|
-
|
|
1454
|
+
if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox download failed");
|
|
1455
|
+
const preservedShadows = [];
|
|
1456
|
+
try {
|
|
1457
|
+
for (const shadowedRoot of roots.slice(index + 1)) {
|
|
1458
|
+
if (root.remote === shadowedRoot.remote || !isOpenShellRemotePathInside(root.remote, shadowedRoot.remote)) continue;
|
|
1459
|
+
const relativeParts = path.posix.relative(root.remote, shadowedRoot.remote).split("/").filter(Boolean);
|
|
1460
|
+
await removeDownloadedWorkspacePath(tmpDir, relativeParts);
|
|
1461
|
+
const preserved = await moveLocalShadowAside({
|
|
1462
|
+
workspaceDir: root.local,
|
|
1463
|
+
tmpDir,
|
|
1464
|
+
relativeParts
|
|
1465
|
+
});
|
|
1466
|
+
if (preserved) preservedShadows.push(preserved);
|
|
1467
|
+
}
|
|
1468
|
+
const relativeSkillsPath = path.posix.relative(root.remote, remoteSkillsWorkspaceDir);
|
|
1469
|
+
if (relativeSkillsPath === "" || !relativeSkillsPath.startsWith("../") && !path.posix.isAbsolute(relativeSkillsPath)) await removeDownloadedWorkspacePath(tmpDir, relativeSkillsPath.split("/").filter(Boolean));
|
|
1470
|
+
if (root.owner === "workspace") {
|
|
1471
|
+
const preserved = await moveLocalShadowAside({
|
|
1472
|
+
workspaceDir: root.local,
|
|
1473
|
+
tmpDir,
|
|
1474
|
+
relativeParts: MATERIALIZED_SKILLS_REMOTE_PARTS
|
|
1475
|
+
});
|
|
1476
|
+
if (preserved) preservedShadows.push(preserved);
|
|
1477
|
+
}
|
|
1478
|
+
await replaceDirectoryContents({
|
|
1479
|
+
sourceDir: tmpDir,
|
|
1480
|
+
targetDir: root.local,
|
|
1481
|
+
excludeDirs: DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS
|
|
1482
|
+
});
|
|
1483
|
+
} finally {
|
|
1484
|
+
for (const preserved of preservedShadows.toReversed()) await restoreLocalShadow({
|
|
1485
|
+
workspaceDir: root.local,
|
|
1486
|
+
preserved
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1343
1491
|
}
|
|
1344
1492
|
async uploadPathToRemote(localPath, remotePath) {
|
|
1345
1493
|
await withTempWorkspace({
|
|
@@ -1350,10 +1498,10 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1350
1498
|
const stagedRoot = path.join(tmpDir, remoteRootName);
|
|
1351
1499
|
await stageDirectoryContents({
|
|
1352
1500
|
sourceDir: localPath,
|
|
1353
|
-
targetDir: stagedRoot
|
|
1501
|
+
targetDir: stagedRoot,
|
|
1502
|
+
excludeDirs: DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS
|
|
1354
1503
|
});
|
|
1355
|
-
const
|
|
1356
|
-
for (const entry of stagedEntries) {
|
|
1504
|
+
for (const entry of (await fsPromises.readdir(stagedRoot)).toSorted()) {
|
|
1357
1505
|
const result = await runOpenShellCli({
|
|
1358
1506
|
context: this.params.execContext,
|
|
1359
1507
|
args: buildOpenShellDirectoryUploadArgs({
|
|
@@ -1367,6 +1515,12 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1367
1515
|
}
|
|
1368
1516
|
});
|
|
1369
1517
|
}
|
|
1518
|
+
async remoteManagedRootsEmpty() {
|
|
1519
|
+
return (await this.runRemoteShellScriptInternal({
|
|
1520
|
+
script: REMOTE_MANAGED_ROOTS_EMPTY_SCRIPT,
|
|
1521
|
+
args: [this.params.remoteWorkspaceDir, this.params.remoteAgentWorkspaceDir]
|
|
1522
|
+
})).stdout.toString("utf8").trim() === "0";
|
|
1523
|
+
}
|
|
1370
1524
|
async maybeSeedRemoteWorkspace() {
|
|
1371
1525
|
if (!this.remoteSeedPending) return false;
|
|
1372
1526
|
this.remoteSeedPending = false;
|
|
@@ -1431,17 +1585,33 @@ function parseOpenShellSandboxPhasePage(stdout, sandboxName) {
|
|
|
1431
1585
|
return;
|
|
1432
1586
|
}
|
|
1433
1587
|
}
|
|
1588
|
+
function parseOpenShellSandboxPhase(stdout) {
|
|
1589
|
+
try {
|
|
1590
|
+
const parsed = JSON.parse(stdout);
|
|
1591
|
+
if (typeof parsed !== "object" || parsed === null || !("phase" in parsed)) return;
|
|
1592
|
+
return typeof parsed.phase === "string" ? parsed.phase : void 0;
|
|
1593
|
+
} catch {
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1434
1597
|
function resolveRemoteMaterializedSkillsWorkspaceDir(remoteWorkspaceDir) {
|
|
1435
1598
|
const root = remoteWorkspaceDir.replace(/\\/g, "/").replace(/\/+$/, "") || "/";
|
|
1436
1599
|
return path.posix.join(root, ...MATERIALIZED_SKILLS_REMOTE_PARTS);
|
|
1437
1600
|
}
|
|
1438
|
-
async function
|
|
1601
|
+
async function removeDownloadedWorkspacePath(tmpDir, parts) {
|
|
1602
|
+
if (parts.length === 0) {
|
|
1603
|
+
for (const entry of await fsPromises.readdir(tmpDir)) await fsPromises.rm(path.join(tmpDir, entry), {
|
|
1604
|
+
recursive: true,
|
|
1605
|
+
force: true
|
|
1606
|
+
});
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1439
1609
|
let cursor = tmpDir;
|
|
1440
|
-
for (const [index, part] of
|
|
1610
|
+
for (const [index, part] of parts.entries()) {
|
|
1441
1611
|
const next = path.join(cursor, part);
|
|
1442
1612
|
const stats = await fsPromises.lstat(next).catch(() => null);
|
|
1443
1613
|
if (!stats) return;
|
|
1444
|
-
if (index ===
|
|
1614
|
+
if (index === parts.length - 1) {
|
|
1445
1615
|
await fsPromises.rm(next, {
|
|
1446
1616
|
recursive: true,
|
|
1447
1617
|
force: true
|
|
@@ -1458,31 +1628,31 @@ async function removeMaterializedSkillsFromDownloadedWorkspace(tmpDir) {
|
|
|
1458
1628
|
cursor = next;
|
|
1459
1629
|
}
|
|
1460
1630
|
}
|
|
1461
|
-
async function
|
|
1462
|
-
const shadowPath = path.join(params.workspaceDir, ...
|
|
1631
|
+
async function moveLocalShadowAside(params) {
|
|
1632
|
+
const shadowPath = path.join(params.workspaceDir, ...params.relativeParts);
|
|
1463
1633
|
const parentStats = await fsPromises.lstat(path.dirname(shadowPath)).catch(() => null);
|
|
1464
1634
|
if (!parentStats?.isDirectory() || parentStats.isSymbolicLink()) return;
|
|
1465
1635
|
const shadowStats = await fsPromises.lstat(shadowPath).catch(() => null);
|
|
1466
1636
|
if (!shadowStats || shadowStats.isSymbolicLink()) return;
|
|
1467
1637
|
const preserveRoot = await fsPromises.mkdtemp(path.join(path.dirname(params.tmpDir), "openclaw-openshell-preserve-"));
|
|
1468
|
-
const preservedPath = path.join(preserveRoot, "
|
|
1638
|
+
const preservedPath = path.join(preserveRoot, "shadow");
|
|
1469
1639
|
await movePathWithCopyFallback({
|
|
1470
1640
|
from: shadowPath,
|
|
1471
1641
|
to: preservedPath
|
|
1472
1642
|
});
|
|
1473
1643
|
return {
|
|
1474
1644
|
preservedPath,
|
|
1475
|
-
preserveRoot
|
|
1645
|
+
preserveRoot,
|
|
1646
|
+
relativeParts: params.relativeParts
|
|
1476
1647
|
};
|
|
1477
1648
|
}
|
|
1478
|
-
async function
|
|
1479
|
-
if (!params.preserved) return;
|
|
1649
|
+
async function restoreLocalShadow(params) {
|
|
1480
1650
|
let restored = false;
|
|
1481
1651
|
try {
|
|
1482
|
-
const shadowPath = path.join(params.workspaceDir, ...
|
|
1652
|
+
const shadowPath = path.join(params.workspaceDir, ...params.preserved.relativeParts);
|
|
1483
1653
|
const parentPath = path.dirname(shadowPath);
|
|
1484
1654
|
const parentStats = await fsPromises.lstat(parentPath).catch(() => null);
|
|
1485
|
-
if (parentStats?.isSymbolicLink()) throw new Error(`Refusing to restore
|
|
1655
|
+
if (parentStats?.isSymbolicLink()) throw new Error(`Refusing to restore workspace shadow through symlink parent: ${parentPath}`);
|
|
1486
1656
|
if (parentStats && !parentStats.isDirectory()) await fsPromises.rm(parentPath, {
|
|
1487
1657
|
recursive: true,
|
|
1488
1658
|
force: true
|
|
@@ -1512,10 +1682,6 @@ function normalizeRemotePath(remotePath) {
|
|
|
1512
1682
|
if (!path.posix.isAbsolute(normalized)) throw new Error(`OpenShell remote path must be absolute: ${remotePath}`);
|
|
1513
1683
|
return normalized;
|
|
1514
1684
|
}
|
|
1515
|
-
function isRemotePathInside(root, candidate) {
|
|
1516
|
-
const relative = path.posix.relative(root, candidate);
|
|
1517
|
-
return relative === "" || relative !== ".." && !relative.startsWith("../") && !path.posix.isAbsolute(relative);
|
|
1518
|
-
}
|
|
1519
1685
|
//#endregion
|
|
1520
1686
|
//#region extensions/openshell/index.ts
|
|
1521
1687
|
var openshell_default = definePluginEntry({
|
|
@@ -1526,11 +1692,18 @@ var openshell_default = definePluginEntry({
|
|
|
1526
1692
|
register(api) {
|
|
1527
1693
|
if (api.registrationMode !== "full") return;
|
|
1528
1694
|
const pluginConfig = resolveOpenShellPluginConfig(api.pluginConfig);
|
|
1529
|
-
registerSandboxBackend("openshell", {
|
|
1695
|
+
const unregister = registerSandboxBackend("openshell", {
|
|
1530
1696
|
factory: createOpenShellSandboxBackendFactory({ pluginConfig }),
|
|
1531
1697
|
manager: createOpenShellSandboxBackendManager({ pluginConfig }),
|
|
1532
1698
|
resolveWorkdir: () => pluginConfig.remoteWorkspaceDir
|
|
1533
1699
|
});
|
|
1700
|
+
api.lifecycle.registerRuntimeLifecycle({
|
|
1701
|
+
id: "openshell-sandbox-cleanup",
|
|
1702
|
+
cleanup: ({ reason, sessionKey, runId }) => {
|
|
1703
|
+
if (sessionKey !== void 0 || runId !== void 0) return;
|
|
1704
|
+
if (reason === "disable" || reason === "restart") unregister();
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1534
1707
|
}
|
|
1535
1708
|
});
|
|
1536
1709
|
//#endregion
|
package/openclaw.plugin.json
CHANGED
|
@@ -54,11 +54,13 @@
|
|
|
54
54
|
},
|
|
55
55
|
"remoteWorkspaceDir": {
|
|
56
56
|
"type": "string",
|
|
57
|
-
"minLength": 1
|
|
57
|
+
"minLength": 1,
|
|
58
|
+
"pattern": "^\\/(?:sandbox|agent)(?:\\/|$)"
|
|
58
59
|
},
|
|
59
60
|
"remoteAgentWorkspaceDir": {
|
|
60
61
|
"type": "string",
|
|
61
|
-
"minLength": 1
|
|
62
|
+
"minLength": 1,
|
|
63
|
+
"pattern": "^\\/(?:sandbox|agent)(?:\\/|$)"
|
|
62
64
|
},
|
|
63
65
|
"timeoutSeconds": {
|
|
64
66
|
"type": "number",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/openshell-sandbox",
|
|
3
|
-
"version": "2026.8.1
|
|
3
|
+
"version": "2026.8.1",
|
|
4
4
|
"description": "OpenClaw sandbox backend for the NVIDIA OpenShell CLI with mirrored local workspaces and SSH command execution.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"minHostVersion": ">=2026.5.12-beta.1"
|
|
22
22
|
},
|
|
23
23
|
"compat": {
|
|
24
|
-
"pluginApi": ">=2026.8.1
|
|
24
|
+
"pluginApi": ">=2026.8.1"
|
|
25
25
|
},
|
|
26
26
|
"build": {
|
|
27
|
-
"openclawVersion": "2026.8.1
|
|
27
|
+
"openclawVersion": "2026.8.1",
|
|
28
28
|
"bundledDist": false
|
|
29
29
|
},
|
|
30
30
|
"release": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"README.md"
|
|
42
42
|
],
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"openclaw": ">=2026.8.1
|
|
44
|
+
"openclaw": ">=2026.8.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependenciesMeta": {
|
|
47
47
|
"openclaw": {
|