failproofai 1.0.2-beta.5 → 1.0.2-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js +3 -3
  23. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  31. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  33. package/.next/standalone/.next/server/app/index.html +1 -1
  34. package/.next/standalone/.next/server/app/index.rsc +14 -14
  35. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  36. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  37. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  38. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  39. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  42. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  55. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1zkek3z._.js → [root-of-the-server]__1tg4pu6._.js} +1 -1
  59. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
  60. package/.next/standalone/.next/server/chunks/{_12moi5-._.js → _19uwam-._.js} +1 -1
  61. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +7 -7
  62. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07gm3zl._.js +5 -5
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  74. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0stn0gx._.js → [root-of-the-server]__128se8m._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/{_1km9-gn._.js → _0no96tu._.js} +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/{_1kesqs-._.js → _11fc-hb._.js} +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  85. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  86. package/.next/standalone/.next/server/pages/404.html +1 -1
  87. package/.next/standalone/.next/server/pages/500.html +1 -1
  88. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  89. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  90. package/.next/standalone/.next/static/chunks/{0tyt-3a5rtzy8.js → 0931avh-06lj5.js} +1 -1
  91. package/.next/standalone/.next/static/chunks/{36-8i_vbkh85w.js → 0h-3zffoy-_ji.js} +1 -1
  92. package/.next/standalone/.next/static/chunks/{21r7iw3rmixf5.js → 0p84npzxdgpc5.js} +1 -1
  93. package/.next/standalone/.next/static/chunks/{2hwc8e923g5df.js → 0sdgnflvw8kta.js} +1 -1
  94. package/.next/standalone/.next/static/chunks/{2_27jraat038x.js → 0z7au3njlak0j.js} +1 -1
  95. package/.next/standalone/.next/static/chunks/{391tpdqcas7rq.js → 1id7o44jjegf0.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{3_b73tc78jwsq.js → 276n-edifojbd.js} +2 -2
  97. package/.next/standalone/.next/static/chunks/{26lfjhb2kzzgb.js → 2jfskwd2u8o5r.js} +1 -1
  98. package/.next/standalone/.next/static/chunks/{3jqm8ubpv3i-4.js → 2pxr7nr1nyotn.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{33np9nrh1mvj9.js → 3vu_7_eor1m-k.js} +1 -1
  100. package/.next/standalone/package.json +9 -9
  101. package/.next/standalone/server.js +1 -1
  102. package/bin/failproofai.mjs +93 -26
  103. package/dist/cli.mjs +791 -588
  104. package/dist/worker.mjs +2 -2
  105. package/package.json +9 -9
  106. package/src/audit/cli.ts +12 -0
  107. package/src/audit/index.ts +33 -2
  108. package/src/hooks/configure-wizard.ts +112 -18
  109. package/src/hooks/fp-reset.ts +1 -1
  110. package/src/hooks/handler.ts +13 -1
  111. package/src/hooks/manager.ts +19 -1
  112. package/src/hooks/onboarding-attempt.ts +4 -4
  113. package/src/hooks/pack-cli.ts +162 -21
  114. package/src/hooks/pack-store.ts +21 -8
  115. package/src/hooks/read-stdin.ts +78 -8
  116. /package/.next/standalone/.next/static/{59jsH11LoyXu9UBoHvgGM → mAYN9BOxWVFREnVp3yCHp}/_buildManifest.js +0 -0
  117. /package/.next/standalone/.next/static/{59jsH11LoyXu9UBoHvgGM → mAYN9BOxWVFREnVp3yCHp}/_clientMiddlewareManifest.js +0 -0
  118. /package/.next/standalone/.next/static/{59jsH11LoyXu9UBoHvgGM → mAYN9BOxWVFREnVp3yCHp}/_ssgManifest.js +0 -0
package/dist/worker.mjs CHANGED
@@ -2559,7 +2559,7 @@ var init_builtin_policies = __esm(() => {
2559
2559
  });
2560
2560
 
2561
2561
  // package.json
2562
- var version = "1.0.2-beta.5";
2562
+ var version = "1.0.2-beta.6";
2563
2563
  var init_package = () => {};
2564
2564
 
2565
2565
  // src/posthog-key.ts
@@ -9408,7 +9408,7 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
9408
9408
  const legacyNames = activePause || packsInstalledHere ? [] : config.enabledPolicies;
9409
9409
  registerBuiltinPolicies(activePause ? [] : legacyNames);
9410
9410
  if (legacyNames.length > 0) {
9411
- hookLogWarn(`enforcing ${legacyNames.length} policies from this build because no pack is installed — ` + `run \`failproofai policies add core\` to move them into a real pack`);
9411
+ hookLogWarn(`enforcing ${legacyNames.length} policies from this build because no pack is installed — ` + `run \`failproofai policies add FailproofAI/policies\` to move them into a real pack`);
9412
9412
  }
9413
9413
  const enabledBuiltinNames = new Set(legacyNames);
9414
9414
  let cloudManagedPolicies = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.2-beta.5",
3
+ "version": "1.0.2-beta.6",
4
4
  "description": "Observability and enforcement for AI agent harnesses. 39 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -120,15 +120,15 @@
120
120
  "sharp": "0.35.0"
121
121
  },
122
122
  "optionalDependencies": {
123
- "@failproofai/failproofaid-linux-x64": "1.0.2-beta.5",
124
- "@failproofai/failproofaid-linux-arm64": "1.0.2-beta.5",
125
- "@failproofai/failproofaid-darwin-x64": "1.0.2-beta.5",
126
- "@failproofai/failproofaid-darwin-arm64": "1.0.2-beta.5"
123
+ "@failproofai/failproofaid-linux-x64": "1.0.2-beta.6",
124
+ "@failproofai/failproofaid-linux-arm64": "1.0.2-beta.6",
125
+ "@failproofai/failproofaid-darwin-x64": "1.0.2-beta.6",
126
+ "@failproofai/failproofaid-darwin-arm64": "1.0.2-beta.6"
127
127
  },
128
128
  "failproofaidBinaries": {
129
- "linux-x64": "e19a1cf9a254df830f205fac024b8a229c162c93ae73851b985d2794e09da035",
130
- "linux-arm64": "fafcff01e4cce00809b5cd9ff2822705d7373acbab79444e0384cb058b6da921",
131
- "darwin-x64": "d7bd609387e13f5e75287674d948c69d739bac8d6963aa731b93111d1bdd2ea2",
132
- "darwin-arm64": "9a57c8eb0f23ed25328e0f7b06638a6ab7f51e1deebf479bac2c779bf9912941"
129
+ "linux-x64": "1e883d50ea40dad46394a137fab9b8ecddb7b37064312af9ddf89cb3de5eb7b8",
130
+ "linux-arm64": "47a51a75a01c021417a4d67fb7eb5caa8fa9c46abca5ec85d23e8ddead1ebfdd",
131
+ "darwin-x64": "bbc4e714c1e961d53d7dd6c65b84dcc74b4e24439349ebdef3f7d8681ece1776",
132
+ "darwin-arm64": "ca6f6e688070a2d2a63fd333f7c3ac7c7d2bd991ca499cfced23b249e7ca684b"
133
133
  }
134
134
  }
