feat(coordinator): MCP orchestration engine, REST API hardening, and task model#120
feat(coordinator): MCP orchestration engine, REST API hardening, and task model#120brooksc wants to merge 2 commits into
Conversation
…tooling atomic.ts / atomic.test.ts - Crash-safe atomic file writes via temp-file + rename - fsync temp file and directory entry for durability - Preserve existing file mode on overwrite; use fchmod after open to set exact mode bits, bypassing process umask - Tests force umask=0o022 in exact-mode cases for CI determinism validation.ts / validation.test.ts - validateBranchName: mirrors git check-ref-format --branch rules (rejects HEAD; accepts FETCH_HEAD/ORIG_HEAD per actual git behaviour) - validateUUID: enforces v4 UUID (version nibble 4, variant nibble 8/9/a/b) Static analysis - .semgrep/filesystem-safety.yml: flag raw writeFileSync in MCP/coordinator paths; pattern-either covers both qualified (fs.writeFileSync) and unqualified forms - .semgrep/electron-security.yml, .semgrep/ipc-auth.yml: IPC auth checks - .dependency-cruiser.cjs: architecture boundary enforcement - .gitleaks.toml: secret-scanning rules - knip.config.ts: dead-export detection; server.ts listed as entry point (lands in coordinator-2-mcp-backend) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…task model This is PR 2 of 4 in the coordinator series (stacked on coordinator-1-security). ## MCP orchestration engine (electron/mcp/) coordinator.ts — core orchestrator singleton - Creates and manages coordinated sub-tasks, each in its own worktree - Three-class token RBAC: coordinator / subtask / mobile - Per-task done tokens (24-byte random, timing-safe comparison) - PTY output subscription for idle detection; sliding-window prompt detector - Batched review notifications with configurable delay and restaging - signal_done / waitForSignalDone with replay cache (requestId dedup) - Atomic preamble injection and strip via atomic.ts - setTaskControl / blockedByHumanControl state machine - cleanupTask with double-resolve guard on anySignalResolvers server.ts — MCP stdio entry point - Speaks MCP over stdio to Claude Code; delegates to Electron app via HTTP - CLI arg validation (rejects \r/\n in --coordinator-id / --task-id) - Exposes: create_task, list_tasks, get_task_status, get_task_diff, get_task_output, send_prompt, wait_for_idle, wait_for_signal_done, merge_task, close_task, review_and_merge_task, signal_done config.ts — MCP JSON config generation - selectMcpJsonDir: finds the right directory for the per-coordinator config - writeMcpConfig: writes config (mode 0o600) via atomicWriteFileSync preamble.ts — preamble injection / strip - injectPreamble: atomically appends <sub-task-mode> block to CLAUDE.md, AGENTS.md, GEMINI.md, .agent.md, or settings.local.json - stripPreambleFromBranch: atomically strips the block on merge/close - buildNormalizedPreambleFileDiff: git diff --no-index with anchored regex path rewriting (prevents false substitutions from content matching tmpdir) sub-task-preamble.ts — sub-task-side preamble injection prompt-detect.ts — detects when Claude is waiting for user input replay-cache.ts — deduplicates wait_for_signal_done retries client.ts — MCPClient used by the MCP stdio server to call the REST API mcp-tool-list.ts — builds the tool list advertised to coordinator/subtask types.ts — shared types (CoordinatedTask, ApiTaskSummary, ApiTaskDetail, etc.) ## REST API (electron/remote/server.ts) New coordinator task routes, all requiring auth: - POST /api/tasks create sub-task - GET /api/tasks list sub-tasks (coordinator-scoped) - GET /api/tasks/:id get status - POST /api/tasks/:id/prompt send prompt - POST /api/tasks/:id/wait wait for idle - GET /api/tasks/:id/diff get diff - GET /api/tasks/:id/output get scrollback - POST /api/tasks/:id/merge merge branch - POST /api/tasks/:id/review-merge diff + merge - DELETE /api/tasks/:id close/cleanup - POST /api/tasks/:id/done signal done (subtask token + X-Done-Token) - POST /api/wait-signal wait for any signal_done Auth scoping hardening: - callerCoordinatorId extracted from verified X-Coordinator-Id header and enforced before all coordinator routes (including wait-signal) - create_task: body coordinatorTaskId ignored; header value is authoritative; mismatched body value returns 403 - wait-signal: body coordinatorTaskId ignored entirely; header-only - Mobile token restricted to /api/agents only (task routes removed) - Coordinator token without X-Coordinator-Id gets 403 on all task routes - task.name sanitized: 200-char max, control characters stripped - Design-intent comment on coordinator-token signal_done exemption ## IPC handlers (electron/ipc/register.ts) New handlers: StartMCPServer, StopMCPServer, GetMCPStatus, GetMCPLogs, HydrateCoordinatedTask, MCP_CoordinatedTaskClosed, MCP_TaskHydrated, MCP_CoordinatorNotificationAck, MCP_CoordinatorNotificationDropAck, MCP_CoordinatorRestageAfterUserSend ## Store / type changes (src/store/) types.ts — new Task fields: coordinatorMode, coordinatedBy, controlledBy, mcpConfigPath, preamble*, signalDone*, needsReview, mcpStartupStatus, etc. New AppStore fields: coordinatorModeEnabled, coordinatorNotificationDelayMs, coordinatorControlHintDismissed. New MCPStatus interface. core.ts, remote.ts, persistence.ts, store.ts, ui.ts — wired up coordinator store state; mcpStatus, coordinatorModeEnabled defaults; persistence round-trip for all new task fields ## OpenSpec openspec/changes/coordinator-mcp-backend/proposal.md — documents the MCP orchestration server, REST task API, three-token auth model, signal/wait lifecycle, preamble injection, and new IPC channels per CLAUDE.md requirement ## Tests electron/mcp/coordinator.test.ts — 180+ cases: lifecycle, preamble injection, waitForIdle, signal/wait, notifications, hydrateTask, setTaskControl, cleanupTask, validateBranchName extended rules electron/mcp/coordinator-sequence.test.ts — end-to-end create→signal→merge electron/mcp/config.test.ts — selectMcpJsonDir, writeMcpConfig electron/mcp/mcp-tool-list.test.ts — tool selection by role electron/mcp/prompt-detect.test.ts — idle detection patterns electron/remote/coordinator-scoping.test.ts — 40 HTTP integration tests: coordinator scoping, subtask token restrictions, mobile token restrictions (GET /api/tasks, GET /api/tasks/:id, POST /api/wait-signal all blocked), create_task scoping (header authoritative, body mismatch → 403) electron/ipc/register-mcp.test.ts — StartMCPServer input validation electron/ipc/register.test.ts — IPC handler registration electron/ipc/docker-config.test.ts — Docker MCP config paths electron/mcp/docker.integration.test.ts — Docker lifecycle (skipped without Docker) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Thank you very much for your work on this! <3 Follow-up review after splitting the pass across remote auth, coordinator lifecycle/persistence, and MCP execution paths. I think these need attention before merge:
Secondary follow-ups from the pass: |
Overview
This is PR 2 of 4 in the coordinator series splitting #100 as requested in the round-4 review. It is stacked on PR 1 (
coordinator-1-security) and should be merged after that one. The diff shown here includes PR 1's content; the meaningful delta for this PR is the coordinator engine and REST hardening described below.PR sequence:
coordinator-1-securitycoordinator-2-mcp-backendcoordinator-3-store-ipccoordinator-4-uiPRs 3–4 are stacked on this one. Nothing coordinator-related is user-visible until PR 4 adds the
NewTaskDialogcheckbox and Settings toggle (coordinatorModeEnableddefaults tofalse).What's in this PR (delta over PR 1)
MCP orchestration engine (
electron/mcp/)coordinator.ts— core orchestrator singletonsignal_done/waitForSignalDonewith replay cache (requestIddedup for safe retries)atomic.tssetTaskControl/blockedByHumanControlstate machine (coordinator ↔ human hand-off)cleanupTaskwith double-resolve guard onanySignalResolversserver.ts— MCP stdio entry point\r/\nin--coordinator-id/--task-id(header injection prevention)create_task,list_tasks,get_task_status,get_task_diff,get_task_output,send_prompt,wait_for_idle,wait_for_signal_done,merge_task,close_task,review_and_merge_task,signal_doneconfig.ts— MCP JSON config generationselectMcpJsonDir: selects the right directory for the per-coordinator configwriteMcpConfig: writes config (mode0o600) atomicallypreamble.ts— preamble injection and strip<sub-task-mode>block to CLAUDE.md, AGENTS.md, GEMINI.md,.agent.md, orsettings.local.jsondepending on agent typestripPreambleFromBranch: atomically strips the block on merge/closebuildNormalizedPreambleFileDiff:git diff --no-indexwith anchored-regex path rewriting (prevents false substitutions when tmpdir path appears in file content)sub-task-preamble.ts— sub-task-side preamble injectionprompt-detect.ts— sliding-window idle/prompt detectorreplay-cache.ts— deduplicateswait_for_signal_doneretries byrequestIdclient.ts—MCPClientused by the MCP stdio process to call the REST APImcp-tool-list.ts— builds the tool list advertised to coordinator vs. sub-task rolestypes.ts— shared types:CoordinatedTask,ApiTaskSummary,ApiTaskDetail, token classes, etc.REST API hardening (
electron/remote/server.ts)New coordinator task routes (all require auth):
/api/tasks/api/tasks/api/tasks/:id/api/tasks/:id/prompt/api/tasks/:id/wait/api/tasks/:id/diff/api/tasks/:id/output/api/tasks/:id/merge/api/tasks/:id/review-merge/api/tasks/:id/api/tasks/:id/doneX-Done-Token)/api/wait-signalsignal_doneAuth scoping hardening:
callerCoordinatorIdextracted from verifiedX-Coordinator-Idheader and enforced before all coordinator routes (includingwait-signal)create_task: bodycoordinatorTaskIdignored; header is authoritative; mismatch → 403wait-signal: bodycoordinatorTaskIdignored entirely; header-only/api/agentsonly — task routes removed (mobile token is embedded in a QR-code URL reachable by anyone on the local network)X-Coordinator-Id→ 403 on all task routestask.name: 200-char max, control characters stripped (prompt injection prevention)IPC handlers (
electron/ipc/register.ts)New handlers wired up:
StartMCPServer,StopMCPServer,GetMCPStatus,GetMCPLogs,HydrateCoordinatedTask,MCP_CoordinatedTaskClosed,MCP_TaskHydrated,MCP_CoordinatorNotificationAck,MCP_CoordinatorNotificationDropAck,MCP_CoordinatorRestageAfterUserSendStore / type changes (
src/store/)New
Taskfields:coordinatorMode,coordinatedBy,controlledBy,mcpConfigPath,preambleFileExistedBefore,signalDone*,needsReview,mcpStartupStatus/ErrorNew
PersistedState/AppStorefields:coordinatorModeEnabled,coordinatorNotificationDelayMs,coordinatorControlHintDismissed,MCPStatuscore.ts,remote.ts,persistence.ts,store.ts,ui.ts— wired up coordinator store state with defaults and persistence round-tripOpenSpec
openspec/changes/coordinator-mcp-backend/proposal.md— documents the MCP orchestration server, REST task API, three-token auth model, signal/wait lifecycle, preamble injection, and new IPC channels perCLAUDE.mdrequirementTests
electron/mcp/coordinator.test.tswaitForIdle, signal/wait, notifications,hydrateTask,setTaskControl,cleanupTaskelectron/mcp/coordinator-sequence.test.tselectron/mcp/config.test.tsselectMcpJsonDir,writeMcpConfigelectron/mcp/mcp-tool-list.test.tselectron/mcp/prompt-detect.test.tselectron/remote/coordinator-scoping.test.tscreate_taskbody-vs-header scopingelectron/ipc/register-mcp.test.tsStartMCPServerinput validationelectron/ipc/register.test.tselectron/ipc/docker-config.test.tselectron/mcp/docker.integration.test.tsTotal: 295+ test cases.
npm test→ 859 pass, 12 skipped.Test plan
npm run compile && npm run typecheck && npm run lint && npm run format:check— passnpm test— 859 pass, 12 skipped (Docker integration skipped without daemon)git diff --check johannesjo/main...HEAD— clean🤖 Generated with Claude Code