From 41fd586ca0039a3220f646aa7088d4639820840c Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 5 May 2026 15:12:14 +0200 Subject: [PATCH] IPC server: per-request timeout, error logging, stale-socket guard (T-77) Three hardening fixes: - 60s per-request timeout (configurable via DaemonServer constructor) prevents a misbehaving handler from blocking the connection's read pipeline indefinitely. On timeout the client gets a clean tool_error response. - broadcast() and the per-request response writeln are wrapped in try/catch with stderr logging. Previously write failures silently dropped clients with no diagnostic; events going missing was invisible. - start() probes for a live daemon before unlinking a stale socket. If something answers within 200ms, refuse to start. Previously two daemons racing to bind would let the second rip the first's live socket out. Co-Authored-By: Claude --- .pql/pql-plan.json | 234 +++++++++++++++++++++------------------- CHANGELOG.md | 5 + lib/src/ipc/server.dart | 58 ++++++++-- 3 files changed, 176 insertions(+), 121 deletions(-) diff --git a/.pql/pql-plan.json b/.pql/pql-plan.json index 115db751..2099b3c1 100644 --- a/.pql/pql-plan.json +++ b/.pql/pql-plan.json @@ -1,5 +1,5 @@ { - "exported_at": "2026-05-05T13:10:43Z", + "exported_at": "2026-05-05T13:12:14Z", "decisions": [ { "id": "D-1", @@ -9,7 +9,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-3", @@ -19,7 +19,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-4", @@ -29,7 +29,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-5", @@ -39,7 +39,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-6", @@ -49,7 +49,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-7", @@ -59,7 +59,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-8", @@ -69,7 +69,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-9", @@ -79,7 +79,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-10", @@ -89,7 +89,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-11", @@ -99,7 +99,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-12", @@ -109,7 +109,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-13", @@ -119,7 +119,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-14", @@ -129,7 +129,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-15", @@ -139,7 +139,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-16", @@ -149,7 +149,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-17", @@ -159,7 +159,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-18", @@ -169,7 +169,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-19", @@ -179,7 +179,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-20", @@ -189,7 +189,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-21", @@ -199,7 +199,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-22", @@ -209,7 +209,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-23", @@ -219,7 +219,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-24", @@ -229,7 +229,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-25", @@ -239,7 +239,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-26", @@ -249,7 +249,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-27", @@ -259,7 +259,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-28", @@ -269,7 +269,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-29", @@ -279,7 +279,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-30", @@ -289,7 +289,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-31", @@ -299,7 +299,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-32", @@ -309,7 +309,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-33", @@ -319,7 +319,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-34", @@ -329,7 +329,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-35", @@ -339,7 +339,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-36", @@ -349,7 +349,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-37", @@ -359,7 +359,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-38", @@ -369,7 +369,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-39", @@ -379,7 +379,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-40", @@ -389,7 +389,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-41", @@ -399,7 +399,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-42", @@ -409,7 +409,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-43", @@ -419,7 +419,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-44", @@ -429,7 +429,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-45", @@ -439,7 +439,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-46", @@ -449,7 +449,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-47", @@ -459,7 +459,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-48", @@ -469,7 +469,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-49", @@ -479,7 +479,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-50", @@ -489,7 +489,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-51", @@ -499,7 +499,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-52", @@ -509,7 +509,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-53", @@ -519,7 +519,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-54", @@ -529,7 +529,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-55", @@ -539,7 +539,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-56", @@ -549,7 +549,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-57", @@ -559,7 +559,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-58", @@ -569,7 +569,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-59", @@ -579,7 +579,7 @@ "status": "active", "date": "2026-04-25", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-60", @@ -589,7 +589,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-61", @@ -599,7 +599,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-62", @@ -609,7 +609,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-63", @@ -619,7 +619,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-64", @@ -629,7 +629,7 @@ "status": "active", "date": "2026-05-03", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "D-65", @@ -639,7 +639,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-1", @@ -648,7 +648,7 @@ "title": "Authorisation granularity on the IPC socket", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-2", @@ -657,7 +657,7 @@ "title": "Back-pressure on event streams", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-3", @@ -666,7 +666,7 @@ "title": "Event persistence + audit/undo", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-4", @@ -675,7 +675,7 @@ "title": "`.canvas` schema compatibility with Obsidian", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-5", @@ -684,7 +684,7 @@ "title": "IPC wire-format stability + `schema_version:`", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-6", @@ -693,7 +693,7 @@ "title": "Window chrome — native frame vs frameless custom", "status": "resolved", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-7", @@ -702,7 +702,7 @@ "title": "macOS app bundle signing / notarisation", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-8", @@ -711,7 +711,7 @@ "title": "Extension API shape — widgets, subcommands, both?", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-9", @@ -720,7 +720,7 @@ "title": "Lua runtime vendoring", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-10", @@ -729,7 +729,7 @@ "title": "Extension manifest `schema_version:`", "status": "open", "file_path": "decisions/questions-extensions.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-11", @@ -738,7 +738,7 @@ "title": "Coverage gates — hard thresholds vs soft reporting", "status": "open", "file_path": "decisions/questions-testing.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-12", @@ -747,7 +747,7 @@ "title": "Screen-reader automation (axe-core via Playwright)", "status": "open", "file_path": "decisions/questions-testing.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-13", @@ -756,7 +756,7 @@ "title": "Web production-mode a11y", "status": "open", "file_path": "decisions/questions-accessibility.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-14", @@ -765,7 +765,7 @@ "title": "i18n plurals / gender / date-format tooling", "status": "open", "file_path": "decisions/questions-accessibility.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-15", @@ -774,7 +774,7 @@ "title": "Editor tab — full LSP vs tree-sitter-only highlight", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-16", @@ -783,7 +783,7 @@ "title": "`tree-sitter-dart` grammar maintenance", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-17", @@ -792,7 +792,7 @@ "title": "Icon set growth", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-18", @@ -801,7 +801,7 @@ "title": "Theme hot-reload in release builds", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-19", @@ -810,7 +810,7 @@ "title": "(withdrawn)", "status": "resolved", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-20", @@ -819,7 +819,7 @@ "title": "Kernel DB service — namespaced SQL access?", "status": "open", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-21", @@ -828,7 +828,7 @@ "title": "Pql absorbs planning vs keeps separate", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-22", @@ -837,7 +837,7 @@ "title": "Ticket persistence strategy", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-23", @@ -846,7 +846,7 @@ "title": "SSH-remote development — run clide against a remote workspace", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-25", @@ -855,7 +855,7 @@ "title": "Body text face — mono everywhere vs Josefin Sans UI + mono code", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-26", @@ -864,7 +864,7 @@ "title": "Small screen layout (\u003c 1000px)", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-27", @@ -873,7 +873,7 @@ "title": "Two-editor split", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-28", @@ -882,7 +882,7 @@ "title": "Terminal strip scope — shell only or logs/errors/tests", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-29", @@ -891,7 +891,7 @@ "title": "Branch picker location", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-30", @@ -900,7 +900,7 @@ "title": "Focus behavior when editor is dirty and viewer is peeked", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "Q-31", @@ -909,7 +909,7 @@ "title": "XWayland fallback for frameless — proper Wayland protocol needed", "status": "open", "file_path": "decisions/questions-architecture.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "R-2", @@ -919,7 +919,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "R-3", @@ -929,7 +929,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "R-4", @@ -939,7 +939,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "R-5", @@ -949,7 +949,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "R-6", @@ -959,7 +959,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "R-7", @@ -969,7 +969,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "R-8", @@ -979,7 +979,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "R-9", @@ -989,7 +989,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "R-10", @@ -999,7 +999,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "R-11", @@ -1009,7 +1009,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" }, { "id": "R-12", @@ -1019,7 +1019,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-05 13:10:16" + "synced_at": "2026-05-05 13:11:46" } ], "decision_refs": [ @@ -2615,19 +2615,19 @@ "id": "T-76", "type": "task", "title": "PTY: fix resource leaks and reader-isolate races", - "status": "in_progress", + "status": "done", "priority": "high", "created_at": "2026-05-05 12:58:59", - "updated_at": "2026-05-05 13:07:34" + "updated_at": "2026-05-05 13:10:50" }, { "id": "T-77", "type": "task", "title": "IPC server: per-request timeout, broadcast logging, stale-socket race", - "status": "backlog", + "status": "in_progress", "priority": "high", "created_at": "2026-05-05 12:58:59", - "updated_at": "2026-05-05 12:58:59" + "updated_at": "2026-05-05 13:10:57" }, { "id": "T-78", @@ -3961,6 +3961,20 @@ "old_value": "backlog", "new_value": "in_progress", "changed_at": "2026-05-05 13:07:34" + }, + { + "ticket_id": "T-76", + "field": "status", + "old_value": "in_progress", + "new_value": "done", + "changed_at": "2026-05-05 13:10:50" + }, + { + "ticket_id": "T-77", + "field": "status", + "old_value": "backlog", + "new_value": "in_progress", + "changed_at": "2026-05-05 13:10:57" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index cc021fb3..3d5a6072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. longer leaks the `ReceivePort` when `Isolate.spawn` throws, and `PtySession.spawn` closes the master fd if any post-receive step fails. +- IPC server hardening: per-request 60s timeout (configurable), + broadcast/response write failures logged instead of swallowed, + client dropped on response-write failure, and the stale-socket + retry now probes for a live daemon before unlinking the socket + (refusing to start if one answers). ### Security diff --git a/lib/src/ipc/server.dart b/lib/src/ipc/server.dart index 6ba45d96..b3a921bb 100644 --- a/lib/src/ipc/server.dart +++ b/lib/src/ipc/server.dart @@ -6,6 +6,12 @@ import 'package:clide/src/ipc/envelope.dart'; typedef RequestDispatcher = Future Function(IpcRequest request); +/// Default per-request timeout. A handler that doesn't return within +/// this window gets a `tool_error` response so the connection's read +/// pipeline isn't blocked indefinitely. Long-running commands (git +/// pull/push, large pql queries) can override per-command later. +const Duration _kDefaultRequestTimeout = Duration(seconds: 60); + /// Unix-socket JSON-lines server. Each connection is an independent /// bidirectional line-framed stream: client writes requests, daemon /// writes responses + events on the same socket. @@ -13,26 +19,27 @@ class DaemonServer { DaemonServer({ required this.socketPath, required this.dispatch, - }); + Duration requestTimeout = _kDefaultRequestTimeout, + }) : _requestTimeout = requestTimeout; final String socketPath; final RequestDispatcher dispatch; + final Duration _requestTimeout; ServerSocket? _server; final Set _clients = {}; - /// Broadcast [event] to every currently-connected client. - /// - /// Future tuning: per-client subsystem/id filter (`tail --filter - /// pane:p_7`). For Tier 1 every client sees everything. Sockets - /// that error on write are silently dropped; the client's read side - /// will notice the close. + /// Broadcast [event] to every currently-connected client. Sockets + /// that error on write are dropped — the client's read side will + /// notice the close. Errors are logged so silent event loss is + /// debuggable. void broadcast(IpcEvent event) { final line = event.encode(); for (final c in List.from(_clients)) { try { c.writeln(line); - } catch (_) { + } catch (e) { + stderr.writeln('clide daemon: broadcast write failed (${event.subsystem}.${event.kind}): $e'); _clients.remove(c); } } @@ -43,7 +50,19 @@ class DaemonServer { try { _server = await ServerSocket.bind(addr, 0); } on SocketException { - // stale socket from a prior crash — unlink and retry once + // Either a stale socket from a prior crash, or a live daemon. + // Probe by trying to connect — if a live peer answers, refuse + // to start so we don't rip its socket out. + try { + final probe = await Socket.connect(addr, 0) + .timeout(const Duration(milliseconds: 200)); + await probe.close(); + throw StateError('clide daemon already running at $socketPath'); + } on TimeoutException { + // No one answered — proceed to unlink and rebind. + } on SocketException { + // No one listening — proceed to unlink and rebind. + } try { await File(socketPath).delete(); } catch (_) {} @@ -92,7 +111,17 @@ class DaemonServer { if (msg is! IpcRequest) return; IpcResponse resp; try { - resp = await dispatch(msg); + resp = await dispatch(msg).timeout(_requestTimeout); + } on TimeoutException { + stderr.writeln('clide daemon: dispatch timeout for ${msg.cmd} (${_requestTimeout.inSeconds}s)'); + resp = IpcResponse.err( + id: msg.id, + error: IpcError( + code: 2, + kind: 'tool_error', + message: 'request timed out after ${_requestTimeout.inSeconds}s: ${msg.cmd}', + ), + ); } catch (e, st) { stderr.writeln('clide daemon: dispatch error for ${msg.cmd}: $e\n$st'); resp = IpcResponse.err( @@ -104,6 +133,13 @@ class DaemonServer { ), ); } - client.writeln(resp.encode()); + try { + client.writeln(resp.encode()); + } catch (e) { + // Client disconnected mid-dispatch — drop it so future events + // don't try to write to a dead socket. + stderr.writeln('clide daemon: response write failed (${msg.cmd}): $e'); + _clients.remove(client); + } } }