@openclaw/openshell-sandbox 2026.6.5-beta.1 → 2026.6.5-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -245,6 +245,11 @@ async function stageDirectoryContents(params) {
245
245
  }
246
246
  //#endregion
247
247
  //#region extensions/openshell/src/fs-bridge.ts
248
+ const MATERIALIZED_SKILLS_CONTAINER_PARTS = [
249
+ ".openclaw",
250
+ "sandbox-skills",
251
+ "skills"
252
+ ];
248
253
  function createOpenShellFsBridge(params) {
249
254
  return new OpenShellFsBridge(params.sandbox, params.backend);
250
255
  }
@@ -392,7 +397,18 @@ var OpenShellFsBridge = class {
392
397
  const hasAgentMount = this.sandbox.workspaceAccess !== "none" && workspaceRoot !== agentRoot;
393
398
  const agentContainerRoot = (this.backend.remoteAgentWorkspaceDir || "/agent").replace(/\\/g, "/");
394
399
  const workspaceContainerRoot = this.sandbox.containerWorkdir.replace(/\\/g, "/");
400
+ const skillsRoot = this.sandbox.skillsWorkspaceDir ? path.resolve(this.sandbox.skillsWorkspaceDir, "skills") : void 0;
401
+ const skillsContainerRoot = path.posix.join(workspaceContainerRoot, ...MATERIALIZED_SKILLS_CONTAINER_PARTS);
402
+ const workspaceSkillsShadowRoot = path.resolve(workspaceRoot, ...MATERIALIZED_SKILLS_CONTAINER_PARTS);
395
403
  const input = params.filePath.trim();
404
+ if (skillsRoot && this.sandbox.workspaceAccess === "rw") {
405
+ const protectedSkillTarget = resolveProtectedSkillTarget({
406
+ input,
407
+ skillsRoot,
408
+ skillsContainerRoot
409
+ });
410
+ if (protectedSkillTarget) return protectedSkillTarget;
411
+ }
396
412
  if (input.startsWith(`${workspaceContainerRoot}/`) || input === workspaceContainerRoot) {
397
413
  const relative = path.posix.relative(workspaceContainerRoot, input) || "";
398
414
  return {
@@ -417,6 +433,15 @@ var OpenShellFsBridge = class {
417
433
  }
418
434
  const cwd = params.cwd ? path.resolve(params.cwd) : workspaceRoot;
419
435
  const hostPath = path.isAbsolute(input) ? path.resolve(input) : path.resolve(cwd, input);
436
+ if (skillsRoot && this.sandbox.workspaceAccess === "rw") {
437
+ const protectedSkillShadowTarget = resolveProtectedSkillShadowTarget({
438
+ hostPath,
439
+ workspaceSkillsShadowRoot,
440
+ skillsRoot,
441
+ skillsContainerRoot
442
+ });
443
+ if (protectedSkillShadowTarget) return protectedSkillShadowTarget;
444
+ }
420
445
  if (isPathInside(workspaceRoot, hostPath)) {
421
446
  const relative = path.relative(workspaceRoot, hostPath).split(path.sep).join(path.posix.sep);
422
447
  return {
@@ -428,6 +453,17 @@ var OpenShellFsBridge = class {
428
453
  source: "workspace"
429
454
  };
430
455
  }
456
+ if (skillsRoot && this.sandbox.workspaceAccess === "rw" && isPathInside(skillsRoot, hostPath)) {
457
+ const relative = path.relative(skillsRoot, hostPath).split(path.sep).join(path.posix.sep);
458
+ return {
459
+ hostPath,
460
+ relativePath: relative ? path.posix.join(...MATERIALIZED_SKILLS_CONTAINER_PARTS, relative) : path.posix.join(...MATERIALIZED_SKILLS_CONTAINER_PARTS),
461
+ containerPath: relative ? path.posix.join(skillsContainerRoot, relative) : skillsContainerRoot,
462
+ mountHostRoot: skillsRoot,
463
+ writable: false,
464
+ source: "protectedSkill"
465
+ };
466
+ }
431
467
  if (hasAgentMount && isPathInside(agentRoot, hostPath)) {
432
468
  const relative = path.relative(agentRoot, hostPath).split(path.sep).join(path.posix.sep);
433
469
  return {
@@ -442,6 +478,38 @@ var OpenShellFsBridge = class {
442
478
  throw new Error(`Path escapes sandbox root (${workspaceRoot}): ${params.filePath}`);
443
479
  }
444
480
  };
481
+ function resolveProtectedSkillTarget(params) {
482
+ const relativeRoot = path.posix.join(...MATERIALIZED_SKILLS_CONTAINER_PARTS);
483
+ const normalizedInput = path.posix.normalize(params.input.replace(/\\/g, "/"));
484
+ const isAbsoluteContainer = normalizedInput === params.skillsContainerRoot || normalizedInput.startsWith(`${params.skillsContainerRoot}/`);
485
+ const isRelativeContainer = normalizedInput === relativeRoot || normalizedInput.startsWith(`${relativeRoot}/`);
486
+ if (!isAbsoluteContainer && !isRelativeContainer) return null;
487
+ const relative = isAbsoluteContainer ? path.posix.relative(params.skillsContainerRoot, normalizedInput) : path.posix.relative(relativeRoot, normalizedInput);
488
+ const safeRelative = relative === "." ? "" : relative;
489
+ return {
490
+ hostPath: safeRelative ? path.resolve(params.skillsRoot, ...safeRelative.split("/")) : params.skillsRoot,
491
+ relativePath: safeRelative ? path.posix.join(relativeRoot, safeRelative) : relativeRoot,
492
+ containerPath: safeRelative ? path.posix.join(params.skillsContainerRoot, safeRelative) : params.skillsContainerRoot,
493
+ mountHostRoot: params.skillsRoot,
494
+ writable: false,
495
+ source: "protectedSkill"
496
+ };
497
+ }
498
+ function resolveProtectedSkillShadowTarget(params) {
499
+ if (!isPathInside(params.workspaceSkillsShadowRoot, params.hostPath)) return null;
500
+ const relative = path.relative(params.workspaceSkillsShadowRoot, params.hostPath).split(path.sep).join(path.posix.sep);
501
+ const safeRelative = relative === "." ? "" : relative;
502
+ const hostPath = safeRelative ? path.resolve(params.skillsRoot, ...safeRelative.split("/")) : params.skillsRoot;
503
+ const relativeRoot = path.posix.join(...MATERIALIZED_SKILLS_CONTAINER_PARTS);
504
+ return {
505
+ hostPath,
506
+ relativePath: safeRelative ? path.posix.join(relativeRoot, safeRelative) : relativeRoot,
507
+ containerPath: safeRelative ? path.posix.join(params.skillsContainerRoot, safeRelative) : params.skillsContainerRoot,
508
+ mountHostRoot: params.skillsRoot,
509
+ writable: false,
510
+ source: "protectedSkill"
511
+ };
512
+ }
445
513
  async function assertLocalPathSafety(params) {
446
514
  if (!params.target.hostPath) throw new Error(`Missing local host path for ${params.target.containerPath}`);
447
515
  if (!isPathInside(await fs.realpath(params.root).catch(() => path.resolve(params.root)), await resolveCanonicalCandidate(params.target.hostPath))) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${params.target.containerPath}`);
@@ -475,6 +543,47 @@ async function resolveCanonicalCandidate(targetPath) {
475
543
  }
476
544
  //#endregion
477
545
  //#region extensions/openshell/src/backend.ts
546
+ const MATERIALIZED_SKILLS_REMOTE_PARTS = [".openclaw", "sandbox-skills"];
547
+ const ENSURE_REMOTE_REAL_DIRECTORY_SCRIPT = [
548
+ "set -e",
549
+ "target=\"$1\"",
550
+ "root=\"${2:-$1}\"",
551
+ "case \"$target\" in /*) ;; *) echo \"remote directory must be absolute: $target\" >&2; exit 1 ;; esac",
552
+ "case \"$root\" in /*) ;; *) echo \"remote root must be absolute: $root\" >&2; exit 1 ;; esac",
553
+ "target=\"${target%/}\"",
554
+ "root=\"${root%/}\"",
555
+ "[ -n \"$target\" ] || target=\"/\"",
556
+ "[ -n \"$root\" ] || root=\"/\"",
557
+ "case \"$target/\" in \"$root\"/*|\"$root/\") ;; *) echo \"remote directory must stay under root: $target\" >&2; exit 1 ;; esac",
558
+ "old_ifs=\"$IFS\"",
559
+ "IFS=\"/\"",
560
+ "set -- ${target#/} ${root#/}",
561
+ "IFS=\"$old_ifs\"",
562
+ "for part do",
563
+ " [ -n \"$part\" ] || continue",
564
+ " case \"$part\" in \".\"|\"..\") echo \"unsafe remote directory component: $part\" >&2; exit 1 ;; esac",
565
+ "done",
566
+ "if [ -L \"$root\" ]; then echo \"unsafe remote root symlink: $root\" >&2; exit 1; fi",
567
+ "mkdir -p -- \"$root\"",
568
+ "canonical_root=\"$(cd \"$root\" && pwd -P)\"",
569
+ "relative=\"${target#\"$root\"}\"",
570
+ "relative=\"${relative#/}\"",
571
+ "current=\"$canonical_root\"",
572
+ "IFS=\"/\"",
573
+ "set -- $relative",
574
+ "IFS=\"$old_ifs\"",
575
+ "for part do",
576
+ " [ -n \"$part\" ] || continue",
577
+ " if [ \"$current\" = \"/\" ]; then next=\"/$part\"; else next=\"$current/$part\"; fi",
578
+ " if [ -L \"$next\" ]; then echo \"unsafe remote directory symlink: $next\" >&2; exit 1; fi",
579
+ " if [ -e \"$next\" ]; then",
580
+ " if [ ! -d \"$next\" ]; then echo \"unsafe remote directory component: $next\" >&2; exit 1; fi",
581
+ " else",
582
+ " mkdir -- \"$next\"",
583
+ " fi",
584
+ " current=\"$next\"",
585
+ "done"
586
+ ].join("\n");
478
587
  function buildOpenShellSshExecEnv() {
479
588
  return sanitizeEnvVars(process.env).allowed;
480
589
  }
@@ -629,7 +738,7 @@ var OpenShellSandboxBackendImpl = class {
629
738
  });
630
739
  await this.ensureSandboxExists();
631
740
  if (this.params.execContext.config.mode === "mirror") await this.syncWorkspaceToRemote();
632
- else await this.maybeSeedRemoteWorkspace();
741
+ else if (!await this.maybeSeedRemoteWorkspace()) await this.syncSkillsWorkspaceToRemote();
633
742
  const sshSession = await createOpenShellSshSession({ context: this.params.execContext });
634
743
  return {
635
744
  argv: [
@@ -662,7 +771,7 @@ var OpenShellSandboxBackendImpl = class {
662
771
  }
663
772
  async runRemoteShellScript(params) {
664
773
  await this.ensureSandboxExists();
665
- await this.maybeSeedRemoteWorkspace();
774
+ if (!await this.maybeSeedRemoteWorkspace()) await this.syncSkillsWorkspaceToRemote();
666
775
  return await this.runRemoteShellScriptInternal(params);
667
776
  }
668
777
  async runRemoteShellScriptInternal(params) {
@@ -786,6 +895,18 @@ var OpenShellSandboxBackendImpl = class {
786
895
  });
787
896
  await this.uploadPathToRemote(this.params.createParams.agentWorkspaceDir, this.params.remoteAgentWorkspaceDir);
788
897
  }
898
+ await this.syncSkillsWorkspaceToRemote();
899
+ }
900
+ async syncSkillsWorkspaceToRemote() {
901
+ if (this.params.createParams.cfg.workspaceAccess !== "rw" || !this.params.createParams.skillsWorkspaceDir) return;
902
+ const remoteSkillsWorkspaceDir = resolveRemoteMaterializedSkillsWorkspaceDir(this.params.remoteWorkspaceDir);
903
+ await this.runRemoteShellScriptInternal({
904
+ script: `${ENSURE_REMOTE_REAL_DIRECTORY_SCRIPT}\nfind "$1" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`,
905
+ args: [remoteSkillsWorkspaceDir, this.params.remoteWorkspaceDir]
906
+ });
907
+ const stats = await fs.lstat(this.params.createParams.skillsWorkspaceDir).catch(() => null);
908
+ if (!stats?.isDirectory() || stats.isSymbolicLink()) return;
909
+ await this.uploadPathToRemote(this.params.createParams.skillsWorkspaceDir, remoteSkillsWorkspaceDir);
789
910
  }
790
911
  async syncWorkspaceFromRemote() {
791
912
  await withTempWorkspace({
@@ -804,11 +925,23 @@ var OpenShellSandboxBackendImpl = class {
804
925
  cwd: this.params.createParams.workspaceDir
805
926
  });
806
927
  if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox download failed");
807
- await replaceDirectoryContents({
808
- sourceDir: tmpDir,
809
- targetDir: this.params.createParams.workspaceDir,
810
- excludeDirs: DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS
928
+ await removeMaterializedSkillsFromDownloadedWorkspace(tmpDir);
929
+ const preservedSandboxSkills = await moveMaterializedSkillsShadowAside({
930
+ workspaceDir: this.params.createParams.workspaceDir,
931
+ tmpDir
811
932
  });
933
+ try {
934
+ await replaceDirectoryContents({
935
+ sourceDir: tmpDir,
936
+ targetDir: this.params.createParams.workspaceDir,
937
+ excludeDirs: DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS
938
+ });
939
+ } finally {
940
+ await restoreMaterializedSkillsShadow({
941
+ workspaceDir: this.params.createParams.workspaceDir,
942
+ preserved: preservedSandboxSkills
943
+ });
944
+ }
812
945
  });
813
946
  }
814
947
  async uploadPathToRemote(localPath, remotePath) {
@@ -836,10 +969,11 @@ var OpenShellSandboxBackendImpl = class {
836
969
  });
837
970
  }
838
971
  async maybeSeedRemoteWorkspace() {
839
- if (!this.remoteSeedPending) return;
972
+ if (!this.remoteSeedPending) return false;
840
973
  this.remoteSeedPending = false;
841
974
  try {
842
975
  await this.syncWorkspaceToRemote();
976
+ return true;
843
977
  } catch (error) {
844
978
  this.remoteSeedPending = true;
845
979
  throw error;
@@ -857,6 +991,79 @@ function buildOpenShellSandboxName(scopeKey) {
857
991
  const hash = Array.from(trimmed).reduce((acc, char) => (acc * 33 ^ char.charCodeAt(0)) >>> 0, 5381);
858
992
  return `openclaw-${safe || "session"}-${hash.toString(16).slice(0, 8)}`;
859
993
  }
994
+ function resolveRemoteMaterializedSkillsWorkspaceDir(remoteWorkspaceDir) {
995
+ const root = remoteWorkspaceDir.replace(/\\/g, "/").replace(/\/+$/, "") || "/";
996
+ return path.posix.join(root, ...MATERIALIZED_SKILLS_REMOTE_PARTS);
997
+ }
998
+ async function removeMaterializedSkillsFromDownloadedWorkspace(tmpDir) {
999
+ let cursor = tmpDir;
1000
+ for (const [index, part] of MATERIALIZED_SKILLS_REMOTE_PARTS.entries()) {
1001
+ const next = path.join(cursor, part);
1002
+ const stats = await fs.lstat(next).catch(() => null);
1003
+ if (!stats) return;
1004
+ if (index === MATERIALIZED_SKILLS_REMOTE_PARTS.length - 1) {
1005
+ await fs.rm(next, {
1006
+ recursive: true,
1007
+ force: true
1008
+ });
1009
+ return;
1010
+ }
1011
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
1012
+ await fs.rm(next, {
1013
+ recursive: true,
1014
+ force: true
1015
+ });
1016
+ return;
1017
+ }
1018
+ cursor = next;
1019
+ }
1020
+ }
1021
+ async function moveMaterializedSkillsShadowAside(params) {
1022
+ const shadowPath = path.join(params.workspaceDir, ...MATERIALIZED_SKILLS_REMOTE_PARTS);
1023
+ const parentStats = await fs.lstat(path.dirname(shadowPath)).catch(() => null);
1024
+ if (!parentStats?.isDirectory() || parentStats.isSymbolicLink()) return;
1025
+ const shadowStats = await fs.lstat(shadowPath).catch(() => null);
1026
+ if (!shadowStats || shadowStats.isSymbolicLink()) return;
1027
+ const preserveRoot = await fs.mkdtemp(path.join(path.dirname(params.tmpDir), "openclaw-openshell-preserve-"));
1028
+ const preservedPath = path.join(preserveRoot, "sandbox-skills");
1029
+ await movePathWithCopyFallback({
1030
+ from: shadowPath,
1031
+ to: preservedPath
1032
+ });
1033
+ return {
1034
+ preservedPath,
1035
+ preserveRoot
1036
+ };
1037
+ }
1038
+ async function restoreMaterializedSkillsShadow(params) {
1039
+ if (!params.preserved) return;
1040
+ let restored = false;
1041
+ try {
1042
+ const shadowPath = path.join(params.workspaceDir, ...MATERIALIZED_SKILLS_REMOTE_PARTS);
1043
+ const parentPath = path.dirname(shadowPath);
1044
+ const parentStats = await fs.lstat(parentPath).catch(() => null);
1045
+ if (parentStats?.isSymbolicLink()) throw new Error(`Refusing to restore sandbox skills through symlink parent: ${parentPath}`);
1046
+ if (parentStats && !parentStats.isDirectory()) await fs.rm(parentPath, {
1047
+ recursive: true,
1048
+ force: true
1049
+ });
1050
+ await fs.mkdir(parentPath, { recursive: true });
1051
+ await fs.rm(shadowPath, {
1052
+ recursive: true,
1053
+ force: true
1054
+ });
1055
+ await movePathWithCopyFallback({
1056
+ from: params.preserved.preservedPath,
1057
+ to: shadowPath
1058
+ });
1059
+ restored = true;
1060
+ } finally {
1061
+ if (restored) await fs.rm(params.preserved.preserveRoot, {
1062
+ recursive: true,
1063
+ force: true
1064
+ });
1065
+ }
1066
+ }
860
1067
  function resolveOpenShellTmpRoot() {
861
1068
  return path.resolve(resolvePreferredOpenClawTmpDir());
862
1069
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/openshell-sandbox",
3
- "version": "2026.6.5-beta.1",
3
+ "version": "2026.6.5-beta.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/openshell-sandbox",
9
- "version": "2026.6.5-beta.1",
9
+ "version": "2026.6.5-beta.3",
10
10
  "dependencies": {
11
11
  "zod": "4.4.3"
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/openshell-sandbox",
3
- "version": "2026.6.5-beta.1",
3
+ "version": "2026.6.5-beta.3",
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",
@@ -20,10 +20,10 @@
20
20
  "minHostVersion": ">=2026.5.12-beta.1"
21
21
  },
22
22
  "compat": {
23
- "pluginApi": ">=2026.6.5-beta.1"
23
+ "pluginApi": ">=2026.6.5-beta.3"
24
24
  },
25
25
  "build": {
26
- "openclawVersion": "2026.6.5-beta.1",
26
+ "openclawVersion": "2026.6.5-beta.3",
27
27
  "bundledDist": false
28
28
  },
29
29
  "release": {
@@ -41,7 +41,7 @@
41
41
  "README.md"
42
42
  ],
43
43
  "peerDependencies": {
44
- "openclaw": ">=2026.6.5-beta.1"
44
+ "openclaw": ">=2026.6.5-beta.3"
45
45
  },
46
46
  "peerDependenciesMeta": {
47
47
  "openclaw": {