mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
fix(sandbox): fallback docker user to workspace owner uid/gid
Co-authored-by: LucasAIBuilder <LucasAIBuilder@users.noreply.github.com>
This commit is contained in:
@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Security/Voice Call: harden media stream WebSocket handling against pre-auth idle-connection DoS by adding strict pre-start timeouts, pending/per-IP connection limits, and total connection caps for streaming endpoints. This ships in the next npm release. Thanks @jiseoung for reporting.
|
||||
- Agents/Exec: honor explicit agent context when resolving `tools.exec` defaults for runs with opaque/non-agent session keys, so per-agent `host/security/ask` policies are applied consistently. (#11832)
|
||||
- Sandbox/Docker: default sandbox container user to the workspace owner `uid:gid` when `agents.*.sandbox.docker.user` is unset, fixing non-root gateway file-tool permissions under capability-dropped containers. (#20979)
|
||||
- Telegram/Discord extensions: propagate trusted `mediaLocalRoots` through extension outbound `sendMedia` options so extension direct-send media paths honor agent-scoped local-media allowlists. (#20029, #21903, #23227)
|
||||
- Exec/Background: stop applying the default exec timeout to background sessions (`background: true` or explicit `yieldMs`) when no explicit timeout is set, so long-running background jobs are no longer terminated at the default timeout boundary. (#23303)
|
||||
- Plugins/Media sandbox: propagate trusted `mediaLocalRoots` through plugin action dispatch (including Discord/Telegram action adapters) so plugin send paths enforce the same agent-scoped local-media sandbox roots as core outbound sends. (#20258, #22718)
|
||||
|
||||
@@ -14,7 +14,7 @@ import { createSandboxFsBridge } from "./fs-bridge.js";
|
||||
import { maybePruneSandboxes } from "./prune.js";
|
||||
import { resolveSandboxRuntimeStatus } from "./runtime-status.js";
|
||||
import { resolveSandboxScopeKey, resolveSandboxWorkspaceDir } from "./shared.js";
|
||||
import type { SandboxContext, SandboxWorkspaceInfo } from "./types.js";
|
||||
import type { SandboxContext, SandboxDockerConfig, SandboxWorkspaceInfo } from "./types.js";
|
||||
import { ensureSandboxWorkspace } from "./workspace.js";
|
||||
|
||||
async function ensureSandboxWorkspaceLayout(params: {
|
||||
@@ -64,6 +64,29 @@ async function ensureSandboxWorkspaceLayout(params: {
|
||||
return { agentWorkspaceDir, scopeKey, sandboxWorkspaceDir, workspaceDir };
|
||||
}
|
||||
|
||||
export async function resolveSandboxDockerUser(params: {
|
||||
docker: SandboxDockerConfig;
|
||||
workspaceDir: string;
|
||||
stat?: (workspaceDir: string) => Promise<{ uid: number; gid: number }>;
|
||||
}): Promise<SandboxDockerConfig> {
|
||||
const configuredUser = params.docker.user?.trim();
|
||||
if (configuredUser) {
|
||||
return params.docker;
|
||||
}
|
||||
const stat = params.stat ?? ((workspaceDir: string) => fs.stat(workspaceDir));
|
||||
try {
|
||||
const workspaceStat = await stat(params.workspaceDir);
|
||||
const uid = Number.isInteger(workspaceStat.uid) ? workspaceStat.uid : null;
|
||||
const gid = Number.isInteger(workspaceStat.gid) ? workspaceStat.gid : null;
|
||||
if (uid === null || gid === null || uid < 0 || gid < 0) {
|
||||
return params.docker;
|
||||
}
|
||||
return { ...params.docker, user: `${uid}:${gid}` };
|
||||
} catch {
|
||||
return params.docker;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSandboxSession(params: { config?: OpenClawConfig; sessionKey?: string }) {
|
||||
const rawSessionKey = params.sessionKey?.trim();
|
||||
if (!rawSessionKey) {
|
||||
@@ -102,11 +125,17 @@ export async function resolveSandboxContext(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
|
||||
const docker = await resolveSandboxDockerUser({
|
||||
docker: cfg.docker,
|
||||
workspaceDir,
|
||||
});
|
||||
const resolvedCfg = docker === cfg.docker ? cfg : { ...cfg, docker };
|
||||
|
||||
const containerName = await ensureSandboxContainer({
|
||||
sessionKey: rawSessionKey,
|
||||
workspaceDir,
|
||||
agentWorkspaceDir,
|
||||
cfg,
|
||||
cfg: resolvedCfg,
|
||||
});
|
||||
|
||||
const evaluateEnabled =
|
||||
@@ -132,7 +161,7 @@ export async function resolveSandboxContext(params: {
|
||||
scopeKey,
|
||||
workspaceDir,
|
||||
agentWorkspaceDir,
|
||||
cfg,
|
||||
cfg: resolvedCfg,
|
||||
evaluateEnabled,
|
||||
bridgeAuth,
|
||||
});
|
||||
@@ -142,12 +171,12 @@ export async function resolveSandboxContext(params: {
|
||||
sessionKey: rawSessionKey,
|
||||
workspaceDir,
|
||||
agentWorkspaceDir,
|
||||
workspaceAccess: cfg.workspaceAccess,
|
||||
workspaceAccess: resolvedCfg.workspaceAccess,
|
||||
containerName,
|
||||
containerWorkdir: cfg.docker.workdir,
|
||||
docker: cfg.docker,
|
||||
tools: cfg.tools,
|
||||
browserAllowHostControl: cfg.browser.allowHostControl,
|
||||
containerWorkdir: resolvedCfg.docker.workdir,
|
||||
docker: resolvedCfg.docker,
|
||||
tools: resolvedCfg.tools,
|
||||
browserAllowHostControl: resolvedCfg.browser.allowHostControl,
|
||||
browser: browser ?? undefined,
|
||||
};
|
||||
|
||||
|
||||
44
src/agents/sandbox/context.user-fallback.test.ts
Normal file
44
src/agents/sandbox/context.user-fallback.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSandboxDockerUser } from "./context.js";
|
||||
import type { SandboxDockerConfig } from "./types.js";
|
||||
|
||||
const baseDocker: SandboxDockerConfig = {
|
||||
image: "ghcr.io/example/sandbox:latest",
|
||||
containerPrefix: "openclaw-sandbox-",
|
||||
workdir: "/workspace",
|
||||
readOnlyRoot: true,
|
||||
tmpfs: ["/tmp"],
|
||||
network: "none",
|
||||
capDrop: ["ALL"],
|
||||
};
|
||||
|
||||
describe("resolveSandboxDockerUser", () => {
|
||||
it("keeps configured docker.user", async () => {
|
||||
const resolved = await resolveSandboxDockerUser({
|
||||
docker: { ...baseDocker, user: "2000:2000" },
|
||||
workspaceDir: "/tmp/unused",
|
||||
stat: async () => ({ uid: 1000, gid: 1000 }),
|
||||
});
|
||||
expect(resolved.user).toBe("2000:2000");
|
||||
});
|
||||
|
||||
it("falls back to workspace ownership when docker.user is unset", async () => {
|
||||
const resolved = await resolveSandboxDockerUser({
|
||||
docker: baseDocker,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
stat: async () => ({ uid: 1001, gid: 1002 }),
|
||||
});
|
||||
expect(resolved.user).toBe("1001:1002");
|
||||
});
|
||||
|
||||
it("leaves docker.user unset when workspace stat fails", async () => {
|
||||
const resolved = await resolveSandboxDockerUser({
|
||||
docker: baseDocker,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
stat: async () => {
|
||||
throw new Error("ENOENT");
|
||||
},
|
||||
});
|
||||
expect(resolved.user).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user