mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 00:18:12 +00:00
sandbox: scope skill loading to workspace for sandboxed agents
Prevents managed/bundled skill file paths from leaking into sandboxed agent skill snapshots, which caused 'path escapes sandbox root' errors. Adds scopeToWorkspace option to loadSkillEntries/buildWorkspaceSkillSnapshot. Also fixes stale Docker mount detection on container probe failure.
This commit is contained in:
@@ -442,6 +442,25 @@ export async function ensureSandboxContainer(params: {
|
||||
});
|
||||
} else if (!running) {
|
||||
await execDocker(["start", containerName]);
|
||||
} else {
|
||||
// Container was already running – verify the workspace bind mount is still
|
||||
// valid. When the host directory backing the mount is deleted while the
|
||||
// container is running, any `docker exec` against it fails with an OCI
|
||||
// "mount namespace" error. Detect this and recreate the container.
|
||||
const probe = await execDocker(["exec", containerName, "true"], { allowFailure: true });
|
||||
if (probe.code !== 0 && probe.stderr.includes("mount namespace")) {
|
||||
defaultRuntime.log(`Sandbox mount stale for ${containerName}; recreating.`);
|
||||
await execDocker(["rm", "-f", containerName], { allowFailure: true });
|
||||
await createSandboxContainer({
|
||||
name: containerName,
|
||||
cfg: params.cfg.docker,
|
||||
workspaceDir: params.workspaceDir,
|
||||
workspaceAccess: params.cfg.workspaceAccess,
|
||||
agentWorkspaceDir: params.agentWorkspaceDir,
|
||||
scopeKey,
|
||||
configHash: expectedHash,
|
||||
});
|
||||
}
|
||||
}
|
||||
await updateRegistry({
|
||||
containerName,
|
||||
|
||||
@@ -108,6 +108,8 @@ function loadSkillEntries(
|
||||
config?: OpenClawConfig;
|
||||
managedSkillsDir?: string;
|
||||
bundledSkillsDir?: string;
|
||||
/** When true, only load skills from the workspace dir (skip managed/bundled/extra). */
|
||||
scopeToWorkspace?: boolean;
|
||||
},
|
||||
): SkillEntry[] {
|
||||
const loadSkills = (params: { dir: string; source: string }): Skill[] => {
|
||||
@@ -126,8 +128,34 @@ function loadSkillEntries(
|
||||
return [];
|
||||
};
|
||||
|
||||
const workspaceSkillsDir = path.join(workspaceDir, "skills");
|
||||
|
||||
// When scoped to workspace, only load skills from the workspace dir.
|
||||
// This prevents managed/bundled skill paths from leaking into sandboxed
|
||||
// agents where those paths are outside the sandbox root.
|
||||
if (opts?.scopeToWorkspace) {
|
||||
const workspaceSkills = loadSkills({
|
||||
dir: workspaceSkillsDir,
|
||||
source: "openclaw-workspace",
|
||||
});
|
||||
return workspaceSkills.map((skill) => {
|
||||
let frontmatter: ParsedSkillFrontmatter = {};
|
||||
try {
|
||||
const raw = fs.readFileSync(skill.filePath, "utf-8");
|
||||
frontmatter = parseFrontmatter(raw);
|
||||
} catch {
|
||||
// ignore malformed skills
|
||||
}
|
||||
return {
|
||||
skill,
|
||||
frontmatter,
|
||||
metadata: resolveOpenClawMetadata(frontmatter),
|
||||
invocation: resolveSkillInvocationPolicy(frontmatter),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const managedSkillsDir = opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills");
|
||||
const workspaceSkillsDir = path.resolve(workspaceDir, "skills");
|
||||
const bundledSkillsDir = opts?.bundledSkillsDir ?? resolveBundledSkillsDir();
|
||||
const extraDirsRaw = opts?.config?.skills?.load?.extraDirs ?? [];
|
||||
const extraDirs = extraDirsRaw
|
||||
@@ -221,6 +249,8 @@ export function buildWorkspaceSkillSnapshot(
|
||||
skillFilter?: string[];
|
||||
eligibility?: SkillEligibilityContext;
|
||||
snapshotVersion?: number;
|
||||
/** When true, only load skills from the workspace dir (for sandboxed agents). */
|
||||
scopeToWorkspace?: boolean;
|
||||
},
|
||||
): SkillSnapshot {
|
||||
const skillEntries = opts?.entries ?? loadSkillEntries(workspaceDir, opts);
|
||||
@@ -356,6 +386,7 @@ export async function syncSkillsToWorkspace(params: {
|
||||
}) {
|
||||
const sourceDir = resolveUserPath(params.sourceWorkspaceDir);
|
||||
const targetDir = resolveUserPath(params.targetWorkspaceDir);
|
||||
|
||||
if (sourceDir === targetDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -65,22 +65,23 @@ async function resolveContextReport(
|
||||
sessionKey: params.sessionKey,
|
||||
sessionId: params.sessionEntry?.sessionId,
|
||||
});
|
||||
const sandboxRuntime = resolveSandboxRuntimeStatus({
|
||||
cfg: params.cfg,
|
||||
sessionKey: params.ctx.SessionKey ?? params.sessionKey,
|
||||
});
|
||||
const skillsSnapshot = (() => {
|
||||
try {
|
||||
return buildWorkspaceSkillSnapshot(workspaceDir, {
|
||||
config: params.cfg,
|
||||
eligibility: { remote: getRemoteSkillEligibility() },
|
||||
snapshotVersion: getSkillsSnapshotVersion(workspaceDir),
|
||||
scopeToWorkspace: sandboxRuntime.sandboxed,
|
||||
});
|
||||
} catch {
|
||||
return { prompt: "", skills: [], resolvedSkills: [] };
|
||||
}
|
||||
})();
|
||||
const skillsPrompt = skillsSnapshot.prompt ?? "";
|
||||
const sandboxRuntime = resolveSandboxRuntimeStatus({
|
||||
cfg: params.cfg,
|
||||
sessionKey: params.ctx.SessionKey ?? params.sessionKey,
|
||||
});
|
||||
const tools = (() => {
|
||||
try {
|
||||
return createOpenClawCodingTools({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import crypto from "node:crypto";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveUserTimezone } from "../../agents/date-time.js";
|
||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox/runtime-status.js";
|
||||
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
||||
import { ensureSkillsWatcher, getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
|
||||
import { type SessionEntry, updateSessionStore } from "../../config/sessions.js";
|
||||
@@ -149,6 +150,10 @@ export async function ensureSkillSnapshot(params: {
|
||||
skillFilter,
|
||||
} = params;
|
||||
|
||||
// Sandboxed agents should only see workspace skills — managed/bundled skill
|
||||
// paths are outside the sandbox root and would be blocked by path assertions.
|
||||
const sandboxed = sessionKey ? resolveSandboxRuntimeStatus({ cfg, sessionKey }).sandboxed : false;
|
||||
|
||||
let nextEntry = sessionEntry;
|
||||
let systemSent = sessionEntry?.systemSent ?? false;
|
||||
const remoteEligibility = getRemoteSkillEligibility();
|
||||
@@ -170,6 +175,7 @@ export async function ensureSkillSnapshot(params: {
|
||||
skillFilter,
|
||||
eligibility: { remote: remoteEligibility },
|
||||
snapshotVersion,
|
||||
scopeToWorkspace: sandboxed,
|
||||
})
|
||||
: current.skillsSnapshot;
|
||||
nextEntry = {
|
||||
@@ -194,6 +200,7 @@ export async function ensureSkillSnapshot(params: {
|
||||
skillFilter,
|
||||
eligibility: { remote: remoteEligibility },
|
||||
snapshotVersion,
|
||||
scopeToWorkspace: sandboxed,
|
||||
})
|
||||
: (nextEntry?.skillsSnapshot ??
|
||||
(isFirstTurnInSession
|
||||
@@ -203,6 +210,7 @@ export async function ensureSkillSnapshot(params: {
|
||||
skillFilter,
|
||||
eligibility: { remote: remoteEligibility },
|
||||
snapshotVersion,
|
||||
scopeToWorkspace: sandboxed,
|
||||
})));
|
||||
if (
|
||||
skillsSnapshot &&
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
resolveThinkingDefault,
|
||||
} from "../agents/model-selection.js";
|
||||
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { resolveSandboxRuntimeStatus } from "../agents/sandbox/runtime-status.js";
|
||||
import { buildWorkspaceSkillSnapshot } from "../agents/skills.js";
|
||||
import { getSkillsSnapshotVersion } from "../agents/skills/refresh.js";
|
||||
import { resolveAgentTimeoutMs } from "../agents/timeout.js";
|
||||
@@ -315,12 +316,16 @@ export async function agentCommand(
|
||||
const needsSkillsSnapshot = isNewSession || !sessionEntry?.skillsSnapshot;
|
||||
const skillsSnapshotVersion = getSkillsSnapshotVersion(workspaceDir);
|
||||
const skillFilter = resolveAgentSkillsFilter(cfg, sessionAgentId);
|
||||
const sandboxed = sessionKey
|
||||
? resolveSandboxRuntimeStatus({ cfg, sessionKey }).sandboxed
|
||||
: false;
|
||||
const skillsSnapshot = needsSkillsSnapshot
|
||||
? buildWorkspaceSkillSnapshot(workspaceDir, {
|
||||
config: cfg,
|
||||
eligibility: { remote: getRemoteSkillEligibility() },
|
||||
snapshotVersion: skillsSnapshotVersion,
|
||||
skillFilter,
|
||||
scopeToWorkspace: sandboxed,
|
||||
})
|
||||
: sessionEntry?.skillsSnapshot;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user