package/src/audit/cli.ts CHANGED
@@ -25,6 +25,8 @@
25
25
  * an exit code. See `runScheduledAudit`.
26
26
  */
27
27
  import { runAudit } from "./index";
28
+ import { hasInstalledPacks } from "../hooks/pack-manifest";
29
+ import { CORE_SOURCE } from "../hooks/pack-store";
28
30
  import { acquireAuditLock, type AuditLockInfo } from "./audit-lock";
29
31
  import { writeDashboardCache } from "./dashboard-cache";
30
32
  import type { AuditResult, RunAuditOptions } from "./types";
@@ -504,6 +506,16 @@ export async function runPostSetupAudit(): Promise<void> {
504
506
  writeDashboardCache({}, result);
505
507
  printSummary(result);
506
508
  process.stdout.write("\n");
509
+ // The audit says what ALREADY happened; nothing here says how to stop it
510
+ // happening again. This is the first thing a new machine runs, and setup
511
+ // installs no policies by design, so without this the whole first session
512
+ // ends on a count of findings and no way to act on it.
513
+ if (!hasInstalledPacks()) {
514
+ process.stdout.write(
515
+ ` ${c(DIM, "none of this is being enforced yet. take ours, or anyone's:")}\n` +
516
+ ` ${c(CYAN, `failproofai policies add ${CORE_SOURCE}`)}\n\n`,
517
+ );
518
+ }
507
519
  } finally {
508
520
  attempt.lock.release();
509
521
  }
