



















@@ -22,6 +22,7 @@ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coer
2222import type { OpenShellSandboxBackend } from "./backend.types.js";
2323import {
2424buildValidatedExecRemoteCommand,
25+buildRemoteWorkdirValidationCommand,
2526buildRemoteCommand,
2627createOpenShellSshSession,
2728runOpenShellCli,
@@ -280,6 +281,13 @@ async function createOpenShellSandboxBackend(params: {
280281mode: params.pluginConfig.mode,
281282configLabel: params.pluginConfig.from,
282283configLabelKind: "Source",
284+workdirValidation: "backend",
285+validateWorkdir: async (workdir) => await impl.validateWorkdir(workdir),
286+discardPreparedWorkdir: (workdir) => impl.discardPreparedWorkdir(workdir),
287+workdirRoots: [
288+params.pluginConfig.remoteWorkspaceDir,
289+params.pluginConfig.remoteAgentWorkspaceDir,
290+],
283291buildExecSpec: async ({ command, workdir, env, usePty }) => {
284292const pending = await impl.prepareExec({ command, workdir, env, usePty });
285293return {
@@ -318,6 +326,10 @@ async function createOpenShellSandboxBackend(params: {
318326319327class OpenShellSandboxBackendImpl {
320328private ensurePromise: Promise<void> | null = null;
329+private preparedRemoteWorkspaceForNextExec: {
330+workdir: string;
331+promise: Promise<void>;
332+} | null = null;
321333private remoteSeedPending = false;
322334323335constructor(
@@ -339,6 +351,10 @@ class OpenShellSandboxBackendImpl {
339351mode: this.params.execContext.config.mode,
340352configLabel: this.params.execContext.config.from,
341353configLabelKind: "Source",
354+workdirValidation: "backend",
355+validateWorkdir: async (workdir) => await this.validateWorkdir(workdir),
356+discardPreparedWorkdir: (workdir) => this.discardPreparedWorkdir(workdir),
357+workdirRoots: [this.params.remoteWorkspaceDir, this.params.remoteAgentWorkspaceDir],
342358remoteWorkspaceDir: this.params.remoteWorkspaceDir,
343359remoteAgentWorkspaceDir: this.params.remoteAgentWorkspaceDir,
344360buildExecSpec: async ({ command, workdir, env, usePty }) => {
@@ -382,20 +398,14 @@ class OpenShellSandboxBackendImpl {
382398env: Record<string, string>;
383399usePty: boolean;
384400}): Promise<{ argv: string[]; token: PendingExec }> {
401+const remoteWorkdir = params.workdir ?? this.params.remoteWorkspaceDir;
402+const preparedWorkspace = this.consumePreparedRemoteWorkspaceForNextExec(remoteWorkdir);
385403const remoteCommand = buildValidatedExecRemoteCommand({
386404command: params.command,
387-workdir: params.workdir ?? this.params.remoteWorkspaceDir,
405+workdir: remoteWorkdir,
388406env: params.env,
389407});
390-await this.ensureSandboxExists();
391-if (this.params.execContext.config.mode === "mirror") {
392-await this.syncWorkspaceToRemote();
393-} else {
394-const seeded = await this.maybeSeedRemoteWorkspace();
395-if (!seeded) {
396-await this.syncSkillsWorkspaceToRemote();
397-}
398-}
408+await (preparedWorkspace ?? this.prepareRemoteWorkspaceForExec());
399409const sshSession = await createOpenShellSshSession({
400410context: this.params.execContext,
401411});
@@ -414,6 +424,85 @@ class OpenShellSandboxBackendImpl {
414424};
415425}
416426427+async validateWorkdir(workdir: string): Promise<string | null> {
428+const preparedWorkspace = this.prepareRemoteWorkspaceForExec();
429+const reusablePreparation = { workdir, promise: preparedWorkspace };
430+this.preparedRemoteWorkspaceForNextExec = reusablePreparation;
431+try {
432+await preparedWorkspace;
433+const sshSession = await createOpenShellSshSession({
434+context: this.params.execContext,
435+});
436+try {
437+const result = await runSshSandboxCommand({
438+session: sshSession,
439+remoteCommand: buildRemoteWorkdirValidationCommand({
440+ workdir,
441+root: this.resolveWorkdirValidationRoot(workdir),
442+}),
443+allowFailure: true,
444+});
445+const resolvedWorkdir = result.code === 0 ? result.stdout.toString("utf8").trim() : "";
446+if (this.preparedRemoteWorkspaceForNextExec === reusablePreparation) {
447+this.preparedRemoteWorkspaceForNextExec = resolvedWorkdir
448+ ? { workdir: resolvedWorkdir, promise: preparedWorkspace }
449+ : null;
450+}
451+return resolvedWorkdir || null;
452+} finally {
453+await disposeSshSandboxSession(sshSession);
454+}
455+} catch (error) {
456+if (this.preparedRemoteWorkspaceForNextExec === reusablePreparation) {
457+this.preparedRemoteWorkspaceForNextExec = null;
458+}
459+throw error;
460+}
461+}
462+463+private resolveWorkdirValidationRoot(workdir: string): string {
464+try {
465+const normalized = normalizeRemotePath(workdir);
466+const roots = [
467+normalizeRemotePath(this.params.remoteAgentWorkspaceDir),
468+normalizeRemotePath(this.params.remoteWorkspaceDir),
469+].toSorted((a, b) => b.length - a.length);
470+return (
471+roots.find((root) => isRemotePathInside(root, normalized)) ?? this.params.remoteWorkspaceDir
472+);
473+} catch {
474+return this.params.remoteWorkspaceDir;
475+}
476+}
477+478+private consumePreparedRemoteWorkspaceForNextExec(workdir: string): Promise<void> | null {
479+const preparedWorkspace = this.preparedRemoteWorkspaceForNextExec;
480+if (!preparedWorkspace || preparedWorkspace.workdir !== workdir) {
481+this.preparedRemoteWorkspaceForNextExec = null;
482+return null;
483+}
484+this.preparedRemoteWorkspaceForNextExec = null;
485+return preparedWorkspace.promise;
486+}
487+488+discardPreparedWorkdir(workdir: string): void {
489+if (this.preparedRemoteWorkspaceForNextExec?.workdir === workdir) {
490+this.preparedRemoteWorkspaceForNextExec = null;
491+}
492+}
493+494+private async prepareRemoteWorkspaceForExec(): Promise<void> {
495+await this.ensureSandboxExists();
496+if (this.params.execContext.config.mode === "mirror") {
497+await this.syncWorkspaceToRemote();
498+return;
499+}
500+const seeded = await this.maybeSeedRemoteWorkspace();
501+if (!seeded) {
502+await this.syncSkillsWorkspaceToRemote();
503+}
504+}
505+417506async finalizeExec(token?: PendingExec): Promise<void> {
418507try {
419508if (this.params.execContext.config.mode === "mirror") {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。