























@@ -45,6 +45,107 @@ type PendingExec = {
4545};
46464747const MATERIALIZED_SKILLS_REMOTE_PARTS = [".openclaw", "sandbox-skills"] as const;
48+export const PINNED_REMOTE_PATH_MUTATION_SCRIPT = [
49+"set -eu",
50+'die() { echo "$1" >&2; exit 1; }',
51+"validate_basename() {",
52+' case "$1" in ""|"."|".."|*/*) die "unsafe remote basename: $1" ;; esac',
53+"}",
54+"pin_dir() {",
55+' root="$1"',
56+' relative="$2"',
57+' create="$3"',
58+' case "$root" in /*) ;; *) die "remote root must be absolute: $root" ;; esac',
59+' root="${root%/}"',
60+' [ -n "$root" ] || root="/"',
61+' if [ -L "$root" ]; then die "unsafe remote root symlink: $root"; fi',
62+' mkdir -p -- "$root"',
63+' canonical_root="$(cd "$root" && pwd -P)"',
64+' current="$canonical_root"',
65+' relative="${relative#/}"',
66+' while [ -n "$relative" ]; do',
67+' part="${relative%%/*}"',
68+' if [ "$part" = "$relative" ]; then relative=""; else relative="${relative#*/}"; fi',
69+' [ -n "$part" ] || continue',
70+' case "$part" in "."|"..") die "unsafe remote directory component: $part" ;; esac',
71+' if [ "$current" = "/" ]; then next="/$part"; else next="$current/$part"; fi',
72+' if [ -L "$next" ]; then die "unsafe remote directory symlink: $next"; fi',
73+' if [ -e "$next" ]; then',
74+' if [ ! -d "$next" ]; then die "unsafe remote directory component: $next"; fi',
75+" else",
76+' if [ "$create" != "1" ]; then die "remote directory not found: $next"; fi',
77+' mkdir -- "$next"',
78+" fi",
79+' current="$next"',
80+" done",
81+' printf "%s\\n" "$current"',
82+"}",
83+"pin_dir_or_missing() {",
84+' root="$1"',
85+' relative="$2"',
86+' missing_ok="$3"',
87+' case "$root" in /*) ;; *) die "remote root must be absolute: $root" ;; esac',
88+' root="${root%/}"',
89+' [ -n "$root" ] || root="/"',
90+' if [ -L "$root" ]; then die "unsafe remote root symlink: $root"; fi',
91+' if [ ! -d "$root" ]; then',
92+' if [ -e "$root" ]; then die "unsafe remote root component: $root"; fi',
93+' if [ "$missing_ok" = "1" ]; then printf "\\n"; return 0; fi',
94+' die "remote directory not found: $root"',
95+" fi",
96+' canonical_root="$(cd "$root" && pwd -P)"',
97+' current="$canonical_root"',
98+' relative="${relative#/}"',
99+' while [ -n "$relative" ]; do',
100+' part="${relative%%/*}"',
101+' if [ "$part" = "$relative" ]; then relative=""; else relative="${relative#*/}"; fi',
102+' [ -n "$part" ] || continue',
103+' case "$part" in "."|"..") die "unsafe remote directory component: $part" ;; esac',
104+' if [ "$current" = "/" ]; then next="/$part"; else next="$current/$part"; fi',
105+' if [ -L "$next" ]; then die "unsafe remote directory symlink: $next"; fi',
106+' if [ -e "$next" ]; then',
107+' if [ ! -d "$next" ]; then die "unsafe remote directory component: $next"; fi',
108+" else",
109+' if [ "$missing_ok" = "1" ]; then printf "\\n"; return 0; fi',
110+' die "remote directory not found: $next"',
111+" fi",
112+' current="$next"',
113+" done",
114+' printf "%s\\n" "$current"',
115+"}",
116+'operation="$1"',
117+'case "$operation" in',
118+" mkdirp)",
119+' pin_dir "$2" "$3" 1 >/dev/null',
120+" ;;",
121+" remove)",
122+' validate_basename "$4"',
123+' parent="$(pin_dir_or_missing "$2" "$3" "${5:-0}")"',
124+' [ -n "$parent" ] || exit 0',
125+' target="$parent/$4"',
126+' if [ -d "$target" ] && [ ! -L "$target" ]; then rm -rf -- "$target"; elif [ -e "$target" ] || [ -L "$target" ]; then rm -f -- "$target"; fi',
127+" ;;",
128+" removefile)",
129+' validate_basename "$4"',
130+' parent="$(pin_dir_or_missing "$2" "$3" "${5:-0}")"',
131+' [ -n "$parent" ] || exit 0',
132+' target="$parent/$4"',
133+' if [ -d "$target" ] && [ ! -L "$target" ]; then rmdir -- "$target"; elif [ -e "$target" ] || [ -L "$target" ]; then rm -f -- "$target"; fi',
134+" ;;",
135+" rename)",
136+' src_parent="$(pin_dir "$2" "$3" 0)"',
137+' validate_basename "$4"',
138+' dst_parent="$(pin_dir "$5" "$6" 1)"',
139+' validate_basename "$7"',
140+' if [ -L "$dst_parent/$7" ]; then die "unsafe remote rename target symlink: $dst_parent/$7"; fi',
141+' if [ -d "$dst_parent/$7" ]; then die "unsafe remote rename target directory: $dst_parent/$7"; fi',
142+' mv -- "$src_parent/$4" "$dst_parent/$7"',
143+" ;;",
144+" *)",
145+' die "unknown remote path mutation: $operation"',
146+" ;;",
147+"esac",
148+].join("\n");
48149const ENSURE_REMOTE_REAL_DIRECTORY_SCRIPT = [
49150"set -e",
50151'target="$1"',
@@ -190,6 +291,11 @@ async function createOpenShellSandboxBackend(params: {
190291remoteWorkspaceDir: params.pluginConfig.remoteWorkspaceDir,
191292remoteAgentWorkspaceDir: params.pluginConfig.remoteAgentWorkspaceDir,
192293runRemoteShellScript: async (command) => await impl.runRemoteShellScript(command),
294+mkdirpRemotePath: async (remotePath, signal) => await impl.mkdirpRemotePath(remotePath, signal),
295+removeRemotePath: async (remotePath, removeParams) =>
296+await impl.removeRemotePath(remotePath, removeParams),
297+renameRemotePath: async (fromRemotePath, toRemotePath, signal) =>
298+await impl.renameRemotePath(fromRemotePath, toRemotePath, signal),
193299syncLocalPathToRemote: async (localPath, remotePath) =>
194300await impl.syncLocalPathToRemote(localPath, remotePath),
195301};
@@ -244,6 +350,12 @@ class OpenShellSandboxBackendImpl {
244350backend: this.asHandle(),
245351}),
246352runRemoteShellScript: async (command) => await this.runRemoteShellScript(command),
353+mkdirpRemotePath: async (remotePath, signal) =>
354+await this.mkdirpRemotePath(remotePath, signal),
355+removeRemotePath: async (remotePath, removeParams) =>
356+await this.removeRemotePath(remotePath, removeParams),
357+renameRemotePath: async (fromRemotePath, toRemotePath, signal) =>
358+await this.renameRemotePath(fromRemotePath, toRemotePath, signal),
247359syncLocalPathToRemote: async (localPath, remotePath) =>
248360await this.syncLocalPathToRemote(localPath, remotePath),
249361};
@@ -310,6 +422,58 @@ class OpenShellSandboxBackendImpl {
310422return await this.runRemoteShellScriptInternal(params);
311423}
312424425+async mkdirpRemotePath(remotePath: string, signal?: AbortSignal): Promise<void> {
426+const target = this.resolveRemoteTarget(remotePath);
427+await this.runPinnedRemotePathMutation({
428+args: ["mkdirp", target.root, target.relativePath],
429+ signal,
430+});
431+}
432+433+async removeRemotePath(
434+remotePath: string,
435+params?: {
436+recursive?: boolean;
437+signal?: AbortSignal;
438+ignoreMissing?: boolean;
439+},
440+): Promise<void> {
441+const target = this.resolveRemoteTarget(remotePath);
442+await this.runPinnedRemotePathMutation({
443+args: [
444+params?.recursive ? "remove" : "removefile",
445+target.root,
446+path.posix.dirname(target.relativePath) === "."
447+ ? ""
448+ : path.posix.dirname(target.relativePath),
449+path.posix.basename(target.relativePath),
450+params?.ignoreMissing ? "1" : "0",
451+],
452+signal: params?.signal,
453+});
454+}
455+456+async renameRemotePath(
457+fromRemotePath: string,
458+toRemotePath: string,
459+signal?: AbortSignal,
460+): Promise<void> {
461+const from = this.resolveRemoteTarget(fromRemotePath);
462+const to = this.resolveRemoteTarget(toRemotePath);
463+await this.runPinnedRemotePathMutation({
464+args: [
465+"rename",
466+from.root,
467+path.posix.dirname(from.relativePath) === "." ? "" : path.posix.dirname(from.relativePath),
468+path.posix.basename(from.relativePath),
469+to.root,
470+path.posix.dirname(to.relativePath) === "." ? "" : path.posix.dirname(to.relativePath),
471+path.posix.basename(to.relativePath),
472+],
473+ signal,
474+});
475+}
476+313477private async runRemoteShellScriptInternal(
314478params: SandboxBackendCommandParams,
315479): Promise<SandboxBackendCommandResult> {
@@ -338,33 +502,48 @@ class OpenShellSandboxBackendImpl {
338502async syncLocalPathToRemote(localPath: string, remotePath: string): Promise<void> {
339503await this.ensureSandboxExists();
340504await this.maybeSeedRemoteWorkspace();
505+const target = this.resolveRemoteTarget(remotePath);
341506const stats = await fs.lstat(localPath).catch(() => null);
342507if (!stats) {
343-await this.runRemoteShellScript({
344-script: 'rm -rf -- "$1"',
345-args: [remotePath],
346-allowFailure: true,
508+await this.runPinnedRemotePathMutation({
509+args: [
510+"remove",
511+target.root,
512+path.posix.dirname(target.relativePath) === "."
513+ ? ""
514+ : path.posix.dirname(target.relativePath),
515+path.posix.basename(target.relativePath),
516+"1",
517+],
347518});
348519return;
349520}
350521if (stats.isSymbolicLink()) {
351-await this.runRemoteShellScript({
352-script: 'rm -rf -- "$1"',
353-args: [remotePath],
354-allowFailure: true,
522+await this.runPinnedRemotePathMutation({
523+args: [
524+"remove",
525+target.root,
526+path.posix.dirname(target.relativePath) === "."
527+ ? ""
528+ : path.posix.dirname(target.relativePath),
529+path.posix.basename(target.relativePath),
530+"1",
531+],
355532});
356533return;
357534}
358535if (stats.isDirectory()) {
359-await this.runRemoteShellScript({
360-script: 'mkdir -p -- "$1"',
361-args: [remotePath],
362-});
536+await this.mkdirpRemotePath(remotePath);
363537return;
364538}
365-await this.runRemoteShellScript({
366-script: 'mkdir -p -- "$(dirname -- "$1")"',
367-args: [remotePath],
539+await this.runPinnedRemotePathMutation({
540+args: [
541+"mkdirp",
542+target.root,
543+path.posix.dirname(target.relativePath) === "."
544+ ? ""
545+ : path.posix.dirname(target.relativePath),
546+],
368547});
369548const result = await runOpenShellCli({
370549context: this.params.execContext,
@@ -383,6 +562,32 @@ class OpenShellSandboxBackendImpl {
383562}
384563}
385564565+private async runPinnedRemotePathMutation(params: {
566+args: string[];
567+signal?: AbortSignal;
568+}): Promise<SandboxBackendCommandResult> {
569+return await this.runRemoteShellScript({
570+script: PINNED_REMOTE_PATH_MUTATION_SCRIPT,
571+args: params.args,
572+signal: params.signal,
573+});
574+}
575+576+private resolveRemoteTarget(remotePath: string): { root: string; relativePath: string } {
577+const normalized = normalizeRemotePath(remotePath);
578+const roots = [
579+normalizeRemotePath(this.params.remoteWorkspaceDir),
580+normalizeRemotePath(this.params.remoteAgentWorkspaceDir),
581+].toSorted((a, b) => b.length - a.length);
582+for (const root of roots) {
583+if (isRemotePathInside(root, normalized)) {
584+const relativePath = path.posix.relative(root, normalized);
585+return { root, relativePath: relativePath === "." ? "" : relativePath };
586+}
587+}
588+throw new Error(`Remote path escapes OpenShell managed roots: ${remotePath}`);
589+}
590+386591private async ensureSandboxExists(): Promise<void> {
387592if (this.ensurePromise) {
388593return await this.ensurePromise;
@@ -676,3 +881,19 @@ async function restoreMaterializedSkillsShadow(params: {
676881function resolveOpenShellTmpRoot(): string {
677882return path.resolve(resolvePreferredOpenClawTmpDir());
678883}
884+885+function normalizeRemotePath(remotePath: string): string {
886+const normalized = path.posix.normalize(remotePath.replace(/\\/g, "/"));
887+if (!path.posix.isAbsolute(normalized)) {
888+throw new Error(`OpenShell remote path must be absolute: ${remotePath}`);
889+}
890+return normalized;
891+}
892+893+function isRemotePathInside(root: string, candidate: string): boolean {
894+const relative = path.posix.relative(root, candidate);
895+return (
896+relative === "" ||
897+(relative !== ".." && !relative.startsWith("../") && !path.posix.isAbsolute(relative))
898+);
899+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。