diff --git a/CHANGELOG.md b/CHANGELOG.md index 0108aee13..54751b84a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ### Fixes - Make `failproofai config` refuse setup on an unsupported platform (Windows, today) instead of completing it unenforced. The wizard used to skip the daemon requirement and finish anyway, leaving the machine reading as configured while enforcing in-process with no fail-closed guarantee — now it prints why and exits 1 before drawing a single prompt, writing nothing. (#664) +### Chores +- Remove compiler-proven dead code left around the `failproofaid` integration, including the obsolete daemon-hook wrapper and unused `html2canvas` dependency. Enable `noUnusedLocals` to prevent that residue returning, and correct stale daemon documentation and package metadata. + ## 1.0.0-beta.12 — 2026-08-07 ### Fixes diff --git a/__tests__/actions/update-scheduled-audit.test.ts b/__tests__/actions/update-scheduled-audit.test.ts index 3a30cf8f6..67aca50db 100644 --- a/__tests__/actions/update-scheduled-audit.test.ts +++ b/__tests__/actions/update-scheduled-audit.test.ts @@ -15,7 +15,7 @@ * parity is real: both write through the same `updateConfig`. */ import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "node:fs"; +import { mkdtempSync, readFileSync, rmSync, mkdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { resolve } from "node:path"; import { configFile } from "../../src/hooks/fp-home"; diff --git a/__tests__/audit/cache.test.ts b/__tests__/audit/cache.test.ts index 59802c07b..04eabc6e9 100644 --- a/__tests__/audit/cache.test.ts +++ b/__tests__/audit/cache.test.ts @@ -1,7 +1,7 @@ // @vitest-environment node import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir, homedir } from "node:os"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { createHash } from "node:crypto"; import { diff --git a/__tests__/components/button.test.tsx b/__tests__/components/button.test.tsx index af3797456..414072793 100644 --- a/__tests__/components/button.test.tsx +++ b/__tests__/components/button.test.tsx @@ -1,4 +1,4 @@ -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect } from "vitest"; import { render, screen } from "@testing-library/react"; import { createRef } from "react"; import { Button } from "@/components/ui/button"; diff --git a/__tests__/components/date-picker-input.test.tsx b/__tests__/components/date-picker-input.test.tsx index 01ae11b46..0d12c2939 100644 --- a/__tests__/components/date-picker-input.test.tsx +++ b/__tests__/components/date-picker-input.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; +import { render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import DatePickerInput from "@/app/components/date-picker-input"; diff --git a/__tests__/e2e/hooks/codex-integration.e2e.test.ts b/__tests__/e2e/hooks/codex-integration.e2e.test.ts index 33311a69f..234092c94 100644 --- a/__tests__/e2e/hooks/codex-integration.e2e.test.ts +++ b/__tests__/e2e/hooks/codex-integration.e2e.test.ts @@ -16,7 +16,6 @@ import { assertAllow, assertPreToolUseDeny, assertPostToolUseDeny, - assertStopInstruct, assertPermissionRequestDeny, } from "../helpers/hook-runner"; import { CodexPayloads } from "../helpers/payloads"; diff --git a/__tests__/hooks/cloud-enrollment-cli.test.ts b/__tests__/hooks/cloud-enrollment-cli.test.ts index 49beb6726..0cc9bc623 100644 --- a/__tests__/hooks/cloud-enrollment-cli.test.ts +++ b/__tests__/hooks/cloud-enrollment-cli.test.ts @@ -6,7 +6,6 @@ import { resolve } from "node:path"; import { runConnectCommand, runDisconnectCommand, connectionStatusLines } from "../../src/hooks/cloud-enrollment-cli"; import { cloudCredentialPath, readCloudCredentials, writeCloudCredentials } from "../../src/hooks/cloud-enrollment"; import { readIngestCredential } from "../../src/hooks/collector-config"; -import { readHooksConfig } from "../../src/hooks/hooks-config"; import { readConfig } from "../../src/hooks/fp-config"; let dir: string; diff --git a/__tests__/hooks/collector-config.test.ts b/__tests__/hooks/collector-config.test.ts index 7e4b20ec6..21f9c2c48 100644 --- a/__tests__/hooks/collector-config.test.ts +++ b/__tests__/hooks/collector-config.test.ts @@ -4,7 +4,7 @@ // `policies-config.json` is 0664 inside a 0775 `~/.failproofai` on a normal // machine, which is exactly why the key lives in its own file at 0600. import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { mkdtempSync, rmSync, statSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from "node:fs"; +import { mkdtempSync, rmSync, statSync, writeFileSync, chmodSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; diff --git a/__tests__/hooks/configure-wizard.test.ts b/__tests__/hooks/configure-wizard.test.ts index cf34d52a1..b3a56874a 100644 --- a/__tests__/hooks/configure-wizard.test.ts +++ b/__tests__/hooks/configure-wizard.test.ts @@ -144,14 +144,13 @@ import { maybeFirstRunConfigure, hasSeenLauncher, markLauncherSeen, - classifyDaemonInstallFailure, } from "../../src/hooks/configure-wizard"; import { resolvePreset, resolveEverything } from "../../src/hooks/policy-presets"; import { INTEGRATION_TYPES, type IntegrationType } from "../../src/hooks/types"; import { getIntegration } from "../../src/hooks/integrations"; import { runPostSetupAudit } from "../../src/audit/cli"; import { trackHookEvent } from "../../src/hooks/hook-telemetry"; -import { globalPolicyConfigFile, configFile as fpConfigFile, launcherMarker } from "../../src/hooks/fp-home"; +import { configFile as fpConfigFile, launcherMarker } from "../../src/hooks/fp-home"; import { readConfig as readFpConfig } from "../../src/hooks/fp-config"; const mkTtyStdin = (): TTYIn => ({ isTTY: true }) as unknown as TTYIn; diff --git a/__tests__/hooks/daemon-client.test.ts b/__tests__/hooks/daemon-client.test.ts index c3c6da493..4a4c5fe16 100644 --- a/__tests__/hooks/daemon-client.test.ts +++ b/__tests__/hooks/daemon-client.test.ts @@ -11,6 +11,7 @@ import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { writeConfig, DEFAULT_CONFIG } from "../../src/hooks/fp-config"; +import type { DaemonHookRequest, DaemonHookResponse } from "../../src/hooks/daemon-client"; vi.mock("../../src/hooks/hook-logger", () => ({ hookLogInfo: vi.fn(), @@ -74,6 +75,12 @@ describe("hooks/daemon-client", () => { await new Promise((resolvePromise) => server!.listen(socketPath, resolvePromise)); } + async function daemonResult(req: DaemonHookRequest): Promise { + const { attemptDaemonHook } = await import("../../src/hooks/daemon-client"); + const attempt = await attemptDaemonHook(req); + return attempt.ok ? attempt.response : null; + } + it("returns the parsed result on a real hookResult response", async () => { await startServer(async (socket) => { const req = await readFrame(socket); @@ -92,8 +99,7 @@ describe("hooks/daemon-client", () => { ); }); - const { tryDaemonHook } = await import("../../src/hooks/daemon-client"); - const result = await tryDaemonHook({ + const result = await daemonResult({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}", @@ -116,8 +122,7 @@ describe("hooks/daemon-client", () => { ); }); - const { tryDaemonHook } = await import("../../src/hooks/daemon-client"); - const result = await tryDaemonHook({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); + const result = await daemonResult({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); expect(result).toEqual({ exitCode: 2, stdout: "", stderr: "blocked: sudo is not allowed" }); }); @@ -127,8 +132,7 @@ describe("hooks/daemon-client", () => { socket.end(encodeFrame({ type: "error", protocolVersion: 1, message: "daemon unreachable" })); }); - const { tryDaemonHook } = await import("../../src/hooks/daemon-client"); - const result = await tryDaemonHook({ hookEvent: "Stop", cli: "codex", stdin: "{}" }); + const result = await daemonResult({ hookEvent: "Stop", cli: "codex", stdin: "{}" }); expect(result).toBeNull(); }); @@ -140,8 +144,7 @@ describe("hooks/daemon-client", () => { ); }); - const { tryDaemonHook } = await import("../../src/hooks/daemon-client"); - const result = await tryDaemonHook({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); + const result = await daemonResult({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); expect(result).toBeNull(); }); @@ -204,16 +207,14 @@ describe("hooks/daemon-client", () => { socket.end(encodeFrame({ type: "hookResult", protocolVersion: 1, stdout: "", stderr: "" })); }); - const { tryDaemonHook } = await import("../../src/hooks/daemon-client"); - const result = await tryDaemonHook({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); + const result = await daemonResult({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); expect(result).toBeNull(); }); it("returns null immediately when no socket file exists at all", async () => { // No server started — socketPath was never bound. - const { tryDaemonHook } = await import("../../src/hooks/daemon-client"); const start = Date.now(); - const result = await tryDaemonHook({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); + const result = await daemonResult({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); const elapsedMs = Date.now() - start; expect(result).toBeNull(); // ENOENT/ECONNREFUSED on a nonexistent socket is a kernel-level rejection, @@ -237,9 +238,8 @@ describe("hooks/daemon-client", () => { }, 600); }); - const { tryDaemonHook } = await import("../../src/hooks/daemon-client"); const start = Date.now(); - const result = await tryDaemonHook({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); + const result = await daemonResult({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); expect(result).toEqual({ exitCode: 0, stdout: "ok", stderr: "" }); expect(Date.now() - start).toBeGreaterThanOrEqual(500); }); @@ -252,9 +252,8 @@ describe("hooks/daemon-client", () => { // Deliberately never write a response. }); - const { tryDaemonHook } = await import("../../src/hooks/daemon-client"); let settled = false; - const pending = tryDaemonHook({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }).then((r) => { + const pending = daemonResult({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }).then((r) => { settled = true; return r; }); @@ -277,8 +276,7 @@ describe("hooks/daemon-client", () => { socket.end(Buffer.concat([header, body])); }); - const { tryDaemonHook } = await import("../../src/hooks/daemon-client"); - const result = await tryDaemonHook({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); + const result = await daemonResult({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); expect(result).toBeNull(); }); @@ -286,9 +284,8 @@ describe("hooks/daemon-client", () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "win32" }); try { - const { tryDaemonHook } = await import("../../src/hooks/daemon-client"); const start = Date.now(); - const result = await tryDaemonHook({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); + const result = await daemonResult({ hookEvent: "PreToolUse", cli: "claude", stdin: "{}" }); const elapsedMs = Date.now() - start; expect(result).toBeNull(); expect(elapsedMs).toBeLessThan(20); diff --git a/__tests__/hooks/fp-home.test.ts b/__tests__/hooks/fp-home.test.ts index 38e26821d..5d962e0fe 100644 --- a/__tests__/hooks/fp-home.test.ts +++ b/__tests__/hooks/fp-home.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs"; +import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { resolve } from "node:path"; import * as H from "../../src/hooks/fp-home"; diff --git a/__tests__/hooks/new-telemetry.test.ts b/__tests__/hooks/new-telemetry.test.ts index eb9983209..20c2f01a7 100644 --- a/__tests__/hooks/new-telemetry.test.ts +++ b/__tests__/hooks/new-telemetry.test.ts @@ -5,10 +5,8 @@ * fire at the trigger site. Keep one focused case per event. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { readFileSync, existsSync } from "node:fs"; import { execSync } from "node:child_process"; -import { resolve } from "node:path"; -import { homedir } from "node:os"; vi.mock("node:fs", () => ({ readFileSync: vi.fn(), diff --git a/__tests__/lib/codex-sessions.test.ts b/__tests__/lib/codex-sessions.test.ts index cbe20942e..e995a5172 100644 --- a/__tests__/lib/codex-sessions.test.ts +++ b/__tests__/lib/codex-sessions.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { tmpdir, homedir } from "node:os"; +import { tmpdir } from "node:os"; const line = (obj: Record): string => JSON.stringify(obj); diff --git a/__tests__/lib/pi-sessions.test.ts b/__tests__/lib/pi-sessions.test.ts index 9fb2ad079..73cb6c4fb 100644 --- a/__tests__/lib/pi-sessions.test.ts +++ b/__tests__/lib/pi-sessions.test.ts @@ -1,6 +1,6 @@ // @vitest-environment node import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { mkdtempSync, mkdirSync, rmSync, writeFileSync, unlinkSync } from "node:fs"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import type { AssistantEntry, ContentBlock, ToolUseBlock } from "@/lib/log-entries"; diff --git a/__tests__/lib/telemetry-id.test.ts b/__tests__/lib/telemetry-id.test.ts index 128a1df68..11aebc1bd 100644 --- a/__tests__/lib/telemetry-id.test.ts +++ b/__tests__/lib/telemetry-id.test.ts @@ -39,7 +39,6 @@ vi.mock("node:crypto", async () => { const mockedFs = vi.mocked(fs); const mockedOs = vi.mocked(os); -const mockedCrypto = vi.mocked(crypto); const mockedExecSync = vi.mocked(execSync); describe("lib/telemetry-id", () => { diff --git a/__tests__/lib/telemetry.test.ts b/__tests__/lib/telemetry.test.ts index 257693131..879fdd05f 100644 --- a/__tests__/lib/telemetry.test.ts +++ b/__tests__/lib/telemetry.test.ts @@ -29,7 +29,6 @@ import { isTelemetryEnabled, initTelemetry, trackEvent, - flushTelemetry, shutdownTelemetry, } from "@/lib/telemetry"; diff --git a/__tests__/scripts/translate-docs/cache.test.ts b/__tests__/scripts/translate-docs/cache.test.ts index f04038804..5466e1393 100644 --- a/__tests__/scripts/translate-docs/cache.test.ts +++ b/__tests__/scripts/translate-docs/cache.test.ts @@ -1,5 +1,5 @@ // @vitest-environment node -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { describe, it, expect } from "vitest"; import { contentHash, getCacheKey, diff --git a/app/actions/get-hooks-config.ts b/app/actions/get-hooks-config.ts index 80a72632a..fb083a08b 100644 --- a/app/actions/get-hooks-config.ts +++ b/app/actions/get-hooks-config.ts @@ -11,7 +11,6 @@ import { customPolicyId, conventionPolicyId, discoverPolicyFiles } from "@/src/h import { findProjectConfigDir } from "@/src/hooks/hooks-config"; import { readFile } from "node:fs/promises"; import { existsSync } from "node:fs"; -import { homedir } from "node:os"; import { basename, resolve } from "node:path"; import { customPoliciesDir } from "@/src/hooks/fp-home"; diff --git a/app/audit/_components/audit-dashboard.tsx b/app/audit/_components/audit-dashboard.tsx index 1b3bc5d06..ce64dfc3f 100644 --- a/app/audit/_components/audit-dashboard.tsx +++ b/app/audit/_components/audit-dashboard.tsx @@ -14,7 +14,7 @@ * * Empty / running states fall back to EmptyState and RunProgress. */ -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { getAuditResultAction } from "@/app/actions/get-audit-result"; import type { AuditResult, RunAuditOptions } from "@/src/audit/types"; import { classifyAgent } from "@/src/audit/archetypes"; @@ -227,8 +227,6 @@ export function AuditDashboard({ initial, projectFromUrl, totalCatalogSize }: Pr const result = cache.status === "cached" ? cache.result : null; if (!result) return null; const cachedAt = cache.status === "cached" ? cache.cachedAt : null; - const params = cache.status === "cached" ? cache.params : undefined; - /* ---- scanned but zero sessions --------------------------------- */ if (result.transcripts.scanned === 0) { return ( diff --git a/app/audit/_components/audit-progress-strip.tsx b/app/audit/_components/audit-progress-strip.tsx index b26d12be6..2973931b1 100644 --- a/app/audit/_components/audit-progress-strip.tsx +++ b/app/audit/_components/audit-progress-strip.tsx @@ -15,7 +15,7 @@ * Reuses the existing `RerunError.kind` discrimination to render a red * error strip with kind-specific copy when a run dies. */ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import type { RerunError } from "./rerun-button"; export type RerunStatus = diff --git a/app/audit/_components/come-back-better-section.tsx b/app/audit/_components/come-back-better-section.tsx index 9a3c12b21..68efcbf57 100644 --- a/app/audit/_components/come-back-better-section.tsx +++ b/app/audit/_components/come-back-better-section.tsx @@ -18,7 +18,7 @@ * link sits under the reminder card so the affordance survives without * dominating the layout. */ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { usePostHog } from "@/contexts/PostHogContext"; import { isAbortError } from "@/lib/fetch-with-timeout"; import { AuthDialog, type AuthedUser } from "./auth-dialog"; diff --git a/app/audit/_components/empty-state.tsx b/app/audit/_components/empty-state.tsx index 4e652810a..2e29a79c7 100644 --- a/app/audit/_components/empty-state.tsx +++ b/app/audit/_components/empty-state.tsx @@ -12,7 +12,6 @@ * sharp `.btn-press` action button. Sized so it occupies the same vertical * space as the loaded dashboard does on its hero — no more cramped popover. */ -import React from "react"; import { triggerRun } from "./rerun-button"; import { usePostHog } from "@/contexts/PostHogContext"; diff --git a/app/audit/_components/how-to-improve-section.tsx b/app/audit/_components/how-to-improve-section.tsx index 188b93e80..878c5f01b 100644 --- a/app/audit/_components/how-to-improve-section.tsx +++ b/app/audit/_components/how-to-improve-section.tsx @@ -10,7 +10,7 @@ * A single "install all" button at the section header copies the * combined install command for every prescribed policy. */ -import React, { useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import type { AuditResult } from "@/src/audit/types"; import { type Grade, tierName } from "@/src/audit/scoring"; import { usePostHog } from "@/contexts/PostHogContext"; diff --git a/app/audit/_components/quirks-section.tsx b/app/audit/_components/quirks-section.tsx index a074a74d7..bfbc6cd38 100644 --- a/app/audit/_components/quirks-section.tsx +++ b/app/audit/_components/quirks-section.tsx @@ -8,7 +8,6 @@ * detector. No per-finding card chrome, no 4-quad body, no corner * crosshairs — evidence and fix live in section 04 (How to improve). */ -import React from "react"; import type { FindingCard } from "@/src/audit/findings"; interface Props { diff --git a/app/audit/_components/report-footer.tsx b/app/audit/_components/report-footer.tsx index 62675e48f..d439d991d 100644 --- a/app/audit/_components/report-footer.tsx +++ b/app/audit/_components/report-footer.tsx @@ -1,7 +1,5 @@ "use client"; -import React from "react"; - interface Props { cachedAt: string | null; fixed?: boolean; diff --git a/app/audit/_components/run-progress.tsx b/app/audit/_components/run-progress.tsx index 751f1dff7..c9e73d7f1 100644 --- a/app/audit/_components/run-progress.tsx +++ b/app/audit/_components/run-progress.tsx @@ -16,7 +16,7 @@ * pink "▮▮" / dim "○" markers, and a marquee progress bar at the bottom * filling pink-on-dark as the run advances. */ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; const STAGES = [ { label: "discovering transcripts", detail: "walking ~/.claude, ~/.codex, ~/.cursor, …" }, diff --git a/app/audit/_components/strengths-section.tsx b/app/audit/_components/strengths-section.tsx index 811245762..06ecef0d0 100644 --- a/app/audit/_components/strengths-section.tsx +++ b/app/audit/_components/strengths-section.tsx @@ -5,7 +5,6 @@ * ✓ glyph · headline + sub · right-aligned metric. No card chrome, * no hover backgrounds, no checkmark backdrop. */ -import React from "react"; import type { Strength } from "@/src/audit/strengths"; interface Props { diff --git a/app/components/pause-notices.tsx b/app/components/pause-notices.tsx index 840862f12..b15fa200f 100644 --- a/app/components/pause-notices.tsx +++ b/app/components/pause-notices.tsx @@ -8,7 +8,7 @@ * where every policy ran and allowed, and without saying so the log asserts a * clean window over exactly the window that was not enforced. */ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { ShieldAlert, TriangleAlert } from "lucide-react"; import type { ActivePause } from "@/src/hooks/session-pause"; diff --git a/app/components/raw-log-viewer.tsx b/app/components/raw-log-viewer.tsx index 7305b3fd7..66a5fe76a 100644 --- a/app/components/raw-log-viewer.tsx +++ b/app/components/raw-log-viewer.tsx @@ -5,7 +5,7 @@ */ "use client"; -import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useWindowVirtualizer } from "@tanstack/react-virtual"; import { ChevronDown, Wrench } from "lucide-react"; import type { LogEntry, ToolUseBlock } from "@/lib/log-entries"; diff --git a/app/components/toast.tsx b/app/components/toast.tsx index ad563c59f..dc5432068 100644 --- a/app/components/toast.tsx +++ b/app/components/toast.tsx @@ -1,6 +1,6 @@ "use client"; -import { useSyncExternalStore, useCallback, useEffect, useState } from "react"; +import { useSyncExternalStore, useEffect, useState } from "react"; interface Toast { id: number; diff --git a/app/policies/hooks-client.tsx b/app/policies/hooks-client.tsx index e6d2e0ea7..cc0ed6c61 100644 --- a/app/policies/hooks-client.tsx +++ b/app/policies/hooks-client.tsx @@ -1,6 +1,7 @@ "use client"; -import React, { useState, useEffect, useCallback, useMemo, useRef, useTransition } from "react"; +import { useState, useEffect, useCallback, useMemo, useRef, useTransition } from "react"; +import * as React from "react"; import { createPortal } from "react-dom"; import Link from "next/link"; import { Check, ChevronDown, Code, Copy, Settings, Shield, ShieldAlert, ShieldCheck, ShieldX, TriangleAlert, X } from "lucide-react"; @@ -11,7 +12,7 @@ import { getActivePausesAction } from "@/app/actions/get-active-pauses"; import type { ActivePause } from "@/src/hooks/session-pause"; import { PausedBanner, PausedNote, PausedPill } from "@/app/components/pause-notices"; import { getHooksConfigAction } from "@/app/actions/get-hooks-config"; -import type { HooksConfigPayload, PolicyInfo, CustomPolicyInfo } from "@/app/actions/get-hooks-config"; +import type { HooksConfigPayload, PolicyInfo } from "@/app/actions/get-hooks-config"; import type { IntegrationType } from "@/src/hooks/types"; import { toggleCustomPolicyAction, togglePolicyAction } from "@/app/actions/update-hooks-config"; import { installHooksWebAction, removeHooksWebAction } from "@/app/actions/install-hooks-web"; diff --git a/app/settings/settings-client.tsx b/app/settings/settings-client.tsx index 4639f9800..48ea6f6ea 100644 --- a/app/settings/settings-client.tsx +++ b/app/settings/settings-client.tsx @@ -17,11 +17,10 @@ * documented on each action module. */ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { getScheduledAuditAction, type ScheduledAuditView } from "@/app/actions/get-scheduled-audit"; import { setAutoAuditAction, setAuditIntervalAction } from "@/app/actions/update-scheduled-audit"; import { triggerRun, RerunError } from "@/app/audit/_components/rerun-button"; -import { AuthDialog } from "@/app/audit/_components/auth-dialog"; import { toast } from "@/app/components/toast"; import { fetchWithTimeout } from "@/lib/fetch-with-timeout"; import { formatRelativeTime } from "@/lib/format-duration"; diff --git a/bin/failproofaid-shim.mjs b/bin/failproofaid-shim.mjs index ba38e10b8..b5ad1cc32 100644 --- a/bin/failproofaid-shim.mjs +++ b/bin/failproofaid-shim.mjs @@ -11,10 +11,10 @@ * keep alive itself. This shim exists only for a user (or script) * invoking `failproofaid` by hand. * - * The npm package deliberately ships no binary: one tarball serves every - * platform, and the four cross-compiled binaries live on the GitHub - * Release for this version (see `src/hooks/daemon-download.ts`). - * `failproofai config` is what fetches one. So "not installed" here is a + * The root npm tarball contains no native binary. A matching optional platform + * package normally supplies it; the GitHub Release for this version is the + * verified fallback (see `src/hooks/daemon-download.ts`). `failproofai config` + * installs either source into `~/.failproofai/bin`. So "not installed" here is a * normal state rather than a broken install, and it degrades with a * one-line message and a non-zero exit — never a stack trace — including * on a platform that has no binary at all (Windows). The daemon-connect diff --git a/bun.lock b/bun.lock index f2943e937..8ae1be8ed 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,6 @@ "name": "failproofai", "dependencies": { "html-to-image": "^1.11.13", - "html2canvas": "^1.4.1", "posthog-node": "^5.47.7", "smol-toml": "^1.7.1", "sql.js": "^1.14.1", @@ -486,8 +485,6 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], @@ -530,8 +527,6 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="], - "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], @@ -738,8 +733,6 @@ "html-to-image": ["html-to-image@1.11.13", "", {}, "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg=="], - "html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], @@ -1178,8 +1171,6 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@1.2.2", "", {}, "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g=="], @@ -1250,8 +1241,6 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="], - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], diff --git a/crates/PROTOCOL.md b/crates/PROTOCOL.md index 266132755..f7de13373 100644 --- a/crates/PROTOCOL.md +++ b/crates/PROTOCOL.md @@ -8,9 +8,8 @@ invocation already has today — see `src/hooks/handler.ts`'s relays it over a socket instead of a fresh process's argv/stdin/stdout. Implemented in `crates/fpai-ipc` (framing + envelope + peer verification) and -`crates/failproofaid` (the socket server itself). As of Stage 2, the daemon -answers `ping` and rejects `hook` with a stub "not implemented" error — Stage 3 -wires `hook` up to a real warm Node/Bun worker. +`crates/failproofaid` (the socket server and worker supervisor). The daemon +answers `ping` directly and relays `hook` requests to its warm Node/Bun worker. ## Transport @@ -25,9 +24,9 @@ this at a directory failproofaid doesn't own itself; see `crates/failproofaid/src/paths.rs`'s `ensure_run_dir`, which refuses to modify permissions on a pre-existing directory it didn't create). -Permissions: the run directory is `0700` and the socket file `0600` — this is -the actual access-control boundary (user-scope only, no elevation, same OS -user only). `crates/fpai-ipc/src/peer.rs`'s `SO_PEERCRED` (Linux) / +Permissions: the run directory is `0700` and the socket file `0600`. Although +the service definition is installed system-wide, the daemon process runs as +the configured OS user. `crates/fpai-ipc/src/peer.rs`'s `SO_PEERCRED` (Linux) / `getpeereid` (macOS) check is defense-in-depth on top of that, not a stronger boundary — same-user access can always reach this daemon regardless. @@ -86,8 +85,8 @@ hazard" the TS-side plan calls out explicitly). } // The daemon accepted the connection and parsed the request, but could not -// produce a verdict (worker down/hung, or — in Stage 2 — hook evaluation -// simply isn't wired up yet). Distinct from hookResult so the client can +// produce a verdict (for example, the worker is down or hung). Distinct from +// hookResult so the client can // tell "ran and decided" apart from "daemon couldn't evaluate at all" — the // latter is what drives the client's fail-closed path. { "type": "error", "protocolVersion": 1, "message": "..." } @@ -96,11 +95,10 @@ hazard" the TS-side plan calls out explicitly). ## Protocol versioning `protocolVersion` is carried on every message in both directions. A mismatch -gets an explicit `error` response from the daemon — the client treats *any* -failure mode (missing socket, refused connection, timeout, malformed -response, or an explicit version mismatch) identically: fall through to -whatever the client's fail-closed/in-process policy dictates. There is no -negotiation, only agree-or-fall-back. +gets an explicit `error` response from the daemon. A daemon-configured client +fails closed on every missing or unusable verdict; it preserves the mismatch +category only to print the correct repair instructions. There is no protocol +negotiation: the CLI and daemon must agree exactly. ## Peer verification diff --git a/crates/failproofaid/Cargo.toml b/crates/failproofaid/Cargo.toml index c930a7bc6..58d53af56 100644 --- a/crates/failproofaid/Cargo.toml +++ b/crates/failproofaid/Cargo.toml @@ -4,7 +4,7 @@ version.workspace = true edition.workspace = true license-file.workspace = true repository.workspace = true -description = "Thin Rust supervisor: owns the failproofai IPC socket, service lifecycle, and warm worker process supervision. Carries no policy logic." +description = "FailproofAI background service for hook evaluation, policy synchronization, collection, scheduled audits, and telemetry." publish = false [[bin]] diff --git a/crates/failproofaid/src/worker.rs b/crates/failproofaid/src/worker.rs index 0f3f789ff..05edfe65e 100644 --- a/crates/failproofaid/src/worker.rs +++ b/crates/failproofaid/src/worker.rs @@ -70,8 +70,8 @@ impl WorkerCommand { if let Ok(cmd) = std::env::var("FAILPROOFAI_WORKER_CMD") { return WorkerCommand::shell(cmd); } - // Packaging (Stage 5) lands dist/worker.mjs; until then this is only - // reachable via the explicit override above in dev/test. + // Published packages place dist/worker.mjs at this relative path; the + // installed service normally supplies an absolute command via env. WorkerCommand::Node { script: PathBuf::from("dist/worker.mjs"), } diff --git a/crates/failproofaid/tests/daemon_e2e.rs b/crates/failproofaid/tests/daemon_e2e.rs index b423905c2..9a8ab0514 100644 --- a/crates/failproofaid/tests/daemon_e2e.rs +++ b/crates/failproofaid/tests/daemon_e2e.rs @@ -1,6 +1,6 @@ //! Black-box tests that spawn the real compiled `failproofaid` binary as a -//! subprocess and talk to it over a real Unix socket — closer to how a -//! systemd unit / launchd agent will actually invoke it (Stage 4) than the +//! subprocess and talk to it over a real Unix socket — closer to how the +//! installed systemd unit / launchd daemon invokes it than the //! in-process unit tests in `src/server.rs` are. use fpai_ipc::{ClientMessage, PROTOCOL_VERSION, ServerMessage, read_message, write_message}; diff --git a/crates/fpai-ipc/src/envelope.rs b/crates/fpai-ipc/src/envelope.rs index 55f867278..2d80a7f65 100644 --- a/crates/fpai-ipc/src/envelope.rs +++ b/crates/fpai-ipc/src/envelope.rs @@ -9,9 +9,9 @@ use serde::{Deserialize, Serialize}; /// Bumped whenever a wire-incompatible change is made to either message -/// enum below. The client treats any mismatch identically to "daemon -/// unreachable" (see `docs/configuration.mdx` and the failproofaid plan) — -/// there is no negotiation, only agree-or-fall-back. +/// enum below. A daemon-configured client fails closed on a mismatch and uses +/// the distinct failure category only to explain how to repair the skew. +/// There is no protocol negotiation. pub const PROTOCOL_VERSION: u32 = 1; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] diff --git a/crates/fpai-ipc/src/peer.rs b/crates/fpai-ipc/src/peer.rs index 1ff61791b..1c8859d4f 100644 --- a/crates/fpai-ipc/src/peer.rs +++ b/crates/fpai-ipc/src/peer.rs @@ -1,10 +1,9 @@ //! Verifies that a Unix socket peer is running as the same OS user as this //! process. //! -//! failproofaid is user-scope only (see the plan: "no elevation, the daemon -//! spawns the worker because the daemon already runs as the user"): the -//! socket directory is `0700` and the socket file `0600`, which is the -//! actual access-control boundary. This check is a second, defense-in-depth +//! failproofaid's service definition is installed system-wide but runs the +//! process as the configured user. The socket directory is `0700` and the +//! socket file `0600`. This check is a second, defense-in-depth //! layer against the narrow window between a socket file existing and its //! permissions having been fully applied, and against misconfigured //! filesystems (e.g. a shared mount with unexpectedly loose permissions). diff --git a/lib/auth/auth-store.ts b/lib/auth/auth-store.ts index 3cfcae5de..a3ec0c1cc 100644 --- a/lib/auth/auth-store.ts +++ b/lib/auth/auth-store.ts @@ -7,7 +7,6 @@ */ import { existsSync, readFileSync, rmSync } from "node:fs"; -import { homedir } from "node:os"; import { join } from "node:path"; import { writeJsonAtomically } from "../atomic-write"; diff --git a/package.json b/package.json index dd2476a2e..fe1cb17b1 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,6 @@ }, "dependencies": { "html-to-image": "^1.11.13", - "html2canvas": "^1.4.1", "posthog-node": "^5.47.7", "smol-toml": "^1.7.1", "sql.js": "^1.14.1", diff --git a/scripts/translate-docs/cache.ts b/scripts/translate-docs/cache.ts index 94bfd7b66..eca8cea4b 100644 --- a/scripts/translate-docs/cache.ts +++ b/scripts/translate-docs/cache.ts @@ -2,7 +2,7 @@ import { createHash } from "node:crypto"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import type { CacheEntry, TranslationCache } from "./types"; +import type { TranslationCache } from "./types"; const __dirname = dirname(fileURLToPath(import.meta.url)); const CACHE_FILE = join(__dirname, ".translation-cache.json"); diff --git a/scripts/translate-docs/cli.ts b/scripts/translate-docs/cli.ts index a708c7884..1f23f68e0 100644 --- a/scripts/translate-docs/cli.ts +++ b/scripts/translate-docs/cli.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun import { parseArgs } from "node:util"; -import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { dirname, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; import { @@ -297,7 +297,7 @@ async function main() { // Translate uncached pages with concurrency limit if (uncachedTasks.length > 0) { - const taskResults = await runWithConcurrency( + await runWithConcurrency( uncachedTasks.map(({ page, relPath, lang }) => async () => { try { const result = await translateMdxPage(page, lang, { diff --git a/scripts/translate-docs/mintlify-nav.ts b/scripts/translate-docs/mintlify-nav.ts index e003c6bf9..24b6f5f32 100644 --- a/scripts/translate-docs/mintlify-nav.ts +++ b/scripts/translate-docs/mintlify-nav.ts @@ -1,7 +1,7 @@ import { readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { getLanguageByCode, NAV_TRANSLATIONS, LANGUAGES } from "./config"; +import { getLanguageByCode, NAV_TRANSLATIONS } from "./config"; const __dirname = dirname(fileURLToPath(import.meta.url)); const DOCS_JSON_PATH = join(__dirname, "..", "..", "docs", "docs.json"); diff --git a/src/audit/cache.ts b/src/audit/cache.ts index 323bc53c8..29f60ca6f 100644 --- a/src/audit/cache.ts +++ b/src/audit/cache.ts @@ -12,7 +12,6 @@ import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs"; import { join } from "node:path"; -import { homedir } from "node:os"; import { BUILTIN_POLICIES } from "../hooks/builtin-policies"; import { AUDIT_DETECTORS } from "./detectors"; import type { TranscriptAuditResult } from "./types"; diff --git a/src/audit/dashboard-cache.ts b/src/audit/dashboard-cache.ts index abb38135f..bc2370b3e 100644 --- a/src/audit/dashboard-cache.ts +++ b/src/audit/dashboard-cache.ts @@ -11,8 +11,6 @@ * makes navigating back to /audit instant without re-running. */ import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; -import { homedir } from "node:os"; import { writeJsonAtomically } from "../../lib/atomic-write"; import type { AuditResult, RunAuditOptions } from "./types"; import { auditDashboardFile } from "../hooks/fp-home"; diff --git a/src/audit/report.ts b/src/audit/report.ts index aec9f6661..31ede51b1 100644 --- a/src/audit/report.ts +++ b/src/audit/report.ts @@ -31,7 +31,6 @@ function noColorEnabled(): boolean { } function stripAnsi(s: string): string { - // eslint-disable-next-line no-control-regex return s.replace(/\x1B\[[0-9;]*m/g, ""); } diff --git a/src/hooks/builtin-policies.ts b/src/hooks/builtin-policies.ts index 2d7bc1325..44220b2e7 100644 --- a/src/hooks/builtin-policies.ts +++ b/src/hooks/builtin-policies.ts @@ -3,7 +3,7 @@ */ import { resolve, join } from "node:path"; import { statSync } from "node:fs"; -import { readFile, writeFile, stat, open } from "node:fs/promises"; +import { readFile, writeFile } from "node:fs/promises"; import { execSync, execFileSync } from "node:child_process"; import { homedir } from "node:os"; import type { BuiltinPolicyDefinition, PolicyContext, PolicyResult, PolicyParamsSchema } from "./policy-types"; diff --git a/src/hooks/cloud-enrollment-cli.ts b/src/hooks/cloud-enrollment-cli.ts index 61e941ebe..01761397c 100644 --- a/src/hooks/cloud-enrollment-cli.ts +++ b/src/hooks/cloud-enrollment-cli.ts @@ -10,7 +10,6 @@ import { resolveMachineLabel, validateCloudUrl, verifyCloudCredentials, - writeCloudCredentials, cloudCredentialPath, } from "./cloud-enrollment"; import { daemonRestartCommand, daemonServiceStatus, daemonVersionSkew } from "./daemon-service"; @@ -23,7 +22,6 @@ import { configuredPaths, connectToCloud, describeOutcome, - ingestUrlFor, } from "./cloud-connection"; import { clearIngestCredential, diff --git a/src/hooks/cloud-enrollment.ts b/src/hooks/cloud-enrollment.ts index 649a76419..e803be511 100644 --- a/src/hooks/cloud-enrollment.ts +++ b/src/hooks/cloud-enrollment.ts @@ -18,8 +18,7 @@ */ import { randomUUID } from "node:crypto"; import { existsSync, readFileSync, rmSync } from "node:fs"; -import { homedir, hostname } from "node:os"; -import { join } from "node:path"; +import { hostname } from "node:os"; import { writeJsonAtomically } from "../../lib/atomic-write"; import { fetchWithTimeout, isAbortError } from "../../lib/fetch-with-timeout"; import { credentialsFile } from "./fp-home"; diff --git a/src/hooks/cloud-managed-policies.ts b/src/hooks/cloud-managed-policies.ts index 40ccfdfd1..f09167e75 100644 --- a/src/hooks/cloud-managed-policies.ts +++ b/src/hooks/cloud-managed-policies.ts @@ -6,7 +6,6 @@ */ import { createHash } from "node:crypto"; import { existsSync, readFileSync, realpathSync, rmSync } from "node:fs"; -import { homedir } from "node:os"; import { isAbsolute, relative, resolve } from "node:path"; import { cloudPoliciesDir } from "./fp-home"; diff --git a/src/hooks/collector-config.ts b/src/hooks/collector-config.ts index 21d409bc2..e8811c913 100644 --- a/src/hooks/collector-config.ts +++ b/src/hooks/collector-config.ts @@ -3,31 +3,15 @@ * * Two files, deliberately: * - * ~/.failproofai/ingest.json mode 0600, the credential ONLY - * ~/.failproofai/policies-config.json the non-secret settings + * ~/.failproofai/credentials.toml mode 0600, including the ingest credential + * ~/.failproofai/config.toml non-secret collector settings * - * The split is not tidiness. `policies-config.json` is written with a bare - * `writeFileSync`, so it inherits the umask and lands at 0664 on a normal - * machine — inside a `~/.failproofai/` that is itself 0775. An API key there - * would be readable by every local user on the box. `~/.agenteye/cli.json` - * already stores its session token at 0600, so the correct precedent existed. + * The split keeps bearer credentials out of the human-readable configuration + * file and gives every token the same owner-only storage boundary. * * The Rust daemon reads both (see `crates/fpai-collect/src/config.rs`); this is * the only thing that writes them. */ -import { - writeFileSync, - mkdirSync, - existsSync, - chmodSync, - statSync, - readFileSync, - rmSync, -} from "node:fs"; -import { join } from "node:path"; -import { homedir } from "node:os"; - -import { readHooksConfig, writeHooksConfig } from "./hooks-config"; import { credentialsFile, failproofaiHome as layoutHome } from "./fp-home"; import { readCredentials, writeCredentials, updateConfig } from "./fp-config"; diff --git a/src/hooks/configure-wizard.ts b/src/hooks/configure-wizard.ts index eeae2357c..08325aaff 100644 --- a/src/hooks/configure-wizard.ts +++ b/src/hooks/configure-wizard.ts @@ -44,8 +44,6 @@ import { import { DEFAULT_INGEST_URL, validateIngestKey, - writeIngestCredential, - writeCollectorSettings, } from "./collector-config"; import { detectInstalledClis, @@ -82,8 +80,6 @@ import { resolveMachineId, resolveMachineLabel, validateCloudUrl, - verifyCloudCredentials, - writeCloudCredentials, } from "./cloud-enrollment"; import { cloudBaseFor, diff --git a/src/hooks/custom-hooks-loader.ts b/src/hooks/custom-hooks-loader.ts index 2dd79baf3..8a627c960 100644 --- a/src/hooks/custom-hooks-loader.ts +++ b/src/hooks/custom-hooks-loader.ts @@ -15,7 +15,6 @@ import { randomUUID } from "crypto"; import { existsSync, readdirSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { pathToFileURL } from "node:url"; -import { homedir } from "node:os"; import { createHash } from "node:crypto"; import { hookLogWarn, hookLogError, hookLogInfo } from "./hook-logger"; import { customPolicies, getCustomHooks, clearCustomHooks } from "./custom-hooks-registry"; diff --git a/src/hooks/daemon-client.ts b/src/hooks/daemon-client.ts index b44304b0f..394d518c6 100644 --- a/src/hooks/daemon-client.ts +++ b/src/hooks/daemon-client.ts @@ -6,17 +6,12 @@ * request, a 4-byte big-endian u32 length prefix followed by that many * bytes of UTF-8 JSON, camelCase fields, `"type"` as the tag. * - * This module makes NO decision about what to do when the daemon can't be - * reached — `tryDaemonHook` just returns `null` on any failure. The caller - * (`bin/failproofai.mjs`) decides: fall back to full in-process evaluation - * on a machine that was never daemon-configured, or fail closed on one that - * was. See `isDaemonConfigured`. + * This module reports whether a daemon request succeeded, was unreachable, or + * used an incompatible protocol. The caller (`bin/failproofai.mjs`) owns the + * fail-closed response for daemon-configured machines. */ import { createConnection } from "node:net"; -import { resolve } from "node:path"; -import { homedir } from "node:os"; import type { IntegrationType } from "./types"; -import { readHooksConfig } from "./hooks-config"; import { existsSync } from "node:fs"; import { daemonSocket as daemonSocketPath } from "./fp-home"; import { readConfig } from "./fp-config"; @@ -181,26 +176,7 @@ function encodeFrame(value: unknown): Buffer { return Buffer.concat([header, body]); } -/** - * Attempts one hook evaluation via the daemon. Returns `null` on **any** - * failure — no socket, connection refused, timeout, malformed response, - * protocol-version mismatch, or an explicit `error` message from the - * daemon. The caller never needs to distinguish failure modes; it just - * falls through to whatever its own fallback policy is. - */ -export async function tryDaemonHook(req: DaemonHookRequest): Promise { - const attempt = await attemptDaemonHook(req); - return attempt.ok ? attempt.response : null; -} - -/** - * The same attempt, but reporting WHY it failed. - * - * `tryDaemonHook` above collapses every failure into `null`, which loses the - * one thing the caller still needs: both failures deny (see `DaemonFailure`), - * but they call for different remedies — reinstall the daemon versus start it — - * and a `null` cannot say which to print. - */ +/** Attempts a daemon evaluation while preserving the failure category. */ export async function attemptDaemonHook( req: DaemonHookRequest, opts?: { diff --git a/src/hooks/daemon-download.ts b/src/hooks/daemon-download.ts index 219a080eb..628311721 100644 --- a/src/hooks/daemon-download.ts +++ b/src/hooks/daemon-download.ts @@ -45,7 +45,6 @@ import { writeFileSync, } from "node:fs"; import { createRequire } from "node:module"; -import { homedir } from "node:os"; import { dirname, resolve } from "node:path"; import { gunzipSync } from "node:zlib"; import { version } from "../../package.json"; diff --git a/src/hooks/daemon-service.ts b/src/hooks/daemon-service.ts index ddf198cf1..12a371aea 100644 --- a/src/hooks/daemon-service.ts +++ b/src/hooks/daemon-service.ts @@ -1,8 +1,7 @@ /** - * Installs/uninstalls/checks failproofaid as a real OS-level user service - * (systemd `--user` on Linux, launchd `LaunchAgent` on macOS) so it's - * "constant" — starts at login, restarts on crash — without ever needing - * elevation. User-scope only, matching the daemon itself. + * Installs, upgrades, checks, and removes failproofaid as a system-managed + * service. The definition is root-owned and starts at boot, but the daemon + * process itself runs as the user who configured failproofai. * * No public `failproofai daemon install`-style subcommand exists — * `configure-wizard.ts` calls the functions here directly, the same @@ -18,18 +17,17 @@ import { rmSync, } from "node:fs"; import { homedir, tmpdir, userInfo } from "node:os"; -import { resolve, dirname } from "node:path"; +import { resolve } from "node:path"; import { execFileSync } from "node:child_process"; import { hookLogWarn } from "./hook-logger"; -import { getConfigPathForScope } from "./hooks-config"; import { downloadFailproofaidBinary, installFromNpmPackage, installedBinaryPath } from "./daemon-download"; import { logsDir } from "./fp-home"; import { version } from "../../package.json"; import { readVersionFile, updateConfig, writeVersionFile } from "./fp-config"; /** - * Every `systemctl --user` / `launchctl` call is bounded. Both talk to a - * per-user session bus or to launchd, and a wedged session makes an + * Every `systemctl` / `launchctl` call is bounded. A wedged service manager + * makes an * unbounded `execFileSync` block forever — inside the interactive wizard * that reads as a hang with no output at all (`stdio: "ignore"`), right * after the user pressed "apply". A timeout throws instead, which the @@ -39,7 +37,7 @@ const SERVICE_CMD_TIMEOUT_MS = 10_000; /** * How long to wait for the service manager to actually get the daemon into - * a running state after `enable --now` / `load -w`. Both commands return as + * a running state after restart/load. Those commands return as * soon as the job is accepted, which is well before the process has proven * it can stay up. */ @@ -47,7 +45,7 @@ const SERVICE_START_TIMEOUT_MS = 5_000; const SERVICE_START_POLL_MS = 100; /** * How long a unit has to still be running after it first reports running. - * `systemctl --user is-active` calls a `Type=simple` unit active the moment + * `systemctl is-active` calls a `Type=simple` unit active the moment * it forks, so a daemon that dies immediately still reports active once — * a single check would wave through exactly the crash-at-startup case this * is here to catch. Comfortably longer than the unit's `RestartSec=2` @@ -688,9 +686,8 @@ ${conditionLines} Type=simple User=${user} # Set explicitly rather than relying on systemd deriving it from User=: -# failproofaid is user-scope by construction and refuses to start without -# HOME ("HOME is not set; failproofaid is user-scope only"), so the one -# variable it cannot do without is not left to a version-dependent default. +# failproofaid stores per-user state and refuses to start without HOME, so this +# required variable is not left to a version-dependent default. Environment="HOME=${homedir()}" ${envLines}ExecStart=${binaryPath} Restart=on-failure @@ -810,9 +807,8 @@ export async function installDaemonService(): Promise { return { installed: false, reason: `failproofaid is not supported on ${process.platform} yet` }; } - // May reach the network: the npm package carries no binary, so this is - // where a machine opting into the daemon fetches the one built for its - // platform from this version's release. + // May reach the network when the matching optional platform package is not + // installed. The GitHub release asset is the verified fallback channel. const { path: binaryPath, reason: binaryReason } = await ensureFailproofaidBinary(); if (!binaryPath) { return { installed: false, reason: binaryReason ?? "failproofaid binary not found for this platform" }; diff --git a/src/hooks/hook-logger.ts b/src/hooks/hook-logger.ts index d197e6459..c800cae3a 100644 --- a/src/hooks/hook-logger.ts +++ b/src/hooks/hook-logger.ts @@ -21,7 +21,6 @@ import { statSync, } from "node:fs"; import { join } from "node:path"; -import { homedir } from "node:os"; import { logsDir } from "./fp-home"; export type LogLevel = "info" | "warn" | "error"; diff --git a/src/hooks/integrations.ts b/src/hooks/integrations.ts index 4a403d93a..f809f61f8 100644 --- a/src/hooks/integrations.ts +++ b/src/hooks/integrations.ts @@ -14,7 +14,6 @@ import { homedir } from "node:os"; import { parseDocument, type Document } from "yaml"; import { listHermesProfiles, hermesRoot } from "../../lib/hermes-profiles"; import { - HOOK_EVENT_TYPES, CLAUDE_INSTALL_EVENT_TYPES, HOOK_SCOPES, CODEX_HOOK_EVENT_TYPES, diff --git a/src/hooks/loader-utils.ts b/src/hooks/loader-utils.ts index 7c788143f..1b17244d8 100644 --- a/src/hooks/loader-utils.ts +++ b/src/hooks/loader-utils.ts @@ -184,7 +184,7 @@ async function writeShim( } export async function createEsmShim( - distIndex: string, + _distIndex: string, distUrl: string, tmpSuffix = TMP_SUFFIX, ): Promise<{ shimPath: string; shimUrl: string }> { diff --git a/src/hooks/manager.ts b/src/hooks/manager.ts index 1202f6eef..8d99cd7f3 100644 --- a/src/hooks/manager.ts +++ b/src/hooks/manager.ts @@ -8,7 +8,7 @@ import { execSync } from "node:child_process"; import { existsSync } from "node:fs"; import { resolve, basename } from "node:path"; -import { homedir, platform, arch, release, hostname } from "node:os"; +import { platform, arch, release, hostname } from "node:os"; import { HOOK_SCOPES, type HookScope, diff --git a/src/hooks/onboarding-lock.ts b/src/hooks/onboarding-lock.ts index 3b78e0736..9b7e00e14 100644 --- a/src/hooks/onboarding-lock.ts +++ b/src/hooks/onboarding-lock.ts @@ -25,9 +25,8 @@ * user one printed hint, where wrongly proceeding costs a duplicated install. * The command the user typed always runs either way. */ -import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; -import { resolve } from "node:path"; import { onboardingLockFile, stateDir } from "./fp-home"; interface LockBody { diff --git a/src/hooks/session-pause.ts b/src/hooks/session-pause.ts index ace34ddc1..c5da82ccc 100644 --- a/src/hooks/session-pause.ts +++ b/src/hooks/session-pause.ts @@ -22,7 +22,6 @@ * never ended". */ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs"; -import { homedir } from "node:os"; import { createHash } from "node:crypto"; import { join, resolve } from "node:path"; import { writeJsonAtomically } from "../../lib/atomic-write"; diff --git a/tsconfig.json b/tsconfig.json index 7d5e98554..bb2ab23dd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "allowJs": true, "skipLibCheck": true, "strict": true, + "noUnusedLocals": true, "noEmit": true, "esModuleInterop": true, "module": "esnext",