diff --git a/plugins/fusion-plugin-acp-runtime/src/__tests__/process-manager.test.ts b/plugins/fusion-plugin-acp-runtime/src/__tests__/process-manager.test.ts index c17a6ad317..41f54e084f 100644 --- a/plugins/fusion-plugin-acp-runtime/src/__tests__/process-manager.test.ts +++ b/plugins/fusion-plugin-acp-runtime/src/__tests__/process-manager.test.ts @@ -10,7 +10,6 @@ import { forceKill, spawnAgent, activeProcessCount, - createIdleTimer, } from "../process-manager.js"; const spawned: ChildProcess[] = []; @@ -160,24 +159,3 @@ describe("spawnAgent", () => { }); }); -describe("createIdleTimer", () => { - it("fires onIdle after the interval and reset re-arms", async () => { - let fired = 0; - const timer = createIdleTimer(30, () => { - fired += 1; - }); - await new Promise((r) => setTimeout(r, 50)); - expect(fired).toBe(1); - timer.clear(); - }); - - it("clear prevents firing", async () => { - let fired = 0; - const timer = createIdleTimer(20, () => { - fired += 1; - }); - timer.clear(); - await new Promise((r) => setTimeout(r, 40)); - expect(fired).toBe(0); - }); -}); diff --git a/plugins/fusion-plugin-acp-runtime/src/control-handler.ts b/plugins/fusion-plugin-acp-runtime/src/control-handler.ts index 066258a73e..689ce4570d 100644 --- a/plugins/fusion-plugin-acp-runtime/src/control-handler.ts +++ b/plugins/fusion-plugin-acp-runtime/src/control-handler.ts @@ -98,11 +98,10 @@ function buildResponse(sel: { } /** - * Read the per-category disposition from the live policy (exempt → allow). - * - * Exported so the fs-capabilities write path (U7) can reuse the exact same - * per-category gate-reading logic for `file_write_delete` instead of duplicating - * it (and risking drift from the U5 security floor). + * Read the raw per-category disposition from the live policy (exempt → allow), + * before the Risk S1 acknowledgement escalation. Callers that gate untrusted + * actions should use `effectiveDisposition` (which applies the escalation); this + * is the unescalated primitive it builds on. */ export function dispositionFor( category: FusionCategory | "exempt", diff --git a/plugins/fusion-plugin-acp-runtime/src/event-bridge.ts b/plugins/fusion-plugin-acp-runtime/src/event-bridge.ts index 9b5949eb50..ff997061c3 100644 --- a/plugins/fusion-plugin-acp-runtime/src/event-bridge.ts +++ b/plugins/fusion-plugin-acp-runtime/src/event-bridge.ts @@ -207,7 +207,8 @@ export function createEventBridge(callbacks: AcpCallbacks): EventBridge { // them (a partial update may only set status/output). if (update.title != null) tracked.title = safeTitle(update.title); if (update.kind != null) tracked.kind = update.kind; - setTracked(update.toolCallId, tracked); + // `id` is already bounded above; setTracked re-keys with the same value. + setTracked(id, tracked); const status = update.status; if (status !== "completed" && status !== "failed") { diff --git a/plugins/fusion-plugin-acp-runtime/src/index.ts b/plugins/fusion-plugin-acp-runtime/src/index.ts index 057d225865..ae20e1f319 100644 --- a/plugins/fusion-plugin-acp-runtime/src/index.ts +++ b/plugins/fusion-plugin-acp-runtime/src/index.ts @@ -2,6 +2,12 @@ import { definePlugin } from "@fusion/plugin-sdk"; import type { FusionPlugin, PluginRuntimeFactory, PluginRuntimeManifestMetadata } from "@fusion/plugin-sdk"; import { resolveCliSettings } from "./cli-spawn.js"; import { AcpRuntimeAdapter } from "./runtime-adapter.js"; +import { killAllProcesses } from "./process-manager.js"; + +// Reap any live agent subprocesses on hard process exit so none are orphaned +// (KTD4 — the registry SIGKILL is the authoritative no-orphan guarantee). Scoped +// to tracked agent subprocesses only; never touches other processes/ports. +process.on("exit", killAllProcesses); export const ACP_RUNTIME_ID = "acp"; const ACP_RUNTIME_VERSION = "0.1.0"; diff --git a/plugins/fusion-plugin-acp-runtime/src/process-manager.ts b/plugins/fusion-plugin-acp-runtime/src/process-manager.ts index e0a0a707fa..4325b817d8 100644 --- a/plugins/fusion-plugin-acp-runtime/src/process-manager.ts +++ b/plugins/fusion-plugin-acp-runtime/src/process-manager.ts @@ -152,38 +152,3 @@ export function captureStderr(child: ChildProcess): () => string { }); return () => buffer; } - -// --- inactivity timer (KTD4: high ceiling, engine is the authority) -------- - -/** Default idle ceiling. The engine's StuckTaskDetector is authoritative. */ -export const DEFAULT_IDLE_CEILING_MS = 30 * 60_000; - -export interface IdleTimer { - reset(): void; - clear(): void; -} - -/** - * Create an inactivity timer that fires `onIdle` after `ms` of no `reset()`. - * The default ceiling is intentionally high (KTD4) — this is a backstop, not - * the primary aborter. - */ -export function createIdleTimer(ms: number, onIdle: () => void): IdleTimer { - let handle: NodeJS.Timeout | undefined; - const arm = () => { - handle = setTimeout(onIdle, ms); - // Don't keep the event loop alive solely for the backstop timer. - handle.unref?.(); - }; - arm(); - return { - reset() { - if (handle) clearTimeout(handle); - arm(); - }, - clear() { - if (handle) clearTimeout(handle); - handle = undefined; - }, - }; -} diff --git a/plugins/fusion-plugin-acp-runtime/src/types.ts b/plugins/fusion-plugin-acp-runtime/src/types.ts index 19c1a92608..b3d14fb0e5 100644 --- a/plugins/fusion-plugin-acp-runtime/src/types.ts +++ b/plugins/fusion-plugin-acp-runtime/src/types.ts @@ -24,8 +24,16 @@ export interface AcpCallbacks { export type GateDisposition = "allow" | "block" | "require-approval"; /** - * Fusion action-gate categories the ACP `toolCall.kind` is classified into - * (KTD3a). `"exempt"` is implicit (read-only / benign) and always allows. + * Fusion action-gate categories — the full policy-rule keyspace, used to read + * `permissionPolicy.rules[category]`. `"exempt"` is implicit (read-only / benign) + * and always allows. + * + * Note: ACP's `ToolKind` has no git/task discriminator, so `classifyToolKind` + * only ever produces `file_write_delete` / `command_execution` / `network_api` + * (+ exempt). `git_write` and `task_agent_mutation` remain part of the category + * type because the policy rules are keyed by all categories — git writes in + * particular route through `file_write_delete` gating PLUS the path-jail's hard + * `.git/**` reject (KTD6a), not a dedicated `git_write` classification. */ export type FusionCategory = | "git_write"