feat(sandbox): per-agent docker setupCommand

This commit is contained in:
Peter Steinberger
2026-01-08 00:52:15 +01:00
parent 6143ad13be
commit b03a1ad814
8 changed files with 109 additions and 5 deletions

View File

@@ -54,6 +54,85 @@ describe("Agent-specific sandbox config", () => {
expect(context?.enabled).toBe(true);
});
it("should allow agent-specific docker setupCommand overrides", async () => {
const { resolveSandboxContext } = await import("./sandbox.js");
const cfg: ClawdbotConfig = {
agent: {
sandbox: {
mode: "all",
scope: "agent",
docker: {
setupCommand: "echo global",
},
},
},
routing: {
agents: {
work: {
workspace: "~/clawd-work",
sandbox: {
mode: "all",
scope: "agent",
docker: {
setupCommand: "echo work",
},
},
},
},
},
};
const context = await resolveSandboxContext({
config: cfg,
sessionKey: "agent:work:main",
workspaceDir: "/tmp/test-work",
});
expect(context).toBeDefined();
expect(context?.docker.setupCommand).toBe("echo work");
});
it("should ignore agent-specific docker overrides when scope is shared", async () => {
const { resolveSandboxContext } = await import("./sandbox.js");
const cfg: ClawdbotConfig = {
agent: {
sandbox: {
mode: "all",
scope: "shared",
docker: {
setupCommand: "echo global",
},
},
},
routing: {
agents: {
work: {
workspace: "~/clawd-work",
sandbox: {
mode: "all",
scope: "shared",
docker: {
setupCommand: "echo work",
},
},
},
},
},
};
const context = await resolveSandboxContext({
config: cfg,
sessionKey: "agent:work:main",
workspaceDir: "/tmp/test-work",
});
expect(context).toBeDefined();
expect(context?.docker.setupCommand).toBe("echo global");
expect(context?.containerName).toContain("shared");
});
it("should override with agent-specific sandbox mode 'off'", async () => {
const { resolveSandboxContext } = await import("./sandbox.js");

View File

@@ -241,12 +241,14 @@ function defaultSandboxConfig(
}
}
const scope = resolveSandboxScope({
scope: agentSandbox?.scope ?? agent?.scope,
perSession: agentSandbox?.perSession ?? agent?.perSession,
});
return {
mode: agentSandbox?.mode ?? agent?.mode ?? "off",
scope: resolveSandboxScope({
scope: agentSandbox?.scope ?? agent?.scope,
perSession: agentSandbox?.perSession ?? agent?.perSession,
}),
scope,
workspaceAccess:
agentSandbox?.workspaceAccess ?? agent?.workspaceAccess ?? "none",
workspaceRoot:
@@ -264,7 +266,10 @@ function defaultSandboxConfig(
user: agent?.docker?.user,
capDrop: agent?.docker?.capDrop ?? ["ALL"],
env: agent?.docker?.env ?? { LANG: "C.UTF-8" },
setupCommand: agent?.docker?.setupCommand,
setupCommand:
scope === "shared"
? agent?.docker?.setupCommand
: (agentSandbox?.docker?.setupCommand ?? agent?.docker?.setupCommand),
pidsLimit: agent?.docker?.pidsLimit,
memory: agent?.docker?.memory,
memorySwap: agent?.docker?.memorySwap,

View File

@@ -617,6 +617,11 @@ export type RoutingConfig = {
/** Legacy alias for scope ("session" when true, "shared" when false). */
perSession?: boolean;
workspaceRoot?: string;
/** Docker-specific sandbox overrides for this agent. */
docker?: {
/** Optional setup command run once after container creation. */
setupCommand?: string;
};
/** Tool allow/deny policy for sandboxed sessions (deny wins). */
tools?: {
allow?: string[];

View File

@@ -265,6 +265,11 @@ const RoutingSchema = z
.optional(),
perSession: z.boolean().optional(),
workspaceRoot: z.string().optional(),
docker: z
.object({
setupCommand: z.string().optional(),
})
.optional(),
tools: z
.object({
allow: z.array(z.string()).optional(),