failproofai 1.0.5 → 1.0.6-beta.0

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 (186) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +5 -5
  3. package/.next/standalone/.next/prerender-manifest.json +5 -5
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app/index.html +1 -1
  36. package/.next/standalone/.next/server/app/index.rsc +14 -14
  37. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  38. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  40. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  60. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  63. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  64. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/chunks/[externals]__0sds_16._.js +3 -0
  67. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +3 -3
  69. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
  72. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
  73. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  82. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__15k-dqb._.js → [root-of-the-server]__1i26dsw._.js} +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0sz_5na._.js → [root-of-the-server]__1ouobn4._.js} +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/{_1b78kcp._.js → _09hs6ms._.js} +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/{_0guaevg._.js → _0lbwa8m._.js} +2 -2
  90. package/.next/standalone/.next/server/chunks/ssr/_0wso5d3._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/_1-3unkg._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +3 -3
  94. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +3 -3
  97. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/node_modules_1hl81y4._.js +3 -0
  103. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
  104. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  105. package/.next/standalone/.next/server/pages/404.html +1 -1
  106. package/.next/standalone/.next/server/pages/500.html +1 -1
  107. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  108. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  109. package/.next/standalone/.next/static/chunks/{3w46euc60dx39.js → 03n_qjih7-w6q.js} +1 -1
  110. package/.next/standalone/.next/static/chunks/{2zafgs90r_leq.js → 06jxjnw8bfczc.js} +1 -1
  111. package/.next/standalone/.next/static/chunks/{252xreaecy80h.js → 0vanab2g65op_.js} +1 -1
  112. package/.next/standalone/.next/static/chunks/17pjz_h45j10c.js +1 -0
  113. package/.next/standalone/.next/static/chunks/1ekh-hj_iom84.js +1 -0
  114. package/.next/standalone/.next/static/chunks/{021ydrq6rhpsi.js → 1so_3s0q4c4j1.js} +1 -1
  115. package/.next/standalone/.next/static/chunks/1xacnwjkbekn7.js +1 -0
  116. package/.next/standalone/.next/static/chunks/2hg_bp8z_vm2g.js +1 -0
  117. package/.next/standalone/.next/static/chunks/{3bleyy40s99f2.js → 2u0kol_boahtv.js} +1 -1
  118. package/.next/standalone/.next/static/chunks/{1vb33xa6j9y2q.js → 3_vesf83-0903.js} +1 -1
  119. package/.next/standalone/.next/static/chunks/3d3hzhcnns6ww.js +1 -0
  120. package/.next/standalone/.next/static/chunks/3iasus89n8mg1.js +6 -0
  121. package/.next/standalone/.next/static/chunks/{11s_v03ub6nss.js → 44_cj1_i_aa2z.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/{turbopack-3p73obe0xmrxm.js → turbopack-38nzuvp95_2rv.js} +1 -1
  123. package/.next/standalone/fp-cloud-cli/uv.lock +3 -3
  124. package/.next/standalone/hermes-plugin/README.md +133 -0
  125. package/.next/standalone/hermes-plugin/__init__.py +248 -0
  126. package/.next/standalone/hermes-plugin/client.py +134 -0
  127. package/.next/standalone/hermes-plugin/ledger.py +187 -0
  128. package/.next/standalone/hermes-plugin/plugin.yaml +37 -0
  129. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  130. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  131. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
  132. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
  133. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  134. package/.next/standalone/node_modules/next/dist/server/app-render/create-component-styles-and-scripts.js +2 -1
  135. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  136. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  137. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  138. package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +14 -0
  139. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  140. package/.next/standalone/node_modules/next/dist/server/lib/disk-lru-cache.external.js +7 -1
  141. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  142. package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-wrapper.js +6 -1
  143. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  144. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  145. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  146. package/.next/standalone/node_modules/next/package.json +10 -10
  147. package/.next/standalone/node_modules/react/cjs/react.development.js +92 -46
  148. package/.next/standalone/node_modules/react/cjs/react.production.js +112 -89
  149. package/.next/standalone/node_modules/react/package.json +1 -1
  150. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +1165 -790
  151. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +1171 -791
  152. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +1294 -862
  153. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +1305 -868
  154. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +1315 -896
  155. package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +24 -5
  156. package/.next/standalone/node_modules/react-dom/package.json +4 -4
  157. package/.next/standalone/package.json +13 -12
  158. package/.next/standalone/sdk/python/uv.lock +18 -18
  159. package/.next/standalone/server.js +1 -1
  160. package/README.md +29 -13
  161. package/bin/failproofai.mjs +3 -1
  162. package/dist/cli.mjs +420 -178
  163. package/dist/worker.mjs +17 -3
  164. package/hermes-plugin/README.md +133 -0
  165. package/hermes-plugin/__init__.py +248 -0
  166. package/hermes-plugin/client.py +134 -0
  167. package/hermes-plugin/ledger.py +187 -0
  168. package/hermes-plugin/plugin.yaml +37 -0
  169. package/package.json +13 -12
  170. package/scripts/translate-docs/validate-translation.ts +48 -0
  171. package/src/hooks/handler.ts +29 -1
  172. package/src/hooks/integrations.ts +264 -73
  173. package/src/hooks/manager.ts +17 -5
  174. package/src/hooks/types.ts +8 -8
  175. package/src/hooks/worker-server.ts +1 -0
  176. package/.next/standalone/.next/server/chunks/[externals]__157pf8j._.js +0 -3
  177. package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +0 -3
  178. package/.next/standalone/.next/static/chunks/0y8_1s45gbqmn.js +0 -6
  179. package/.next/standalone/.next/static/chunks/14b3ezufe5411.js +0 -1
  180. package/.next/standalone/.next/static/chunks/1oskk7pdo8aon.js +0 -1
  181. package/.next/standalone/.next/static/chunks/2xycd6mjg00fb.js +0 -1
  182. package/.next/standalone/.next/static/chunks/3l36d73kj18zw.js +0 -1
  183. package/.next/standalone/.next/static/chunks/40lpt2o2b283e.js +0 -1
  184. /package/.next/standalone/.next/static/{uFpAs_vIXSo6qBzmkXPWD → -7OT2NwGPxAu-skEL_Xee}/_buildManifest.js +0 -0
  185. /package/.next/standalone/.next/static/{uFpAs_vIXSo6qBzmkXPWD → -7OT2NwGPxAu-skEL_Xee}/_clientMiddlewareManifest.js +0 -0
  186. /package/.next/standalone/.next/static/{uFpAs_vIXSo6qBzmkXPWD → -7OT2NwGPxAu-skEL_Xee}/_ssgManifest.js +0 -0
@@ -7,7 +7,17 @@
7
7
  * is agent-agnostic — only install/uninstall plumbing varies.
8
8
  */
9
9
  import { execSync } from "node:child_process";
10
- import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
10
+ import {
11
+ cpSync,
12
+ readFileSync,
13
+ writeFileSync,
14
+ existsSync,
15
+ lstatSync,
16
+ mkdirSync,
17
+ renameSync,
18
+ rmSync,
19
+ unlinkSync,
20
+ } from "node:fs";
11
21
  import { resolve, dirname } from "node:path";
12
22
  import { fileURLToPath } from "node:url";
13
23
  import { homedir } from "node:os";
@@ -135,6 +145,9 @@ export interface Integration {
135
145
  /** Read the raw settings/hooks file (returns {} when missing). */
136
146
  readSettings(settingsPath: string): Record<string, unknown>;
137
147
 
148
+ /** Install integration-owned files before enabling them in settings. */
149
+ prepareInstall?(settingsPath: string): void;
150
+
138
151
  /** Write the settings/hooks file. */
139
152
  writeSettings(settingsPath: string, settings: Record<string, unknown>): void;
140
153
 
@@ -1363,14 +1376,14 @@ function makePiProjectRelativeEntry(extPath: string): string {
1363
1376
  // works for the local user.
1364
1377
  return extResolved;
1365
1378
  }
1366
- // ── Hermes (hermes-agent) — live hooks (Pillar 1) ────────────────────────────
1379
+ // ── Hermes (hermes-agent) — native plugin (Pillar 1) ────────────────────────
1367
1380
  //
1368
- // External-command CLI like codex/cursor, but its config is YAML
1369
- // (`~/.hermes/config.yaml`) under a `hooks:` map, so the I/O layer uses the yaml
1370
- // Document API (comment-preserving). Flat per-event arrays like cursor:
1371
- // `hooks: { pre_tool_call: [ { command, timeout, <marker> } ], … }`. Hermes
1372
- // reads a `{"decision":"block",…}` JSON response on stdout (see
1373
- // policy-evaluator.ts); exit codes are ignored. User-scope only.
1381
+ // Hermes loads trusted Python plugins from `<HERMES_HOME>/plugins/<name>/`.
1382
+ // The shipped plugin registers Hermes-native hooks and talks directly to the
1383
+ // local failproofaid socket, avoiding a fresh CLI process per event. Config is
1384
+ // YAML and profile-scoped; install copies the plugin into every profile and
1385
+ // adds `failproofai` to `plugins.enabled`. Legacy shell-hook entries are removed
1386
+ // during migration so one tool call is never evaluated twice.
1374
1387
 
1375
1388
  /** One hook entry as stored under a `hooks:` event key in config.yaml. */
1376
1389
  interface HermesHookEntry {
@@ -1379,6 +1392,200 @@ interface HermesHookEntry {
1379
1392
  [key: string]: unknown;
1380
1393
  }
1381
1394
 
1395
+ interface HermesPluginsConfig {
1396
+ enabled?: unknown[];
1397
+ disabled?: unknown[];
1398
+ entries?: Record<string, unknown>;
1399
+ [key: string]: unknown;
1400
+ }
1401
+
1402
+ const HERMES_PLUGIN_ID = "failproofai";
1403
+ const HERMES_PLUGIN_MARKER = ".failproofai-managed";
1404
+ const HERMES_PLUGIN_FILES = ["plugin.yaml", "__init__.py", "client.py", "ledger.py"] as const;
1405
+
1406
+ function getHermesPluginSourcePath(): string {
1407
+ const fromEnv = process.env.FAILPROOFAI_PACKAGE_ROOT;
1408
+ if (fromEnv) return resolve(fromEnv, "hermes-plugin");
1409
+ return resolve(fileURLToPath(import.meta.url), "..", "..", "..", "hermes-plugin");
1410
+ }
1411
+
1412
+ export function hermesPluginPathForSettings(settingsPath: string): string {
1413
+ return resolve(dirname(settingsPath), "plugins", HERMES_PLUGIN_ID);
1414
+ }
1415
+
1416
+ function hasManagedHermesPlugin(settingsPath: string): boolean {
1417
+ const pluginPath = hermesPluginPathForSettings(settingsPath);
1418
+ try {
1419
+ if (lstatSync(pluginPath).isSymbolicLink()) return false;
1420
+ if (!existsSync(resolve(pluginPath, HERMES_PLUGIN_MARKER))) return false;
1421
+ return HERMES_PLUGIN_FILES.every((file) => existsSync(resolve(pluginPath, file)));
1422
+ } catch {
1423
+ return false;
1424
+ }
1425
+ }
1426
+
1427
+ function hasHermesPluginMarker(settingsPath: string): boolean {
1428
+ const pluginPath = hermesPluginPathForSettings(settingsPath);
1429
+ try {
1430
+ return !lstatSync(pluginPath).isSymbolicLink() && existsSync(resolve(pluginPath, HERMES_PLUGIN_MARKER));
1431
+ } catch {
1432
+ return false;
1433
+ }
1434
+ }
1435
+
1436
+ function installHermesPlugin(settingsPath: string): void {
1437
+ const source = getHermesPluginSourcePath();
1438
+ for (const file of HERMES_PLUGIN_FILES) {
1439
+ if (!existsSync(resolve(source, file))) {
1440
+ throw new Error(`Hermes plugin asset is missing from the failproofai package: ${file}`);
1441
+ }
1442
+ }
1443
+
1444
+ const destination = hermesPluginPathForSettings(settingsPath);
1445
+ if (existsSync(destination)) {
1446
+ if (lstatSync(destination).isSymbolicLink() || !existsSync(resolve(destination, HERMES_PLUGIN_MARKER))) {
1447
+ throw new Error(
1448
+ `Refusing to overwrite an unmanaged Hermes plugin at ${destination}. ` +
1449
+ `Move it aside or install FailproofAI under a different Hermes profile.`,
1450
+ );
1451
+ }
1452
+ }
1453
+
1454
+ mkdirSync(dirname(destination), { recursive: true });
1455
+ const suffix = `${process.pid}-${Date.now()}`;
1456
+ const temporary = `${destination}.install-${suffix}`;
1457
+ const backup = `${destination}.backup-${suffix}`;
1458
+ let movedExisting = false;
1459
+ try {
1460
+ mkdirSync(temporary, { recursive: true });
1461
+ for (const file of HERMES_PLUGIN_FILES) {
1462
+ cpSync(resolve(source, file), resolve(temporary, file));
1463
+ }
1464
+ writeFileSync(
1465
+ resolve(temporary, HERMES_PLUGIN_MARKER),
1466
+ "Managed by failproofai. Remove with `failproofai policies --uninstall --cli hermes`.\n",
1467
+ { encoding: "utf8", mode: 0o600 },
1468
+ );
1469
+ if (existsSync(destination)) {
1470
+ renameSync(destination, backup);
1471
+ movedExisting = true;
1472
+ }
1473
+ renameSync(temporary, destination);
1474
+ if (movedExisting) rmSync(backup, { recursive: true, force: true });
1475
+ } catch (err) {
1476
+ rmSync(temporary, { recursive: true, force: true });
1477
+ if (movedExisting && !existsSync(destination) && existsSync(backup)) {
1478
+ renameSync(backup, destination);
1479
+ }
1480
+ throw err;
1481
+ }
1482
+ }
1483
+
1484
+ function removeManagedHermesPlugin(settingsPath: string): boolean {
1485
+ const pluginPath = hermesPluginPathForSettings(settingsPath);
1486
+ // The marker is the ownership boundary. Remove even an incomplete managed
1487
+ // install so an interrupted copy can always be repaired or uninstalled.
1488
+ if (!hasHermesPluginMarker(settingsPath)) return false;
1489
+ rmSync(pluginPath, { recursive: true, force: true });
1490
+ return true;
1491
+ }
1492
+
1493
+ function removeLegacyHermesHooks(doc: Document): number {
1494
+ const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
1495
+ const hooks = js.hooks;
1496
+ let removed = 0;
1497
+ if (hooks && typeof hooks === "object") {
1498
+ for (const eventType of Object.keys(hooks)) {
1499
+ const entries = hooks[eventType];
1500
+ if (!Array.isArray(entries)) continue;
1501
+ const before = entries.length;
1502
+ const filtered = entries.filter((hook) => !isMarkedHook(hook));
1503
+ removed += before - filtered.length;
1504
+ if (filtered.length === 0) delete hooks[eventType];
1505
+ else hooks[eventType] = filtered;
1506
+ }
1507
+ if (Object.keys(hooks).length === 0) {
1508
+ doc.delete("hooks");
1509
+ // Older FailproofAI installs set this broad Hermes switch so headless
1510
+ // shell hooks could run. It is safe to remove only when no operator hook
1511
+ // remains that may rely on it.
1512
+ if (removed > 0) doc.delete("hooks_auto_accept");
1513
+ } else {
1514
+ doc.set("hooks", hooks);
1515
+ }
1516
+ }
1517
+ return removed;
1518
+ }
1519
+
1520
+ function hermesConfigState(settingsPath: string): {
1521
+ pluginEnabled: boolean;
1522
+ legacyShellHookPresent: boolean;
1523
+ } {
1524
+ if (!existsSync(settingsPath)) {
1525
+ return { pluginEnabled: false, legacyShellHookPresent: false };
1526
+ }
1527
+ const doc = readYamlDoc(settingsPath);
1528
+ const js = (doc.toJS() ?? {}) as {
1529
+ hooks?: Record<string, HermesHookEntry[]>;
1530
+ plugins?: HermesPluginsConfig;
1531
+ };
1532
+ const enabled = js.plugins?.enabled;
1533
+ const legacyShellHookPresent =
1534
+ !!js.hooks &&
1535
+ typeof js.hooks === "object" &&
1536
+ Object.values(js.hooks).some(
1537
+ (entries) => Array.isArray(entries) && entries.some((entry) => isMarkedHook(entry)),
1538
+ );
1539
+ return {
1540
+ pluginEnabled: Array.isArray(enabled) && enabled.includes(HERMES_PLUGIN_ID),
1541
+ legacyShellHookPresent,
1542
+ };
1543
+ }
1544
+
1545
+ export interface HermesProfileHealth {
1546
+ name: string;
1547
+ home: string;
1548
+ settingsPath: string;
1549
+ pluginInstalled: boolean;
1550
+ pluginEnabled: boolean;
1551
+ legacyShellHookPresent: boolean;
1552
+ healthy: boolean;
1553
+ }
1554
+
1555
+ /** Read-only, per-profile Hermes installation health for setup/status surfaces. */
1556
+ export function hermesProfileHealth(): HermesProfileHealth[] {
1557
+ return listHermesProfiles().map((profile) => {
1558
+ const settingsPath = resolve(profile.home, "config.yaml");
1559
+ const config = hermesConfigState(settingsPath);
1560
+ const pluginInstalled = hasManagedHermesPlugin(settingsPath);
1561
+ return {
1562
+ name: profile.name,
1563
+ home: profile.home,
1564
+ settingsPath,
1565
+ pluginInstalled,
1566
+ pluginEnabled: config.pluginEnabled,
1567
+ legacyShellHookPresent: config.legacyShellHookPresent,
1568
+ healthy: pluginInstalled && config.pluginEnabled && !config.legacyShellHookPresent,
1569
+ };
1570
+ });
1571
+ }
1572
+
1573
+ /** Compact rows for the machine-status display; absent Hermes homes produce no noise. */
1574
+ export function hermesProfileStatusRows(): Array<[string, string]> {
1575
+ return hermesProfileHealth()
1576
+ .filter((profile) => existsSync(profile.home))
1577
+ .map((profile) => {
1578
+ const problems: string[] = [];
1579
+ if (!profile.pluginInstalled) problems.push("plugin files missing or incomplete");
1580
+ if (!profile.pluginEnabled) problems.push("plugin not enabled");
1581
+ if (profile.legacyShellHookPresent) problems.push("legacy shell hook also present");
1582
+ return [
1583
+ "hermes/" + profile.name,
1584
+ problems.length === 0 ? "native plugin enabled" : "UNHEALTHY — " + problems.join("; "),
1585
+ ];
1586
+ });
1587
+ }
1588
+
1382
1589
  export const hermes: Integration = {
1383
1590
  id: "hermes",
1384
1591
  displayName: "Hermes",
@@ -1410,75 +1617,66 @@ export const hermes: Integration = {
1410
1617
  writeYamlDoc(settingsPath, settings as unknown as Document);
1411
1618
  },
1412
1619
 
1413
- buildHookEntry(binaryPath, eventType, scope) {
1414
- // No matcher → fires for ALL tools / all platforms (slack/telegram/cli/cron)
1415
- // and internal subagents. `timeout` is in seconds; Hermes runs the command
1416
- // via shlex.split (shell=false).
1417
- const command =
1418
- scope === "project"
1419
- ? `npx -y failproofai --hook ${eventType} --cli hermes`
1420
- : `"${binaryPath}" --hook ${eventType} --cli hermes`;
1620
+ prepareInstall(settingsPath) {
1621
+ installHermesPlugin(settingsPath);
1622
+ },
1623
+
1624
+ buildHookEntry(_binaryPath, _eventType, _scope) {
1625
+ // Native plugins are package-level registrations. This sentinel preserves
1626
+ // the common Integration interface; writeHookEntries does the real work.
1421
1627
  return {
1422
- command,
1423
- timeout: 30,
1424
1628
  [FAILPROOFAI_HOOK_MARKER]: true,
1629
+ _hermesPluginPath: getHermesPluginSourcePath(),
1425
1630
  };
1426
1631
  },
1427
1632
 
1428
1633
  isFailproofaiHook: isMarkedHook,
1429
1634
 
1430
- writeHookEntries(settings, binaryPath, scope) {
1635
+ writeHookEntries(settings) {
1431
1636
  const doc = settings as unknown as Document;
1432
- // Read the current hooks map as plain JS, then re-set ONLY the `hooks` key —
1433
- // preserving comments on every other part of config.yaml.
1434
- const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
1435
- const hooks: Record<string, HermesHookEntry[]> =
1436
- js.hooks && typeof js.hooks === "object" ? js.hooks : {};
1437
-
1438
- for (const eventType of HERMES_HOOK_EVENT_TYPES) {
1439
- const entry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as HermesHookEntry;
1440
- const arr = Array.isArray(hooks[eventType]) ? hooks[eventType] : [];
1441
- const idx = arr.findIndex((h) => isMarkedHook(h));
1442
- if (idx >= 0) arr[idx] = entry;
1443
- else arr.push(entry);
1444
- hooks[eventType] = arr;
1637
+ // A native plugin and the legacy shell bridge running together would
1638
+ // evaluate every event twice. Remove only failproofai-owned shell entries;
1639
+ // unrelated operator hooks remain intact.
1640
+ removeLegacyHermesHooks(doc);
1641
+
1642
+ const js = (doc.toJS() ?? {}) as { plugins?: HermesPluginsConfig };
1643
+ const plugins: HermesPluginsConfig =
1644
+ js.plugins && typeof js.plugins === "object" ? js.plugins : {};
1645
+ const enabled = Array.isArray(plugins.enabled) ? [...plugins.enabled] : [];
1646
+ if (!enabled.includes(HERMES_PLUGIN_ID)) enabled.push(HERMES_PLUGIN_ID);
1647
+ plugins.enabled = enabled;
1648
+ if (Array.isArray(plugins.disabled)) {
1649
+ plugins.disabled = plugins.disabled.filter((name) => name !== HERMES_PLUGIN_ID);
1650
+ if (plugins.disabled.length === 0) delete plugins.disabled;
1445
1651
  }
1446
- doc.set("hooks", hooks);
1447
- // The headless gateway has no TTY to answer Hermes's first-use hook-consent
1448
- // prompt, so auto-accept declared hooks. Tradeoff: also auto-accepts any
1449
- // other hook the operator adds; a targeted `shell-hooks-allowlist.json`
1450
- // pre-seed is a future refinement.
1451
- doc.set("hooks_auto_accept", true);
1652
+ doc.set("plugins", plugins);
1452
1653
  },
1453
1654
 
1454
1655
  removeHooksFromFile(settingsPath) {
1455
- if (!existsSync(settingsPath)) return 0;
1456
1656
  const doc = readYamlDoc(settingsPath);
1457
- const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
1458
- const hooks = js.hooks;
1459
-
1460
- let removed = 0;
1461
- if (hooks && typeof hooks === "object") {
1462
- for (const eventType of Object.keys(hooks)) {
1463
- const entries = hooks[eventType];
1464
- if (!Array.isArray(entries)) continue;
1465
- const before = entries.length;
1466
- const filtered = entries.filter((h) => !isMarkedHook(h));
1467
- removed += before - filtered.length;
1468
- if (filtered.length === 0) delete hooks[eventType];
1469
- else hooks[eventType] = filtered;
1657
+ let removed = removeLegacyHermesHooks(doc);
1658
+ const js = (doc.toJS() ?? {}) as { plugins?: HermesPluginsConfig };
1659
+ const plugins = js.plugins;
1660
+ let configChanged = removed > 0;
1661
+ if (plugins && typeof plugins === "object") {
1662
+ if (Array.isArray(plugins.enabled)) {
1663
+ const before = plugins.enabled.length;
1664
+ plugins.enabled = plugins.enabled.filter((name) => name !== HERMES_PLUGIN_ID);
1665
+ if (plugins.enabled.length !== before) {
1666
+ removed += 1;
1667
+ configChanged = true;
1668
+ }
1669
+ if (plugins.enabled.length === 0) delete plugins.enabled;
1470
1670
  }
1471
- if (removed > 0) {
1472
- if (Object.keys(hooks).length === 0) doc.delete("hooks");
1473
- else doc.set("hooks", hooks);
1671
+ if (plugins.entries && HERMES_PLUGIN_ID in plugins.entries) {
1672
+ delete plugins.entries[HERMES_PLUGIN_ID];
1673
+ configChanged = true;
1474
1674
  }
1675
+ if (Object.keys(plugins).length === 0) doc.delete("plugins");
1676
+ else doc.set("plugins", plugins);
1475
1677
  }
1476
-
1477
- // Always drop our headless-consent flag on uninstall — even if the hooks were
1478
- // already removed manually — so it can't silently auto-accept future operator
1479
- // hooks. `doc.delete` returns true iff the key was present.
1480
- const droppedAutoAccept = doc.delete("hooks_auto_accept");
1481
- if (removed > 0 || droppedAutoAccept) writeYamlDoc(settingsPath, doc);
1678
+ if (configChanged) writeYamlDoc(settingsPath, doc);
1679
+ if (removeManagedHermesPlugin(settingsPath)) removed += 1;
1482
1680
  return removed;
1483
1681
  },
1484
1682
 
@@ -1497,15 +1695,9 @@ export const hermes: Integration = {
1497
1695
  };
1498
1696
 
1499
1697
  function hermesConfigHasHooks(settingsPath: string): boolean {
1500
- if (!existsSync(settingsPath)) return false;
1501
1698
  try {
1502
- const doc = readYamlDoc(settingsPath);
1503
- const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
1504
- const hooks = js.hooks;
1505
- if (!hooks || typeof hooks !== "object") return false;
1506
- for (const entries of Object.values(hooks)) {
1507
- if (Array.isArray(entries) && entries.some((h) => isMarkedHook(h))) return true;
1508
- }
1699
+ const state = hermesConfigState(settingsPath);
1700
+ return state.pluginEnabled && hasManagedHermesPlugin(settingsPath) && !state.legacyShellHookPresent;
1509
1701
  } catch {
1510
1702
  // Corrupt config — treat as not installed.
1511
1703
  }
@@ -1523,10 +1715,9 @@ function hermesConfigHasHooks(settingsPath: string): boolean {
1523
1715
  * warning.
1524
1716
  */
1525
1717
  export function unhookedHermesProfiles(): string[] {
1526
- return listHermesProfiles()
1527
- .filter((p) => existsSync(p.home))
1528
- .filter((p) => !hermesConfigHasHooks(resolve(p.home, "config.yaml")))
1529
- .map((p) => p.name);
1718
+ return hermesProfileHealth()
1719
+ .filter((profile) => existsSync(profile.home) && !profile.healthy)
1720
+ .map((profile) => profile.name);
1530
1721
  }
1531
1722
 
1532
1723
  // ── OpenClaw integration ────────────────────────────────────────────────────
@@ -672,6 +672,7 @@ async function installHooksImpl(
672
672
  const settingsPaths = settingsPathsFor(integration, scope, cwd);
673
673
  try {
674
674
  for (const settingsPath of settingsPaths) {
675
+ integration.prepareInstall?.(settingsPath);
675
676
  const settings = integration.readSettings(settingsPath);
676
677
  integration.writeHookEntries(settings, binaryPath, scope);
677
678
  integration.writeSettings(settingsPath, settings);
@@ -738,10 +739,17 @@ async function installHooksImpl(
738
739
 
739
740
  for (const { cli: cliId, path } of writtenSettingsPaths) {
740
741
  const integration = getIntegration(cliId);
741
- console.log(
742
- `Failproof AI hooks installed for ${integration.displayName} ` +
743
- `(${integration.eventTypes.length} event types, scope: ${scope}).`
744
- );
742
+ if (cliId === "hermes") {
743
+ console.log(
744
+ `Failproof AI native plugin installed for ${integration.displayName} ` +
745
+ `(8 registered hooks, scope: ${scope}).`
746
+ );
747
+ } else {
748
+ console.log(
749
+ `Failproof AI hooks installed for ${integration.displayName} ` +
750
+ `(${integration.eventTypes.length} event types, scope: ${scope}).`
751
+ );
752
+ }
745
753
  console.log(`Settings: ${path}`);
746
754
  }
747
755
  if (scope === "project") {
@@ -876,7 +884,11 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
876
884
  for (const s of scopesToRemove) {
877
885
  // Usually one path; Hermes returns one per profile.
878
886
  const settingsPaths = settingsPathsFor(integration, s, cwd);
879
- const existing = settingsPaths.filter((p) => existsSync(p));
887
+ // A Hermes install copies its managed plugin before updating config.yaml.
888
+ // If the config write is interrupted, uninstall must still call the
889
+ // integration so it can remove that orphaned managed directory.
890
+ const existing =
891
+ cliId === "hermes" ? settingsPaths : settingsPaths.filter((p) => existsSync(p));
880
892
 
881
893
  if (existing.length === 0) {
882
894
  if (scope !== "all" && selectedClis.length === 1) {
@@ -113,13 +113,13 @@ export const HERMES_TOOL_INPUT_MAP: Record<string, Record<string, string>> = {
113
113
  Edit: { path: "file_path" },
114
114
  };
115
115
 
116
- // Hermes live-hook (Pillar 1) events + scopes. Hermes fires these snake_case
117
- // events with a JSON payload on stdin; the command we install runs
118
- // `failproofai --hook <event> --cli hermes`. Config is USER-scope only
119
- // (`~/.hermes/config.yaml`; Hermes has no project scope). `pre_tool_call` is the
120
- // core deny point — it fires for tool calls from every source
121
- // (slack/telegram/cli/cron) and internal subagents, so a single install
122
- // intercepts all platforms.
116
+ // Hermes live-hook (Pillar 1) events + scopes. The native Python plugin sends
117
+ // these events to failproofaid's warm worker. It also handles `pre_llm_call`,
118
+ // `on_session_reset`, and `on_session_finalize` locally for instruction context
119
+ // and ledger cleanup, so those do not need canonical policy-event mappings.
120
+ // Config is USER-scope only (`~/.hermes/config.yaml`; Hermes has no project
121
+ // scope). `pre_tool_call` is the core gate and fires for tool calls from every
122
+ // source (Slack/Telegram/CLI/cron) and internal subagents.
123
123
  //
124
124
  // `pre_verify` IS a turn-end gate — the earlier claim here that Hermes has none
125
125
  // was wrong. We deliberately do NOT install it (product decision, 2026-07-29);
@@ -143,7 +143,7 @@ export const HERMES_TOOL_INPUT_MAP: Record<string, Record<string, string>> = {
143
143
  // 2. Capped at 3 nudges per turn (`DEFAULT_MAX_VERIFY_NUDGES`,
144
144
  // `agent/verify_hooks.py:21`), operator-overridable; resets each turn.
145
145
  // 3. It landed upstream ~2026-06-30. Older Hermes fails the key against
146
- // VALID_HOOKS and warn-and-skips it SILENTLY (`agent/shell_hooks.py:325`).
146
+ // VALID_HOOKS and warn-and-skips it silently.
147
147
  //
148
148
  // Until it is installed, `HERMES_EVENT_MAP` emits no `Stop` and those 5
149
149
  // builtins remain inapplicable on Hermes.
@@ -192,6 +192,7 @@ function handleConnection(socket: Socket, shutdown: () => void): void {
192
192
  exitCode: result.exitCode,
193
193
  stdout: result.stdout,
194
194
  stderr: result.stderr,
195
+ evaluation: result.evaluation,
195
196
  }),
196
197
  );
197
198
  } catch (err) {
@@ -1,3 +0,0 @@
1
- module.exports=[78500,(e,o,r)=>{o.exports=e.x("node:async_hooks",()=>require("node:async_hooks"))},2157,(e,o,r)=>{o.exports=e.x("node:fs",()=>require("node:fs"))},12714,(e,o,r)=>{o.exports=e.x("node:fs/promises",()=>require("node:fs/promises"))},50227,(e,o,r)=>{o.exports=e.x("node:path",()=>require("node:path"))},1457,(e,o,r)=>{o.exports=e.x("node:readline",()=>require("node:readline"))},12057,(e,o,r)=>{o.exports=e.x("node:util",()=>require("node:util"))},27028,(e,o,r)=>{o.exports=e.x("node:zlib",()=>require("node:zlib"))},14747,(e,o,r)=>{o.exports=e.x("path",()=>require("path"))}];
2
-
3
- //# sourceMappingURL=%5Bexternals%5D__157pf8j._.js.map