fix(security): align agent roster default audit with runtime semantics (#124398)

A fresh openclaw onboard (and agents add) writes agents.entries without any
default:true marker; runtime resolves the sole agent as default via
tryResolveSoleAgentId, and read-time migration strips valid legacy markers
before the audit ever sees the config. The audit still demanded exactly one
default=true marker on non-explicit rosters, so every fresh install got a
false config.agent_roster.invalid_default_count warning.

Make the audit mirror runtime truth: a non-explicit roster is valid exactly
when the canonical resolver (tryResolveLegacyCompatibilityAgentId) finds an
owner - sole agent, one legacy marker, or the retained migration owner.
Explicit fleets keep the strict no-marker rule. This also fixes the sibling
false alarm for migrated legacy multi-agent configs whose marker was removed
at load time with the owner retained in-process.

Regression test: fresh-install-shaped sole-agent roster produces no roster
finding (fails pre-fix).
This commit is contained in:
Peter Steinberger
2026-08-15 21:50:09 -07:00
committed by GitHub
parent 0c7b560b23
commit 345fde4e14
2 changed files with 36 additions and 7 deletions
+20
View File
@@ -102,6 +102,26 @@ describe("security audit rosterless configs", () => {
);
});
it("accepts a fresh-install sole-agent roster without a default marker", async () => {
const { stateDir, workspaceDir } = makeAuditPaths("fresh-install-roster");
// `openclaw onboard` and `agents add` write markerless entries; runtime
// resolves the sole agent as default, so the audit must not warn.
const report = await runSecurityAuditCore({
config: { agents: { entries: { main: {} } } } as never,
stateDir,
configPath: path.join(stateDir, "openclaw.json"),
workspaceDir,
env: {},
includeFilesystem: true,
includeChannelSecurity: false,
});
expect(report.findings).not.toContainEqual(
expect.objectContaining({ checkId: "config.agent_roster.invalid_default_count" }),
);
});
it.each([
{
label: "an explicitly empty roster",
+16 -7
View File
@@ -4,7 +4,11 @@ import { redactSensitiveUrlLikeString } from "@openclaw/net-policy/redact-sensit
import { asNullableRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
import { hasAgentRosterProperty, listAgentEntries } from "../agents/agent-scope-config.js";
import {
hasAgentRosterProperty,
listAgentEntries,
tryResolveLegacyCompatibilityAgentId,
} from "../agents/agent-scope-config.js";
import { tryResolveDefaultAgentId } from "../agents/agent-scope.js";
import { resolveExecDefaults } from "../agents/exec-defaults.js";
import { resolveSandboxConfigForAgent } from "../agents/sandbox/config.js";
@@ -955,8 +959,14 @@ function collectAgentRosterFindings(cfg: OpenClawConfig): SecurityAuditFinding[]
return [];
}
const defaultCount = agents.filter((agent) => agent?.default === true).length;
const expectedDefaultCount = cfg.agents?.ownership === "explicit" ? 0 : 1;
if (defaultCount === expectedDefaultCount) {
const explicitOwnership = cfg.agents?.ownership === "explicit";
// Mirror runtime default resolution: explicit fleets are ownerless by design,
// otherwise the roster is valid exactly when the canonical resolver finds an
// owner (sole agent, one legacy marker, or a retained migration owner).
const resolvable = explicitOwnership
? defaultCount === 0
: tryResolveLegacyCompatibilityAgentId(cfg) !== undefined;
if (resolvable) {
return [];
}
return [
@@ -964,10 +974,9 @@ function collectAgentRosterFindings(cfg: OpenClawConfig): SecurityAuditFinding[]
checkId: "config.agent_roster.invalid_default_count",
severity: "warn",
title: "Agent roster has an invalid default selection",
detail:
expectedDefaultCount === 0
? `Expected no agents.entries default=true entries with agents.ownership=explicit, found ${defaultCount}.`
: `Expected exactly one agents.entries default=true entry, found ${defaultCount}.`,
detail: explicitOwnership
? `Expected no agents.entries default=true entries with agents.ownership=explicit, found ${defaultCount}.`
: `Expected a resolvable default agent (sole entry, one default=true marker, or agents.ownership=explicit); found ${defaultCount} default markers across ${agents.length} configured agents.`,
remediation: "Run `openclaw doctor --fix` to repair the authored agent roster.",
},
];