fix(tools): clear timeout timers and update audit state

This commit is contained in:
William Valentin
2026-02-15 21:44:40 -08:00
parent d93c1c9f8d
commit 4cdad8eee9
4 changed files with 607 additions and 491 deletions
@@ -3,6 +3,13 @@
Date: 2026-02-16 Date: 2026-02-16
Scope: Production-risk-first audit of bugs, code improvements, and feature opportunities. Scope: Production-risk-first audit of bugs, code improvements, and feature opportunities.
## Remediation Status (2026-02-16)
- ✅ F-001 addressed: chat markdown rendering now sanitizes HTML before DOM insertion in `src/gateway/ui/pages/chat.js` (and legacy `src/gateway/ui/chat.html`).
- ✅ F-006 addressed: inbound HTTP request bodies now enforce a configurable max-size limit (`server.max_request_body_bytes`) with `413 Payload Too Large` responses.
- ✅ F-007 addressed: `ToolExecutor` timeout timer handles are now cleared in `finally`, preventing orphan timers on fast/failed tool calls.
- ✅ F-016 partially addressed: gateway + webhook body readers were consolidated into shared utility `src/utils/httpBody.ts` with size-limit enforcement.
## Executive Summary ## Executive Summary
Current health snapshot: Current health snapshot:
+131 -46
View File
@@ -2,7 +2,6 @@
"version": "1.0", "version": "1.0",
"updated_at": "2026-02-16", "updated_at": "2026-02-16",
"description": "Tracks the status of all Flynn plans and implementation phases", "description": "Tracks the status of all Flynn plans and implementation phases",
"plans": { "plans": {
"docs-agent-oriented-diagrams-pass": { "docs-agent-oriented-diagrams-pass": {
"status": "completed", "status": "completed",
@@ -42,7 +41,6 @@
], ],
"test_status": "pnpm test:run + pnpm typecheck passing" "test_status": "pnpm test:run + pnpm typecheck passing"
}, },
"zai-auth-resolution-and-401-hints": { "zai-auth-resolution-and-401-hints": {
"status": "completed", "status": "completed",
"date": "2026-02-16", "date": "2026-02-16",
@@ -56,7 +54,6 @@
], ],
"test_status": "pnpm test:run src/daemon/clientFactory.test.ts src/models/openai.test.ts + pnpm typecheck passing (updated to cover textual 401 without status field)" "test_status": "pnpm test:run src/daemon/clientFactory.test.ts src/models/openai.test.ts + pnpm typecheck passing (updated to cover textual 401 without status field)"
}, },
"tui-model-provider-switch-activates-tier": { "tui-model-provider-switch-activates-tier": {
"status": "completed", "status": "completed",
"date": "2026-02-16", "date": "2026-02-16",
@@ -68,7 +65,6 @@
], ],
"test_status": "pnpm test:run src/frontends/tui/minimal.test.ts src/models/openai.test.ts src/daemon/clientFactory.test.ts + pnpm typecheck passing" "test_status": "pnpm test:run src/frontends/tui/minimal.test.ts src/models/openai.test.ts src/daemon/clientFactory.test.ts + pnpm typecheck passing"
}, },
"zai-auth-reauthenticate-confirmation": { "zai-auth-reauthenticate-confirmation": {
"status": "completed", "status": "completed",
"date": "2026-02-16", "date": "2026-02-16",
@@ -80,7 +76,6 @@
], ],
"test_status": "pnpm test:run src/cli/zai-auth.test.ts + pnpm typecheck passing" "test_status": "pnpm test:run src/cli/zai-auth.test.ts + pnpm typecheck passing"
}, },
"auth-commands-reauthenticate-confirmation": { "auth-commands-reauthenticate-confirmation": {
"status": "completed", "status": "completed",
"date": "2026-02-16", "date": "2026-02-16",
@@ -96,7 +91,6 @@
], ],
"test_status": "pnpm test:run src/cli/openai-key.test.ts src/cli/anthropic-auth.test.ts src/cli/openai-auth.test.ts src/cli/zai-auth.test.ts + pnpm typecheck passing" "test_status": "pnpm test:run src/cli/openai-key.test.ts src/cli/anthropic-auth.test.ts src/cli/openai-auth.test.ts src/cli/zai-auth.test.ts + pnpm typecheck passing"
}, },
"cli-suppress-punycode-deprecation-warning": { "cli-suppress-punycode-deprecation-warning": {
"status": "completed", "status": "completed",
"date": "2026-02-16", "date": "2026-02-16",
@@ -109,7 +103,6 @@
], ],
"test_status": "pnpm test:run src/cli/suppressNodeWarnings.test.ts src/cli/index.test.ts + pnpm typecheck + pnpm build passing" "test_status": "pnpm test:run src/cli/suppressNodeWarnings.test.ts src/cli/index.test.ts + pnpm typecheck + pnpm build passing"
}, },
"zai-auth-mode-selection-api-vs-plan": { "zai-auth-mode-selection-api-vs-plan": {
"status": "completed", "status": "completed",
"date": "2026-02-16", "date": "2026-02-16",
@@ -122,7 +115,6 @@
], ],
"test_status": "pnpm test:run src/cli/zai-auth.test.ts src/frontends/tui/minimal.test.ts + pnpm typecheck passing" "test_status": "pnpm test:run src/cli/zai-auth.test.ts src/frontends/tui/minimal.test.ts + pnpm typecheck passing"
}, },
"tui-login-reauth-confirmation-all-providers": { "tui-login-reauth-confirmation-all-providers": {
"status": "completed", "status": "completed",
"date": "2026-02-16", "date": "2026-02-16",
@@ -134,7 +126,6 @@
], ],
"test_status": "pnpm test:run src/frontends/tui/minimal.login.test.ts src/frontends/tui/minimal.test.ts + pnpm typecheck passing" "test_status": "pnpm test:run src/frontends/tui/minimal.login.test.ts src/frontends/tui/minimal.test.ts + pnpm typecheck passing"
}, },
"anthropic-auth-mode-flag": { "anthropic-auth-mode-flag": {
"status": "completed", "status": "completed",
"date": "2026-02-16", "date": "2026-02-16",
@@ -146,7 +137,6 @@
], ],
"test_status": "pnpm test:run src/cli/anthropic-auth.test.ts src/cli/index.test.ts + pnpm typecheck passing" "test_status": "pnpm test:run src/cli/anthropic-auth.test.ts src/cli/index.test.ts + pnpm typecheck passing"
}, },
"deployment-port-env-override": { "deployment-port-env-override": {
"status": "completed", "status": "completed",
"date": "2026-02-16", "date": "2026-02-16",
@@ -158,7 +148,6 @@
], ],
"test_status": "pnpm test:run + pnpm typecheck passing" "test_status": "pnpm test:run + pnpm typecheck passing"
}, },
"deployment-targets-nix": { "deployment-targets-nix": {
"file": "2026-02-16-deployment-targets.md", "file": "2026-02-16-deployment-targets.md",
"status": "completed", "status": "completed",
@@ -178,7 +167,6 @@
], ],
"test_status": "nix build dependency hash resolved (fetchPnpmDeps hash pinned from build output); run nix build .#flynn to verify end-to-end in host environment" "test_status": "nix build dependency hash resolved (fetchPnpmDeps hash pinned from build output); run nix build .#flynn to verify end-to-end in host environment"
}, },
"deployment-targets-paas": { "deployment-targets-paas": {
"file": "2026-02-16-deployment-targets.md", "file": "2026-02-16-deployment-targets.md",
"status": "completed", "status": "completed",
@@ -200,7 +188,6 @@
], ],
"test_status": "pnpm typecheck passing (no runtime code changes)" "test_status": "pnpm typecheck passing (no runtime code changes)"
}, },
"openclaw-gap-roadmap": { "openclaw-gap-roadmap": {
"file": "2026-02-15-openclaw-gap-roadmap.md", "file": "2026-02-15-openclaw-gap-roadmap.md",
"status": "completed", "status": "completed",
@@ -372,7 +359,6 @@
], ],
"test_status": "pnpm typecheck + pnpm test:run passing" "test_status": "pnpm typecheck + pnpm test:run passing"
}, },
"elevated-mode": { "elevated-mode": {
"status": "completed", "status": "completed",
"date": "2026-02-16", "date": "2026-02-16",
@@ -394,7 +380,6 @@
], ],
"test_status": "pnpm typecheck + pnpm test:run passing" "test_status": "pnpm typecheck + pnpm test:run passing"
}, },
"matrix-channel-adapter": { "matrix-channel-adapter": {
"file": "2026-02-15-matrix-channel-adapter.md", "file": "2026-02-15-matrix-channel-adapter.md",
"status": "completed", "status": "completed",
@@ -611,7 +596,10 @@
"priority": "P0", "priority": "P0",
"status": "completed", "status": "completed",
"description": "Persistent memory with file-based storage, memory tools, auto-extraction after compaction", "description": "Persistent memory with file-based storage, memory tools, auto-extraction after compaction",
"depends_on": ["phase_0", "phase_1"], "depends_on": [
"phase_0",
"phase_1"
],
"files_created": [ "files_created": [
"src/memory/store.ts", "src/memory/store.ts",
"src/memory/store.test.ts", "src/memory/store.test.ts",
@@ -664,7 +652,9 @@
"src/daemon/index.ts", "src/daemon/index.ts",
"package.json" "package.json"
], ],
"new_dependencies": ["@slack/bolt"], "new_dependencies": [
"@slack/bolt"
],
"test_status": "22/22 passing", "test_status": "22/22 passing",
"notes": "Socket Mode only (HTTP fallback deferred). Slash commands deferred. User ID used as senderName (display name resolution is a follow-up)." "notes": "Socket Mode only (HTTP fallback deferred). Slash commands deferred. User ID used as senderName (display name resolution is a follow-up)."
}, },
@@ -682,7 +672,9 @@
"src/daemon/index.ts", "src/daemon/index.ts",
"package.json" "package.json"
], ],
"new_dependencies": ["whatsapp-web.js"], "new_dependencies": [
"whatsapp-web.js"
],
"test_status": "25/25 passing", "test_status": "25/25 passing",
"notes": "QR code auth via LocalAuth. Session persistence via data_dir. Group messages filtered (DM only). Phone number allowlist. Headless Chrome with Puppeteer." "notes": "QR code auth via LocalAuth. Session persistence via data_dir. Group messages filtered (DM only). Phone number allowlist. Headless Chrome with Puppeteer."
} }
@@ -748,7 +740,12 @@
"src/tools/builtin/web-fetch.ts", "src/tools/builtin/web-fetch.ts",
"src/tools/builtin/web-fetch.test.ts" "src/tools/builtin/web-fetch.test.ts"
], ],
"new_dependencies": ["turndown", "linkedom", "@mozilla/readability", "@types/turndown"], "new_dependencies": [
"turndown",
"linkedom",
"@mozilla/readability",
"@types/turndown"
],
"test_status": "10/10 passing" "test_status": "10/10 passing"
} }
} }
@@ -953,22 +950,30 @@
"telegram": { "telegram": {
"status": "completed", "status": "completed",
"description": "Handle message:photo (largest size, download via getFile API, base64) and image message:document events with caption text", "description": "Handle message:photo (largest size, download via getFile API, base64) and image message:document events with caption text",
"files_modified": ["src/channels/telegram/adapter.ts"] "files_modified": [
"src/channels/telegram/adapter.ts"
]
}, },
"discord": { "discord": {
"status": "completed", "status": "completed",
"description": "Extract image attachments from message.attachments Collection, pass Discord CDN URLs directly", "description": "Extract image attachments from message.attachments Collection, pass Discord CDN URLs directly",
"files_modified": ["src/channels/discord/adapter.ts"] "files_modified": [
"src/channels/discord/adapter.ts"
]
}, },
"slack": { "slack": {
"status": "completed", "status": "completed",
"description": "Download image files via url_private_download with bot token auth, base64 encode", "description": "Download image files via url_private_download with bot token auth, base64 encode",
"files_modified": ["src/channels/slack/adapter.ts"] "files_modified": [
"src/channels/slack/adapter.ts"
]
}, },
"whatsapp": { "whatsapp": {
"status": "completed", "status": "completed",
"description": "Use downloadMedia() from whatsapp-web.js (returns base64 natively)", "description": "Use downloadMedia() from whatsapp-web.js (returns base64 natively)",
"files_modified": ["src/channels/whatsapp/adapter.ts"] "files_modified": [
"src/channels/whatsapp/adapter.ts"
]
}, },
"webchat": { "webchat": {
"status": "completed", "status": "completed",
@@ -1250,12 +1255,13 @@
"src/daemon/index.ts", "src/daemon/index.ts",
"package.json" "package.json"
], ],
"new_dependencies": ["googleapis"], "new_dependencies": [
"googleapis"
],
"test_status": "16/16 passing" "test_status": "16/16 passing"
} }
} }
}, },
"gmail-push-revisit": { "gmail-push-revisit": {
"status": "completed", "status": "completed",
"date": "2026-02-13", "date": "2026-02-13",
@@ -1278,10 +1284,11 @@
"package.json", "package.json",
"pnpm-lock.yaml" "pnpm-lock.yaml"
], ],
"new_dependencies": ["@google-cloud/pubsub"], "new_dependencies": [
"@google-cloud/pubsub"
],
"test_status": "pnpm typecheck + pnpm test:run passing" "test_status": "pnpm typecheck + pnpm test:run passing"
}, },
"openai-oauth-codex": { "openai-oauth-codex": {
"status": "completed", "status": "completed",
"date": "2026-02-13", "date": "2026-02-13",
@@ -1304,7 +1311,6 @@
], ],
"test_status": "pnpm typecheck + pnpm test:run passing" "test_status": "pnpm typecheck + pnpm test:run passing"
}, },
"zai-glm-4.7-credential-integration": { "zai-glm-4.7-credential-integration": {
"status": "completed", "status": "completed",
"date": "2026-02-13", "date": "2026-02-13",
@@ -1566,7 +1572,9 @@
"status": "completed", "status": "completed",
"date": "2026-02-09", "date": "2026-02-09",
"summary": "Configurable log-level system to suppress noisy fallback debug output in TUI. Replaces console.debug/log/warn with structured logger respecting config log_level (default: info).", "summary": "Configurable log-level system to suppress noisy fallback debug output in TUI. Replaces console.debug/log/warn with structured logger respecting config log_level (default: info).",
"files_created": ["src/logger.ts"], "files_created": [
"src/logger.ts"
],
"files_modified": [ "files_modified": [
"src/models/router.ts", "src/models/router.ts",
"src/models/retry.ts", "src/models/retry.ts",
@@ -2360,26 +2368,103 @@
}, },
"earlier_plans": { "earlier_plans": {
"plans": [ "plans": [
{ "file": "2026-02-02-flynn-design.md", "status": "completed" }, {
{ "file": "2026-02-02-flynn-phase1-implementation.md", "status": "completed" }, "file": "2026-02-02-flynn-design.md",
{ "file": "2026-02-02-flynn-phase2-implementation.md", "status": "completed" }, "status": "completed"
{ "file": "2026-02-05-flynn-phase3-implementation.md", "status": "completed" }, },
{ "file": "2026-02-05-tui-redesign.md", "status": "completed" }, {
{ "file": "2026-02-05-tui-redesign-implementation.md", "status": "completed" }, "file": "2026-02-02-flynn-phase1-implementation.md",
{ "file": "2026-02-05-llamacpp-integration-design.md", "status": "completed" }, "status": "completed"
{ "file": "2026-02-05-llamacpp-implementation.md", "status": "completed" }, },
{ "file": "2026-02-05-backend-switch-design.md", "status": "completed" }, {
{ "file": "2026-02-05-backend-switch-implementation.md", "status": "completed" }, "file": "2026-02-02-flynn-phase2-implementation.md",
{ "file": "2026-02-05-openclaw-parity-design.md", "status": "completed" }, "status": "completed"
{ "file": "2026-02-05-phase1-tool-framework.md", "status": "completed" }, },
{ "file": "2026-02-05-phase2-websocket-gateway.md", "status": "completed" }, {
{ "file": "2026-02-05-phase3-channel-adapters.md", "status": "completed" }, "file": "2026-02-05-flynn-phase3-implementation.md",
{ "file": "2026-02-05-phase5-cli-cron-doctor-design.md", "status": "completed" }, "status": "completed"
{ "file": "2026-02-05-phase5a-implementation.md", "status": "completed" } },
{
"file": "2026-02-05-tui-redesign.md",
"status": "completed"
},
{
"file": "2026-02-05-tui-redesign-implementation.md",
"status": "completed"
},
{
"file": "2026-02-05-llamacpp-integration-design.md",
"status": "completed"
},
{
"file": "2026-02-05-llamacpp-implementation.md",
"status": "completed"
},
{
"file": "2026-02-05-backend-switch-design.md",
"status": "completed"
},
{
"file": "2026-02-05-backend-switch-implementation.md",
"status": "completed"
},
{
"file": "2026-02-05-openclaw-parity-design.md",
"status": "completed"
},
{
"file": "2026-02-05-phase1-tool-framework.md",
"status": "completed"
},
{
"file": "2026-02-05-phase2-websocket-gateway.md",
"status": "completed"
},
{
"file": "2026-02-05-phase3-channel-adapters.md",
"status": "completed"
},
{
"file": "2026-02-05-phase5-cli-cron-doctor-design.md",
"status": "completed"
},
{
"file": "2026-02-05-phase5a-implementation.md",
"status": "completed"
}
] ]
},
"audit-hardening-xss-body-limits-timeout-leak": {
"status": "completed",
"date": "2026-02-16",
"updated": "2026-02-16",
"summary": "Addressed high-priority codebase audit findings by hardening gateway chat markdown rendering (sanitization before DOM insertion), adding configurable inbound HTTP body-size limits for gateway/webhooks with 413 responses, centralizing body parsing utility, and fixing ToolExecutor timeout timer cleanup to avoid orphaned timers.",
"files_created": [
"src/gateway/ui/lib/markdown.js",
"src/gateway/ui/lib/markdown.test.ts",
"src/utils/httpBody.ts",
"src/utils/httpBody.test.ts"
],
"files_modified": [
"src/gateway/ui/pages/chat.js",
"src/gateway/ui/chat.html",
"src/gateway/server.ts",
"src/gateway/server.test.ts",
"src/automation/webhooks.ts",
"src/automation/webhooks.test.ts",
"src/config/schema.ts",
"src/config/schema.test.ts",
"config/default.yaml",
"src/daemon/services.ts",
"src/daemon/channels.ts",
"src/tools/executor.ts",
"src/tools/executor.test.ts",
"README.md",
"docs/deployment/PRODUCTION.md"
],
"test_status": "targeted: pnpm test:run src/gateway/server.test.ts src/automation/webhooks.test.ts src/tools/executor.test.ts src/config/schema.test.ts src/gateway/ui/lib/markdown.test.ts src/utils/httpBody.test.ts + pnpm typecheck"
} }
}, },
"overall_progress": { "overall_progress": {
"total_test_count": 1703, "total_test_count": 1703,
"all_tests_passing": true, "all_tests_passing": true,
+17 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { ToolExecutor } from './executor.js'; import { ToolExecutor } from './executor.js';
import { ToolRegistry } from './registry.js'; import { ToolRegistry } from './registry.js';
import { HookEngine } from '../hooks/engine.js'; import { HookEngine } from '../hooks/engine.js';
@@ -98,6 +98,22 @@ describe('ToolExecutor', () => {
expect(result.output).toContain('[truncated]'); expect(result.output).toContain('[truncated]');
}); });
it('clears timeout timer after fast tool completion', async () => {
vi.useFakeTimers();
try {
const registry = new ToolRegistry();
registry.register(echoTool);
const hooks = new HookEngine({ confirm: [], log: [], silent: [] });
const executor = new ToolExecutor(registry, hooks, { defaultTimeoutMs: 30_000 });
const result = await executor.execute('test.echo', { text: 'hello' });
expect(result.success).toBe(true);
expect(vi.getTimerCount()).toBe(0);
} finally {
vi.useRealTimers();
}
});
it('blocks on confirm hook and resolves when approved', async () => { it('blocks on confirm hook and resolves when approved', async () => {
const registry = new ToolRegistry(); const registry = new ToolRegistry();
registry.register(echoTool); registry.register(echoTool);
+11 -3
View File
@@ -224,6 +224,7 @@ export class ToolExecutor {
agent_tier: context?.tier, agent_tier: context?.tier,
}); });
let timeoutHandle: NodeJS.Timeout | undefined;
try { try {
const result = await Promise.race([ const result = await Promise.race([
(async () => { (async () => {
@@ -239,9 +240,12 @@ export class ToolExecutor {
} }
return tool.execute(args); return tool.execute(args);
})(), })(),
new Promise<ToolResult>((_, reject) => new Promise<ToolResult>((_, reject) => {
setTimeout(() => reject(new Error(`Tool '${toolName}' timed out after ${this.defaultTimeoutMs}ms`)), this.defaultTimeoutMs), timeoutHandle = setTimeout(
), () => reject(new Error(`Tool '${toolName}' timed out after ${this.defaultTimeoutMs}ms`)),
this.defaultTimeoutMs,
);
}),
]); ]);
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
@@ -286,6 +290,10 @@ export class ToolExecutor {
output: '', output: '',
error: String(errorRedaction.value), error: String(errorRedaction.value),
}; };
} finally {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
} }
} }