Doctor: warn on implicit heartbeat directPolicy (#36789)

* Changelog: note heartbeat directPolicy doctor warning

* Tests: cover heartbeat directPolicy doctor warning

* Doctor: warn on implicit heartbeat directPolicy

* Tests: cover per-agent heartbeat directPolicy warning

* Update CHANGELOG.md
This commit is contained in:
Vincent Koc
2026-03-05 23:22:39 -05:00
committed by GitHub
parent 87e38da826
commit e5481ac79f
3 changed files with 104 additions and 0 deletions

View File

@@ -170,6 +170,7 @@ Docs: https://docs.openclaw.ai
- Agents/failover cooldown classification: stop treating generic `cooling down` text as provider `rate_limit` so healthy models no longer show false global cooldown/rate-limit warnings while explicit `model_cooldown` markers still trigger failover. (#32972) thanks @stakeswky.
- Agents/failover service-unavailable handling: stop treating bare proxy/CDN `service unavailable` errors as provider overload while keeping them retryable via the timeout/failover path, so transient outages no longer show false rate-limit warnings or block fallback. (#36646) thanks @jnMetaCode.
- Doctor/Heartbeat upgrade diagnostics: warn when heartbeat delivery is configured with an implicit `directPolicy` so upgrades pin direct/DM behavior explicitly instead of relying on the current default. (#36789) Thanks @vincentkoc.
- Agents/current-time UTC anchor: append a machine-readable UTC suffix alongside local `Current time:` lines in shared cron-style prompt contexts so agents can compare UTC-stamped workspace timestamps without doing timezone math. (#32423) thanks @jriff.
- TUI/webchat command-owner scope alignment: treat internal-channel gateway sessions with `operator.admin` as owner-authorized in command auth, restoring cron/gateway/connector tool access for affected TUI/webchat sessions while keeping external channels on identity-based owner checks. (from #35666, #35673, #35704) Thanks @Naylenv, @Octane0411, and @Sid-Qin.
- Discord/inbound timeout isolation: separate inbound worker timeout tracking from listener timeout budgets so queued Discord replies are no longer dropped when listener watchdog windows expire mid-run. (#36602) Thanks @dutifulbob.

View File

@@ -135,4 +135,66 @@ describe("noteSecurityWarnings gateway exposure", () => {
expect(message).toContain("exec-approvals.json");
expect(message).toContain("openclaw approvals get --gateway");
});
it("warns when heartbeat delivery relies on implicit directPolicy defaults", async () => {
const cfg = {
agents: {
defaults: {
heartbeat: {
target: "last",
},
},
},
} as OpenClawConfig;
await noteSecurityWarnings(cfg);
const message = lastMessage();
expect(message).toContain("Heartbeat defaults");
expect(message).toContain("agents.defaults.heartbeat.directPolicy");
expect(message).toContain("direct/DM targets by default");
});
it("warns when a per-agent heartbeat relies on implicit directPolicy", async () => {
const cfg = {
agents: {
list: [
{
id: "ops",
heartbeat: {
target: "last",
},
},
],
},
} as OpenClawConfig;
await noteSecurityWarnings(cfg);
const message = lastMessage();
expect(message).toContain('Heartbeat agent "ops"');
expect(message).toContain('heartbeat.directPolicy for agent "ops"');
expect(message).toContain("direct/DM targets by default");
});
it("skips heartbeat directPolicy warning when delivery is internal-only or explicit", async () => {
const cfg = {
agents: {
defaults: {
heartbeat: {
target: "none",
},
},
list: [
{
id: "ops",
heartbeat: {
target: "last",
directPolicy: "block",
},
},
],
},
} as OpenClawConfig;
await noteSecurityWarnings(cfg);
const message = lastMessage();
expect(message).not.toContain("Heartbeat defaults");
expect(message).not.toContain('Heartbeat agent "ops"');
});
});

View File

@@ -2,6 +2,7 @@ import { listChannelPlugins } from "../channels/plugins/index.js";
import type { ChannelId } from "../channels/plugins/types.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig, GatewayBindMode } from "../config/config.js";
import type { AgentConfig } from "../config/types.agents.js";
import { hasConfiguredSecretInput } from "../config/types.secrets.js";
import { resolveGatewayAuth } from "../gateway/auth.js";
import { isLoopbackHost, resolveGatewayBindHost } from "../gateway/net.js";
@@ -9,6 +10,44 @@ import { resolveDmAllowState } from "../security/dm-policy-shared.js";
import { note } from "../terminal/note.js";
import { resolveDefaultChannelAccountContext } from "./channel-account-context.js";
function collectImplicitHeartbeatDirectPolicyWarnings(cfg: OpenClawConfig): string[] {
const warnings: string[] = [];
const maybeWarn = (params: {
label: string;
heartbeat: AgentConfig["heartbeat"] | undefined;
pathHint: string;
}) => {
const heartbeat = params.heartbeat;
if (!heartbeat || heartbeat.target === undefined || heartbeat.target === "none") {
return;
}
if (heartbeat.directPolicy !== undefined) {
return;
}
warnings.push(
`- ${params.label}: heartbeat delivery is configured while ${params.pathHint} is unset.`,
' Heartbeat now allows direct/DM targets by default. Set it explicitly to "allow" or "block" to pin upgrade behavior.',
);
};
maybeWarn({
label: "Heartbeat defaults",
heartbeat: cfg.agents?.defaults?.heartbeat,
pathHint: "agents.defaults.heartbeat.directPolicy",
});
for (const agent of cfg.agents?.list ?? []) {
maybeWarn({
label: `Heartbeat agent "${agent.id}"`,
heartbeat: agent.heartbeat,
pathHint: `heartbeat.directPolicy for agent "${agent.id}"`,
});
}
return warnings;
}
export async function noteSecurityWarnings(cfg: OpenClawConfig) {
const warnings: string[] = [];
const auditHint = `- Run: ${formatCliCommand("openclaw security audit --deep")}`;
@@ -21,6 +60,8 @@ export async function noteSecurityWarnings(cfg: OpenClawConfig) {
);
}
warnings.push(...collectImplicitHeartbeatDirectPolicyWarnings(cfg));
// ===========================================
// GATEWAY NETWORK EXPOSURE CHECK
// ===========================================