@openclaw/openshell-sandbox 2026.7.2-beta.5 → 2026.7.2-beta.7

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 CHANGED
@@ -4,6 +4,14 @@ Official NVIDIA OpenShell sandbox backend for OpenClaw.
4
4
 
5
5
  This plugin lets OpenClaw use OpenShell-managed sandboxes with mirrored local workspaces and SSH command execution.
6
6
 
7
+ Configuring an OpenShell workspace requires OpenShell `v0.0.88` or newer. The
8
+ plugin supports OpenShell control-plane workspaces through
9
+ `plugins.entries.openshell.config.workspace`; this is separate from OpenClaw's
10
+ local/remote filesystem workspace mode. The setting applies to the whole plugin
11
+ instance, not individual agents or sessions. When unset, the plugin preserves
12
+ the OpenShell CLI's ambient `OPENSHELL_WORKSPACE` selection, or its `default`
13
+ fallback when no ambient selection exists.
14
+
7
15
  ## Install
8
16
 
9
17
  ```bash
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
2
2
  import { buildRemoteWorkdirValidationCommand, buildValidatedExecRemoteCommand, createRemoteShellSandboxFsBridge, createSshSandboxSessionFromConfigText, createWritableRenameTargetResolver, disposeSshSandboxSession, registerSandboxBackend, resolvePreferredOpenClawTmpDir, runPluginCommandWithTimeout, runSshSandboxCommand, sanitizeEnvVars, shellEscape, withTempWorkspace } from "openclaw/plugin-sdk/sandbox";
3
+ import { createHash } from "node:crypto";
3
4
  import fsPromises from "node:fs/promises";
4
5
  import path from "node:path";
5
6
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -15,6 +16,7 @@ function buildOpenShellBaseArgv(config) {
15
16
  const argv = [config.command];
16
17
  if (config.gateway) argv.push("--gateway", config.gateway);
17
18
  if (config.gatewayEndpoint) argv.push("--gateway-endpoint", config.gatewayEndpoint);
19
+ if (config.workspace) argv.push("--workspace", config.workspace);
18
20
  return argv;
19
21
  }
20
22
  function buildRemoteCommand(argv) {
@@ -73,11 +75,13 @@ function normalizeProviders(value) {
73
75
  return providers;
74
76
  }
75
77
  const nonEmptyTrimmedString = (message) => z.string({ error: message }).trim().min(1, { error: message });
78
+ 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
79
  const OpenShellPluginConfigSchema = z.strictObject({
77
80
  mode: z.enum(["mirror", "remote"], { error: "mode must be one of mirror, remote" }).optional(),
78
81
  command: nonEmptyTrimmedString("command must be a non-empty string").optional(),
79
82
  gateway: nonEmptyTrimmedString("gateway must be a non-empty string").optional(),
80
83
  gatewayEndpoint: nonEmptyTrimmedString("gatewayEndpoint must be a non-empty string").optional(),
84
+ workspace: openShellWorkspaceName.optional(),
81
85
  from: nonEmptyTrimmedString("from must be a non-empty string").optional(),
82
86
  policy: nonEmptyTrimmedString("policy must be a non-empty string").optional(),
83
87
  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(),
@@ -120,6 +124,7 @@ function resolveOpenShellPluginConfig(value) {
120
124
  command: DEFAULT_COMMAND,
121
125
  gateway: void 0,
122
126
  gatewayEndpoint: void 0,
127
+ workspace: void 0,
123
128
  from: DEFAULT_SOURCE,
124
129
  policy: void 0,
125
130
  providers: [],
@@ -140,6 +145,7 @@ function resolveOpenShellPluginConfig(value) {
140
145
  command: cfg.command ?? DEFAULT_COMMAND,
141
146
  gateway: cfg.gateway,
142
147
  gatewayEndpoint: cfg.gatewayEndpoint,
148
+ workspace: cfg.workspace,
143
149
  from: cfg.from ?? DEFAULT_SOURCE,
144
150
  policy: cfg.policy,
145
151
  providers: normalizeProviders(cfg.providers),
@@ -185,7 +191,12 @@ var OpenShellFsBridge = class {
185
191
  allowMissingLeaf: false,
186
192
  allowFinalSymlinkForUnlink: false
187
193
  });
188
- opened = await (await root(target.mountHostRoot)).open(path.relative(target.mountHostRoot, hostPath), { hardlinks: "reject" });
194
+ const root$1 = await root(target.mountHostRoot);
195
+ if (params.maxBytes !== void 0) return (await root$1.read(path.relative(target.mountHostRoot, hostPath), {
196
+ hardlinks: "reject",
197
+ maxBytes: params.maxBytes
198
+ })).buffer;
199
+ opened = await root$1.open(path.relative(target.mountHostRoot, hostPath), { hardlinks: "reject" });
189
200
  try {
190
201
  return await opened.handle.readFile();
191
202
  } finally {
@@ -209,6 +220,27 @@ var OpenShellFsBridge = class {
209
220
  await (await root(target.mountHostRoot)).write(path.relative(target.mountHostRoot, hostPath), buffer, { mkdir: params.mkdir });
210
221
  await this.backend.syncLocalPathToRemote(hostPath, target.containerPath);
211
222
  }
223
+ async createFileExclusive(params) {
224
+ const target = this.resolveTarget(params);
225
+ const hostPath = this.requireHostPath(target);
226
+ this.ensureWritable(target, "create files");
227
+ await assertLocalPathSafety({
228
+ target,
229
+ root: target.mountHostRoot,
230
+ allowMissingLeaf: true,
231
+ allowFinalSymlinkForUnlink: false
232
+ });
233
+ const buffer = Buffer.isBuffer(params.data) ? params.data : Buffer.from(params.data, params.encoding ?? "utf8");
234
+ const root$2 = await root(target.mountHostRoot);
235
+ try {
236
+ await root$2.create(path.relative(target.mountHostRoot, hostPath), buffer, { mkdir: params.mkdir !== false });
237
+ } catch (error) {
238
+ if (error instanceof FsSafeError && error.code === "already-exists") return "exists";
239
+ throw error;
240
+ }
241
+ await this.backend.syncLocalPathToRemote(hostPath, target.containerPath);
242
+ return "created";
243
+ }
212
244
  async mkdirp(params) {
213
245
  const target = this.resolveTarget(params);
214
246
  const hostPath = this.requireHostPath(target);
@@ -395,7 +427,7 @@ async function mkdirLocalRootPath(params) {
395
427
  await (await root(params.target.mountHostRoot)).mkdir(relativePath);
396
428
  }
397
429
  async function removeLocalRootPath(params) {
398
- const root$1 = await root(params.target.mountHostRoot);
430
+ const root$3 = await root(params.target.mountHostRoot);
399
431
  const relativePath = relativeToRoot(params.target, params.hostPath);
400
432
  try {
401
433
  if (params.force === false) await fsPromises.lstat(params.hostPath);
@@ -404,13 +436,13 @@ async function removeLocalRootPath(params) {
404
436
  if (isNotFoundError(err)) return null;
405
437
  throw err;
406
438
  }))?.isSymbolicLink()) {
407
- await root$1.remove(relativePath);
439
+ await root$3.remove(relativePath);
408
440
  return;
409
441
  }
410
- await removeRootTree(root$1, relativePath);
442
+ await removeRootTree(root$3, relativePath);
411
443
  return;
412
444
  }
413
- await root$1.remove(relativePath);
445
+ await root$3.remove(relativePath);
414
446
  } catch (err) {
415
447
  if (params.force !== false && isNotFoundError(err)) return;
416
448
  throw err;
@@ -426,11 +458,11 @@ async function removeRootTree(root, relativePath, knownStats) {
426
458
  await root.remove(relativePath);
427
459
  }
428
460
  async function moveLocalRootPath(params) {
429
- const root$2 = await root(params.from.mountHostRoot);
461
+ const root$4 = await root(params.from.mountHostRoot);
430
462
  const fromRelativePath = relativeToRoot(params.from, params.fromHostPath);
431
463
  const toRelativePath = relativeToRoot(params.to, params.toHostPath);
432
- await mkdirParentPath(root$2, toRelativePath);
433
- await root$2.move(fromRelativePath, toRelativePath, { overwrite: true });
464
+ await mkdirParentPath(root$4, toRelativePath);
465
+ await root$4.move(fromRelativePath, toRelativePath, { overwrite: true });
434
466
  }
435
467
  async function mkdirParentPath(root, relativePath) {
436
468
  const parentPath = path.dirname(relativePath);
@@ -813,7 +845,11 @@ function createOpenShellSandboxBackendManager(params) {
813
845
  }
814
846
  async function createOpenShellSandboxBackend(params) {
815
847
  if ((params.createParams.cfg.docker.binds?.length ?? 0) > 0) throw new Error("OpenShell sandbox backend does not support sandbox.docker.binds.");
816
- const sandboxName = buildOpenShellSandboxName(params.createParams.scopeKey);
848
+ const resolvedSandboxName = resolveOpenShellSandboxName({
849
+ scopeKey: params.createParams.scopeKey,
850
+ registeredRuntimeIds: params.createParams.registeredRuntimeIds
851
+ });
852
+ const sandboxName = resolvedSandboxName.sandboxName;
817
853
  const execContext = {
818
854
  config: params.pluginConfig,
819
855
  sandboxName
@@ -821,6 +857,7 @@ async function createOpenShellSandboxBackend(params) {
821
857
  const impl = new OpenShellSandboxBackendImpl({
822
858
  createParams: params.createParams,
823
859
  execContext,
860
+ legacyRuntimeAdopted: resolvedSandboxName.legacyRuntimeAdopted,
824
861
  remoteWorkspaceDir: params.pluginConfig.remoteWorkspaceDir,
825
862
  remoteAgentWorkspaceDir: params.pluginConfig.remoteAgentWorkspaceDir
826
863
  });
@@ -1169,7 +1206,7 @@ var OpenShellSandboxBackendImpl = class {
1169
1206
  }
1170
1207
  }
1171
1208
  async ensureSandboxExistsInner() {
1172
- if ((await runOpenShellCli({
1209
+ const getResult = await runOpenShellCli({
1173
1210
  context: this.params.execContext,
1174
1211
  args: [
1175
1212
  "sandbox",
@@ -1177,7 +1214,16 @@ var OpenShellSandboxBackendImpl = class {
1177
1214
  this.params.execContext.sandboxName
1178
1215
  ],
1179
1216
  cwd: this.params.createParams.workspaceDir
1180
- })).code === 0) return;
1217
+ });
1218
+ if (getResult.code === 0) {
1219
+ if (this.params.legacyRuntimeAdopted) {
1220
+ const phase = await this.resolveLegacyRuntimePhase();
1221
+ if (!phase) throw this.buildLegacyRuntimeUnavailableError("OpenShell did not report a lifecycle phase for this sandbox.");
1222
+ if (phase !== "Ready") throw this.buildLegacyRuntimeUnavailableError(`OpenShell reports phase "${phase}".`);
1223
+ }
1224
+ return;
1225
+ }
1226
+ if (this.params.legacyRuntimeAdopted) throw this.buildLegacyRuntimeUnavailableError(getResult.stderr.trim());
1181
1227
  const createArgs = [
1182
1228
  "sandbox",
1183
1229
  "create",
@@ -1201,6 +1247,38 @@ var OpenShellSandboxBackendImpl = class {
1201
1247
  if (createResult.code !== 0) throw new Error(createResult.stderr.trim() || "openshell sandbox create failed");
1202
1248
  this.remoteSeedPending = true;
1203
1249
  }
1250
+ async resolveLegacyRuntimePhase() {
1251
+ const pageSize = 100;
1252
+ for (let offset = 0;; offset += pageSize) {
1253
+ const listResult = await runOpenShellCli({
1254
+ context: this.params.execContext,
1255
+ args: [
1256
+ "sandbox",
1257
+ "list",
1258
+ "--limit",
1259
+ String(pageSize),
1260
+ "--offset",
1261
+ String(offset),
1262
+ "--output",
1263
+ "json"
1264
+ ],
1265
+ cwd: this.params.createParams.workspaceDir
1266
+ });
1267
+ if (listResult.code !== 0) throw this.buildLegacyRuntimeUnavailableError(listResult.stderr.trim());
1268
+ const page = parseOpenShellSandboxPhasePage(listResult.stdout, this.params.execContext.sandboxName);
1269
+ if (!page) throw this.buildLegacyRuntimeUnavailableError("OpenShell returned malformed sandbox lifecycle data.");
1270
+ if (page.phase) return page.phase;
1271
+ if (page.count < pageSize) return;
1272
+ }
1273
+ }
1274
+ buildLegacyRuntimeUnavailableError(detail) {
1275
+ const recreateCommand = `openclaw sandbox recreate --session ${shellEscape(this.params.createParams.scopeKey)}`;
1276
+ return new Error([
1277
+ `Registered legacy OpenShell sandbox "${this.params.execContext.sandboxName}" is not usable.`,
1278
+ detail,
1279
+ `OpenClaw will not recreate this retired runtime name. Run \`${recreateCommand}\` to migrate this scope to the current naming format.`
1280
+ ].filter(Boolean).join(" "));
1281
+ }
1204
1282
  async syncWorkspaceToRemote() {
1205
1283
  await this.runRemoteShellScriptInternal({
1206
1284
  script: "mkdir -p -- \"$1\" && find \"$1\" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +",
@@ -1307,11 +1385,52 @@ function resolveOpenShellPluginConfigFromConfig(config, fallback) {
1307
1385
  return resolveOpenShellPluginConfig(pluginConfig);
1308
1386
  }
1309
1387
  function buildOpenShellSandboxName(scopeKey) {
1388
+ const trimmed = scopeKey.trim() || "session";
1389
+ if (/:workspace:[a-f0-9]{32}$/i.test(trimmed)) {
1390
+ const hash = createHash("sha256").update(trimmed).digest("hex").slice(0, 20);
1391
+ return `oc-${BigInt(`0x${hash}`).toString(36).padStart(16, "0")}`;
1392
+ }
1393
+ return `oc-${createHash("sha256").update(trimmed).digest("hex").slice(0, 16)}`;
1394
+ }
1395
+ function buildLegacyOpenShellSandboxName(scopeKey) {
1310
1396
  const trimmed = scopeKey.trim() || "session";
1311
1397
  const safe = normalizeLowercaseStringOrEmpty(trimmed).replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
1312
1398
  const hash = Array.from(trimmed).reduce((acc, char) => (acc * 33 ^ char.charCodeAt(0)) >>> 0, 5381);
1313
1399
  return `openclaw-${safe || "session"}-${hash.toString(16).slice(0, 8)}`;
1314
1400
  }
1401
+ function resolveOpenShellSandboxName(params) {
1402
+ const sandboxName = buildOpenShellSandboxName(params.scopeKey);
1403
+ if (params.registeredRuntimeIds?.includes(sandboxName)) return {
1404
+ sandboxName,
1405
+ legacyRuntimeAdopted: false
1406
+ };
1407
+ const legacySandboxName = buildLegacyOpenShellSandboxName(params.scopeKey);
1408
+ if (params.registeredRuntimeIds?.includes(legacySandboxName)) return {
1409
+ sandboxName: legacySandboxName,
1410
+ legacyRuntimeAdopted: true
1411
+ };
1412
+ return {
1413
+ sandboxName,
1414
+ legacyRuntimeAdopted: false
1415
+ };
1416
+ }
1417
+ function parseOpenShellSandboxPhasePage(stdout, sandboxName) {
1418
+ try {
1419
+ const parsed = JSON.parse(stdout);
1420
+ if (!Array.isArray(parsed)) return;
1421
+ for (const entry of parsed) {
1422
+ if (!entry || typeof entry !== "object") continue;
1423
+ const record = entry;
1424
+ if (record.name === sandboxName && typeof record.phase === "string") return {
1425
+ count: parsed.length,
1426
+ phase: record.phase
1427
+ };
1428
+ }
1429
+ return { count: parsed.length };
1430
+ } catch {
1431
+ return;
1432
+ }
1433
+ }
1315
1434
  function resolveRemoteMaterializedSkillsWorkspaceDir(remoteWorkspaceDir) {
1316
1435
  const root = remoteWorkspaceDir.replace(/\\/g, "/").replace(/\/+$/, "") || "/";
1317
1436
  return path.posix.join(root, ...MATERIALIZED_SKILLS_REMOTE_PARTS);
@@ -17,7 +17,7 @@ export type LimitFunction = {
17
17
  /**
18
18
  Discard pending promises that are waiting to run.
19
19
 
20
- This might be useful if you want to teardown the queue at the end of your program's lifecycle or discard any function calls referencing an intermediary state of your app.
20
+ This might be useful if you want to tear down the queue at the end of your program's lifecycle or discard any function calls referencing an intermediary state of your app.
21
21
 
22
22
  Note: This does not cancel promises that are already running.
23
23
 
@@ -105,7 +105,7 @@ export default function pLimit(concurrency) {
105
105
  },
106
106
  map: {
107
107
  async value(iterable, function_) {
108
- const promises = Array.from(iterable, (value, index) => this(function_, value, index));
108
+ const promises = Array.from(iterable, (value, index) => generator(function_, value, index));
109
109
  return Promise.all(promises);
110
110
  },
111
111
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "p-limit",
3
- "version": "7.3.0",
3
+ "version": "7.3.1",
4
4
  "description": "Run multiple promise-returning & async functions with limited concurrency",
5
5
  "license": "MIT",
6
6
  "repository": "sindresorhus/p-limit",
@@ -97,7 +97,7 @@ The number of promises that are waiting to run (i.e. their internal `fn` was not
97
97
 
98
98
  Discard pending promises that are waiting to run.
99
99
 
100
- This might be useful if you want to teardown the queue at the end of your program's lifecycle or discard any function calls referencing an intermediary state of your app.
100
+ This might be useful if you want to tear down the queue at the end of your program's lifecycle or discard any function calls referencing an intermediary state of your app.
101
101
 
102
102
  Note: This does not cancel promises that are already running.
103
103
 
@@ -25,6 +25,12 @@
25
25
  "type": "string",
26
26
  "minLength": 1
27
27
  },
28
+ "workspace": {
29
+ "type": "string",
30
+ "minLength": 1,
31
+ "maxLength": 19,
32
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
33
+ },
28
34
  "from": {
29
35
  "type": "string",
30
36
  "minLength": 1
@@ -78,6 +84,10 @@
78
84
  "label": "Gateway Endpoint",
79
85
  "help": "Optional OpenShell gateway endpoint passed as --gateway-endpoint."
80
86
  },
87
+ "workspace": {
88
+ "label": "OpenShell Workspace",
89
+ "help": "Existing OpenShell control-plane workspace for sandboxes, providers, policies, and inference routes. When unset, preserves the OpenShell CLI's ambient workspace selection."
90
+ },
81
91
  "from": {
82
92
  "label": "Sandbox Source",
83
93
  "help": "OpenShell sandbox source for first-time create. Defaults to openclaw."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/openshell-sandbox",
3
- "version": "2026.7.2-beta.5",
3
+ "version": "2026.7.2-beta.7",
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",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "type": "module",
10
10
  "dependencies": {
11
- "p-limit": "7.3.0",
11
+ "p-limit": "7.3.1",
12
12
  "zod": "4.4.3"
13
13
  },
14
14
  "openclaw": {
@@ -21,10 +21,10 @@
21
21
  "minHostVersion": ">=2026.5.12-beta.1"
22
22
  },
23
23
  "compat": {
24
- "pluginApi": ">=2026.7.2-beta.5"
24
+ "pluginApi": ">=2026.7.2-beta.7"
25
25
  },
26
26
  "build": {
27
- "openclawVersion": "2026.7.2-beta.5",
27
+ "openclawVersion": "2026.7.2-beta.7",
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.7.2-beta.5"
44
+ "openclaw": ">=2026.7.2-beta.7"
45
45
  },
46
46
  "peerDependenciesMeta": {
47
47
  "openclaw": {