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:
Peter Steinberger
2026-07-25 03:19:07 -07:00
committed by GitHub
parent faaab9bd48
commit e26fc17c77
19 changed files with 197 additions and 92 deletions
+1 -1
View File
@@ -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,
+7 -8
View File
@@ -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",
});
});
});
+6 -10
View File
@@ -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 () => {
+13 -16
View File
@@ -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"],
+20 -1
View File
@@ -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: {
+1 -1
View File
@@ -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;
}
+4 -4
View File
@@ -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.',
});
+3 -3
View File
@@ -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`);
}
}
}
+13
View File
@@ -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: {
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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");
},
+5 -5
View File
@@ -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})`,
);
}
}
+11 -7
View File
@@ -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`,
);
}
+9 -12
View File
@@ -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,
+1 -1
View File
@@ -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 };
}