@@ -10,6 +10,7 @@
10
10
  import { batchAll } from "../../lib/concurrency";
11
11
  import { BUILTIN_POLICIES } from "../hooks/builtin-policies";
12
12
  import { readMergedHooksConfig } from "../hooks/hooks-config";
13
+ import { readInstalledPacks } from "../hooks/pack-manifest";
13
14
  import { normalizePolicyName } from "../hooks/policy-registry";
14
15
  import { INTEGRATION_TYPES, type IntegrationType } from "../hooks/types";
15
16
  import { ADAPTERS } from "./cli-adapters";
@@ -379,12 +380,42 @@ async function runAuditInner(opts: RunAuditOptions, startedAt: number): Promise<
379
380
  const clis = (opts.clis ?? Array.from(INTEGRATION_TYPES)) as IntegrationType[];
380
381
  const sinceMs = parseSinceOpt(opts.since);
381
382
 
382
- // Snapshot which builtin policies the user currently has enabled — drives
383
- // the "already protected" vs "slipping through" split in the report.
383
+ // What the user actually has switched on — drives the "already protected"
384
+ // vs "slipping through" split.
385
+ //
386
+ // Read from BOTH places it can live, because it moved and this only followed
387
+ // it half way. `enabledPolicies` is the pre-packs key, still written by
388
+ // `policies add <name>` and still true on a machine running the migration
389
+ // shim. Everything installed as a PACK records its selection in
390
+ // `installed.json` instead, and `pack-store` never writes `enabledPolicies`
391
+ // at all — so reading that key alone meant installing a pack could not move
392
+ // this split. Every policy read as slipping through no matter what was
393
+ // enforcing, and the number the audit leads with never changed however many
394
+ // policies you installed. Verified: same 302 hits, same 17 findings, before
395
+ // and after installing all 38.
396
+ //
397
+ // The split, not the REPLAY. What gets replayed stays the compiled builtins
398
+ // on purpose — see `initReplay`: an audit is a fixed yardstick, and one that
399
+ // changed shape with whatever pack a machine happened to have could not be
400
+ // compared against its own history. A pack policy sharing a builtin's name
401
+ // marks that builtin protected, which is exactly right: the same rule is
402
+ // enforcing, whichever artifact carries it.
384
403
  const userConfig = readMergedHooksConfig();
385
404
  const enabledBuiltins = new Set(
386
405
  (userConfig.enabledPolicies ?? []).map((n) => normalizePolicyName(n)),
387
406
  );
407
+ try {
408
+ for (const pack of readInstalledPacks().packs) {
409
+ // `enabled: null` means the whole pack was taken, so every policy in its
410
+ // catalog counts — the field records a SELECTION, and its absence is not
411
+ // an empty one.
412
+ const taken = pack.enabled ?? pack.policies.map((entry) => entry.name);
413
+ for (const name of taken) enabledBuiltins.add(normalizePolicyName(name));
414
+ }
415
+ } catch {
416
+ // An unreadable manifest must not fail the audit. It costs the split its
417
+ // pack half, which is the same answer this gave for every machine before.
418
+ }
388
419
 
389
420
  // 1. Discover transcripts across all selected CLIs.
390
421
  const allTranscripts: TranscriptMetadata[] = [];
@@ -30,6 +30,8 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
30
30
  import { homedir } from "node:os";
31
31
  import { dirname, resolve, sep } from "node:path";
32
32
 
