fix(webchat): suppress heartbeat ok broadcasts; stabilize audit/status tests
This commit is contained in:
parent
cec996c812
commit
1284c3d868
@ -370,7 +370,7 @@ describe("channels command", () => {
|
||||
});
|
||||
expect(lines.join("\n")).toMatch(/Warnings:/);
|
||||
expect(lines.join("\n")).toMatch(/Message Content Intent is disabled/i);
|
||||
expect(lines.join("\n")).toMatch(/Run: clawdbot doctor/);
|
||||
expect(lines.join("\n")).toMatch(/Run: .*doctor/i);
|
||||
});
|
||||
|
||||
it("surfaces Discord permission audit issues in channels status output", () => {
|
||||
|
||||
@ -312,7 +312,7 @@ describe("statusCommand", () => {
|
||||
expect(logs.some((l) => l.includes("FAQ:"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Troubleshooting:"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Next steps:"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("clawdbot status --all"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("status --all"))).toBe(true);
|
||||
});
|
||||
|
||||
it("shows gateway auth when reachable", async () => {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { stripHeartbeatToken, DEFAULT_HEARTBEAT_ACK_MAX_CHARS } from "../auto-reply/heartbeat.js";
|
||||
import { normalizeVerboseLevel } from "../auto-reply/thinking.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { type AgentEventPayload, getAgentRunContext } from "../infra/agent-events.js";
|
||||
@ -5,14 +6,52 @@ import { resolveHeartbeatVisibility } from "../infra/heartbeat-visibility.js";
|
||||
import { loadSessionEntry } from "./session-utils.js";
|
||||
import { formatForLog } from "./ws-log.js";
|
||||
|
||||
function resolveWebchatHeartbeatShowOk(): boolean {
|
||||
type WebchatHeartbeatPolicy = {
|
||||
showOk: boolean;
|
||||
ackMaxChars: number;
|
||||
};
|
||||
|
||||
let webchatHeartbeatPolicyCache:
|
||||
| { policy: WebchatHeartbeatPolicy; loadedAtMs: number }
|
||||
| undefined;
|
||||
|
||||
function resolveWebchatHeartbeatPolicy(): WebchatHeartbeatPolicy {
|
||||
// loadConfig() reads from disk + validates, so avoid doing it on every token stream event.
|
||||
const now = Date.now();
|
||||
const cached = webchatHeartbeatPolicyCache;
|
||||
if (cached && now - cached.loadedAtMs < 5_000) return cached.policy;
|
||||
|
||||
let policy: WebchatHeartbeatPolicy;
|
||||
try {
|
||||
const cfg = loadConfig();
|
||||
return resolveHeartbeatVisibility({ cfg, channel: "webchat" }).showOk;
|
||||
const visibility = resolveHeartbeatVisibility({ cfg, channel: "webchat" });
|
||||
const ackMaxChars = Math.max(
|
||||
0,
|
||||
cfg.agents?.defaults?.heartbeat?.ackMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
);
|
||||
policy = { showOk: visibility.showOk, ackMaxChars };
|
||||
} catch {
|
||||
// Default: hide HEARTBEAT_OK noise from webchat
|
||||
return false;
|
||||
// Safe fallback: treat HEARTBEAT_OK as hidden, but don't suppress alerts.
|
||||
policy = { showOk: false, ackMaxChars: DEFAULT_HEARTBEAT_ACK_MAX_CHARS };
|
||||
}
|
||||
|
||||
webchatHeartbeatPolicyCache = { policy, loadedAtMs: now };
|
||||
return policy;
|
||||
}
|
||||
|
||||
function shouldSuppressHeartbeatBroadcast(runId: string, text: string | undefined): boolean {
|
||||
const runContext = getAgentRunContext(runId);
|
||||
if (!runContext?.isHeartbeat) return false;
|
||||
|
||||
const policy = resolveWebchatHeartbeatPolicy();
|
||||
if (policy.showOk) return false;
|
||||
|
||||
const normalized = String(text ?? "").trim();
|
||||
if (!normalized) return true;
|
||||
|
||||
// Only suppress if this looks like a heartbeat ack-only response.
|
||||
return stripHeartbeatToken(normalized, { mode: "heartbeat", maxAckChars: policy.ackMaxChars })
|
||||
.shouldSkip;
|
||||
}
|
||||
|
||||
export type ChatRunEntry = {
|
||||
@ -156,8 +195,12 @@ export function createAgentEventHandler({
|
||||
timestamp: now,
|
||||
},
|
||||
};
|
||||
<<<<<<< HEAD
|
||||
// Suppress webchat broadcast for heartbeat runs when showOk is false
|
||||
if (!shouldSuppressHeartbeatBroadcast(agentRunId)) {
|
||||
=======
|
||||
if (!shouldSuppressHeartbeatBroadcast(clientRunId, text)) {
|
||||
>>>>>>> 3f72ad7bd (fix(webchat): suppress heartbeat ok broadcasts; stabilize audit/status tests)
|
||||
broadcast("chat", payload, { dropIfSlow: true });
|
||||
}
|
||||
nodeSendToSession(sessionKey, "chat", payload);
|
||||
@ -188,8 +231,12 @@ export function createAgentEventHandler({
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
<<<<<<< HEAD
|
||||
// Suppress webchat broadcast for heartbeat runs when showOk is false
|
||||
if (!shouldSuppressHeartbeatBroadcast(agentRunId)) {
|
||||
=======
|
||||
if (!shouldSuppressHeartbeatBroadcast(clientRunId, text)) {
|
||||
>>>>>>> 3f72ad7bd (fix(webchat): suppress heartbeat ok broadcasts; stabilize audit/status tests)
|
||||
broadcast("chat", payload);
|
||||
}
|
||||
nodeSendToSession(sessionKey, "chat", payload);
|
||||
|
||||
@ -44,6 +44,7 @@ describe("security audit", () => {
|
||||
|
||||
const res = await runSecurityAudit({
|
||||
config: cfg,
|
||||
env: {},
|
||||
includeFilesystem: false,
|
||||
includeChannelSecurity: false,
|
||||
});
|
||||
@ -88,6 +89,7 @@ describe("security audit", () => {
|
||||
|
||||
const res = await runSecurityAudit({
|
||||
config: cfg,
|
||||
env: {},
|
||||
includeFilesystem: false,
|
||||
includeChannelSecurity: false,
|
||||
});
|
||||
|
||||
@ -247,12 +247,15 @@ async function collectFilesystemFindings(params: {
|
||||
return findings;
|
||||
}
|
||||
|
||||
function collectGatewayConfigFindings(cfg: ClawdbotConfig): SecurityAuditFinding[] {
|
||||
function collectGatewayConfigFindings(
|
||||
cfg: ClawdbotConfig,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): SecurityAuditFinding[] {
|
||||
const findings: SecurityAuditFinding[] = [];
|
||||
|
||||
const bind = typeof cfg.gateway?.bind === "string" ? cfg.gateway.bind : "loopback";
|
||||
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
|
||||
const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, tailscaleMode });
|
||||
const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, tailscaleMode, env });
|
||||
const controlUiEnabled = cfg.gateway?.controlUi?.enabled !== false;
|
||||
const trustedProxies = Array.isArray(cfg.gateway?.trustedProxies)
|
||||
? cfg.gateway.trustedProxies
|
||||
@ -905,7 +908,7 @@ export async function runSecurityAudit(opts: SecurityAuditOptions): Promise<Secu
|
||||
findings.push(...collectAttackSurfaceSummaryFindings(cfg));
|
||||
findings.push(...collectSyncedFolderFindings({ stateDir, configPath }));
|
||||
|
||||
findings.push(...collectGatewayConfigFindings(cfg));
|
||||
findings.push(...collectGatewayConfigFindings(cfg, env));
|
||||
findings.push(...collectBrowserControlFindings(cfg));
|
||||
findings.push(...collectLoggingFindings(cfg));
|
||||
findings.push(...collectElevatedFindings(cfg));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user