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(/Warnings:/);
|
||||||
expect(lines.join("\n")).toMatch(/Message Content Intent is disabled/i);
|
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", () => {
|
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("FAQ:"))).toBe(true);
|
||||||
expect(logs.some((l) => l.includes("Troubleshooting:"))).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("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 () => {
|
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 { normalizeVerboseLevel } from "../auto-reply/thinking.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { type AgentEventPayload, getAgentRunContext } from "../infra/agent-events.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 { loadSessionEntry } from "./session-utils.js";
|
||||||
import { formatForLog } from "./ws-log.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 {
|
try {
|
||||||
const cfg = loadConfig();
|
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 {
|
} catch {
|
||||||
// Default: hide HEARTBEAT_OK noise from webchat
|
// Safe fallback: treat HEARTBEAT_OK as hidden, but don't suppress alerts.
|
||||||
return false;
|
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 = {
|
export type ChatRunEntry = {
|
||||||
@ -156,8 +195,12 @@ export function createAgentEventHandler({
|
|||||||
timestamp: now,
|
timestamp: now,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
<<<<<<< HEAD
|
||||||
// Suppress webchat broadcast for heartbeat runs when showOk is false
|
// Suppress webchat broadcast for heartbeat runs when showOk is false
|
||||||
if (!shouldSuppressHeartbeatBroadcast(agentRunId)) {
|
if (!shouldSuppressHeartbeatBroadcast(agentRunId)) {
|
||||||
|
=======
|
||||||
|
if (!shouldSuppressHeartbeatBroadcast(clientRunId, text)) {
|
||||||
|
>>>>>>> 3f72ad7bd (fix(webchat): suppress heartbeat ok broadcasts; stabilize audit/status tests)
|
||||||
broadcast("chat", payload, { dropIfSlow: true });
|
broadcast("chat", payload, { dropIfSlow: true });
|
||||||
}
|
}
|
||||||
nodeSendToSession(sessionKey, "chat", payload);
|
nodeSendToSession(sessionKey, "chat", payload);
|
||||||
@ -188,8 +231,12 @@ export function createAgentEventHandler({
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
<<<<<<< HEAD
|
||||||
// Suppress webchat broadcast for heartbeat runs when showOk is false
|
// Suppress webchat broadcast for heartbeat runs when showOk is false
|
||||||
if (!shouldSuppressHeartbeatBroadcast(agentRunId)) {
|
if (!shouldSuppressHeartbeatBroadcast(agentRunId)) {
|
||||||
|
=======
|
||||||
|
if (!shouldSuppressHeartbeatBroadcast(clientRunId, text)) {
|
||||||
|
>>>>>>> 3f72ad7bd (fix(webchat): suppress heartbeat ok broadcasts; stabilize audit/status tests)
|
||||||
broadcast("chat", payload);
|
broadcast("chat", payload);
|
||||||
}
|
}
|
||||||
nodeSendToSession(sessionKey, "chat", payload);
|
nodeSendToSession(sessionKey, "chat", payload);
|
||||||
|
|||||||
@ -44,6 +44,7 @@ describe("security audit", () => {
|
|||||||
|
|
||||||
const res = await runSecurityAudit({
|
const res = await runSecurityAudit({
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
env: {},
|
||||||
includeFilesystem: false,
|
includeFilesystem: false,
|
||||||
includeChannelSecurity: false,
|
includeChannelSecurity: false,
|
||||||
});
|
});
|
||||||
@ -88,6 +89,7 @@ describe("security audit", () => {
|
|||||||
|
|
||||||
const res = await runSecurityAudit({
|
const res = await runSecurityAudit({
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
env: {},
|
||||||
includeFilesystem: false,
|
includeFilesystem: false,
|
||||||
includeChannelSecurity: false,
|
includeChannelSecurity: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -247,12 +247,15 @@ async function collectFilesystemFindings(params: {
|
|||||||
return findings;
|
return findings;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectGatewayConfigFindings(cfg: ClawdbotConfig): SecurityAuditFinding[] {
|
function collectGatewayConfigFindings(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
env: NodeJS.ProcessEnv,
|
||||||
|
): SecurityAuditFinding[] {
|
||||||
const findings: SecurityAuditFinding[] = [];
|
const findings: SecurityAuditFinding[] = [];
|
||||||
|
|
||||||
const bind = typeof cfg.gateway?.bind === "string" ? cfg.gateway.bind : "loopback";
|
const bind = typeof cfg.gateway?.bind === "string" ? cfg.gateway.bind : "loopback";
|
||||||
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
|
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 controlUiEnabled = cfg.gateway?.controlUi?.enabled !== false;
|
||||||
const trustedProxies = Array.isArray(cfg.gateway?.trustedProxies)
|
const trustedProxies = Array.isArray(cfg.gateway?.trustedProxies)
|
||||||
? cfg.gateway.trustedProxies
|
? cfg.gateway.trustedProxies
|
||||||
@ -905,7 +908,7 @@ export async function runSecurityAudit(opts: SecurityAuditOptions): Promise<Secu
|
|||||||
findings.push(...collectAttackSurfaceSummaryFindings(cfg));
|
findings.push(...collectAttackSurfaceSummaryFindings(cfg));
|
||||||
findings.push(...collectSyncedFolderFindings({ stateDir, configPath }));
|
findings.push(...collectSyncedFolderFindings({ stateDir, configPath }));
|
||||||
|
|
||||||
findings.push(...collectGatewayConfigFindings(cfg));
|
findings.push(...collectGatewayConfigFindings(cfg, env));
|
||||||
findings.push(...collectBrowserControlFindings(cfg));
|
findings.push(...collectBrowserControlFindings(cfg));
|
||||||
findings.push(...collectLoggingFindings(cfg));
|
findings.push(...collectLoggingFindings(cfg));
|
||||||
findings.push(...collectElevatedFindings(cfg));
|
findings.push(...collectElevatedFindings(cfg));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user