@openclaw/openshell-sandbox 2026.6.8 → 2026.6.9

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
@@ -312,12 +312,8 @@ var OpenShellFsBridge = class {
312
312
  allowMissingLeaf: true,
313
313
  allowFinalSymlinkForUnlink: false
314
314
  });
315
+ await this.backend.mkdirpRemotePath(target.containerPath, params.signal);
315
316
  await fs.mkdir(hostPath, { recursive: true });
316
- await this.backend.runRemoteShellScript({
317
- script: "mkdir -p -- \"$1\"",
318
- args: [target.containerPath],
319
- signal: params.signal
320
- });
321
317
  }
322
318
  async remove(params) {
323
319
  const target = this.resolveTarget(params);
@@ -329,16 +325,15 @@ var OpenShellFsBridge = class {
329
325
  allowMissingLeaf: params.force !== false,
330
326
  allowFinalSymlinkForUnlink: true
331
327
  });
328
+ await this.backend.removeRemotePath(target.containerPath, {
329
+ recursive: params.recursive ?? false,
330
+ signal: params.signal,
331
+ ignoreMissing: params.force !== false
332
+ });
332
333
  await fs.rm(hostPath, {
333
334
  recursive: params.recursive ?? false,
334
335
  force: params.force !== false
335
336
  });
336
- await this.backend.runRemoteShellScript({
337
- script: params.recursive ? "rm -rf -- \"$1\"" : "if [ -d \"$1\" ] && [ ! -L \"$1\" ]; then rmdir -- \"$1\"; elif [ -e \"$1\" ] || [ -L \"$1\" ]; then rm -f -- \"$1\"; fi",
338
- args: [target.containerPath],
339
- signal: params.signal,
340
- allowFailure: params.force !== false
341
- });
342
337
  }
343
338
  async rename(params) {
344
339
  const { from, to } = this.resolveRenameTargets(params);
@@ -356,16 +351,12 @@ var OpenShellFsBridge = class {
356
351
  allowMissingLeaf: true,
357
352
  allowFinalSymlinkForUnlink: false
358
353
  });
354
+ await this.backend.renameRemotePath(from.containerPath, to.containerPath, params.signal);
359
355
  await fs.mkdir(path.dirname(toHostPath), { recursive: true });
360
356
  await movePathWithCopyFallback({
361
357
  from: fromHostPath,
362
358
  to: toHostPath
363
359
  });
364
- await this.backend.runRemoteShellScript({
365
- script: "mkdir -p -- \"$(dirname -- \"$2\")\" && mv -- \"$1\" \"$2\"",
366
- args: [from.containerPath, to.containerPath],
367
- signal: params.signal
368
- });
369
360
  }
