mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(security): report canonical agents.entries paths in audit and diagnostics (#113603)
Audit findings, dangerous-flag labels, exec-approval provenance, filesystem policy drift, and the skill-workshop tool diagnostic printed agents.list.* paths users cannot paste into openclaw.json: dotted agents.list.<id> is valid in no shape, and index-keyed agents.list.<n> only names the internal validation projection. All display surfaces now emit agents.entries.<id>; the summary agent-id extractor regex follows the new labels. Roster-aware builders keep an indexed fallback only for id-less malformed legacy rows, and validation/doctor/legacy-migration paths intentionally keep the legacy form because they point into the user's actual file. Follow-up deferred from #113160.
This commit is contained in:
committed by
GitHub
parent
faaab9bd48
commit
e26fc17c77
@@ -333,7 +333,7 @@ export function collectExecPolicyScopeSnapshots(params: {
|
||||
approvals: params.approvals,
|
||||
scopeExecConfig: agentConfig?.tools?.exec,
|
||||
globalExecConfig,
|
||||
configPath: `agents.list.${agentId}.tools.exec`,
|
||||
configPath: `agents.entries.${agentId}.tools.exec`,
|
||||
hostPath: params.hostPath,
|
||||
hostDefaults: params.hostDefaults,
|
||||
hostDefaultSource: params.hostDefaultSource,
|
||||
|
||||
@@ -829,7 +829,7 @@ describe("exec approvals policy helpers", () => {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
list: [{ id: "runner", default: true }],
|
||||
entries: { runner: { default: true } },
|
||||
},
|
||||
} satisfies OpenClawConfig,
|
||||
approvals: {
|
||||
@@ -869,7 +869,7 @@ describe("exec approvals policy helpers", () => {
|
||||
ask: "off",
|
||||
},
|
||||
},
|
||||
agents: { list: [{ id: DEFAULT_AGENT_ID, default: true }] },
|
||||
agents: { entries: { [DEFAULT_AGENT_ID]: { default: true } } },
|
||||
} satisfies OpenClawConfig,
|
||||
approvals: {
|
||||
version: 1,
|
||||
@@ -903,9 +903,8 @@ describe("exec approvals policy helpers", () => {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: DEFAULT_AGENT_ID,
|
||||
entries: {
|
||||
[DEFAULT_AGENT_ID]: {
|
||||
default: true,
|
||||
tools: {
|
||||
exec: {
|
||||
@@ -913,7 +912,7 @@ describe("exec approvals policy helpers", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig,
|
||||
approvals: {
|
||||
@@ -924,7 +923,7 @@ describe("exec approvals policy helpers", () => {
|
||||
expect(snapshots.map((snapshot) => snapshot.scopeLabel)).toEqual(["tools.exec", "agent:main"]);
|
||||
expectFields(snapshots[1]?.ask, {
|
||||
requested: "always",
|
||||
requestedSource: "agents.list.main.tools.exec.ask",
|
||||
requestedSource: "agents.entries.main.tools.exec.ask",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -947,7 +946,7 @@ describe("exec approvals policy helpers", () => {
|
||||
]);
|
||||
expectFields(snapshots[1]?.ask, {
|
||||
requested: "always",
|
||||
requestedSource: "agents.list.runner.tools.exec.ask",
|
||||
requestedSource: "agents.entries.runner.tools.exec.ask",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,21 +49,17 @@ describe("security audit config basics", () => {
|
||||
profile: "minimal",
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "owner",
|
||||
entries: {
|
||||
owner: {
|
||||
tools: { profile: "full" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
findings.some(
|
||||
(finding) =>
|
||||
finding.checkId === "tools.profile_minimal_overridden" && finding.severity === "warn",
|
||||
),
|
||||
).toBe(true);
|
||||
const finding = findings.find((entry) => entry.checkId === "tools.profile_minimal_overridden");
|
||||
expect(finding?.severity).toBe("warn");
|
||||
expect(finding?.detail).toContain("agents.entries.owner=full");
|
||||
});
|
||||
|
||||
it("flags tools.elevated allowFrom wildcard as critical", async () => {
|
||||
|
||||
@@ -36,9 +36,8 @@ describe("security audit exec safe-bin findings", () => {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "ops",
|
||||
entries: {
|
||||
ops: {
|
||||
default: true,
|
||||
tools: {
|
||||
exec: {
|
||||
@@ -46,7 +45,7 @@ describe("security audit exec safe-bin findings", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig,
|
||||
expected: true,
|
||||
@@ -65,9 +64,8 @@ describe("security audit exec safe-bin findings", () => {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "ops",
|
||||
entries: {
|
||||
ops: {
|
||||
default: true,
|
||||
tools: {
|
||||
exec: {
|
||||
@@ -80,7 +78,7 @@ describe("security audit exec safe-bin findings", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig,
|
||||
expected: false,
|
||||
@@ -101,7 +99,7 @@ describe("security audit exec safe-bin findings", () => {
|
||||
{
|
||||
name: "jq configured globally",
|
||||
cfg: {
|
||||
agents: { list: [{ id: "main", default: true }] },
|
||||
agents: { entries: { main: { default: true } } },
|
||||
tools: {
|
||||
exec: {
|
||||
safeBins: ["jq"],
|
||||
@@ -113,7 +111,7 @@ describe("security audit exec safe-bin findings", () => {
|
||||
{
|
||||
name: "jq not configured",
|
||||
cfg: {
|
||||
agents: { list: [{ id: "main", default: true }] },
|
||||
agents: { entries: { main: { default: true } } },
|
||||
tools: {
|
||||
exec: {
|
||||
safeBins: ["cut"],
|
||||
@@ -143,9 +141,8 @@ describe("security audit exec safe-bin findings", () => {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "ops",
|
||||
entries: {
|
||||
ops: {
|
||||
default: true,
|
||||
tools: {
|
||||
exec: {
|
||||
@@ -153,7 +150,7 @@ describe("security audit exec safe-bin findings", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig);
|
||||
|
||||
@@ -161,7 +158,7 @@ describe("security audit exec safe-bin findings", () => {
|
||||
expect(riskyFinding.severity).toBe("warn");
|
||||
expect(riskyFinding.detail).toContain(riskyGlobalTrustedDirs[0]);
|
||||
expect(riskyFinding.detail).toContain(riskyGlobalTrustedDirs[1]);
|
||||
expect(riskyFinding.detail).toContain("agents.list.ops.tools.exec");
|
||||
expect(riskyFinding.detail).toContain("agents.entries.ops.tools.exec");
|
||||
});
|
||||
|
||||
it("ignores non-risky absolute dirs", async () => {
|
||||
@@ -169,7 +166,7 @@ describe("security audit exec safe-bin findings", () => {
|
||||
hasFinding(
|
||||
"tools.exec.safe_bin_trusted_dirs_risky",
|
||||
await collectSecurityAuditFindings({
|
||||
agents: { list: [{ id: "main", default: true }] },
|
||||
agents: { entries: { main: { default: true } } },
|
||||
tools: {
|
||||
exec: {
|
||||
safeBinTrustedDirs: ["/usr/libexec"],
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("security audit exec surface findings", () => {
|
||||
"warn",
|
||||
await collectSecurityAuditFindings({
|
||||
agents: {
|
||||
list: [{ id: "ops" }],
|
||||
entries: { ops: {} },
|
||||
},
|
||||
} satisfies OpenClawConfig),
|
||||
),
|
||||
@@ -253,6 +253,25 @@ describe("security audit exec surface findings", () => {
|
||||
expect(finding.remediation).toContain("deny exec and process");
|
||||
});
|
||||
|
||||
it("reports canonical agent paths for filesystem policy drift", async () => {
|
||||
const findings = await collectSecurityAuditFindings({
|
||||
agents: {
|
||||
entries: {
|
||||
ops: {
|
||||
default: true,
|
||||
tools: {
|
||||
allow: ["read", "exec", "process"],
|
||||
deny: ["write", "edit", "apply_patch"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig);
|
||||
|
||||
const finding = requireFinding("tools.exec.fs_tools_disabled_but_exec_enabled", findings);
|
||||
expect(finding.detail).toContain("agents.entries.ops.tools");
|
||||
});
|
||||
|
||||
it("does not warn when sandbox filesystem policy constrains exec", async () => {
|
||||
const findings = await collectSecurityAuditFindings({
|
||||
agents: {
|
||||
|
||||
@@ -59,7 +59,7 @@ function summarizeGroupPolicy(cfg: OpenClawConfig): {
|
||||
}
|
||||
|
||||
function extractAgentIdFromSource(source: string): string | null {
|
||||
const match = source.match(/^agents\.list\.([^.]*)\./);
|
||||
const match = source.match(/^agents\.entries\.([^.]*)\./);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
|
||||
@@ -487,7 +487,7 @@ function listAuditAgentToolContexts(cfg: OpenClawConfig): AuditAgentToolContext[
|
||||
continue;
|
||||
}
|
||||
contexts.push({
|
||||
label: `agents.list.${agent.id}`,
|
||||
label: `agents.entries.${agent.id}`,
|
||||
agentId: agent.id,
|
||||
tools: agent.tools,
|
||||
});
|
||||
@@ -824,7 +824,7 @@ export function collectSandboxDockerNoopFindings(cfg: OpenClawConfig): SecurityA
|
||||
continue;
|
||||
}
|
||||
if (resolveSandboxConfigForAgent(cfg, entry.id).mode === "off") {
|
||||
configuredPaths.push(`agents.list.${entry.id}.sandbox.docker`);
|
||||
configuredPaths.push(`agents.entries.${entry.id}.sandbox.docker`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,7 +865,7 @@ export function collectSandboxDangerousConfigFindings(cfg: OpenClawConfig): Secu
|
||||
const agentDocker = entry.sandbox?.docker;
|
||||
if (agentDocker && typeof agentDocker === "object") {
|
||||
configs.push({
|
||||
source: `agents.list.${entry.id}.sandbox.docker`,
|
||||
source: `agents.entries.${entry.id}.sandbox.docker`,
|
||||
docker: agentDocker as Record<string, unknown>,
|
||||
});
|
||||
}
|
||||
@@ -1083,7 +1083,7 @@ export function collectMinimalProfileOverrideFindings(cfg: OpenClawConfig): Secu
|
||||
title: "Global tools.profile=minimal is overridden by agent profiles",
|
||||
detail:
|
||||
"Global minimal profile is set, but these agent profiles take precedence:\n" +
|
||||
overrides.map((entry) => `- agents.list.${entry}`).join("\n"),
|
||||
overrides.map((entry) => `- agents.entries.${entry}`).join("\n"),
|
||||
remediation:
|
||||
'Set those agents to `tools.profile="minimal"` (or remove the agent override) if you want minimal tools enforced globally.',
|
||||
});
|
||||
|
||||
@@ -88,13 +88,13 @@ export function collectAuditModelRefs(cfg: OpenClawConfig): AuditModelRef[] {
|
||||
typeof (agent as { id?: unknown }).id === "string" ? (agent as { id: string }).id : "";
|
||||
const model = (agent as { model?: unknown }).model;
|
||||
if (typeof model === "string") {
|
||||
add(model, `agents.list.${id}.model`);
|
||||
add(model, `agents.entries.${id}.model`);
|
||||
} else if (model && typeof model === "object") {
|
||||
add((model as { primary?: unknown }).primary, `agents.list.${id}.model.primary`);
|
||||
add((model as { primary?: unknown }).primary, `agents.entries.${id}.model.primary`);
|
||||
const fallbacks = (model as { fallbacks?: unknown }).fallbacks;
|
||||
if (Array.isArray(fallbacks)) {
|
||||
for (const fallback of fallbacks) {
|
||||
add(fallback, `agents.list.${id}.model.fallbacks`);
|
||||
add(fallback, `agents.entries.${id}.model.fallbacks`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,6 +544,19 @@ describe("security audit extension tool reachability findings", () => {
|
||||
).toBe(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reports canonical agent paths for permissive tool policy",
|
||||
cfg: {
|
||||
plugins: { allow: ["some-plugin"] },
|
||||
agents: { entries: { ops: { tools: { profile: "full" } } } },
|
||||
} satisfies OpenClawConfig,
|
||||
assert: (findings: Awaited<ReturnType<typeof runSharedExtensionsAudit>>) => {
|
||||
const finding = findings.find(
|
||||
(entry) => entry.checkId === "plugins.tools_reachable_permissive_policy",
|
||||
);
|
||||
expect(finding?.detail).toContain("- agents.entries.ops");
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not flag plugin tool reachability when profile is restrictive",
|
||||
cfg: {
|
||||
|
||||
@@ -363,7 +363,7 @@ export async function collectPluginsTrustFindings(params: {
|
||||
continue;
|
||||
}
|
||||
contexts.push({
|
||||
label: `agents.list.${entry.id}`,
|
||||
label: `agents.entries.${entry.id}`,
|
||||
agentId: entry.id,
|
||||
tools: entry.tools,
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("security audit sandbox docker config", () => {
|
||||
docker: { image: "ghcr.io/example/sandbox:latest" },
|
||||
},
|
||||
},
|
||||
list: [{ id: "ops", sandbox: { mode: "all" } }],
|
||||
entries: { ops: { sandbox: { mode: "all" } } },
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
expectedFindings: [],
|
||||
@@ -190,4 +190,32 @@ describe("security audit sandbox docker config", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("reports canonical agent paths for docker sandbox findings", () => {
|
||||
const config = {
|
||||
agents: {
|
||||
entries: {
|
||||
ops: {
|
||||
sandbox: {
|
||||
mode: "off",
|
||||
docker: {
|
||||
image: "ghcr.io/example/sandbox:latest",
|
||||
binds: ["/etc/passwd:/mnt/passwd:ro"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const noOpFinding = collectSandboxDockerNoopFindings(config).find(
|
||||
(entry) => entry.checkId === "sandbox.docker_config_mode_off",
|
||||
);
|
||||
expect(noOpFinding?.detail).toContain("agents.entries.ops.sandbox.docker");
|
||||
|
||||
const dangerousFinding = collectSandboxDangerousConfigFindings(config).find(
|
||||
(entry) => entry.checkId === "sandbox.dangerous_bind_mount",
|
||||
);
|
||||
expect(dangerousFinding?.detail).toContain("agents.entries.ops.sandbox.docker.binds");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { collectSmallModelRiskFindings } from "./audit-extra.summary.js";
|
||||
import { collectAuditModelRefs } from "./audit-model-refs.js";
|
||||
|
||||
function requireFirstSmallModelFinding(
|
||||
findings: ReturnType<typeof collectSmallModelRiskFindings>,
|
||||
@@ -15,6 +16,54 @@ function requireFirstSmallModelFinding(
|
||||
}
|
||||
|
||||
describe("security audit small-model risk findings", () => {
|
||||
it("reports canonical paths for agent model references", () => {
|
||||
expect(
|
||||
collectAuditModelRefs({
|
||||
agents: {
|
||||
entries: {
|
||||
simple: { model: "ollama/mistral-8b" },
|
||||
structured: {
|
||||
model: {
|
||||
primary: "ollama/gemma-4b",
|
||||
fallbacks: ["ollama/phi-3b"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig),
|
||||
).toEqual([
|
||||
{ id: "ollama/mistral-8b", source: "agents.entries.simple.model" },
|
||||
{ id: "ollama/gemma-4b", source: "agents.entries.structured.model.primary" },
|
||||
{ id: "ollama/phi-3b", source: "agents.entries.structured.model.fallbacks" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves agent policy context for canonical model source paths", () => {
|
||||
const finding = requireFirstSmallModelFinding(
|
||||
collectSmallModelRiskFindings({
|
||||
cfg: {
|
||||
agents: {
|
||||
entries: {
|
||||
ops: {
|
||||
default: true,
|
||||
model: { primary: "ollama/mistral-8b" },
|
||||
tools: { deny: ["web_search", "web_fetch", "browser"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: { web: { search: { enabled: true }, fetch: { enabled: true } } },
|
||||
browser: { enabled: true },
|
||||
} satisfies OpenClawConfig,
|
||||
env: {},
|
||||
}),
|
||||
"agent policy context",
|
||||
);
|
||||
|
||||
expect(finding.severity).toBe("info");
|
||||
expect(finding.detail).toContain("@ agents.entries.ops.model.primary");
|
||||
expect(finding.detail).toContain("web=[off]");
|
||||
});
|
||||
|
||||
it("scores small-model risk by tool/sandbox exposure", () => {
|
||||
const cases: Array<{
|
||||
name: string;
|
||||
|
||||
@@ -288,7 +288,7 @@ describe("security audit trust model findings", () => {
|
||||
channels: { whatsapp: { groupPolicy: "open" } },
|
||||
tools: { elevated: { enabled: false }, profile: "messaging" },
|
||||
agents: {
|
||||
list: [{ id: "ops", tools: { profile: "messaging", alsoAllow: ["gateway"] } }],
|
||||
entries: { ops: { tools: { profile: "messaging", alsoAllow: ["gateway"] } } },
|
||||
},
|
||||
} satisfies OpenClawConfig,
|
||||
assert: (findings: ReturnType<typeof audit>) => {
|
||||
@@ -296,7 +296,7 @@ describe("security audit trust model findings", () => {
|
||||
(entry) => entry.checkId === "security.exposure.open_groups_with_control_plane_tools",
|
||||
);
|
||||
expect(finding?.detail).toContain(
|
||||
"agents.list.ops (profile=messaging; controlPlane=[gateway])",
|
||||
"agents.entries.ops (profile=messaging; controlPlane=[gateway])",
|
||||
);
|
||||
expect(finding?.detail).not.toContain("agents.defaults (profile=messaging");
|
||||
},
|
||||
|
||||
@@ -668,7 +668,7 @@ function collectExecRuntimeFindings(cfg: OpenClawConfig): SecurityAuditFinding[]
|
||||
severity: "warn",
|
||||
title: "Agent exec host uses sandbox while sandbox mode is off",
|
||||
detail:
|
||||
`agents.list.*.tools.exec.host is set to sandbox for: ${riskyAgents.join(", ")}. ` +
|
||||
`agents.entries.*.tools.exec.host is set to sandbox for: ${riskyAgents.join(", ")}. ` +
|
||||
"With sandbox mode off, exec fails closed for those agents.",
|
||||
remediation:
|
||||
'Enable sandbox mode for these agents (`agents.entries.*.sandbox.mode`) or set their tools.exec.host to "gateway".',
|
||||
@@ -849,7 +849,7 @@ function collectExecRuntimeFindings(cfg: OpenClawConfig): SecurityAuditFinding[]
|
||||
continue;
|
||||
}
|
||||
collectRiskyTrustedDirHits(
|
||||
`agents.list.${entry.id}.tools.exec`,
|
||||
`agents.entries.${entry.id}.tools.exec`,
|
||||
entry.tools?.exec?.safeBinTrustedDirs,
|
||||
);
|
||||
}
|
||||
@@ -886,17 +886,17 @@ function collectExecRuntimeFindings(cfg: OpenClawConfig): SecurityAuditFinding[]
|
||||
if (interpreters.length === 0) {
|
||||
for (const hit of listRiskyConfiguredSafeBins(agentSafeBins)) {
|
||||
riskySemanticSafeBinHits.push(
|
||||
`- agents.list.${entry.id}.tools.exec.safeBins: ${hit.bin} (${hit.warning})`,
|
||||
`- agents.entries.${entry.id}.tools.exec.safeBins: ${hit.bin} (${hit.warning})`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
interpreterHits.push(
|
||||
`- agents.list.${entry.id}.tools.exec.safeBins: ${interpreters.join(", ")}`,
|
||||
`- agents.entries.${entry.id}.tools.exec.safeBins: ${interpreters.join(", ")}`,
|
||||
);
|
||||
for (const hit of listRiskyConfiguredSafeBins(agentSafeBins)) {
|
||||
riskySemanticSafeBinHits.push(
|
||||
`- agents.list.${entry.id}.tools.exec.safeBins: ${hit.bin} (${hit.warning})`,
|
||||
`- agents.entries.${entry.id}.tools.exec.safeBins: ${hit.bin} (${hit.warning})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Defines core dangerous config flag metadata for security audits.
|
||||
import { listAgentEntriesWithSource } from "../agents/agent-scope-config.js";
|
||||
import { listAgentEntriesWithSource, type ListedAgentEntry } from "../agents/agent-scope-config.js";
|
||||
import { DANGEROUS_SANDBOX_DOCKER_BOOLEAN_KEYS } from "../agents/sandbox/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
@@ -41,10 +41,13 @@ function formatDangerousConfigFlagValue(value: DangerousFlagValue): string {
|
||||
return value === null ? "null" : String(value);
|
||||
}
|
||||
|
||||
function getAgentDangerousFlagPathSegment(
|
||||
source: ReturnType<typeof listAgentEntriesWithSource>[number]["source"],
|
||||
): string {
|
||||
return source.kind === "entries" ? `agents.entries.${source.key}` : `agents.list.${source.index}`;
|
||||
function getAgentDangerousFlagPathSegment(listed: ListedAgentEntry): string {
|
||||
if (listed.source.kind === "entries") {
|
||||
return `agents.entries.${listed.source.key}`;
|
||||
}
|
||||
return typeof listed.entry.id === "string" && listed.entry.id.length > 0
|
||||
? `agents.entries.${listed.entry.id}`
|
||||
: `agents.list.${listed.source.index}`;
|
||||
}
|
||||
|
||||
function collectExactPluginConfigContractMatches({
|
||||
@@ -98,10 +101,11 @@ export function collectEnabledInsecureOrDangerousFlagsFromContracts(
|
||||
: undefined,
|
||||
"agents.defaults.sandbox.docker",
|
||||
);
|
||||
for (const { entry: agent, source } of listAgentEntriesWithSource(cfg)) {
|
||||
for (const listed of listAgentEntriesWithSource(cfg)) {
|
||||
const agent = listed.entry;
|
||||
collectSandboxDockerDangerousFlags(
|
||||
isRecord(agent?.sandbox?.docker) ? agent.sandbox.docker : undefined,
|
||||
`${getAgentDangerousFlagPathSegment(source)}.sandbox.docker`,
|
||||
`${getAgentDangerousFlagPathSegment(listed)}.sandbox.docker`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -198,16 +198,15 @@ describe("collectEnabledInsecureOrDangerousFlags", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "worker",
|
||||
entries: {
|
||||
worker: {
|
||||
sandbox: {
|
||||
docker: {
|
||||
dangerouslyAllowExternalBindSources: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
allowRequestSessionKey: true,
|
||||
@@ -231,7 +230,7 @@ describe("collectEnabledInsecureOrDangerousFlags", () => {
|
||||
"tools.fs.workspaceOnly=false",
|
||||
"agents.defaults.sandbox.docker.dangerouslyAllowReservedContainerTargets=true",
|
||||
"agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin=true",
|
||||
"agents.list.0.sandbox.docker.dangerouslyAllowExternalBindSources=true",
|
||||
"agents.entries.worker.sandbox.docker.dangerouslyAllowExternalBindSources=true",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -249,7 +248,7 @@ describe("collectEnabledInsecureOrDangerousFlags", () => {
|
||||
).toContain("security.audit.suppressions configured (1)");
|
||||
});
|
||||
|
||||
it("uses legacy list indices for list-shaped dangerous sandbox flags", () => {
|
||||
it("uses canonical entry paths for id-bearing legacy list rows", () => {
|
||||
expect(
|
||||
collectEnabledInsecureOrDangerousFlagsFromContracts(
|
||||
asConfig({
|
||||
@@ -263,25 +262,23 @@ describe("collectEnabledInsecureOrDangerousFlags", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "helper",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toContain("agents.list.0.sandbox.docker.dangerouslyAllowContainerNamespaceJoin=true");
|
||||
).toContain("agents.entries.worker.sandbox.docker.dangerouslyAllowContainerNamespaceJoin=true");
|
||||
});
|
||||
|
||||
it("keeps legacy list indices for id-less dangerous sandbox rows", () => {
|
||||
expect(
|
||||
collectEnabledInsecureOrDangerousFlagsFromContracts(
|
||||
asConfig({
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "helper",
|
||||
id: "worker",
|
||||
},
|
||||
{
|
||||
id: "worker",
|
||||
sandbox: {
|
||||
docker: {
|
||||
dangerouslyAllowContainerNamespaceJoin: true,
|
||||
|
||||
@@ -57,7 +57,7 @@ export function collectExecFilesystemPolicyDriftHits(
|
||||
continue;
|
||||
}
|
||||
contexts.push({
|
||||
scopeLabel: `agents.list.${agent.id}.tools`,
|
||||
scopeLabel: `agents.entries.${agent.id}.tools`,
|
||||
agentId: agent.id,
|
||||
tools: agent.tools,
|
||||
});
|
||||
|
||||
@@ -46,17 +46,17 @@ describe("detectSkillWorkshopToolPolicyDiagnostic", () => {
|
||||
agents: { list: [{ id: "main", default: true, tools: { profile: "messaging" } }] },
|
||||
}),
|
||||
).toMatchObject({
|
||||
source: "agents.list[0].tools.profile",
|
||||
fix: 'Add agents.list[0].tools.alsoAllow: ["skill_workshop"].',
|
||||
source: "agents.entries.main.tools.profile",
|
||||
fix: 'Add agents.entries.main.tools.alsoAllow: ["skill_workshop"].',
|
||||
});
|
||||
|
||||
expect(
|
||||
detect({
|
||||
agents: { list: [{ id: "main", default: true, tools: { allow: ["read"] } }] },
|
||||
agents: { entries: { main: { default: true, tools: { allow: ["read"] } } } },
|
||||
}),
|
||||
).toMatchObject({
|
||||
source: "agents.list[0].tools.allow",
|
||||
fix: 'Add "skill_workshop" to agents.list[0].tools.allow.',
|
||||
source: "agents.entries.main.tools.allow",
|
||||
fix: 'Add "skill_workshop" to agents.entries.main.tools.allow.',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,11 +64,11 @@ describe("detectSkillWorkshopToolPolicyDiagnostic", () => {
|
||||
expect(
|
||||
detect({
|
||||
tools: { profile: "messaging" },
|
||||
agents: { list: [{ id: "main", default: true, tools: { alsoAllow: ["read"] } }] },
|
||||
agents: { entries: { main: { default: true, tools: { alsoAllow: ["read"] } } } },
|
||||
}),
|
||||
).toMatchObject({
|
||||
source: "tools.profile",
|
||||
fix: 'Add agents.list[0].tools.alsoAllow: ["skill_workshop"].',
|
||||
fix: 'Add agents.entries.main.tools.alsoAllow: ["skill_workshop"].',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,19 +89,18 @@ describe("detectSkillWorkshopToolPolicyDiagnostic", () => {
|
||||
detect({
|
||||
agents: {
|
||||
defaults: { model: { primary: "openai/gpt-5.5" } },
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
entries: {
|
||||
main: {
|
||||
default: true,
|
||||
tools: { byProvider: { openai: { alsoAllow: ["read"] } } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
tools: { byProvider: { openai: { profile: "messaging" } } },
|
||||
}),
|
||||
).toMatchObject({
|
||||
source: 'tools.byProvider["openai"].profile',
|
||||
fix: 'Add agents.list[0].tools.byProvider["openai"].alsoAllow: ["skill_workshop"].',
|
||||
fix: 'Add agents.entries.main.tools.byProvider["openai"].alsoAllow: ["skill_workshop"].',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,18 +109,17 @@ describe("detectSkillWorkshopToolPolicyDiagnostic", () => {
|
||||
detect({
|
||||
agents: {
|
||||
defaults: { model: { primary: "openai/gpt-5.5" } },
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
entries: {
|
||||
main: {
|
||||
default: true,
|
||||
tools: { byProvider: { openai: { allow: ["read"] } } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
source: 'agents.list[0].tools.byProvider["openai"].allow',
|
||||
fix: 'Add "skill_workshop" to agents.list[0].tools.byProvider["openai"].allow.',
|
||||
source: 'agents.entries.main.tools.byProvider["openai"].allow',
|
||||
fix: 'Add "skill_workshop" to agents.entries.main.tools.byProvider["openai"].allow.',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -36,10 +36,15 @@ function findAgentTools(config: OpenClawConfig, agentId: string): AgentToolsLoca
|
||||
if (!listed?.entry.tools) {
|
||||
return undefined;
|
||||
}
|
||||
// Report the canonical entries path; an id-less legacy list row (which
|
||||
// normalizeAgentId maps to the default id) keeps its indexed path so the
|
||||
// remediation never points at agents.entries.undefined.
|
||||
const path =
|
||||
listed.source.kind === "entries"
|
||||
? `agents.entries.${listed.source.key}.tools`
|
||||
: `agents.list[${listed.source.index}].tools`;
|
||||
: typeof listed.entry.id === "string" && listed.entry.id.length > 0
|
||||
? `agents.entries.${listed.entry.id}.tools`
|
||||
: `agents.list[${listed.source.index}].tools`;
|
||||
return { path, tools: listed.entry.tools };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user