fix(agents): map sandbox workdir from container path

This commit is contained in:
liuxiaopai-ai
2026-03-02 23:24:35 +08:00
committed by Peter Steinberger
parent b1cc8ffe9e
commit c48a0621ff
3 changed files with 114 additions and 1 deletions

View File

@@ -99,6 +99,7 @@ Docs: https://docs.openclaw.ai
- Feishu/Duplicate replies: suppress same-target reply dispatch when message-tool sends use generic provider metadata (`provider: "message"`) and normalize `lark`/`feishu` provider aliases during duplicate-target checks, preventing double-delivery in Feishu sessions. (#31526)
- Feishu/Plugin sdk compatibility: add safe webhook default fallbacks when loading Feishu monitor state so mixed-version installs no longer crash if older `openclaw/plugin-sdk` builds omit webhook default constants. (#31606)
- Pairing/AllowFrom account fallback: handle omitted `accountId` values in `readChannelAllowFromStore` and `readChannelAllowFromStoreSync` as `default`, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc.
- Agents/Sandbox workdir mapping: map container workdir paths (for example `/workspace`) back to the host workspace before sandbox path validation so exec requests keep the intended directory in containerized runs instead of falling back to an unavailable host path. (Related #30711)
- Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204.
- BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound `message_id` selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204.
- Gateway/Control UI method guard: allow POST requests to non-UI routes to fall through when no base path is configured, and add POST regression coverage for fallthrough and base-path 405 behavior. (#23970) Thanks @tyler6204.

View File

@@ -0,0 +1,77 @@
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { resolveSandboxWorkdir } from "./bash-tools.shared.js";
async function withTempDir(run: (dir: string) => Promise<void>) {
const dir = await mkdtemp(path.join(os.tmpdir(), "openclaw-bash-workdir-"));
try {
await run(dir);
} finally {
await rm(dir, { recursive: true, force: true });
}
}
describe("resolveSandboxWorkdir", () => {
it("maps container root workdir to host workspace", async () => {
await withTempDir(async (workspaceDir) => {
const warnings: string[] = [];
const resolved = await resolveSandboxWorkdir({
workdir: "/workspace",
sandbox: {
containerName: "sandbox-1",
workspaceDir,
containerWorkdir: "/workspace",
},
warnings,
});
expect(resolved.hostWorkdir).toBe(workspaceDir);
expect(resolved.containerWorkdir).toBe("/workspace");
expect(warnings).toEqual([]);
});
});
it("maps nested container workdir under the container workspace", async () => {
await withTempDir(async (workspaceDir) => {
const nested = path.join(workspaceDir, "scripts", "runner");
await mkdir(nested, { recursive: true });
const warnings: string[] = [];
const resolved = await resolveSandboxWorkdir({
workdir: "/workspace/scripts/runner",
sandbox: {
containerName: "sandbox-2",
workspaceDir,
containerWorkdir: "/workspace",
},
warnings,
});
expect(resolved.hostWorkdir).toBe(nested);
expect(resolved.containerWorkdir).toBe("/workspace/scripts/runner");
expect(warnings).toEqual([]);
});
});
it("supports custom container workdir prefixes", async () => {
await withTempDir(async (workspaceDir) => {
const nested = path.join(workspaceDir, "project");
await mkdir(nested, { recursive: true });
const warnings: string[] = [];
const resolved = await resolveSandboxWorkdir({
workdir: "/sandbox-root/project",
sandbox: {
containerName: "sandbox-3",
workspaceDir,
containerWorkdir: "/sandbox-root",
},
warnings,
});
expect(resolved.hostWorkdir).toBe(nested);
expect(resolved.containerWorkdir).toBe("/sandbox-root/project");
expect(warnings).toEqual([]);
});
});
});

View File

@@ -85,9 +85,14 @@ export async function resolveSandboxWorkdir(params: {
warnings: string[];
}) {
const fallback = params.sandbox.workspaceDir;
const mappedHostWorkdir = mapContainerWorkdirToHost({
workdir: params.workdir,
sandbox: params.sandbox,
});
const candidateWorkdir = mappedHostWorkdir ?? params.workdir;
try {
const resolved = await assertSandboxPath({
filePath: params.workdir,
filePath: candidateWorkdir,
cwd: process.cwd(),
root: params.sandbox.workspaceDir,
});
@@ -113,6 +118,36 @@ export async function resolveSandboxWorkdir(params: {
}
}
function mapContainerWorkdirToHost(params: {
workdir: string;
sandbox: BashSandboxConfig;
}): string | undefined {
const workdir = normalizeContainerPath(params.workdir);
const containerRoot = normalizeContainerPath(params.sandbox.containerWorkdir);
if (containerRoot === ".") {
return undefined;
}
if (workdir === containerRoot) {
return path.resolve(params.sandbox.workspaceDir);
}
if (!workdir.startsWith(`${containerRoot}/`)) {
return undefined;
}
const rel = workdir
.slice(containerRoot.length + 1)
.split("/")
.filter(Boolean);
return path.resolve(params.sandbox.workspaceDir, ...rel);
}
function normalizeContainerPath(input: string): string {
const normalized = input.trim().replace(/\\/g, "/");
if (!normalized) {
return ".";
}
return path.posix.normalize(normalized);
}
export function resolveWorkdir(workdir: string, warnings: string[]) {
const current = safeCwd();
const fallback = current ?? homedir();