370
361
  async stat(params) {
371
362
  const target = this.resolveTarget(params);
@@ -544,7 +535,108 @@ async function resolveCanonicalCandidate(targetPath) {
544
535
  //#endregion
545
536
  //#region extensions/openshell/src/backend.ts
546
537
  const MATERIALIZED_SKILLS_REMOTE_PARTS = [".openclaw", "sandbox-skills"];
547
- const ENSURE_REMOTE_REAL_DIRECTORY_SCRIPT = [
538
+ const PINNED_REMOTE_PATH_MUTATION_SCRIPT = [
539
+ "set -eu",
540
+ "die() { echo \"$1\" >&2; exit 1; }",
541
+ "validate_basename() {",
542
+ " case \"$1\" in \"\"|\".\"|\"..\"|*/*) die \"unsafe remote basename: $1\" ;; esac",
543
+ "}",
544
+ "pin_dir() {",
545
+ " root=\"$1\"",
546
+ " relative=\"$2\"",
547
+ " create=\"$3\"",
548
+ " case \"$root\" in /*) ;; *) die \"remote root must be absolute: $root\" ;; esac",
549
+ " root=\"${root%/}\"",
550
+ " [ -n \"$root\" ] || root=\"/\"",
551
+ " if [ -L \"$root\" ]; then die \"unsafe remote root symlink: $root\"; fi",
552
+ " mkdir -p -- \"$root\"",
553
+ " canonical_root=\"$(cd \"$root\" && pwd -P)\"",
554
+ " current=\"$canonical_root\"",
555
+ " relative=\"${relative#/}\"",
556
+ " while [ -n \"$relative\" ]; do",
557
+ " part=\"${relative%%/*}\"",
558
+ " if [ \"$part\" = \"$relative\" ]; then relative=\"\"; else relative=\"${relative#*/}\"; fi",
559
+ " [ -n \"$part\" ] || continue",
560
+ " case \"$part\" in \".\"|\"..\") die \"unsafe remote directory component: $part\" ;; esac",
561
+ " if [ \"$current\" = \"/\" ]; then next=\"/$part\"; else next=\"$current/$part\"; fi",
562
+ " if [ -L \"$next\" ]; then die \"unsafe remote directory symlink: $next\"; fi",
563
+ " if [ -e \"$next\" ]; then",
564
+ " if [ ! -d \"$next\" ]; then die \"unsafe remote directory component: $next\"; fi",
565
+ " else",
566
+ " if [ \"$create\" != \"1\" ]; then die \"remote directory not found: $next\"; fi",
567
+ " mkdir -- \"$next\"",
568
+ " fi",
569
+ " current=\"$next\"",
570
+ " done",
571
+ " printf \"%s\\n\" \"$current\"",
572
+ "}",
573
+ "pin_dir_or_missing() {",
574
+ " root=\"$1\"",
575
+ " relative=\"$2\"",
576
+ " missing_ok=\"$3\"",
577
+ " case \"$root\" in /*) ;; *) die \"remote root must be absolute: $root\" ;; esac",
578
+ " root=\"${root%/}\"",
579
+ " [ -n \"$root\" ] || root=\"/\"",
580
+ " if [ -L \"$root\" ]; then die \"unsafe remote root symlink: $root\"; fi",
581
+ " if [ ! -d \"$root\" ]; then",
582
+ " if [ -e \"$root\" ]; then die \"unsafe remote root component: $root\"; fi",
583
+ " if [ \"$missing_ok\" = \"1\" ]; then printf \"\\n\"; return 0; fi",
584
+ " die \"remote directory not found: $root\"",
585
+ " fi",
586
+ " canonical_root=\"$(cd \"$root\" && pwd -P)\"",
587
+ " current=\"$canonical_root\"",
588
+ " relative=\"${relative#/}\"",
589
+ " while [ -n \"$relative\" ]; do",
590
+ " part=\"${relative%%/*}\"",
591
+ " if [ \"$part\" = \"$relative\" ]; then relative=\"\"; else relative=\"${relative#*/}\"; fi",
592
+ " [ -n \"$part\" ] || continue",
593
+ " case \"$part\" in \".\"|\"..\") die \"unsafe remote directory component: $part\" ;; esac",
594
+ " if [ \"$current\" = \"/\" ]; then next=\"/$part\"; else next=\"$current/$part\"; fi",
595
+ " if [ -L \"$next\" ]; then die \"unsafe remote directory symlink: $next\"; fi",
596
+ " if [ -e \"$next\" ]; then",
597
+ " if [ ! -d \"$next\" ]; then die \"unsafe remote directory component: $next\"; fi",
598
+ " else",
599
+ " if [ \"$missing_ok\" = \"1\" ]; then printf \"\\n\"; return 0; fi",
600
+ " die \"remote directory not found: $next\"",
601
+ " fi",
602
+ " current=\"$next\"",
603
+ " done",
604
+ " printf \"%s\\n\" \"$current\"",
605
+ "}",
606
+ "operation=\"$1\"",
607
+ "case \"$operation\" in",
608
+ " mkdirp)",
609
+ " pin_dir \"$2\" \"$3\" 1 >/dev/null",
610
+ " ;;",
611
+ " remove)",
612
+ " validate_basename \"$4\"",
613
+ " parent=\"$(pin_dir_or_missing \"$2\" \"$3\" \"${5:-0}\")\"",
614
+ " [ -n \"$parent\" ] || exit 0",
615
+ " target=\"$parent/$4\"",
616
+ " if [ -d \"$target\" ] && [ ! -L \"$target\" ]; then rm -rf -- \"$target\"; elif [ -e \"$target\" ] || [ -L \"$target\" ]; then rm -f -- \"$target\"; fi",
617
+ " ;;",
618
+ " removefile)",
619
+ " validate_basename \"$4\"",
620
+ " parent=\"$(pin_dir_or_missing \"$2\" \"$3\" \"${5:-0}\")\"",
621
+ " [ -n \"$parent\" ] || exit 0",
622
+ " target=\"$parent/$4\"",
623
+ " if [ -d \"$target\" ] && [ ! -L \"$target\" ]; then rmdir -- \"$target\"; elif [ -e \"$target\" ] || [ -L \"$target\" ]; then rm -f -- \"$target\"; fi",
624
+ " ;;",
625
+ " rename)",
626
+ " src_parent=\"$(pin_dir \"$2\" \"$3\" 0)\"",
627
+ " validate_basename \"$4\"",
628
+ " dst_parent=\"$(pin_dir \"$5\" \"$6\" 1)\"",
629
+ " validate_basename \"$7\"",
630
+ " if [ -L \"$dst_parent/$7\" ]; then die \"unsafe remote rename target symlink: $dst_parent/$7\"; fi",
631
+ " if [ -d \"$dst_parent/$7\" ]; then die \"unsafe remote rename target directory: $dst_parent/$7\"; fi",
632
+ " mv -- \"$src_parent/$4\" \"$dst_parent/$7\"",
633
+ " ;;",
634
+ " *)",
635
+ " die \"unknown remote path mutation: $operation\"",
636
+ " ;;",
637
+ "esac"
638
+ ].join("\n");
639
+ const ENSURE_OPEN_SHELL_REMOTE_REAL_DIRECTORY_SCRIPT = [
548
640
  "set -e",
549
641
  "target=\"$1\"",
550
642
  "root=\"${2:-$1}\"",
@@ -555,13 +647,14 @@ const ENSURE_REMOTE_REAL_DIRECTORY_SCRIPT = [
555
647
  "[ -n \"$target\" ] || target=\"/\"",
556
648
  "[ -n \"$root\" ] || root=\"/\"",
557
649
  "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",
650
+ "for path_to_check in \"$target\" \"$root\"; do",
651
+ " relative=\"${path_to_check#/}\"",
652
+ " while [ -n \"$relative\" ]; do",
653
+ " part=\"${relative%%/*}\"",
654
+ " if [ \"$part\" = \"$relative\" ]; then relative=\"\"; else relative=\"${relative#*/}\"; fi",
655
+ " [ -n \"$part\" ] || continue",
656
+ " case \"$part\" in \".\"|\"..\") echo \"unsafe remote directory component: $part\" >&2; exit 1 ;; esac",
657
+ " done",
565
658
  "done",
566
659
  "if [ -L \"$root\" ]; then echo \"unsafe remote root symlink: $root\" >&2; exit 1; fi",
567
660
  "mkdir -p -- \"$root\"",
@@ -569,10 +662,9 @@ const ENSURE_REMOTE_REAL_DIRECTORY_SCRIPT = [
569
662
  "relative=\"${target#\"$root\"}\"",
570
663
  "relative=\"${relative#/}\"",
571
664
  "current=\"$canonical_root\"",
572
- "IFS=\"/\"",
573
- "set -- $relative",
574
- "IFS=\"$old_ifs\"",
575
- "for part do",
665
+ "while [ -n \"$relative\" ]; do",
666
+ " part=\"${relative%%/*}\"",
667
+ " if [ \"$part\" = \"$relative\" ]; then relative=\"\"; else relative=\"${relative#*/}\"; fi",
576
668
  " [ -n \"$part\" ] || continue",
577
669
  " if [ \"$current\" = \"/\" ]; then next=\"/$part\"; else next=\"$current/$part\"; fi",
578
670
  " if [ -L \"$next\" ]; then echo \"unsafe remote directory symlink: $next\" >&2; exit 1; fi",
@@ -680,6 +772,9 @@ async function createOpenShellSandboxBackend(params) {
680
772
  remoteWorkspaceDir: params.pluginConfig.remoteWorkspaceDir,
681
773
  remoteAgentWorkspaceDir: params.pluginConfig.remoteAgentWorkspaceDir,
682
774
  runRemoteShellScript: async (command) => await impl.runRemoteShellScript(command),
775
+ mkdirpRemotePath: async (remotePath, signal) => await impl.mkdirpRemotePath(remotePath, signal),
776
+ removeRemotePath: async (remotePath, removeParams) => await impl.removeRemotePath(remotePath, removeParams),
777
+ renameRemotePath: async (fromRemotePath, toRemotePath, signal) => await impl.renameRemotePath(fromRemotePath, toRemotePath, signal),
683
778
  syncLocalPathToRemote: async (localPath, remotePath) => await impl.syncLocalPathToRemote(localPath, remotePath)
684
779
  };
685
780
  }
@@ -727,6 +822,9 @@ var OpenShellSandboxBackendImpl = class {
727
822
  backend: this.asHandle()
728
823
  }),
729
824
  runRemoteShellScript: async (command) => await this.runRemoteShellScript(command),
825
+ mkdirpRemotePath: async (remotePath, signal) => await this.mkdirpRemotePath(remotePath, signal),
826
+ removeRemotePath: async (remotePath, removeParams) => await this.removeRemotePath(remotePath, removeParams),
827
+ renameRemotePath: async (fromRemotePath, toRemotePath, signal) => await this.renameRemotePath(fromRemotePath, toRemotePath, signal),
730
828
  syncLocalPathToRemote: async (localPath, remotePath) => await this.syncLocalPathToRemote(localPath, remotePath)
731
829
  };
732
830
  }
@@ -774,6 +872,46 @@ var OpenShellSandboxBackendImpl = class {
774
872
  if (!await this.maybeSeedRemoteWorkspace()) await this.syncSkillsWorkspaceToRemote();
775
873
  return await this.runRemoteShellScriptInternal(params);
776
874
  }
875
+ async mkdirpRemotePath(remotePath, signal) {
876
+ const target = this.resolveRemoteTarget(remotePath);
877
+ await this.runPinnedRemotePathMutation({
878
+ args: [
879
+ "mkdirp",
880
+ target.root,
881
+ target.relativePath
882
+ ],
883
+ signal
884
+ });
885
+ }
886
+ async removeRemotePath(remotePath, params) {
887
+ const target = this.resolveRemoteTarget(remotePath);
888
+ await this.runPinnedRemotePathMutation({
889
+ args: [
890
+ params?.recursive ? "remove" : "removefile",
891
+ target.root,
892
+ path.posix.dirname(target.relativePath) === "." ? "" : path.posix.dirname(target.relativePath),
893
+ path.posix.basename(target.relativePath),
894
+ params?.ignoreMissing ? "1" : "0"
895
+ ],
896
+ signal: params?.signal
897
+ });
898
+ }
899
+ async renameRemotePath(fromRemotePath, toRemotePath, signal) {
900
+ const from = this.resolveRemoteTarget(fromRemotePath);
901
+ const to = this.resolveRemoteTarget(toRemotePath);
902
+ await this.runPinnedRemotePathMutation({
903
+ args: [
904
+ "rename",
905
+ from.root,
906
+ path.posix.dirname(from.relativePath) === "." ? "" : path.posix.dirname(from.relativePath),
907
+ path.posix.basename(from.relativePath),
908
+ to.root,
909
+ path.posix.dirname(to.relativePath) === "." ? "" : path.posix.dirname(to.relativePath),
910
+ path.posix.basename(to.relativePath)
911
+ ],
912
+ signal
913
+ });
914
+ }
777
915
  async runRemoteShellScriptInternal(params) {
778
916
  const session = await createOpenShellSshSession({ context: this.params.execContext });
779
917
  try {
@@ -797,34 +935,37 @@ var OpenShellSandboxBackendImpl = class {
797
935
  async syncLocalPathToRemote(localPath, remotePath) {
798
936
  await this.ensureSandboxExists();
799
937
  await this.maybeSeedRemoteWorkspace();
938
+ const target = this.resolveRemoteTarget(remotePath);
800
939
  const stats = await fs.lstat(localPath).catch(() => null);
801
940
  if (!stats) {
802
- await this.runRemoteShellScript({
803
- script: "rm -rf -- \"$1\"",
804
- args: [remotePath],
805
- allowFailure: true
806
- });
941
+ await this.runPinnedRemotePathMutation({ args: [
942
+ "remove",
943
+ target.root,
944
+ path.posix.dirname(target.relativePath) === "." ? "" : path.posix.dirname(target.relativePath),
945
+ path.posix.basename(target.relativePath),
946
+ "1"
947
+ ] });
807
948
  return;
808
949
  }
809
950
  if (stats.isSymbolicLink()) {
810
- await this.runRemoteShellScript({
811
- script: "rm -rf -- \"$1\"",
812
- args: [remotePath],
813
- allowFailure: true
814
- });
951
+ await this.runPinnedRemotePathMutation({ args: [
952
+ "remove",
953
+ target.root,
954
+ path.posix.dirname(target.relativePath) === "." ? "" : path.posix.dirname(target.relativePath),
955
+ path.posix.basename(target.relativePath),
956
+ "1"
957
+ ] });
815
958
  return;
816
959
  }
817
960
  if (stats.isDirectory()) {
818
- await this.runRemoteShellScript({
819
- script: "mkdir -p -- \"$1\"",
820
- args: [remotePath]
821
- });
961
+ await this.mkdirpRemotePath(remotePath);
822
962
  return;
823
963
  }
824
- await this.runRemoteShellScript({
825
- script: "mkdir -p -- \"$(dirname -- \"$1\")\"",
826
- args: [remotePath]
827
- });
964
+ await this.runPinnedRemotePathMutation({ args: [
965
+ "mkdirp",
966
+ target.root,
967
+ path.posix.dirname(target.relativePath) === "." ? "" : path.posix.dirname(target.relativePath)
968
+ ] });
828
969
  const result = await runOpenShellCli({
829
970
  context: this.params.execContext,
830
971
  args: [
@@ -839,6 +980,25 @@ var OpenShellSandboxBackendImpl = class {
839
980
  });
840
981
  if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox upload failed");
841
982
  }
983
+ async runPinnedRemotePathMutation(params) {
984
+ return await this.runRemoteShellScript({
985
+ script: PINNED_REMOTE_PATH_MUTATION_SCRIPT,
986
+ args: params.args,
987
+ signal: params.signal
988
+ });
989
+ }
990
+ resolveRemoteTarget(remotePath) {
991
+ const normalized = normalizeRemotePath(remotePath);
992
+ const roots = [normalizeRemotePath(this.params.remoteWorkspaceDir), normalizeRemotePath(this.params.remoteAgentWorkspaceDir)].toSorted((a, b) => b.length - a.length);
993
+ for (const root of roots) if (isRemotePathInside(root, normalized)) {
994
+ const relativePath = path.posix.relative(root, normalized);
995
+ return {
996
+ root,
997
+ relativePath: relativePath === "." ? "" : relativePath
998
+ };
999
+ }
1000
+ throw new Error(`Remote path escapes OpenShell managed roots: ${remotePath}`);
1001
+ }
842
1002
  async ensureSandboxExists() {
843
1003
  if (this.ensurePromise) return await this.ensurePromise;
844
1004
  this.ensurePromise = this.ensureSandboxExistsInner();
@@ -901,7 +1061,7 @@ var OpenShellSandboxBackendImpl = class {
901
1061
  if (this.params.createParams.cfg.workspaceAccess !== "rw" || !this.params.createParams.skillsWorkspaceDir) return;
902
1062
  const remoteSkillsWorkspaceDir = resolveRemoteMaterializedSkillsWorkspaceDir(this.params.remoteWorkspaceDir);
903
1063
  await this.runRemoteShellScriptInternal({
904
- script: `${ENSURE_REMOTE_REAL_DIRECTORY_SCRIPT}\nfind "$1" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`,
1064
+ script: `${ENSURE_OPEN_SHELL_REMOTE_REAL_DIRECTORY_SCRIPT}\nfind "$1" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`,
905
1065
  args: [remoteSkillsWorkspaceDir, this.params.remoteWorkspaceDir]
906
1066
  });
907
1067
  const stats = await fs.lstat(this.params.createParams.skillsWorkspaceDir).catch(() => null);
@@ -1067,6 +1227,15 @@ async function restoreMaterializedSkillsShadow(params) {
1067
1227
  function resolveOpenShellTmpRoot() {
1068
1228
  return path.resolve(resolvePreferredOpenClawTmpDir());
1069
1229
  }
1230
+ function normalizeRemotePath(remotePath) {
1231
+ const normalized = path.posix.normalize(remotePath.replace(/\\/g, "/"));
1232
+ if (!path.posix.isAbsolute(normalized)) throw new Error(`OpenShell remote path must be absolute: ${remotePath}`);
1233
+ return normalized;
1234
+ }
1235
+ function isRemotePathInside(root, candidate) {
1236
+ const relative = path.posix.relative(root, candidate);
1237
+ return relative === "" || relative !== ".." && !relative.startsWith("../") && !path.posix.isAbsolute(relative);
1238
+ }
1070
1239
  //#endregion
1071
1240
  //#region extensions/openshell/index.ts
1072
1241
  var openshell_default = definePluginEntry({
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/openshell-sandbox",
3
- "version": "2026.6.8",
3
+ "version": "2026.6.9",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/openshell-sandbox",
9
- "version": "2026.6.8",
9
+ "version": "2026.6.9",
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.8",
3
+ "version": "2026.6.9",
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.8"
23
+ "pluginApi": ">=2026.6.9"
24
24
  },
25
25
  "build": {
26
- "openclawVersion": "2026.6.8",
26
+ "openclawVersion": "2026.6.9",
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.8"
44
+ "openclaw": ">=2026.6.9"
45
45
  },
46
46
  "peerDependenciesMeta": {
47
47
  "openclaw": {