33
+ import { CORE_SOURCE } from "./pack-store";
34
+
33
35
  import {
34
36
  selectOne,
35
37
  promptText,
@@ -109,6 +111,31 @@ export interface WizardIO {
109
111
  stdout?: TTYOut;
110
112
  }
111
113
 
114
+ /**
115
+ * Answers supplied up front, so the run needs nobody at the keyboard.
116
+ *
117
+ * This is the whole of headless setup, and it is small for a reason nobody
118
+ * planned: the wizard stopped DECIDING most of what it used to. Scope, agents,
119
+ * policies and the custom-policy toggle are all hardcoded now (see the
120
+ * constants further down), so there is no decision surface left to expose —
121
+ * only the three questions that remain, and each has one sensible unattended
122
+ * answer.
123
+ *
124
+ * None of this skips sudo. Installing a system service needs root and nothing
125
+ * here changes that: elevation goes through `sudo -n`, which never prompts, and
126
+ * a machine that cannot elevate is told exactly what to run as root.
127
+ */
128
+ export interface WizardAnswers {
129
+ /** Cloud API key. Its presence IS the request to connect. */
130
+ token?: string;
131
+ /** Where to connect. Defaults to the same URL the wizard infers. */
132
+ url?: string;
133
+ machineId?: string;
134
+ machineLabel?: string;
135
+ /** Record decisions only, no session transcripts. */
136
+ noTranscripts?: boolean;
137
+ }
138
+
112
139
  /**
113
140
  * Why a wizard run ended without applying. Distinguished so the caller can
114
141
  * pick an exit code — a user who pressed Esc did nothing wrong (exit 0), a
@@ -120,8 +147,9 @@ export type WizardAbort =
120
147
  | "needs_root"
121
148
  | "daemon_failed"
122
149
  | "unsupported_platform"
123
- | "not_a_tty"
124
- | "running_as_sudo";
150
+ | "running_as_sudo"
151
+ /** A cloud key was supplied and the server refused it. Unattended only. */
152
+ | "cloud_unverified";
125
153
 
126
154
  export interface WizardResult {
127
155
  applied: boolean;
@@ -695,19 +723,27 @@ export async function maybeFirstRunConfigure(
695
723
 
696
724
  // ── The wizard ───────────────────────────────────────────────────────────────
697
725
 
698
- export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResult> {
726
+ export async function runConfigureWizard(
727
+ io: WizardIO = {},
728
+ answers: WizardAnswers = {},
729
+ ): Promise<WizardResult> {
699
730
  const stdin: TTYIn = io.stdin ?? process.stdin;
700
731
  const stdout: TTYOut = io.stdout ?? process.stdout;
701
732
  const cwd = process.cwd();
733
+ // No terminal means no questions — not a refusal.
734
+ //
735
+ // `failproofai config` IS the authorisation: somebody typed the command whose
736
+ // entire job is to configure this machine. Requiring a flag on top of that to
737
+ // permit what the command already means is asking the same question twice,
738
+ // and this used to refuse outright — so a CI job, a container or an agent
739
+ // could not set a machine up at all, which is the gap this closes.
740
+ //
741
+ // Safe to do here because the IMPLICIT path is guarded separately:
742
+ // `maybeFirstRunConfigure` has its own TTY check and returns before ever
743
+ // reaching this function, so setup never runs off the back of some other
744
+ // command on a headless box.
745
+ const unattended = !stdin.isTTY || !stdout.isTTY;
702
746
 
703
- if (!stdin.isTTY || !stdout.isTTY) {
704
- stdout.write(
705
- "failproofai config needs an interactive terminal.\n" +
706
- "Use the flag form instead, e.g.:\n" +
707
- " failproofai policies --install --scope user --cli claude\n",
708
- );
709
- return { applied: false };
710
- }
711
747
 
712
748
  // Running the wizard itself under sudo configures the WRONG ACCOUNT, and
713
749
  // does it silently: homedir() becomes /root, so the hooks land in root's
@@ -724,7 +760,10 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
724
760
  "The one step that needs root (installing the service) asks for your password\n" +
725
761
  "on its own.\n",
726
762
  );
727
- return { applied: false };
763
+ // Same reason as above: this configured nothing, and it is not a
764
+ // cancellation. `running_as_sudo` has been in `WizardAbort` since it was
765
+ // written and was never once assigned.
766
+ return { applied: false, abort: "running_as_sudo" };
728
767
  }
729
768
 
730
769
  // Fire-and-forget: never block the wizard's first paint on telemetry.
@@ -847,7 +886,13 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
847
886
  ? "failproofaid can't evaluate — every tool call is denied. Rebuilding needs sudo.\n\n"
848
887
  : "Installing failproofaid — needs sudo once.\n\n",
849
888
  );
850
- if (!primeElevation()) {
889
+ // `primeElevation` runs `sudo -v`, which PROMPTS. An unattended run has
890
+ // nobody to type a password, so it goes straight to the `sudo -n` that
891
+ // `runPrivileged` uses anyway — exactly what `failproofai update` already
892
+ // does — and a machine that cannot elevate gets the commands below instead
893
+ // of a hung terminal. This is the same answer for both: sudo cannot be
894
+ // conjured away, only asked for at a moment somebody can answer.
895
+ if (!(unattended ? canElevate() : primeElevation())) {
851
896
  // Required means required: write nothing at all, so a machine that could
852
897
  // not be set up is left exactly as it was found rather than carrying
853
898
  // half a configuration. The commands are printed so an admin can do the
@@ -866,7 +911,7 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
866
911
  "failproofaid is running, but from a service definition written by an older\n" +
867
912
  "version — it cannot start a scheduled audit. Refreshing it needs root once.\n\n",
868
913
  );
869
- if (!primeElevation()) {
914
+ if (!(unattended ? canElevate() : primeElevation())) {
870
915
  // NOT an abort, unlike the install branch above. There is a working
871
916
  // daemon here and hooks are enforcing; only the scheduled audit is out
872
917
  // of reach. Stopping setup over that would make an upgrade the thing
@@ -914,7 +959,7 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
914
959
  //
915
960
  // Setup no longer asks which policies to enable, and that is deliberate.
916
961
  // failproofai ships no policies of its own any more: they arrive as packs —
917
- // from inside this package (`pack add core`) or from anyone's GitHub release.
962
+ // from inside this package (`policies add FailproofAI/policies`) or from anyone's GitHub release.
918
963
  // A wizard that pre-ticks OUR list makes a product decision on behalf of
919
964
  // somebody who has not seen the list yet, and not everyone wants the set we
920
965
  // would have chosen. So setup wires the hooks, and choosing what they
@@ -971,7 +1016,13 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
971
1016
  let connect: { url: string; token: string; machineId: string; machineLabel: string } | null = null;
972
1017
 
973
1018
  {
974
- const choice = await selectOne<"key" | "local">({
1019
+ // Supplying a key IS asking to connect — there is no other reason to pass
1020
+ // one — so an unattended run with a token skips straight past the question,
1021
+ // and one without a token stays local, which is the same "Not now" branch a
1022
+ // person picks. Neither needs a separate flag to say so.
1023
+ const choice = unattended
1024
+ ? (answers.token ? "key" : "local")
1025
+ : await selectOne<"key" | "local">({
975
1026
  message: "Connect this machine to FailproofAI Cloud?",
976
1027
  // The hint says what you GET; the body says what LEAVES.
977
1028
  //
@@ -1054,7 +1105,16 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1054
1105
  let machineId = resolveMachineId();
1055
1106
  let machineLabel = existing?.machineLabel ?? resolveMachineLabel();
1056
1107
 
1057
- if (existing) {
1108
+ if (existing && unattended && !answers.url) {
1109
+ // Already enrolled, and this run did not name a different place to
1110
+ // enrol. Reusing is what the person picks here too, and re-verifying a
1111
+ // working credential is the one thing an unattended re-run must not
1112
+ // turn into a failure.
1113
+ url = existing.url;
1114
+ token = answers.token ?? existing.token;
1115
+ machineId = answers.machineId ?? existing.machineId;
1116
+ machineLabel = answers.machineLabel ?? existing.machineLabel ?? machineLabel;
1117
+ } else if (existing && !unattended) {
1058
1118
  const reuse = await selectOne<"reuse" | "other">({
1059
1119
  message: `Use this machine's existing connection to ${existing.url}?`,
1060
1120
  choices: [
@@ -1126,6 +1186,12 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1126
1186
  }
1127
1187
  }
1128
1188
 
1189
+ if (token === null && unattended) {
1190
+ // Supplied on the command line, so nothing is asked and nothing is
1191
+ // echoed. Reaching here without one cannot happen — an unattended run
1192
+ // only takes the "key" branch BECAUSE a token was given.
1193
+ token = answers.token ?? null;
1194
+ }
1129
1195
  if (token === null) {
1130
1196
  token = await promptText({
1131
1197
  // The destination is in the question now that it is no longer a
@@ -1149,6 +1215,14 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1149
1215
  // persisted here.
1150
1216
  stdout.write("\nChecking the key… ");
1151
1217
  const probe = await validateIngestKey({ url: ingestUrlFor(url), key: token });
1218
+ if (!probe.ok && unattended) {
1219
+ // A key that does not work is a FAILED setup, not a prompt. The
1220
+ // interactive path offers "save it anyway" because a person can weigh
1221
+ // an outage against their own impatience; a script cannot, and one that
1222
+ // exited 0 here would leave a fleet believing it was reporting.
1223
+ stdout.write(`\nThat key did not work: ${probe.reason}\n`);
1224
+ return { applied: false, abort: "cloud_unverified" };
1225
+ }
1152
1226
  if (!probe.ok) {
1153
1227
  stdout.write(`\nThat did not work: ${probe.reason}\n`);
1154
1228
  const retry = await selectOne<"skip" | "anyway">({
@@ -1173,7 +1247,15 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1173
1247
  }
1174
1248
  }
1175
1249
  // 5 — Review & apply
1176
- const decision = await selectOne<"apply" | "cancel">({
1250
+ //
1251
+ // The last question, and the only one a headless run skips outright rather
1252
+ // than answering from a flag. There is nothing to confirm when nobody is
1253
+ // watching, and the command itself was the confirmation.
1254
+ //
1255
+ // NOT the same call as `uninstall`, which does require --yes: that one
1256
+ // REMOVES things, so silence there would destroy on a signal as weak as a
1257
+ // missing terminal. This one does exactly what its name says.
1258
+ const decision = unattended ? "apply" : await selectOne<"apply" | "cancel">({
1177
1259
  message: "Ready to apply?",
1178
1260
  body: reviewLines({
1179
1261
  target,
@@ -1463,6 +1545,18 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1463
1545
  { ok: true },
1464
1546
  stdout,
1465
1547
  );
1548
+ // Setup wires the hooks and deliberately chooses NO policies, so a machine
1549
+ // that has just finished it enforces almost nothing — and the summary line
1550
+ // says "0 policies" without saying what to do about it. Anyone else's pack is
1551
+ // typed the same way, which is the point: ours is named in full rather than
1552
+ // by a short name only we could use.
1553
+ if (policies.length === 0) {
1554
+ stdout.write(
1555
+ `\n Nothing is enforcing yet. Take ours, or anyone's:\n` +
1556
+ ` failproofai policies add ${CORE_SOURCE}\n` +
1557
+ ` failproofai policies show <owner>/<repo> (look first)\n\n`,
1558
+ );
1559
+ }
1466
1560
  return {
1467
1561
  applied: true,
1468
1562
  target,
@@ -973,7 +973,7 @@ export function resetHome(from: number, to: number = LAYOUT_VERSION): ResetOutco
973
973
  //
974
974
  // Nothing needs carrying either: `enabledPolicies` is left exactly where it
975
975
  // is, and the no-pack fallback reads it, so a machine mid-upgrade keeps
976
- // enforcing precisely what it enforced before. `failproofai policies add core`
976
+ // enforcing precisely what it enforced before. `failproofai policies add FailproofAI/policies`
977
977
  // turns those names into a real pack, carrying the same selection into it.
978
978
  // The step's OWN target, not LAYOUT_VERSION.
979
979
  //
@@ -323,7 +323,7 @@ export async function evaluateHookEvent(
323
323
  if (legacyNames.length > 0) {
324
324
  hookLogWarn(
325
325
  `enforcing ${legacyNames.length} policies from this build because no pack is installed — ` +
326
- `run \`failproofai policies add core\` to move them into a real pack`,
326
+ `run \`failproofai policies add FailproofAI/policies\` to move them into a real pack`,
327
327
  );
328
328
  }
329
329
  // What actually registered, for the pack-duplicate check below. A paused
@@ -786,6 +786,18 @@ export async function handleHookEvent(eventType: string, cli: IntegrationType =
786
786
  error_type: "oversized",
787
787
  });
788
788
  }
789
+ if (stdinRead.timedOut) {
790
+ // Reported distinctly from a read error because the remedy is different and
791
+ // the cause is upstream: the agent spawned the hook and did not close its
792
+ // stdin. Before this was bounded it was not reported at all — the process
793
+ // simply never returned, and the tool call it was gating never resolved.
794
+ hookLogWarn(`stdin never closed for ${eventType}; evaluated nothing`);
795
+ void trackHookEvent(getInstanceId(), "hook_stdin_error", {
796
+ event_type: eventType,
797
+ cli,
798
+ error_type: "timeout",
799
+ });
800
+ }
789
801
 
790
802
  const result = await evaluateHookEvent(eventType, cli, stdinRead.payload);
791
803
 
@@ -578,7 +578,7 @@ async function installHooksImpl(
578
578
  } catch (err) {
579
579
  console.log(
580
580
  `\nWarning: could not fetch the policy pack (${err instanceof Error ? err.message : String(err)}).\n` +
581
- `Run \`failproofai policies add core\` once you are online.`,
581
+ `Run \`failproofai policies add FailproofAI/policies\` once you are online.`,
582
582
  );
583
583
  }
584
584
  } else {
@@ -991,6 +991,24 @@ export async function listHooks(cwd?: string): Promise<void> {
991
991
  // a reader scrolling back from their prompt actually sees.
992
992
  const footer: Array<string[] | null> = [note(`Config: ${globalPolicyConfigFile()}`, opts)];
993
993
 
994
+ // A machine with hooks wired and no policies to run is the normal state right
995
+ // after setup — setup installs the hooks and deliberately chooses nothing —
996
+ // and this listing said NOTHING about how to leave it. A reader who had just
997
+ // finished `failproofai config` saw a header, a config path, and no next
998
+ // step. Ours is named in full, the same way anyone else's pack is typed,
999
+ // because it IS one.
1000
+ if (packCount === 0) {
1001
+ footer.unshift(
1002
+ nextStep(
1003
+ `failproofai policies add ${CORE_SOURCE}`,
1004
+ "Nothing is enforcing yet. Take ours, or anyone's:",
1005
+ opts,
1006
+ ),
1007
+ note("Someone else's: failproofai policies add <owner>/<repo>", opts),
1008
+ note("Look first: failproofai policies show <owner>/<repo>", opts),
1009
+ );
1010
+ }
1011
+
994
1012
  if (installedScopes.length > 1) {
995
1013
  footer.push(
996
1014
  warning(
@@ -157,10 +157,11 @@ export function blockerCleared(attempt: OnboardingAttempt, probe: RetryProbe): b
157
157
  // command would just repeat the hard-fail. Only a new release is a reason
158
158
  // to ask again (it might add support for this platform).
159
159
  return probe.cliVersion !== attempt.cliVersion;
160
- case "not_a_tty":
161
160
  case "running_as_sudo":
162
- // Both are properties of the invocation, not of the machine, so the next
163
- // one may well be fine.
161
+ // A property of the invocation, not of the machine, so the next one may
162
+ // well be fine. (`not_a_tty` used to sit here too. It is gone because
163
+ // there is no such refusal any more: a run with no terminal has no
164
+ // questions to ask, so it applies rather than declining.)
164
165
  return true;
165
166
  default:
166
167
  return true;
@@ -174,7 +175,6 @@ export function attemptHintLines(attempt: OnboardingAttempt): string[] {
174
175
  daemon_failed: "the failproofaid service could not be started",
175
176
  cancelled: "it was cancelled",
176
177
  unsupported_platform: "failproofaid does not run on this platform",
177
- not_a_tty: "there was no terminal to ask in",
178
178
  running_as_sudo: "it was run under sudo",
179
179
  };
180
180
  const detail = why[attempt.reason] ?? "it did not finish";