@openclaw/openshell-sandbox 2026.8.1-beta.3 → 2026.8.2
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 +466 -247
- 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 { buildRemoteCommand, 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
13
|
import { isPathInside, root } from "openclaw/plugin-sdk/file-access-runtime";
|
|
12
|
-
import { FsSafeError, movePathWithCopyFallback } from "openclaw/plugin-sdk/security-runtime";
|
|
13
14
|
import pLimit from "p-limit";
|
|
14
15
|
//#region extensions/openshell/src/cli.ts
|
|
15
16
|
function buildOpenShellBaseArgv(config) {
|
|
@@ -72,6 +73,7 @@ function normalizeProviders(value) {
|
|
|
72
73
|
return providers;
|
|
73
74
|
}
|
|
74
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` });
|
|
75
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" });
|
|
76
78
|
const OpenShellPluginConfigSchema = z.strictObject({
|
|
77
79
|
mode: z.enum(["mirror", "remote"], { error: "mode must be one of mirror, remote" }).optional(),
|
|
@@ -84,8 +86,8 @@ const OpenShellPluginConfigSchema = z.strictObject({
|
|
|
84
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(),
|
|
85
87
|
gpu: z.boolean({ error: "gpu must be a boolean" }).optional(),
|
|
86
88
|
autoProviders: z.boolean({ error: "autoProviders must be a boolean" }).optional(),
|
|
87
|
-
remoteWorkspaceDir:
|
|
88
|
-
remoteAgentWorkspaceDir:
|
|
89
|
+
remoteWorkspaceDir: openShellManagedRemotePath("remoteWorkspaceDir").optional(),
|
|
90
|
+
remoteAgentWorkspaceDir: openShellManagedRemotePath("remoteAgentWorkspaceDir").optional(),
|
|
89
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()
|
|
90
92
|
});
|
|
91
93
|
function isManagedOpenShellRemotePath(value) {
|
|
@@ -154,6 +156,24 @@ function resolveOpenShellPluginConfig(value) {
|
|
|
154
156
|
};
|
|
155
157
|
}
|
|
156
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
|
|
157
177
|
//#region extensions/openshell/src/fs-bridge.ts
|
|
158
178
|
const MATERIALIZED_SKILLS_CONTAINER_PARTS = [
|
|
159
179
|
".openclaw",
|
|
@@ -349,27 +369,43 @@ var OpenShellFsBridge = class {
|
|
|
349
369
|
});
|
|
350
370
|
if (protectedSkillTarget) return protectedSkillTarget;
|
|
351
371
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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}`);
|
|
365
393
|
return {
|
|
366
|
-
hostPath
|
|
367
|
-
relativePath: relative ?
|
|
368
|
-
containerPath: relative ? path.posix.join(
|
|
369
|
-
mountHostRoot:
|
|
370
|
-
writable:
|
|
371
|
-
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
|
|
372
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}`);
|
|
373
409
|
}
|
|
374
410
|
const cwd = params.cwd ? path.resolve(params.cwd) : workspaceRoot;
|
|
375
411
|
const hostPath = path.isAbsolute(input) ? path.resolve(input) : path.resolve(cwd, input);
|
|
@@ -384,14 +420,7 @@ var OpenShellFsBridge = class {
|
|
|
384
420
|
}
|
|
385
421
|
if (isPathInside(workspaceRoot, hostPath)) {
|
|
386
422
|
const relative = path.relative(workspaceRoot, hostPath).split(path.sep).join(path.posix.sep);
|
|
387
|
-
return
|
|
388
|
-
hostPath,
|
|
389
|
-
relativePath: relative,
|
|
390
|
-
containerPath: relative ? path.posix.join(workspaceContainerRoot, relative) : workspaceContainerRoot,
|
|
391
|
-
mountHostRoot: workspaceRoot,
|
|
392
|
-
writable: this.sandbox.workspaceAccess === "rw",
|
|
393
|
-
source: "workspace"
|
|
394
|
-
};
|
|
423
|
+
return expectResolvedContainerTarget(resolveContainerTarget(path.posix.join(workspaceContainerRoot, relative)), input);
|
|
395
424
|
}
|
|
396
425
|
if (skillsRoot && this.sandbox.workspaceAccess === "rw" && isPathInside(skillsRoot, hostPath)) {
|
|
397
426
|
const relative = path.relative(skillsRoot, hostPath).split(path.sep).join(path.posix.sep);
|
|
@@ -406,18 +435,15 @@ var OpenShellFsBridge = class {
|
|
|
406
435
|
}
|
|
407
436
|
if (hasAgentMount && isPathInside(agentRoot, hostPath)) {
|
|
408
437
|
const relative = path.relative(agentRoot, hostPath).split(path.sep).join(path.posix.sep);
|
|
409
|
-
return
|
|
410
|
-
hostPath,
|
|
411
|
-
relativePath: relative ? `${agentContainerRoot}/${relative}` : agentContainerRoot,
|
|
412
|
-
containerPath: relative ? path.posix.join(agentContainerRoot, relative) : agentContainerRoot,
|
|
413
|
-
mountHostRoot: agentRoot,
|
|
414
|
-
writable: this.sandbox.workspaceAccess === "rw",
|
|
415
|
-
source: "agent"
|
|
416
|
-
};
|
|
438
|
+
return expectResolvedContainerTarget(resolveContainerTarget(path.posix.join(agentContainerRoot, relative)), input);
|
|
417
439
|
}
|
|
418
440
|
throw new Error(`Path escapes sandbox root (${workspaceRoot}): ${params.filePath}`);
|
|
419
441
|
}
|
|
420
442
|
};
|
|
443
|
+
function expectResolvedContainerTarget(target, input) {
|
|
444
|
+
if (!target) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${input}`);
|
|
445
|
+
return target;
|
|
446
|
+
}
|
|
421
447
|
async function mkdirLocalRootPath(params) {
|
|
422
448
|
const relativePath = relativeToRoot(params.target, params.hostPath);
|
|
423
449
|
if (!relativePath) return;
|
|
@@ -540,7 +566,8 @@ async function assertLocalPathSafety(params) {
|
|
|
540
566
|
if (!params.target.hostPath) throw new Error(`Missing local host path for ${params.target.containerPath}`);
|
|
541
567
|
const canonicalRoot = await fsPromises.realpath(params.root).catch(() => path.resolve(params.root));
|
|
542
568
|
const targetStats = await fsPromises.lstat(params.target.hostPath).catch(() => null);
|
|
543
|
-
|
|
569
|
+
const candidate = params.allowFinalSymlinkForUnlink && targetStats?.isSymbolicLink() ? path.resolve(canonicalRoot, path.relative(params.root, params.target.hostPath)) : await resolveCanonicalCandidate(params.target.hostPath);
|
|
570
|
+
if (!isPathInside(canonicalRoot, candidate)) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${params.target.containerPath}`);
|
|
544
571
|
const segments = path.relative(params.root, params.target.hostPath).split(path.sep).filter(Boolean);
|
|
545
572
|
let cursor = params.root;
|
|
546
573
|
for (const [index, segment] of segments.entries()) {
|
|
@@ -582,66 +609,65 @@ function createExcludeMatcher(excludeDirs) {
|
|
|
582
609
|
}
|
|
583
610
|
const runLimitedFs = pLimit(COPY_TREE_FS_CONCURRENCY);
|
|
584
611
|
async function lstatIfExists(targetPath) {
|
|
585
|
-
return await runLimitedFs(
|
|
612
|
+
return await runLimitedFs(fsPromises.lstat, targetPath).catch((error) => {
|
|
613
|
+
if (extractErrorCode(error) === "ENOENT") return null;
|
|
614
|
+
throw error;
|
|
615
|
+
});
|
|
586
616
|
}
|
|
587
|
-
async function
|
|
588
|
-
const stats = await runLimitedFs(async () => await fsPromises.lstat(params.sourcePath));
|
|
589
|
-
if (stats.isSymbolicLink()) return;
|
|
617
|
+
async function reconcileMirrorPath(params) {
|
|
590
618
|
const targetStats = await lstatIfExists(params.targetPath);
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
619
|
+
if (targetStats && !targetStats.isDirectory() && !targetStats.isFile()) return true;
|
|
620
|
+
const sourceStats = params.sourcePath ? await runLimitedFs(fsPromises.lstat, params.sourcePath) : null;
|
|
621
|
+
const sourceDir = sourceStats?.isDirectory() ? params.sourcePath : void 0;
|
|
622
|
+
const sourceFile = sourceStats?.isFile() ? params.sourcePath : void 0;
|
|
623
|
+
if (!params.replace && !sourceDir && !sourceFile) return false;
|
|
624
|
+
if (sourceDir || targetStats?.isDirectory()) {
|
|
625
|
+
if (targetStats && !targetStats.isDirectory()) await runLimitedFs(fsPromises.rm, params.targetPath, { force: true });
|
|
626
|
+
const preservedEntries = await reconcileMirrorDirectory({
|
|
627
|
+
sourceDir,
|
|
628
|
+
targetDir: params.targetPath,
|
|
629
|
+
replace: params.replace
|
|
630
|
+
});
|
|
631
|
+
if (sourceDir || preservedEntries) return preservedEntries;
|
|
632
|
+
await runLimitedFs(fsPromises.rmdir, params.targetPath);
|
|
633
|
+
} else if (targetStats) await runLimitedFs(fsPromises.rm, params.targetPath, { force: true });
|
|
634
|
+
if (sourceFile) await runLimitedFs(fsPromises.copyFile, sourceFile, params.targetPath);
|
|
635
|
+
return false;
|
|
608
636
|
}
|
|
609
|
-
async function
|
|
637
|
+
async function reconcileMirrorDirectory(params) {
|
|
638
|
+
const { sourceDir } = params;
|
|
610
639
|
const isExcluded = createExcludeMatcher(params.excludeDirs);
|
|
611
|
-
await fsPromises.mkdir
|
|
612
|
-
const
|
|
613
|
-
await
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if (isExcluded(entry)) continue;
|
|
624
|
-
await copyTreeWithoutSymlinks({
|
|
625
|
-
sourcePath: path.join(params.sourceDir, entry),
|
|
626
|
-
targetPath: path.join(params.targetDir, entry),
|
|
627
|
-
preserveTargetSymlinks: true
|
|
628
|
-
});
|
|
640
|
+
await runLimitedFs(fsPromises.mkdir, params.targetDir, { recursive: true });
|
|
641
|
+
const sourceEntries = new Set(sourceDir ? await runLimitedFs(async () => await fsPromises.readdir(sourceDir)) : []);
|
|
642
|
+
const targetEntries = params.replace ? await runLimitedFs(async () => await fsPromises.readdir(params.targetDir)) : [];
|
|
643
|
+
const results = await Promise.allSettled([.../* @__PURE__ */ new Set([...sourceEntries, ...targetEntries])].filter((entry) => !isExcluded(entry)).map((entry) => reconcileMirrorPath({
|
|
644
|
+
sourcePath: sourceDir && sourceEntries.has(entry) ? path.join(sourceDir, entry) : void 0,
|
|
645
|
+
targetPath: path.join(params.targetDir, entry),
|
|
646
|
+
replace: params.replace
|
|
647
|
+
})));
|
|
648
|
+
let preservedEntries = false;
|
|
649
|
+
for (const result of results) {
|
|
650
|
+
if (result.status === "rejected") throw result.reason;
|
|
651
|
+
preservedEntries ||= result.value;
|
|
629
652
|
}
|
|
653
|
+
return preservedEntries;
|
|
654
|
+
}
|
|
655
|
+
async function replaceDirectoryContents(params) {
|
|
656
|
+
await reconcileMirrorDirectory({
|
|
657
|
+
...params,
|
|
658
|
+
replace: true
|
|
659
|
+
});
|
|
630
660
|
}
|
|
631
661
|
async function stageDirectoryContents(params) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (isExcluded(entry)) continue;
|
|
637
|
-
await copyTreeWithoutSymlinks({
|
|
638
|
-
sourcePath: path.join(params.sourceDir, entry),
|
|
639
|
-
targetPath: path.join(params.targetDir, entry)
|
|
640
|
-
});
|
|
641
|
-
}
|
|
662
|
+
await reconcileMirrorDirectory({
|
|
663
|
+
...params,
|
|
664
|
+
replace: false
|
|
665
|
+
});
|
|
642
666
|
}
|
|
643
667
|
//#endregion
|
|
644
668
|
//#region extensions/openshell/src/backend.ts
|
|
669
|
+
const openShellWorkspaceOperations = new KeyedAsyncQueue();
|
|
670
|
+
let openShellDetachedCreateSupport;
|
|
645
671
|
const MATERIALIZED_SKILLS_REMOTE_PARTS = [".openclaw", "sandbox-skills"];
|
|
646
672
|
function buildOpenShellDirectoryUploadArgs(params) {
|
|
647
673
|
return [
|
|
@@ -650,9 +676,10 @@ function buildOpenShellDirectoryUploadArgs(params) {
|
|
|
650
676
|
"--no-git-ignore",
|
|
651
677
|
params.sandboxName,
|
|
652
678
|
params.localPath,
|
|
653
|
-
normalizeRemotePath(params.remotePath)
|
|
679
|
+
`${normalizeRemotePath(params.remotePath)}/`
|
|
654
680
|
];
|
|
655
681
|
}
|
|
682
|
+
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\"";
|
|
656
683
|
const PINNED_REMOTE_PATH_MUTATION_SCRIPT = [
|
|
657
684
|
"set -eu",
|
|
658
685
|
"die() { echo \"$1\" >&2; exit 1; }",
|
|
@@ -758,6 +785,7 @@ const ENSURE_OPEN_SHELL_REMOTE_REAL_DIRECTORY_SCRIPT = [
|
|
|
758
785
|
"set -e",
|
|
759
786
|
"target=\"$1\"",
|
|
760
787
|
"root=\"${2:-$1}\"",
|
|
788
|
+
"replace_blocking=\"${3:-0}\"",
|
|
761
789
|
"case \"$target\" in /*) ;; *) echo \"remote directory must be absolute: $target\" >&2; exit 1 ;; esac",
|
|
762
790
|
"case \"$root\" in /*) ;; *) echo \"remote root must be absolute: $root\" >&2; exit 1 ;; esac",
|
|
763
791
|
"target=\"${target%/}\"",
|
|
@@ -785,9 +813,15 @@ const ENSURE_OPEN_SHELL_REMOTE_REAL_DIRECTORY_SCRIPT = [
|
|
|
785
813
|
" if [ \"$part\" = \"$relative\" ]; then relative=\"\"; else relative=\"${relative#*/}\"; fi",
|
|
786
814
|
" [ -n \"$part\" ] || continue",
|
|
787
815
|
" if [ \"$current\" = \"/\" ]; then next=\"/$part\"; else next=\"$current/$part\"; fi",
|
|
788
|
-
" if [ -L \"$next\" ]; then
|
|
816
|
+
" if [ -L \"$next\" ]; then",
|
|
817
|
+
" if [ \"$replace_blocking\" != \"1\" ]; then echo \"unsafe remote directory symlink: $next\" >&2; exit 1; fi",
|
|
818
|
+
" rm -rf -- \"$next\"",
|
|
819
|
+
" elif [ -e \"$next\" ] && [ ! -d \"$next\" ]; then",
|
|
820
|
+
" if [ \"$replace_blocking\" != \"1\" ]; then echo \"unsafe remote directory component: $next\" >&2; exit 1; fi",
|
|
821
|
+
" rm -rf -- \"$next\"",
|
|
822
|
+
" fi",
|
|
789
823
|
" if [ -e \"$next\" ]; then",
|
|
790
|
-
"
|
|
824
|
+
" [ -d \"$next\" ] || { echo \"unsafe remote directory component: $next\" >&2; exit 1; }",
|
|
791
825
|
" else",
|
|
792
826
|
" mkdir -- \"$next\"",
|
|
793
827
|
" fi",
|
|
@@ -815,20 +849,22 @@ function createOpenShellSandboxBackendManager(params) {
|
|
|
815
849
|
args: [
|
|
816
850
|
"sandbox",
|
|
817
851
|
"get",
|
|
818
|
-
entry.containerName
|
|
852
|
+
entry.containerName,
|
|
853
|
+
"--output",
|
|
854
|
+
"json"
|
|
819
855
|
]
|
|
820
856
|
});
|
|
821
857
|
const configuredSource = execContext.config.from;
|
|
822
858
|
return {
|
|
823
|
-
running: result.code === 0,
|
|
859
|
+
running: result.code === 0 && parseOpenShellSandboxPhase(result.stdout) === "Ready",
|
|
824
860
|
actualConfigLabel: entry.image,
|
|
825
861
|
configLabelMatch: entry.image === configuredSource
|
|
826
862
|
};
|
|
827
863
|
},
|
|
828
|
-
async removeRuntime({ entry }) {
|
|
829
|
-
await runOpenShellCli({
|
|
864
|
+
async removeRuntime({ entry, config }) {
|
|
865
|
+
const result = await runOpenShellCli({
|
|
830
866
|
context: {
|
|
831
|
-
config: params.pluginConfig,
|
|
867
|
+
config: resolveOpenShellPluginConfigFromConfig(config, params.pluginConfig),
|
|
832
868
|
sandboxName: entry.containerName
|
|
833
869
|
},
|
|
834
870
|
args: [
|
|
@@ -837,6 +873,7 @@ function createOpenShellSandboxBackendManager(params) {
|
|
|
837
873
|
entry.containerName
|
|
838
874
|
]
|
|
839
875
|
});
|
|
876
|
+
if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox delete failed");
|
|
840
877
|
}
|
|
841
878
|
};
|
|
842
879
|
}
|
|
@@ -864,11 +901,11 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
864
901
|
this.params = params;
|
|
865
902
|
this.handle = null;
|
|
866
903
|
this.ensurePromise = null;
|
|
867
|
-
this.preparedRemoteWorkspaceForNextExec = null;
|
|
868
904
|
this.remoteSeedPending = false;
|
|
869
905
|
}
|
|
870
906
|
asHandle() {
|
|
871
907
|
if (this.handle) return this.handle;
|
|
908
|
+
const runRemoteShellScript = (command) => this.params.execContext.config.mode === "mirror" ? this.runWorkspaceOperation(() => this.runRemoteShellScript(command), command.signal) : this.runRemoteShellScript(command);
|
|
872
909
|
const handle = {
|
|
873
910
|
id: "openshell",
|
|
874
911
|
runtimeId: this.params.execContext.sandboxName,
|
|
@@ -880,7 +917,6 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
880
917
|
configLabelKind: "Source",
|
|
881
918
|
workdirValidation: "backend",
|
|
882
919
|
validateWorkdir: async (workdir) => await this.validateWorkdir(workdir),
|
|
883
|
-
discardPreparedWorkdir: (workdir) => this.discardPreparedWorkdir(workdir),
|
|
884
920
|
workdirRoots: [this.params.remoteWorkspaceDir, this.params.remoteAgentWorkspaceDir],
|
|
885
921
|
remoteWorkspaceDir: this.params.remoteWorkspaceDir,
|
|
886
922
|
remoteAgentWorkspaceDir: this.params.remoteAgentWorkspaceDir,
|
|
@@ -901,126 +937,189 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
901
937
|
finalizeExec: async ({ token }) => {
|
|
902
938
|
await this.finalizeExec(token);
|
|
903
939
|
},
|
|
904
|
-
runShellCommand:
|
|
940
|
+
runShellCommand: runRemoteShellScript,
|
|
905
941
|
createFsBridge: ({ sandbox }) => this.params.execContext.config.mode === "remote" ? createRemoteShellSandboxFsBridge({
|
|
906
942
|
sandbox,
|
|
907
943
|
runtime: handle
|
|
908
|
-
}) :
|
|
909
|
-
|
|
910
|
-
backend: handle
|
|
911
|
-
}),
|
|
912
|
-
runRemoteShellScript: async (command) => await this.runRemoteShellScript(command),
|
|
913
|
-
mkdirpRemotePath: async (remotePath, signal) => await this.mkdirpRemotePath(remotePath, signal),
|
|
914
|
-
removeRemotePath: async (remotePath, removeParams) => await this.removeRemotePath(remotePath, removeParams),
|
|
915
|
-
renameRemotePath: async (fromRemotePath, toRemotePath, signal) => await this.renameRemotePath(fromRemotePath, toRemotePath, signal),
|
|
916
|
-
syncLocalPathToRemote: async (localPath, remotePath) => await this.syncLocalPathToRemote(localPath, remotePath)
|
|
944
|
+
}) : this.createMirrorFsBridge(sandbox),
|
|
945
|
+
runRemoteShellScript
|
|
917
946
|
};
|
|
918
947
|
this.handle = handle;
|
|
919
948
|
return handle;
|
|
920
949
|
}
|
|
950
|
+
createMirrorFsBridge(sandbox) {
|
|
951
|
+
const bridge = createOpenShellFsBridge({
|
|
952
|
+
sandbox,
|
|
953
|
+
backend: {
|
|
954
|
+
remoteAgentWorkspaceDir: this.params.remoteAgentWorkspaceDir,
|
|
955
|
+
mkdirpRemotePath: (remotePath, signal) => this.mkdirpRemotePath(remotePath, signal),
|
|
956
|
+
removeRemotePath: (remotePath, params) => this.removeRemotePath(remotePath, params),
|
|
957
|
+
renameRemotePath: (from, to, signal) => this.renameRemotePath(from, to, signal),
|
|
958
|
+
syncLocalPathToRemote: (localPath, remotePath) => this.syncLocalPathToRemote(localPath, remotePath)
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
return {
|
|
962
|
+
resolvePath: (params) => bridge.resolvePath(params),
|
|
963
|
+
readFile: (params) => this.runWorkspaceOperation(() => bridge.readFile(params), params.signal),
|
|
964
|
+
writeFile: (params) => this.runWorkspaceOperation(() => bridge.writeFile(params), params.signal),
|
|
965
|
+
createFileExclusive: (params) => this.runWorkspaceOperation(() => bridge.createFileExclusive(params), params.signal),
|
|
966
|
+
mkdirp: (params) => this.runWorkspaceOperation(() => bridge.mkdirp(params), params.signal),
|
|
967
|
+
remove: (params) => this.runWorkspaceOperation(() => bridge.remove(params), params.signal),
|
|
968
|
+
rename: (params) => this.runWorkspaceOperation(() => bridge.rename(params), params.signal),
|
|
969
|
+
stat: (params) => this.runWorkspaceOperation(() => bridge.stat(params), params.signal)
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
async runWorkspaceOperation(operation, signal) {
|
|
973
|
+
signal?.throwIfAborted();
|
|
974
|
+
const lease = await this.acquireWorkspaceLease();
|
|
975
|
+
try {
|
|
976
|
+
signal?.throwIfAborted();
|
|
977
|
+
return await operation();
|
|
978
|
+
} finally {
|
|
979
|
+
lease.release();
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
async acquireWorkspaceLease() {
|
|
983
|
+
const { config, sandboxName } = this.params.execContext;
|
|
984
|
+
const keys = [`host:${await canonicalPathFromExistingAncestor(this.params.createParams.workspaceDir)}`, `runtime:${JSON.stringify([
|
|
985
|
+
config.gatewayEndpoint ?? "",
|
|
986
|
+
config.gateway ?? "",
|
|
987
|
+
config.workspace ?? process.env.OPENSHELL_WORKSPACE ?? "",
|
|
988
|
+
sandboxName
|
|
989
|
+
])}`].toSorted();
|
|
990
|
+
const releases = [];
|
|
991
|
+
for (const key of keys) {
|
|
992
|
+
const acquired = createDeferred();
|
|
993
|
+
openShellWorkspaceOperations.enqueue(key, () => new Promise(acquired.resolve));
|
|
994
|
+
releases.push(await acquired.promise);
|
|
995
|
+
}
|
|
996
|
+
return { release: () => {
|
|
997
|
+
for (const release of releases.toReversed()) release();
|
|
998
|
+
} };
|
|
999
|
+
}
|
|
921
1000
|
async prepareExec(params) {
|
|
922
1001
|
const remoteWorkdir = params.workdir ?? this.params.remoteWorkspaceDir;
|
|
923
|
-
const preparedWorkspace = this.consumePreparedRemoteWorkspaceForNextExec(remoteWorkdir);
|
|
924
1002
|
const remoteCommand = buildValidatedExecRemoteCommand({
|
|
925
1003
|
command: params.command,
|
|
926
1004
|
workdir: remoteWorkdir,
|
|
927
|
-
env:
|
|
1005
|
+
env: {}
|
|
928
1006
|
});
|
|
929
|
-
|
|
930
|
-
const sshSession = await createOpenShellSshSession({ context: this.params.execContext });
|
|
931
|
-
return {
|
|
932
|
-
argv: [
|
|
933
|
-
"ssh",
|
|
934
|
-
"-F",
|
|
935
|
-
sshSession.configPath,
|
|
936
|
-
...params.usePty ? [
|
|
937
|
-
"-tt",
|
|
938
|
-
"-o",
|
|
939
|
-
"RequestTTY=force",
|
|
940
|
-
"-o",
|
|
941
|
-
"SetEnv=TERM=xterm-256color"
|
|
942
|
-
] : [
|
|
943
|
-
"-T",
|
|
944
|
-
"-o",
|
|
945
|
-
"RequestTTY=no"
|
|
946
|
-
],
|
|
947
|
-
sshSession.host,
|
|
948
|
-
remoteCommand
|
|
949
|
-
],
|
|
950
|
-
token: { sshSession }
|
|
951
|
-
};
|
|
952
|
-
}
|
|
953
|
-
async validateWorkdir(workdir) {
|
|
954
|
-
const preparedWorkspace = this.prepareRemoteWorkspaceForExec();
|
|
955
|
-
const reusablePreparation = {
|
|
956
|
-
workdir,
|
|
957
|
-
promise: preparedWorkspace
|
|
958
|
-
};
|
|
959
|
-
this.preparedRemoteWorkspaceForNextExec = reusablePreparation;
|
|
1007
|
+
const workspaceLease = this.params.execContext.config.mode === "mirror" ? await this.acquireWorkspaceLease() : void 0;
|
|
960
1008
|
try {
|
|
961
|
-
await
|
|
1009
|
+
await this.ensureSandboxExists();
|
|
1010
|
+
if (workspaceLease) {
|
|
1011
|
+
await this.syncWorkspaceToRemote();
|
|
1012
|
+
this.remoteSeedPending = false;
|
|
1013
|
+
}
|
|
962
1014
|
const sshSession = await createOpenShellSshSession({ context: this.params.execContext });
|
|
963
1015
|
try {
|
|
964
|
-
const
|
|
1016
|
+
const prepared = await prepareSshSandboxExec({
|
|
965
1017
|
session: sshSession,
|
|
966
|
-
remoteCommand
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
}),
|
|
970
|
-
allowFailure: true
|
|
1018
|
+
remoteCommand,
|
|
1019
|
+
env: params.env,
|
|
1020
|
+
tty: params.usePty
|
|
971
1021
|
});
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1022
|
+
return {
|
|
1023
|
+
argv: prepared.argv,
|
|
1024
|
+
token: {
|
|
1025
|
+
workspaceLease,
|
|
1026
|
+
cleanup: async () => {
|
|
1027
|
+
try {
|
|
1028
|
+
await prepared.cleanup();
|
|
1029
|
+
} finally {
|
|
1030
|
+
await disposeSshSandboxSession(sshSession);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
} catch (error) {
|
|
979
1036
|
await disposeSshSandboxSession(sshSession);
|
|
1037
|
+
throw error;
|
|
980
1038
|
}
|
|
981
1039
|
} catch (error) {
|
|
982
|
-
|
|
1040
|
+
workspaceLease?.release();
|
|
983
1041
|
throw error;
|
|
984
1042
|
}
|
|
985
1043
|
}
|
|
1044
|
+
async validateWorkdir(workdir) {
|
|
1045
|
+
if (this.params.execContext.config.mode === "mirror") return await this.runWorkspaceOperation(() => this.validateMirrorWorkdir(workdir));
|
|
1046
|
+
await this.ensureSandboxExists();
|
|
1047
|
+
const sshSession = await createOpenShellSshSession({ context: this.params.execContext });
|
|
1048
|
+
try {
|
|
1049
|
+
const result = await runSshSandboxCommand({
|
|
1050
|
+
session: sshSession,
|
|
1051
|
+
remoteCommand: buildRemoteWorkdirValidationCommand({
|
|
1052
|
+
workdir,
|
|
1053
|
+
root: this.resolveWorkdirValidationRoot(workdir)
|
|
1054
|
+
}),
|
|
1055
|
+
allowFailure: true
|
|
1056
|
+
});
|
|
1057
|
+
return result.code === 0 ? result.stdout.toString("utf8").trim() || null : null;
|
|
1058
|
+
} finally {
|
|
1059
|
+
await disposeSshSandboxSession(sshSession);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
986
1062
|
resolveWorkdirValidationRoot(workdir) {
|
|
987
1063
|
try {
|
|
988
1064
|
const normalized = normalizeRemotePath(workdir);
|
|
989
|
-
return [
|
|
1065
|
+
return resolveOpenShellWorkspaceRoot([{
|
|
1066
|
+
remote: normalizeRemotePath(this.params.remoteWorkspaceDir),
|
|
1067
|
+
owner: "workspace",
|
|
1068
|
+
value: void 0
|
|
1069
|
+
}, {
|
|
1070
|
+
remote: normalizeRemotePath(this.params.remoteAgentWorkspaceDir),
|
|
1071
|
+
owner: "agent",
|
|
1072
|
+
value: void 0
|
|
1073
|
+
}], normalized)?.remote ?? this.params.remoteWorkspaceDir;
|
|
990
1074
|
} catch {
|
|
991
1075
|
return this.params.remoteWorkspaceDir;
|
|
992
1076
|
}
|
|
993
1077
|
}
|
|
994
|
-
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
return;
|
|
1078
|
+
async validateMirrorWorkdir(workdir) {
|
|
1079
|
+
const normalized = normalizeRemotePath(workdir);
|
|
1080
|
+
const roots = this.workspaceUploadRoots();
|
|
1081
|
+
if (!roots.some((root) => isOpenShellRemotePathInside(root.remote, normalized))) return null;
|
|
1082
|
+
const { cfg, skillsWorkspaceDir } = this.params.createParams;
|
|
1083
|
+
if (cfg.workspaceAccess === "rw" && skillsWorkspaceDir) roots.push({
|
|
1084
|
+
remote: resolveRemoteMaterializedSkillsWorkspaceDir(this.params.remoteWorkspaceDir),
|
|
1085
|
+
local: skillsWorkspaceDir,
|
|
1086
|
+
owner: "workspace"
|
|
1087
|
+
});
|
|
1088
|
+
let createdAncestor = false;
|
|
1089
|
+
for (const root of roots.toReversed()) {
|
|
1090
|
+
if (!isOpenShellRemotePathInside(root.remote, normalized)) {
|
|
1091
|
+
createdAncestor ||= isOpenShellRemotePathInside(normalized, root.remote);
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
if (createdAncestor) return normalized;
|
|
1095
|
+
const relative = path.posix.relative(root.remote, normalized);
|
|
1096
|
+
if (!relative) return normalized;
|
|
1097
|
+
const parts = relative.split("/");
|
|
1098
|
+
if (DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS.some((excluded) => excluded === normalizeLowercaseStringOrEmpty(parts[0]))) return null;
|
|
1099
|
+
let local = root.local;
|
|
1100
|
+
for (const part of root.local === skillsWorkspaceDir ? ["", ...parts] : parts) {
|
|
1101
|
+
local = path.join(local, part);
|
|
1102
|
+
if (!(await fsPromises.lstat(local).catch(() => null))?.isDirectory()) return null;
|
|
1103
|
+
}
|
|
1104
|
+
return normalized;
|
|
1011
1105
|
}
|
|
1012
|
-
|
|
1106
|
+
return createdAncestor ? normalized : null;
|
|
1013
1107
|
}
|
|
1014
1108
|
async finalizeExec(token) {
|
|
1109
|
+
const workspaceLease = token?.workspaceLease ?? (this.params.execContext.config.mode === "mirror" ? await this.acquireWorkspaceLease() : void 0);
|
|
1015
1110
|
try {
|
|
1016
1111
|
if (this.params.execContext.config.mode === "mirror") await this.syncWorkspaceFromRemote();
|
|
1017
1112
|
} finally {
|
|
1018
|
-
|
|
1113
|
+
try {
|
|
1114
|
+
await token?.cleanup();
|
|
1115
|
+
} finally {
|
|
1116
|
+
workspaceLease?.release();
|
|
1117
|
+
}
|
|
1019
1118
|
}
|
|
1020
1119
|
}
|
|
1021
1120
|
async runRemoteShellScript(params) {
|
|
1022
1121
|
await this.ensureSandboxExists();
|
|
1023
|
-
if (
|
|
1122
|
+
if (this.params.execContext.config.mode === "mirror") await this.maybeSeedRemoteWorkspace();
|
|
1024
1123
|
return await this.runRemoteShellScriptInternal(params);
|
|
1025
1124
|
}
|
|
1026
1125
|
async mkdirpRemotePath(remotePath, signal) {
|
|
@@ -1125,7 +1224,7 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1125
1224
|
"--no-git-ignore",
|
|
1126
1225
|
this.params.execContext.sandboxName,
|
|
1127
1226
|
localPath,
|
|
1128
|
-
|
|
1227
|
+
remotePath
|
|
1129
1228
|
],
|
|
1130
1229
|
cwd: this.params.createParams.workspaceDir
|
|
1131
1230
|
});
|
|
@@ -1140,8 +1239,16 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1140
1239
|
}
|
|
1141
1240
|
resolveRemoteTarget(remotePath) {
|
|
1142
1241
|
const normalized = normalizeRemotePath(remotePath);
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1242
|
+
const root = resolveOpenShellWorkspaceRoot([{
|
|
1243
|
+
remote: normalizeRemotePath(this.params.remoteWorkspaceDir),
|
|
1244
|
+
owner: "workspace",
|
|
1245
|
+
value: void 0
|
|
1246
|
+
}, {
|
|
1247
|
+
remote: normalizeRemotePath(this.params.remoteAgentWorkspaceDir),
|
|
1248
|
+
owner: "agent",
|
|
1249
|
+
value: void 0
|
|
1250
|
+
}], normalized)?.remote;
|
|
1251
|
+
if (root) {
|
|
1145
1252
|
const relativePath = path.posix.relative(root, normalized);
|
|
1146
1253
|
return {
|
|
1147
1254
|
root,
|
|
@@ -1152,7 +1259,11 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1152
1259
|
}
|
|
1153
1260
|
async ensureSandboxExists() {
|
|
1154
1261
|
if (this.ensurePromise) return await this.ensurePromise;
|
|
1155
|
-
|
|
1262
|
+
const initialize = async () => {
|
|
1263
|
+
await this.ensureSandboxExistsInner();
|
|
1264
|
+
if (this.params.execContext.config.mode === "remote" && !await this.maybeSeedRemoteWorkspace()) await this.syncSkillsWorkspaceToRemote();
|
|
1265
|
+
};
|
|
1266
|
+
this.ensurePromise = this.params.execContext.config.mode === "remote" ? this.runWorkspaceOperation(initialize) : initialize();
|
|
1156
1267
|
try {
|
|
1157
1268
|
await this.ensurePromise;
|
|
1158
1269
|
} catch (error) {
|
|
@@ -1176,9 +1287,12 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1176
1287
|
if (!phase) throw this.buildLegacyRuntimeUnavailableError("OpenShell did not report a lifecycle phase for this sandbox.");
|
|
1177
1288
|
if (phase !== "Ready") throw this.buildLegacyRuntimeUnavailableError(`OpenShell reports phase "${phase}".`);
|
|
1178
1289
|
}
|
|
1290
|
+
if (this.params.execContext.config.mode === "remote" && await this.remoteManagedRootsEmpty()) this.remoteSeedPending = true;
|
|
1179
1291
|
return;
|
|
1180
1292
|
}
|
|
1181
1293
|
if (this.params.legacyRuntimeAdopted) throw this.buildLegacyRuntimeUnavailableError(getResult.stderr.trim());
|
|
1294
|
+
if (!/\bsandbox not found\b/iu.test(getResult.stderr)) throw new Error(getResult.stderr.trim() || "openshell sandbox get failed");
|
|
1295
|
+
const detachedCreateSupported = await this.supportsDetachedSandboxCreation();
|
|
1182
1296
|
const createArgs = [
|
|
1183
1297
|
"sandbox",
|
|
1184
1298
|
"create",
|
|
@@ -1190,8 +1304,12 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1190
1304
|
...this.params.execContext.config.gpu ? ["--gpu"] : [],
|
|
1191
1305
|
...this.params.execContext.config.autoProviders ? ["--auto-providers"] : ["--no-auto-providers"],
|
|
1192
1306
|
...this.params.execContext.config.providers.flatMap((provider) => ["--provider", provider]),
|
|
1193
|
-
|
|
1194
|
-
|
|
1307
|
+
...detachedCreateSupported ? [
|
|
1308
|
+
"--detach",
|
|
1309
|
+
"--",
|
|
1310
|
+
"sleep",
|
|
1311
|
+
"infinity"
|
|
1312
|
+
] : ["--", "true"]
|
|
1195
1313
|
];
|
|
1196
1314
|
const createResult = await runOpenShellCli({
|
|
1197
1315
|
context: this.params.execContext,
|
|
@@ -1202,6 +1320,41 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1202
1320
|
if (createResult.code !== 0) throw new Error(createResult.stderr.trim() || "openshell sandbox create failed");
|
|
1203
1321
|
this.remoteSeedPending = true;
|
|
1204
1322
|
}
|
|
1323
|
+
async supportsDetachedSandboxCreation() {
|
|
1324
|
+
const { config } = this.params.execContext;
|
|
1325
|
+
const cliIdentity = JSON.stringify([
|
|
1326
|
+
config.command,
|
|
1327
|
+
config.gatewayEndpoint ?? "",
|
|
1328
|
+
config.gateway ?? "",
|
|
1329
|
+
config.workspace ?? process.env.OPENSHELL_WORKSPACE ?? ""
|
|
1330
|
+
]);
|
|
1331
|
+
let support = openShellDetachedCreateSupport?.key === cliIdentity ? openShellDetachedCreateSupport.promise : void 0;
|
|
1332
|
+
if (!support) {
|
|
1333
|
+
support = (async () => {
|
|
1334
|
+
const result = await runOpenShellCli({
|
|
1335
|
+
context: this.params.execContext,
|
|
1336
|
+
args: [
|
|
1337
|
+
"sandbox",
|
|
1338
|
+
"create",
|
|
1339
|
+
"--help"
|
|
1340
|
+
],
|
|
1341
|
+
cwd: this.params.createParams.workspaceDir
|
|
1342
|
+
});
|
|
1343
|
+
if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox create capability check failed");
|
|
1344
|
+
return /^\s*--detach(?:\s|$)/mu.test(result.stdout);
|
|
1345
|
+
})();
|
|
1346
|
+
openShellDetachedCreateSupport = {
|
|
1347
|
+
key: cliIdentity,
|
|
1348
|
+
promise: support
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
try {
|
|
1352
|
+
return await support;
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
if (openShellDetachedCreateSupport?.promise === support) openShellDetachedCreateSupport = void 0;
|
|
1355
|
+
throw error;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1205
1358
|
async resolveLegacyRuntimePhase() {
|
|
1206
1359
|
const pageSize = 100;
|
|
1207
1360
|
for (let offset = 0;; offset += pageSize) {
|
|
@@ -1234,18 +1387,37 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1234
1387
|
`OpenClaw will not recreate this retired runtime name. Run \`${recreateCommand}\` to migrate this scope to the current naming format.`
|
|
1235
1388
|
].filter(Boolean).join(" "));
|
|
1236
1389
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1390
|
+
workspaceUploadRoots() {
|
|
1391
|
+
const { createParams, remoteWorkspaceDir, remoteAgentWorkspaceDir } = this.params;
|
|
1392
|
+
const roots = [{
|
|
1393
|
+
remote: remoteWorkspaceDir,
|
|
1394
|
+
owner: "workspace",
|
|
1395
|
+
value: createParams.workspaceDir
|
|
1396
|
+
}];
|
|
1397
|
+
if (createParams.cfg.workspaceAccess !== "none" && path.resolve(createParams.agentWorkspaceDir) !== path.resolve(createParams.workspaceDir)) roots.push({
|
|
1398
|
+
remote: remoteAgentWorkspaceDir,
|
|
1399
|
+
owner: "agent",
|
|
1400
|
+
value: createParams.agentWorkspaceDir
|
|
1241
1401
|
});
|
|
1242
|
-
|
|
1243
|
-
|
|
1402
|
+
return orderOpenShellWorkspaceRoots(roots).map(({ remote, owner, value: local }) => ({
|
|
1403
|
+
remote,
|
|
1404
|
+
local,
|
|
1405
|
+
owner
|
|
1406
|
+
}));
|
|
1407
|
+
}
|
|
1408
|
+
async syncWorkspaceToRemote() {
|
|
1409
|
+
const roots = this.workspaceUploadRoots();
|
|
1410
|
+
for (const [index, root] of roots.entries()) {
|
|
1411
|
+
const containingRoot = roots.slice(0, index).toReversed().find((candidate) => isOpenShellRemotePathInside(candidate.remote, root.remote));
|
|
1244
1412
|
await this.runRemoteShellScriptInternal({
|
|
1245
|
-
script:
|
|
1246
|
-
args: [
|
|
1413
|
+
script: `${ENSURE_OPEN_SHELL_REMOTE_REAL_DIRECTORY_SCRIPT}\nfind "$1" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`,
|
|
1414
|
+
args: [
|
|
1415
|
+
root.remote,
|
|
1416
|
+
containingRoot?.remote ?? root.remote,
|
|
1417
|
+
containingRoot ? "1" : "0"
|
|
1418
|
+
]
|
|
1247
1419
|
});
|
|
1248
|
-
await this.uploadPathToRemote(
|
|
1420
|
+
await this.uploadPathToRemote(root.local, root.remote);
|
|
1249
1421
|
}
|
|
1250
1422
|
await this.syncSkillsWorkspaceToRemote();
|
|
1251
1423
|
}
|
|
@@ -1261,40 +1433,62 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1261
1433
|
await this.uploadPathToRemote(this.params.createParams.skillsWorkspaceDir, remoteSkillsWorkspaceDir);
|
|
1262
1434
|
}
|
|
1263
1435
|
async syncWorkspaceFromRemote() {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
this.params.execContext
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
workspaceDir: this.params.createParams.workspaceDir,
|
|
1283
|
-
tmpDir
|
|
1284
|
-
});
|
|
1285
|
-
try {
|
|
1286
|
-
await replaceDirectoryContents({
|
|
1287
|
-
sourceDir: tmpDir,
|
|
1288
|
-
targetDir: this.params.createParams.workspaceDir,
|
|
1289
|
-
excludeDirs: DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS
|
|
1290
|
-
});
|
|
1291
|
-
} finally {
|
|
1292
|
-
await restoreMaterializedSkillsShadow({
|
|
1293
|
-
workspaceDir: this.params.createParams.workspaceDir,
|
|
1294
|
-
preserved: preservedSandboxSkills
|
|
1436
|
+
const remoteSkillsWorkspaceDir = resolveRemoteMaterializedSkillsWorkspaceDir(this.params.remoteWorkspaceDir);
|
|
1437
|
+
const roots = this.workspaceUploadRoots();
|
|
1438
|
+
for (const [index, root] of roots.entries()) {
|
|
1439
|
+
if (root.owner === "agent") continue;
|
|
1440
|
+
await withTempWorkspace({
|
|
1441
|
+
rootDir: resolveOpenShellTmpRoot(),
|
|
1442
|
+
prefix: "openclaw-openshell-sync-"
|
|
1443
|
+
}, async ({ dir: tmpDir }) => {
|
|
1444
|
+
const result = await runOpenShellCli({
|
|
1445
|
+
context: this.params.execContext,
|
|
1446
|
+
args: [
|
|
1447
|
+
"sandbox",
|
|
1448
|
+
"download",
|
|
1449
|
+
this.params.execContext.sandboxName,
|
|
1450
|
+
root.remote,
|
|
1451
|
+
tmpDir
|
|
1452
|
+
],
|
|
1453
|
+
cwd: this.params.createParams.workspaceDir
|
|
1295
1454
|
});
|
|
1296
|
-
|
|
1297
|
-
|
|
1455
|
+
if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox download failed");
|
|
1456
|
+
const preservedShadows = [];
|
|
1457
|
+
try {
|
|
1458
|
+
for (const shadowedRoot of roots.slice(index + 1)) {
|
|
1459
|
+
if (root.remote === shadowedRoot.remote || !isOpenShellRemotePathInside(root.remote, shadowedRoot.remote)) continue;
|
|
1460
|
+
const relativeParts = path.posix.relative(root.remote, shadowedRoot.remote).split("/").filter(Boolean);
|
|
1461
|
+
await removeDownloadedWorkspacePath(tmpDir, relativeParts);
|
|
1462
|
+
const preserved = await moveLocalShadowAside({
|
|
1463
|
+
workspaceDir: root.local,
|
|
1464
|
+
tmpDir,
|
|
1465
|
+
relativeParts
|
|
1466
|
+
});
|
|
1467
|
+
if (preserved) preservedShadows.push(preserved);
|
|
1468
|
+
}
|
|
1469
|
+
const relativeSkillsPath = path.posix.relative(root.remote, remoteSkillsWorkspaceDir);
|
|
1470
|
+
if (relativeSkillsPath === "" || !relativeSkillsPath.startsWith("../") && !path.posix.isAbsolute(relativeSkillsPath)) await removeDownloadedWorkspacePath(tmpDir, relativeSkillsPath.split("/").filter(Boolean));
|
|
1471
|
+
if (root.owner === "workspace") {
|
|
1472
|
+
const preserved = await moveLocalShadowAside({
|
|
1473
|
+
workspaceDir: root.local,
|
|
1474
|
+
tmpDir,
|
|
1475
|
+
relativeParts: MATERIALIZED_SKILLS_REMOTE_PARTS
|
|
1476
|
+
});
|
|
1477
|
+
if (preserved) preservedShadows.push(preserved);
|
|
1478
|
+
}
|
|
1479
|
+
await replaceDirectoryContents({
|
|
1480
|
+
sourceDir: tmpDir,
|
|
1481
|
+
targetDir: root.local,
|
|
1482
|
+
excludeDirs: DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS
|
|
1483
|
+
});
|
|
1484
|
+
} finally {
|
|
1485
|
+
for (const preserved of preservedShadows.toReversed()) await restoreLocalShadow({
|
|
1486
|
+
workspaceDir: root.local,
|
|
1487
|
+
preserved
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1298
1492
|
}
|
|
1299
1493
|
async uploadPathToRemote(localPath, remotePath) {
|
|
1300
1494
|
await withTempWorkspace({
|
|
@@ -1305,10 +1499,10 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1305
1499
|
const stagedRoot = path.join(tmpDir, remoteRootName);
|
|
1306
1500
|
await stageDirectoryContents({
|
|
1307
1501
|
sourceDir: localPath,
|
|
1308
|
-
targetDir: stagedRoot
|
|
1502
|
+
targetDir: stagedRoot,
|
|
1503
|
+
excludeDirs: DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS
|
|
1309
1504
|
});
|
|
1310
|
-
const
|
|
1311
|
-
for (const entry of stagedEntries) {
|
|
1505
|
+
for (const entry of (await fsPromises.readdir(stagedRoot)).toSorted()) {
|
|
1312
1506
|
const result = await runOpenShellCli({
|
|
1313
1507
|
context: this.params.execContext,
|
|
1314
1508
|
args: buildOpenShellDirectoryUploadArgs({
|
|
@@ -1322,6 +1516,12 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1322
1516
|
}
|
|
1323
1517
|
});
|
|
1324
1518
|
}
|
|
1519
|
+
async remoteManagedRootsEmpty() {
|
|
1520
|
+
return (await this.runRemoteShellScriptInternal({
|
|
1521
|
+
script: REMOTE_MANAGED_ROOTS_EMPTY_SCRIPT,
|
|
1522
|
+
args: [this.params.remoteWorkspaceDir, this.params.remoteAgentWorkspaceDir]
|
|
1523
|
+
})).stdout.toString("utf8").trim() === "0";
|
|
1524
|
+
}
|
|
1325
1525
|
async maybeSeedRemoteWorkspace() {
|
|
1326
1526
|
if (!this.remoteSeedPending) return false;
|
|
1327
1527
|
this.remoteSeedPending = false;
|
|
@@ -1386,17 +1586,33 @@ function parseOpenShellSandboxPhasePage(stdout, sandboxName) {
|
|
|
1386
1586
|
return;
|
|
1387
1587
|
}
|
|
1388
1588
|
}
|
|
1589
|
+
function parseOpenShellSandboxPhase(stdout) {
|
|
1590
|
+
try {
|
|
1591
|
+
const parsed = JSON.parse(stdout);
|
|
1592
|
+
if (typeof parsed !== "object" || parsed === null || !("phase" in parsed)) return;
|
|
1593
|
+
return typeof parsed.phase === "string" ? parsed.phase : void 0;
|
|
1594
|
+
} catch {
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1389
1598
|
function resolveRemoteMaterializedSkillsWorkspaceDir(remoteWorkspaceDir) {
|
|
1390
1599
|
const root = remoteWorkspaceDir.replace(/\\/g, "/").replace(/\/+$/, "") || "/";
|
|
1391
1600
|
return path.posix.join(root, ...MATERIALIZED_SKILLS_REMOTE_PARTS);
|
|
1392
1601
|
}
|
|
1393
|
-
async function
|
|
1602
|
+
async function removeDownloadedWorkspacePath(tmpDir, parts) {
|
|
1603
|
+
if (parts.length === 0) {
|
|
1604
|
+
for (const entry of await fsPromises.readdir(tmpDir)) await fsPromises.rm(path.join(tmpDir, entry), {
|
|
1605
|
+
recursive: true,
|
|
1606
|
+
force: true
|
|
1607
|
+
});
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1394
1610
|
let cursor = tmpDir;
|
|
1395
|
-
for (const [index, part] of
|
|
1611
|
+
for (const [index, part] of parts.entries()) {
|
|
1396
1612
|
const next = path.join(cursor, part);
|
|
1397
1613
|
const stats = await fsPromises.lstat(next).catch(() => null);
|
|
1398
1614
|
if (!stats) return;
|
|
1399
|
-
if (index ===
|
|
1615
|
+
if (index === parts.length - 1) {
|
|
1400
1616
|
await fsPromises.rm(next, {
|
|
1401
1617
|
recursive: true,
|
|
1402
1618
|
force: true
|
|
@@ -1413,31 +1629,31 @@ async function removeMaterializedSkillsFromDownloadedWorkspace(tmpDir) {
|
|
|
1413
1629
|
cursor = next;
|
|
1414
1630
|
}
|
|
1415
1631
|
}
|
|
1416
|
-
async function
|
|
1417
|
-
const shadowPath = path.join(params.workspaceDir, ...
|
|
1632
|
+
async function moveLocalShadowAside(params) {
|
|
1633
|
+
const shadowPath = path.join(params.workspaceDir, ...params.relativeParts);
|
|
1418
1634
|
const parentStats = await fsPromises.lstat(path.dirname(shadowPath)).catch(() => null);
|
|
1419
1635
|
if (!parentStats?.isDirectory() || parentStats.isSymbolicLink()) return;
|
|
1420
1636
|
const shadowStats = await fsPromises.lstat(shadowPath).catch(() => null);
|
|
1421
1637
|
if (!shadowStats || shadowStats.isSymbolicLink()) return;
|
|
1422
1638
|
const preserveRoot = await fsPromises.mkdtemp(path.join(path.dirname(params.tmpDir), "openclaw-openshell-preserve-"));
|
|
1423
|
-
const preservedPath = path.join(preserveRoot, "
|
|
1639
|
+
const preservedPath = path.join(preserveRoot, "shadow");
|
|
1424
1640
|
await movePathWithCopyFallback({
|
|
1425
1641
|
from: shadowPath,
|
|
1426
1642
|
to: preservedPath
|
|
1427
1643
|
});
|
|
1428
1644
|
return {
|
|
1429
1645
|
preservedPath,
|
|
1430
|
-
preserveRoot
|
|
1646
|
+
preserveRoot,
|
|
1647
|
+
relativeParts: params.relativeParts
|
|
1431
1648
|
};
|
|
1432
1649
|
}
|
|
1433
|
-
async function
|
|
1434
|
-
if (!params.preserved) return;
|
|
1650
|
+
async function restoreLocalShadow(params) {
|
|
1435
1651
|
let restored = false;
|
|
1436
1652
|
try {
|
|
1437
|
-
const shadowPath = path.join(params.workspaceDir, ...
|
|
1653
|
+
const shadowPath = path.join(params.workspaceDir, ...params.preserved.relativeParts);
|
|
1438
1654
|
const parentPath = path.dirname(shadowPath);
|
|
1439
1655
|
const parentStats = await fsPromises.lstat(parentPath).catch(() => null);
|
|
1440
|
-
if (parentStats?.isSymbolicLink()) throw new Error(`Refusing to restore
|
|
1656
|
+
if (parentStats?.isSymbolicLink()) throw new Error(`Refusing to restore workspace shadow through symlink parent: ${parentPath}`);
|
|
1441
1657
|
if (parentStats && !parentStats.isDirectory()) await fsPromises.rm(parentPath, {
|
|
1442
1658
|
recursive: true,
|
|
1443
1659
|
force: true
|
|
@@ -1467,10 +1683,6 @@ function normalizeRemotePath(remotePath) {
|
|
|
1467
1683
|
if (!path.posix.isAbsolute(normalized)) throw new Error(`OpenShell remote path must be absolute: ${remotePath}`);
|
|
1468
1684
|
return normalized;
|
|
1469
1685
|
}
|
|
1470
|
-
function isRemotePathInside(root, candidate) {
|
|
1471
|
-
const relative = path.posix.relative(root, candidate);
|
|
1472
|
-
return relative === "" || relative !== ".." && !relative.startsWith("../") && !path.posix.isAbsolute(relative);
|
|
1473
|
-
}
|
|
1474
1686
|
//#endregion
|
|
1475
1687
|
//#region extensions/openshell/index.ts
|
|
1476
1688
|
var openshell_default = definePluginEntry({
|
|
@@ -1481,11 +1693,18 @@ var openshell_default = definePluginEntry({
|
|
|
1481
1693
|
register(api) {
|
|
1482
1694
|
if (api.registrationMode !== "full") return;
|
|
1483
1695
|
const pluginConfig = resolveOpenShellPluginConfig(api.pluginConfig);
|
|
1484
|
-
registerSandboxBackend("openshell", {
|
|
1696
|
+
const unregister = registerSandboxBackend("openshell", {
|
|
1485
1697
|
factory: createOpenShellSandboxBackendFactory({ pluginConfig }),
|
|
1486
1698
|
manager: createOpenShellSandboxBackendManager({ pluginConfig }),
|
|
1487
1699
|
resolveWorkdir: () => pluginConfig.remoteWorkspaceDir
|
|
1488
1700
|
});
|
|
1701
|
+
api.lifecycle.registerRuntimeLifecycle({
|
|
1702
|
+
id: "openshell-sandbox-cleanup",
|
|
1703
|
+
cleanup: ({ reason, sessionKey, runId }) => {
|
|
1704
|
+
if (sessionKey !== void 0 || runId !== void 0) return;
|
|
1705
|
+
if (reason === "disable" || reason === "restart") unregister();
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1489
1708
|
}
|
|
1490
1709
|
});
|
|
1491
1710
|
//#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.
|
|
3
|
+
"version": "2026.8.2",
|
|
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.
|
|
24
|
+
"pluginApi": ">=2026.8.2"
|
|
25
25
|
},
|
|
26
26
|
"build": {
|
|
27
|
-
"openclawVersion": "2026.8.
|
|
27
|
+
"openclawVersion": "2026.8.2",
|
|
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.
|
|
44
|
+
"openclaw": ">=2026.8.2"
|
|
45
45
|
},
|
|
46
46
|
"peerDependenciesMeta": {
|
|
47
47
|
"openclaw": {
|