@openclaw/openshell-sandbox 2026.8.2 → 2026.9.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.
Files changed (2) hide show
  1. package/dist/index.js +28 -76
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
2
- import { buildRemoteCommand, buildRemoteWorkdirValidationCommand, buildValidatedExecRemoteCommand, createRemoteShellSandboxFsBridge, createSshSandboxSessionFromConfigText, createWritableRenameTargetResolver, disposeSshSandboxSession, prepareSshSandboxExec, registerSandboxBackend, resolvePreferredOpenClawTmpDir, runPluginCommandWithTimeout, runSshSandboxCommand, sanitizeEnvVars, shellEscape, withTempWorkspace } from "openclaw/plugin-sdk/sandbox";
2
+ import { buildRemoteCommand, buildRemoteWorkdirValidationCommand, buildValidatedExecRemoteCommand, createRemoteShellSandboxFsBridge, createSshSandboxSessionFromConfigText, createWritableRenameTargetResolver, disposeSshSandboxSession, prepareSshSandboxExec, registerSandboxBackend, resolvePreferredOpenClawTmpDir, resolveReadOnlyWorkspaceSkillMounts, 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";
@@ -175,11 +175,6 @@ function resolveOpenShellWorkspaceRoot(roots, candidate) {
175
175
  }
176
176
  //#endregion
177
177
  //#region extensions/openshell/src/fs-bridge.ts
178
- const MATERIALIZED_SKILLS_CONTAINER_PARTS = [
179
- ".openclaw",
180
- "sandbox-skills",
181
- "skills"
182
- ];
183
178
  function createOpenShellFsBridge(params) {
184
179
  return new OpenShellFsBridge(params.sandbox, params.backend);
185
180
  }
@@ -187,7 +182,7 @@ var OpenShellFsBridge = class {
187
182
  constructor(sandbox, backend) {
188
183
  this.sandbox = sandbox;
189
184
  this.backend = backend;
190
- this.resolveRenameTargets = createWritableRenameTargetResolver((target) => this.resolveTarget(target), (target, action) => this.ensureWritable(target, action));
185
+ this.resolveRenameTargets = createWritableRenameTargetResolver((target) => this.resolveTarget(target), (target, action) => this.ensureWritable(target, action, true));
191
186
  }
192
187
  resolvePath(params) {
193
188
  const target = this.resolveTarget(params);
@@ -277,7 +272,7 @@ var OpenShellFsBridge = class {
277
272
  async remove(params) {
278
273
  const target = this.resolveTarget(params);
279
274
  const hostPath = this.requireHostPath(target);
280
- this.ensureWritable(target, "remove files");
275
+ this.ensureWritable(target, "remove files", params.recursive);
281
276
  await assertLocalPathSafety({
282
277
  target,
283
278
  root: target.mountHostRoot,
@@ -344,8 +339,16 @@ var OpenShellFsBridge = class {
344
339
  mtimeMs: stats.mtimeMs
345
340
  };
346
341
  }
347
- ensureWritable(target, action) {
348
- if (this.sandbox.workspaceAccess !== "rw" || !target.writable) throw new Error(`Sandbox path is read-only; cannot ${action}: ${target.containerPath}`);
342
+ ensureWritable(target, action, includeDescendants = false) {
343
+ if (this.sandbox.workspaceAccess === "ro" || !target.writable || includeDescendants && this.readOnlyMounts().some((mount) => isOpenShellRemotePathInside(target.containerPath, mount.containerPath))) throw new Error(`Sandbox path is read-only; cannot ${action}: ${target.containerPath}`);
344
+ }
345
+ readOnlyMounts() {
346
+ const workdirs = [this.sandbox.containerWorkdir];
347
+ if (this.sandbox.workspaceAccess !== "none" && path.resolve(this.sandbox.workspaceDir) !== path.resolve(this.sandbox.agentWorkspaceDir)) workdirs.push(this.backend.remoteAgentWorkspaceDir || "/agent");
348
+ return [...workdirs.flatMap((workdir) => resolveReadOnlyWorkspaceSkillMounts({
349
+ ...this.sandbox,
350
+ workdir
351
+ })), ...this.sandbox.readOnlyResourceMounts ?? []];
349
352
  }
350
353
  requireHostPath(target) {
351
354
  if (!target.hostPath) throw new Error(`OpenShell mirror bridge requires a local host path: ${target.containerPath}`);
@@ -357,24 +360,14 @@ var OpenShellFsBridge = class {
357
360
  const hasAgentMount = this.sandbox.workspaceAccess !== "none" && workspaceRoot !== agentRoot;
358
361
  const agentContainerRoot = (this.backend.remoteAgentWorkspaceDir || "/agent").replace(/\\/g, "/");
359
362
  const workspaceContainerRoot = this.sandbox.containerWorkdir.replace(/\\/g, "/");
360
- const skillsRoot = this.sandbox.skillsWorkspaceDir ? path.resolve(this.sandbox.skillsWorkspaceDir, "skills") : void 0;
361
- const skillsContainerRoot = path.posix.join(workspaceContainerRoot, ...MATERIALIZED_SKILLS_CONTAINER_PARTS);
362
- const workspaceSkillsShadowRoot = path.resolve(workspaceRoot, ...MATERIALIZED_SKILLS_CONTAINER_PARTS);
363
363
  const input = params.filePath.trim();
364
- if (skillsRoot && this.sandbox.workspaceAccess === "rw") {
365
- const protectedSkillTarget = resolveProtectedSkillTarget({
366
- input,
367
- skillsRoot,
368
- skillsContainerRoot
369
- });
370
- if (protectedSkillTarget) return protectedSkillTarget;
371
- }
364
+ const readOnlyMounts = this.readOnlyMounts();
372
365
  const containerMounts = [{
373
366
  remote: workspaceContainerRoot,
374
367
  owner: "workspace",
375
368
  value: {
376
369
  hostRoot: workspaceRoot,
377
- writable: this.sandbox.workspaceAccess === "rw"
370
+ writable: this.sandbox.workspaceAccess !== "ro"
378
371
  }
379
372
  }, ...hasAgentMount ? [{
380
373
  remote: agentContainerRoot,
@@ -384,6 +377,14 @@ var OpenShellFsBridge = class {
384
377
  writable: this.sandbox.workspaceAccess === "rw"
385
378
  }
386
379
  }] : []];
380
+ containerMounts.unshift(...readOnlyMounts.map((mount) => ({
381
+ remote: mount.containerPath,
382
+ owner: resolveOpenShellWorkspaceRoot(containerMounts, mount.containerPath)?.owner ?? "workspace",
383
+ value: {
384
+ hostRoot: path.resolve(mount.hostPath),
385
+ writable: false
386
+ }
387
+ })));
387
388
  const resolveContainerTarget = (containerPath) => {
388
389
  const containerMount = resolveOpenShellWorkspaceRoot(containerMounts, containerPath);
389
390
  if (!containerMount) return;
@@ -392,11 +393,10 @@ var OpenShellFsBridge = class {
392
393
  if (!isPathInside(containerMount.value.hostRoot, hostPath)) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${input}`);
393
394
  return {
394
395
  hostPath,
395
- relativePath: containerMount.owner === "agent" ? relative ? containerMount.remote + "/" + relative : containerMount.remote : relative,
396
+ relativePath: containerMount.owner === "agent" ? relative ? containerMount.remote + "/" + relative : containerMount.remote : path.posix.relative(workspaceContainerRoot, containerPath),
396
397
  containerPath: relative ? path.posix.join(containerMount.remote, relative) : containerMount.remote,
397
398
  mountHostRoot: containerMount.value.hostRoot,
398
- writable: containerMount.value.writable,
399
- source: containerMount.owner
399
+ writable: containerMount.value.writable
400
400
  };
401
401
  };
402
402
  const containerCwd = params.cwd?.replace(/\\/g, "/");
@@ -409,30 +409,14 @@ var OpenShellFsBridge = class {
409
409
  }
410
410
  const cwd = params.cwd ? path.resolve(params.cwd) : workspaceRoot;
411
411
  const hostPath = path.isAbsolute(input) ? path.resolve(input) : path.resolve(cwd, input);
412
- if (skillsRoot && this.sandbox.workspaceAccess === "rw") {
413
- const protectedSkillShadowTarget = resolveProtectedSkillShadowTarget({
414
- hostPath,
415
- workspaceSkillsShadowRoot,
416
- skillsRoot,
417
- skillsContainerRoot
418
- });
419
- if (protectedSkillShadowTarget) return protectedSkillShadowTarget;
412
+ for (const mount of readOnlyMounts) if (isPathInside(mount.hostPath, hostPath)) {
413
+ const relative = path.relative(mount.hostPath, hostPath).split(path.sep).join(path.posix.sep);
414
+ return expectResolvedContainerTarget(resolveContainerTarget(path.posix.join(mount.containerPath, relative)), input);
420
415
  }
421
416
  if (isPathInside(workspaceRoot, hostPath)) {
422
417
  const relative = path.relative(workspaceRoot, hostPath).split(path.sep).join(path.posix.sep);
423
418
  return expectResolvedContainerTarget(resolveContainerTarget(path.posix.join(workspaceContainerRoot, relative)), input);
424
419
  }
425
- if (skillsRoot && this.sandbox.workspaceAccess === "rw" && isPathInside(skillsRoot, hostPath)) {
426
- const relative = path.relative(skillsRoot, hostPath).split(path.sep).join(path.posix.sep);
427
- return {
428
- hostPath,
429
- relativePath: relative ? path.posix.join(...MATERIALIZED_SKILLS_CONTAINER_PARTS, relative) : path.posix.join(...MATERIALIZED_SKILLS_CONTAINER_PARTS),
430
- containerPath: relative ? path.posix.join(skillsContainerRoot, relative) : skillsContainerRoot,
431
- mountHostRoot: skillsRoot,
432
- writable: false,
433
- source: "protectedSkill"
434
- };
435
- }
436
420
  if (hasAgentMount && isPathInside(agentRoot, hostPath)) {
437
421
  const relative = path.relative(agentRoot, hostPath).split(path.sep).join(path.posix.sep);
438
422
  return expectResolvedContainerTarget(resolveContainerTarget(path.posix.join(agentContainerRoot, relative)), input);
@@ -530,38 +514,6 @@ async function nearestExistingDirectoryStats(params) {
530
514
  function isNotFoundError(err) {
531
515
  return err instanceof FsSafeError && err.code === "not-found" || typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
532
516
  }
533
- function resolveProtectedSkillTarget(params) {
534
- const relativeRoot = path.posix.join(...MATERIALIZED_SKILLS_CONTAINER_PARTS);
535
- const normalizedInput = path.posix.normalize(params.input.replace(/\\/g, "/"));
536
- const isAbsoluteContainer = normalizedInput === params.skillsContainerRoot || normalizedInput.startsWith(`${params.skillsContainerRoot}/`);
537
- const isRelativeContainer = normalizedInput === relativeRoot || normalizedInput.startsWith(`${relativeRoot}/`);
538
- if (!isAbsoluteContainer && !isRelativeContainer) return null;
539
- const relative = isAbsoluteContainer ? path.posix.relative(params.skillsContainerRoot, normalizedInput) : path.posix.relative(relativeRoot, normalizedInput);
540
- const safeRelative = relative === "." ? "" : relative;
541
- return {
542
- hostPath: safeRelative ? path.resolve(params.skillsRoot, ...safeRelative.split("/")) : params.skillsRoot,
543
- relativePath: safeRelative ? path.posix.join(relativeRoot, safeRelative) : relativeRoot,
544
- containerPath: safeRelative ? path.posix.join(params.skillsContainerRoot, safeRelative) : params.skillsContainerRoot,
545
- mountHostRoot: params.skillsRoot,
546
- writable: false,
547
- source: "protectedSkill"
548
- };
549
- }
550
- function resolveProtectedSkillShadowTarget(params) {
551
- if (!isPathInside(params.workspaceSkillsShadowRoot, params.hostPath)) return null;
552
- const relative = path.relative(params.workspaceSkillsShadowRoot, params.hostPath).split(path.sep).join(path.posix.sep);
553
- const safeRelative = relative === "." ? "" : relative;
554
- const hostPath = safeRelative ? path.resolve(params.skillsRoot, ...safeRelative.split("/")) : params.skillsRoot;
555
- const relativeRoot = path.posix.join(...MATERIALIZED_SKILLS_CONTAINER_PARTS);
556
- return {
557
- hostPath,
558
- relativePath: safeRelative ? path.posix.join(relativeRoot, safeRelative) : relativeRoot,
559
- containerPath: safeRelative ? path.posix.join(params.skillsContainerRoot, safeRelative) : params.skillsContainerRoot,
560
- mountHostRoot: params.skillsRoot,
561
- writable: false,
562
- source: "protectedSkill"
563
- };
564
- }
565
517
  async function assertLocalPathSafety(params) {
566
518
  if (!params.target.hostPath) throw new Error(`Missing local host path for ${params.target.containerPath}`);
567
519
  const canonicalRoot = await fsPromises.realpath(params.root).catch(() => path.resolve(params.root));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/openshell-sandbox",
3
- "version": "2026.8.2",
3
+ "version": "2026.9.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.2"
24
+ "pluginApi": ">=2026.9.1"
25
25
  },
26
26
  "build": {
27
- "openclawVersion": "2026.8.2",
27
+ "openclawVersion": "2026.9.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.2"
44
+ "openclaw": ">=2026.9.1"
45
45
  },
46
46
  "peerDependenciesMeta": {
47
47
  "openclaw": {