// RFC-027 §2.3 race-free invariant — shared helper for the inbox- // enqueue lifecycle guard. // // PR1.1 (#346) added an `assertNodeActive` helper inside the // registerTools() closure and applied it at the 6 INSERT INTO inbox // sites in server/src/tools.ts. PR1.1 review (通信龙 #346 ack) caught // that the closure scope made the helper unreachable from REST // handlers in server/src/index.ts — POST /api/task (:1631) and // POST /api/broadcast (:1706) are dashboard's "Dispatch" entry // points and bypassed the guard, leaving the §2.3 race open on the // REST path. // // PR1.2a (#346 follow-up) extracts the helper to this module so both // the MCP tools and the REST handlers can import the same code path, // per team rule (grep every write site before adding a guard): any SQL-level // guard's helper lives at module scope so every write site // (MCP + REST + internal db.ts helpers) can use it. // // Behaviour: // - no nodes row for the alias → ok:true (brand-new alias allowed; // could be the first INSERT for a child about to register) // - lifecycle_state = 'active' OR NULL (pre-RFC-027 row) → ok:true // - anything else (stopping / stopped / stop_failed / deleting) → // ok:false + structured `node_not_active` reply // // COALESCE on the WHERE keeps NULL-network inbox rows / legacy nodes // rows correctly scoped (PR1 SF-5 lineage). import { db } from "./db.js"; export type LifecycleGuardResult = | { ok: true } | { ok: false; error: "node_not_active"; lifecycle_state: string; alias: string }; export function assertNodeActive( sessionAlias: string, networkId: string | null, ): LifecycleGuardResult { if (!sessionAlias) return { ok: true }; const row = db.get<{ lifecycle_state: string | null }>( networkId ? `SELECT lifecycle_state FROM nodes WHERE alias = ?1 AND COALESCE(network_id, ?2) = ?2 LIMIT 1` : `SELECT lifecycle_state FROM nodes WHERE alias = ?1 LIMIT 1`, ...(networkId ? [sessionAlias, networkId] : [sessionAlias]), ); if (!row) return { ok: true }; const st = row.lifecycle_state ?? "active"; if (st === "active") return { ok: true }; return { ok: false, error: "node_not_active", lifecycle_state: st, alias: sessionAlias }; }