mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 12:14:58 +00:00
fix(agents): map sandbox workdir from container path
This commit is contained in:
committed by
Peter Steinberger
parent
b1cc8ffe9e
commit
c48a0621ff
@@ -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/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)
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
77
src/agents/bash-tools.shared.test.ts
Normal file
77
src/agents/bash-tools.shared.test.ts
Normal 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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -85,9 +85,14 @@ export async function resolveSandboxWorkdir(params: {
|
|||||||
warnings: string[];
|
warnings: string[];
|
||||||
}) {
|
}) {
|
||||||
const fallback = params.sandbox.workspaceDir;
|
const fallback = params.sandbox.workspaceDir;
|
||||||
|
const mappedHostWorkdir = mapContainerWorkdirToHost({
|
||||||
|
workdir: params.workdir,
|
||||||
|
sandbox: params.sandbox,
|
||||||
|
});
|
||||||
|
const candidateWorkdir = mappedHostWorkdir ?? params.workdir;
|
||||||
try {
|
try {
|
||||||
const resolved = await assertSandboxPath({
|
const resolved = await assertSandboxPath({
|
||||||
filePath: params.workdir,
|
filePath: candidateWorkdir,
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
root: params.sandbox.workspaceDir,
|
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[]) {
|
export function resolveWorkdir(workdir: string, warnings: string[]) {
|
||||||
const current = safeCwd();
|
const current = safeCwd();
|
||||||
const fallback = current ?? homedir();
|
const fallback = current ?? homedir();
|
||||||
|
|||||||
Reference in New Issue
Block a user