failproofai 1.0.5 → 1.0.6-beta.1

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 (204) 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 +4 -4
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  34. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/index.html +1 -1
  37. package/.next/standalone/.next/server/app/index.rsc +14 -14
  38. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  39. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  40. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  41. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  42. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  46. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  47. package/.next/standalone/.next/server/app/policies/page.js +6 -5
  48. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  58. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  62. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  63. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  66. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  67. package/.next/standalone/.next/server/app/settings/page.js +3 -2
  68. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/chunks/[externals]__0sds_16._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zlqo2t._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__112b4go._.js → [root-of-the-server]__1_r2rbg._.js} +4 -4
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1anxb5k._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +3 -3
  76. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
  79. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
  80. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +4 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0576iuy._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0bym1os._.js → [root-of-the-server]__0cg-bgc._.js} +2 -2
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  90. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00y9ww2._.js → [root-of-the-server]__0u3w0ll._.js} +2 -2
  91. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0sz_5na._.js → [root-of-the-server]__1ouobn4._.js} +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1py795m._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__15k-dqb._.js → [root-of-the-server]__1x3j9mj._.js} +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +5 -0
  98. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/{_0guaevg._.js → _0qlygw6._.js} +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/_0wso5d3._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/_1-3unkg._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/{_1b78kcp._.js → _1_ddpm3._.js} +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/{_14c6q09._.js → _1_qswah._.js} +2 -2
  104. package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +3 -3
  106. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +12 -11
  109. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  113. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/node_modules_1hl81y4._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
  116. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  117. package/.next/standalone/.next/server/pages/404.html +1 -1
  118. package/.next/standalone/.next/server/pages/500.html +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  121. package/.next/standalone/.next/static/chunks/{3w46euc60dx39.js → 02vwr5js1x28j.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/06fjx8-0_qufs.js +1 -0
  123. package/.next/standalone/.next/static/chunks/{2zafgs90r_leq.js → 06jxjnw8bfczc.js} +1 -1
  124. package/.next/standalone/.next/static/chunks/0cbvytf8epn34.js +1 -0
  125. package/.next/standalone/.next/static/chunks/0y227_ohdqwvn.js +1 -0
  126. package/.next/standalone/.next/static/chunks/{252xreaecy80h.js → 1p4xauf-zu6zk.js} +1 -1
  127. package/.next/standalone/.next/static/chunks/{021ydrq6rhpsi.js → 1qf2uiipatr80.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/1uc_s_dftgxiq.js +6 -0
  129. package/.next/standalone/.next/static/chunks/1xacnwjkbekn7.js +1 -0
  130. package/.next/standalone/.next/static/chunks/{11s_v03ub6nss.js → 27_bwbg8sv6vj.js} +1 -1
  131. package/.next/standalone/.next/static/chunks/2hg_bp8z_vm2g.js +1 -0
  132. package/.next/standalone/.next/static/chunks/{3bleyy40s99f2.js → 2x776s0x3n_tu.js} +1 -1
  133. package/.next/standalone/.next/static/chunks/{1vb33xa6j9y2q.js → 3ht0xsco46pul.js} +1 -1
  134. package/.next/standalone/.next/static/chunks/{turbopack-3p73obe0xmrxm.js → turbopack-38nzuvp95_2rv.js} +1 -1
  135. package/.next/standalone/fp-cloud-cli/uv.lock +3 -3
  136. package/.next/standalone/hermes-plugin/README.md +138 -0
  137. package/.next/standalone/hermes-plugin/__init__.py +248 -0
  138. package/.next/standalone/hermes-plugin/client.py +150 -0
  139. package/.next/standalone/hermes-plugin/ledger.py +187 -0
  140. package/.next/standalone/hermes-plugin/plugin.yaml +37 -0
  141. package/.next/standalone/lib/hermes-profiles.ts +67 -19
  142. package/.next/standalone/lib/hermes-sessions.ts +3 -2
  143. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  144. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  145. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
  146. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
  147. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  148. package/.next/standalone/node_modules/next/dist/server/app-render/create-component-styles-and-scripts.js +2 -1
  149. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  150. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  151. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  152. package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +14 -0
  153. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  154. package/.next/standalone/node_modules/next/dist/server/lib/disk-lru-cache.external.js +7 -1
  155. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  156. package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-wrapper.js +6 -1
  157. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  158. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  159. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  160. package/.next/standalone/node_modules/next/package.json +10 -10
  161. package/.next/standalone/node_modules/react/cjs/react.development.js +92 -46
  162. package/.next/standalone/node_modules/react/cjs/react.production.js +112 -89
  163. package/.next/standalone/node_modules/react/package.json +1 -1
  164. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +1165 -790
  165. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +1171 -791
  166. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +1294 -862
  167. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +1305 -868
  168. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +1315 -896
  169. package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +24 -5
  170. package/.next/standalone/node_modules/react-dom/package.json +4 -4
  171. package/.next/standalone/package.json +13 -12
  172. package/.next/standalone/sdk/python/uv.lock +18 -18
  173. package/.next/standalone/server.js +1 -1
  174. package/README.md +29 -13
  175. package/bin/failproofai.mjs +3 -1
  176. package/dist/cli.mjs +655 -360
  177. package/dist/worker.mjs +78 -34
  178. package/hermes-plugin/README.md +138 -0
  179. package/hermes-plugin/__init__.py +248 -0
  180. package/hermes-plugin/client.py +150 -0
  181. package/hermes-plugin/ledger.py +187 -0
  182. package/hermes-plugin/plugin.yaml +37 -0
  183. package/lib/hermes-profiles.ts +67 -19
  184. package/lib/hermes-sessions.ts +3 -2
  185. package/package.json +13 -12
  186. package/scripts/translate-docs/validate-translation.ts +48 -0
  187. package/src/hooks/handler.ts +29 -1
  188. package/src/hooks/integrations.ts +271 -73
  189. package/src/hooks/manager.ts +41 -7
  190. package/src/hooks/types.ts +8 -8
  191. package/src/hooks/worker-server.ts +1 -0
  192. package/.next/standalone/.next/server/chunks/[externals]__157pf8j._.js +0 -3
  193. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +0 -3
  194. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +0 -6
  195. package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +0 -3
  196. package/.next/standalone/.next/static/chunks/0y8_1s45gbqmn.js +0 -6
  197. package/.next/standalone/.next/static/chunks/14b3ezufe5411.js +0 -1
  198. package/.next/standalone/.next/static/chunks/1oskk7pdo8aon.js +0 -1
  199. package/.next/standalone/.next/static/chunks/2xycd6mjg00fb.js +0 -1
  200. package/.next/standalone/.next/static/chunks/3l36d73kj18zw.js +0 -1
  201. package/.next/standalone/.next/static/chunks/40lpt2o2b283e.js +0 -1
  202. /package/.next/standalone/.next/static/{uFpAs_vIXSo6qBzmkXPWD → QCNYz2yCxxdeggg1k0iEq}/_buildManifest.js +0 -0
  203. /package/.next/standalone/.next/static/{uFpAs_vIXSo6qBzmkXPWD → QCNYz2yCxxdeggg1k0iEq}/_clientMiddlewareManifest.js +0 -0
  204. /package/.next/standalone/.next/static/{uFpAs_vIXSo6qBzmkXPWD → QCNYz2yCxxdeggg1k0iEq}/_ssgManifest.js +0 -0
@@ -102,6 +102,21 @@ export interface HookEventOutcome {
102
102
  exitCode: number;
103
103
  stdout: string;
104
104
  stderr: string;
105
+ /**
106
+ * Structured policy metadata for in-process adapters. Existing shell-hook
107
+ * callers intentionally ignore this and keep consuming stdout/stderr.
108
+ */
109
+ evaluation?: HookEvaluationSummary;
110
+ }
111
+
112
+ export interface HookEvaluationSummary {
113
+ decision: "allow" | "deny" | "instruct";
114
+ policyName: string | null;
115
+ policyNames: string[];
116
+ reason: string | null;
117
+ matchedPolicies: string[];
118
+ durationMs: number;
119
+ toolName: string | null;
105
120
  }
106
121
 
107
122
  export interface EvaluateHookEventOptions {
@@ -757,7 +772,20 @@ export async function evaluateHookEvent(
757
772
  }
758
773
  }
759
774
 
760
- return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
775
+ return {
776
+ exitCode: result.exitCode,
777
+ stdout: result.stdout,
778
+ stderr: result.stderr,
779
+ evaluation: {
780
+ decision: result.decision,
781
+ policyName: result.policyName,
782
+ policyNames: result.policyNames ?? (result.policyName ? [result.policyName] : []),
783
+ reason: result.reason,
784
+ matchedPolicies,
785
+ durationMs,
786
+ toolName: (parsed.tool_name as string) ?? null,
787
+ },
788
+ };
761
789
  } finally {
762
790
  if (opts?.awaitTelemetryFlush ?? true) {
763
791
  // Await any un-awaited (`void trackHookEvent(...)`) events fired during
@@ -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";
@@ -112,6 +122,12 @@ function binaryExists(name: string): boolean {
112
122
  export interface Integration {
113
123
  id: IntegrationType;
114
124
  displayName: string;
125
+ /**
126
+ * This integration has no in-process/CLI fallback and cannot evaluate policy
127
+ * unless failproofaid answers an end-to-end probe. Installation must stop
128
+ * before writing any hook or plugin when the daemon is unavailable.
129
+ */
130
+ requiresHealthyDaemon?: boolean;
115
131
  /** Settings scopes this integration supports (e.g. claude: user/project/local; codex: user/project). */
116
132
  scopes: readonly HookScope[];
117
133
  /** Hook events this integration fires (Claude: PascalCase, Codex: snake_case stored as Pascal in settings). */
@@ -135,6 +151,9 @@ export interface Integration {
135
151
  /** Read the raw settings/hooks file (returns {} when missing). */
136
152
  readSettings(settingsPath: string): Record<string, unknown>;
137
153
 
154
+ /** Install integration-owned files before enabling them in settings. */
155
+ prepareInstall?(settingsPath: string): void;
156
+
138
157
  /** Write the settings/hooks file. */
139
158
  writeSettings(settingsPath: string, settings: Record<string, unknown>): void;
140
159
 
@@ -1363,14 +1382,14 @@ function makePiProjectRelativeEntry(extPath: string): string {
1363
1382
  // works for the local user.
1364
1383
  return extResolved;
1365
1384
  }
1366
- // ── Hermes (hermes-agent) — live hooks (Pillar 1) ────────────────────────────
1385
+ // ── Hermes (hermes-agent) — native plugin (Pillar 1) ────────────────────────
1367
1386
  //
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.
1387
+ // Hermes loads trusted Python plugins from `<HERMES_HOME>/plugins/<name>/`.
1388
+ // The shipped plugin registers Hermes-native hooks and talks directly to the
1389
+ // local failproofaid socket, avoiding a fresh CLI process per event. Config is
1390
+ // YAML and profile-scoped; install copies the plugin into every profile and
1391
+ // adds `failproofai` to `plugins.enabled`. Legacy shell-hook entries are removed
1392
+ // during migration so one tool call is never evaluated twice.
1374
1393
 
1375
1394
  /** One hook entry as stored under a `hooks:` event key in config.yaml. */
1376
1395
  interface HermesHookEntry {
@@ -1379,9 +1398,204 @@ interface HermesHookEntry {
1379
1398
  [key: string]: unknown;
1380
1399
  }
1381
1400
 
1401
+ interface HermesPluginsConfig {
1402
+ enabled?: unknown[];
1403
+ disabled?: unknown[];
1404
+ entries?: Record<string, unknown>;
1405
+ [key: string]: unknown;
1406
+ }
1407
+
1408
+ const HERMES_PLUGIN_ID = "failproofai";
1409
+ const HERMES_PLUGIN_MARKER = ".failproofai-managed";
1410
+ const HERMES_PLUGIN_FILES = ["plugin.yaml", "__init__.py", "client.py", "ledger.py"] as const;
1411
+
1412
+ function getHermesPluginSourcePath(): string {
1413
+ const fromEnv = process.env.FAILPROOFAI_PACKAGE_ROOT;
1414
+ if (fromEnv) return resolve(fromEnv, "hermes-plugin");
1415
+ return resolve(fileURLToPath(import.meta.url), "..", "..", "..", "hermes-plugin");
1416
+ }
1417
+
1418
+ export function hermesPluginPathForSettings(settingsPath: string): string {
1419
+ return resolve(dirname(settingsPath), "plugins", HERMES_PLUGIN_ID);
1420
+ }
1421
+
1422
+ function hasManagedHermesPlugin(settingsPath: string): boolean {
1423
+ const pluginPath = hermesPluginPathForSettings(settingsPath);
1424
+ try {
1425
+ if (lstatSync(pluginPath).isSymbolicLink()) return false;
1426
+ if (!existsSync(resolve(pluginPath, HERMES_PLUGIN_MARKER))) return false;
1427
+ return HERMES_PLUGIN_FILES.every((file) => existsSync(resolve(pluginPath, file)));
1428
+ } catch {
1429
+ return false;
1430
+ }
1431
+ }
1432
+
1433
+ function hasHermesPluginMarker(settingsPath: string): boolean {
1434
+ const pluginPath = hermesPluginPathForSettings(settingsPath);
1435
+ try {
1436
+ return !lstatSync(pluginPath).isSymbolicLink() && existsSync(resolve(pluginPath, HERMES_PLUGIN_MARKER));
1437
+ } catch {
1438
+ return false;
1439
+ }
1440
+ }
1441
+
1442
+ function installHermesPlugin(settingsPath: string): void {
1443
+ const source = getHermesPluginSourcePath();
1444
+ for (const file of HERMES_PLUGIN_FILES) {
1445
+ if (!existsSync(resolve(source, file))) {
1446
+ throw new Error(`Hermes plugin asset is missing from the failproofai package: ${file}`);
1447
+ }
1448
+ }
1449
+
1450
+ const destination = hermesPluginPathForSettings(settingsPath);
1451
+ if (existsSync(destination)) {
1452
+ if (lstatSync(destination).isSymbolicLink() || !existsSync(resolve(destination, HERMES_PLUGIN_MARKER))) {
1453
+ throw new Error(
1454
+ `Refusing to overwrite an unmanaged Hermes plugin at ${destination}. ` +
1455
+ `Move it aside or install FailproofAI under a different Hermes profile.`,
1456
+ );
1457
+ }
1458
+ }
1459
+
1460
+ mkdirSync(dirname(destination), { recursive: true });
1461
+ const suffix = `${process.pid}-${Date.now()}`;
1462
+ const temporary = `${destination}.install-${suffix}`;
1463
+ const backup = `${destination}.backup-${suffix}`;
1464
+ let movedExisting = false;
1465
+ try {
1466
+ mkdirSync(temporary, { recursive: true });
1467
+ for (const file of HERMES_PLUGIN_FILES) {
1468
+ cpSync(resolve(source, file), resolve(temporary, file));
1469
+ }
1470
+ writeFileSync(
1471
+ resolve(temporary, HERMES_PLUGIN_MARKER),
1472
+ "Managed by failproofai. Remove with `failproofai policies --uninstall --cli hermes`.\n",
1473
+ { encoding: "utf8", mode: 0o600 },
1474
+ );
1475
+ if (existsSync(destination)) {
1476
+ renameSync(destination, backup);
1477
+ movedExisting = true;
1478
+ }
1479
+ renameSync(temporary, destination);
1480
+ if (movedExisting) rmSync(backup, { recursive: true, force: true });
1481
+ } catch (err) {
1482
+ rmSync(temporary, { recursive: true, force: true });
1483
+ if (movedExisting && !existsSync(destination) && existsSync(backup)) {
1484
+ renameSync(backup, destination);
1485
+ }
1486
+ throw err;
1487
+ }
1488
+ }
1489
+
1490
+ function removeManagedHermesPlugin(settingsPath: string): boolean {
1491
+ const pluginPath = hermesPluginPathForSettings(settingsPath);
1492
+ // The marker is the ownership boundary. Remove even an incomplete managed
1493
+ // install so an interrupted copy can always be repaired or uninstalled.
1494
+ if (!hasHermesPluginMarker(settingsPath)) return false;
1495
+ rmSync(pluginPath, { recursive: true, force: true });
1496
+ return true;
1497
+ }
1498
+
1499
+ function removeLegacyHermesHooks(doc: Document): number {
1500
+ const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
1501
+ const hooks = js.hooks;
1502
+ let removed = 0;
1503
+ if (hooks && typeof hooks === "object") {
1504
+ for (const eventType of Object.keys(hooks)) {
1505
+ const entries = hooks[eventType];
1506
+ if (!Array.isArray(entries)) continue;
1507
+ const before = entries.length;
1508
+ const filtered = entries.filter((hook) => !isMarkedHook(hook));
1509
+ removed += before - filtered.length;
1510
+ if (filtered.length === 0) delete hooks[eventType];
1511
+ else hooks[eventType] = filtered;
1512
+ }
1513
+ if (Object.keys(hooks).length === 0) {
1514
+ doc.delete("hooks");
1515
+ // Older FailproofAI installs set this broad Hermes switch so headless
1516
+ // shell hooks could run. It is safe to remove only when no operator hook
1517
+ // remains that may rely on it.
1518
+ if (removed > 0) doc.delete("hooks_auto_accept");
1519
+ } else {
1520
+ doc.set("hooks", hooks);
1521
+ }
1522
+ }
1523
+ return removed;
1524
+ }
1525
+
1526
+ function hermesConfigState(settingsPath: string): {
1527
+ pluginEnabled: boolean;
1528
+ legacyShellHookPresent: boolean;
1529
+ } {
1530
+ if (!existsSync(settingsPath)) {
1531
+ return { pluginEnabled: false, legacyShellHookPresent: false };
1532
+ }
1533
+ const doc = readYamlDoc(settingsPath);
1534
+ const js = (doc.toJS() ?? {}) as {
1535
+ hooks?: Record<string, HermesHookEntry[]>;
1536
+ plugins?: HermesPluginsConfig;
1537
+ };
1538
+ const enabled = js.plugins?.enabled;
1539
+ const legacyShellHookPresent =
1540
+ !!js.hooks &&
1541
+ typeof js.hooks === "object" &&
1542
+ Object.values(js.hooks).some(
1543
+ (entries) => Array.isArray(entries) && entries.some((entry) => isMarkedHook(entry)),
1544
+ );
1545
+ return {
1546
+ pluginEnabled: Array.isArray(enabled) && enabled.includes(HERMES_PLUGIN_ID),
1547
+ legacyShellHookPresent,
1548
+ };
1549
+ }
1550
+
1551
+ export interface HermesProfileHealth {
1552
+ name: string;
1553
+ home: string;
1554
+ settingsPath: string;
1555
+ pluginInstalled: boolean;
1556
+ pluginEnabled: boolean;
1557
+ legacyShellHookPresent: boolean;
1558
+ healthy: boolean;
1559
+ }
1560
+
1561
+ /** Read-only, per-profile Hermes installation health for setup/status surfaces. */
1562
+ export function hermesProfileHealth(): HermesProfileHealth[] {
1563
+ return listHermesProfiles().map((profile) => {
1564
+ const settingsPath = resolve(profile.home, "config.yaml");
1565
+ const config = hermesConfigState(settingsPath);
1566
+ const pluginInstalled = hasManagedHermesPlugin(settingsPath);
1567
+ return {
1568
+ name: profile.name,
1569
+ home: profile.home,
1570
+ settingsPath,
1571
+ pluginInstalled,
1572
+ pluginEnabled: config.pluginEnabled,
1573
+ legacyShellHookPresent: config.legacyShellHookPresent,
1574
+ healthy: pluginInstalled && config.pluginEnabled && !config.legacyShellHookPresent,
1575
+ };
1576
+ });
1577
+ }
1578
+
1579
+ /** Compact rows for the machine-status display; absent Hermes homes produce no noise. */
1580
+ export function hermesProfileStatusRows(): Array<[string, string]> {
1581
+ return hermesProfileHealth()
1582
+ .filter((profile) => existsSync(profile.home))
1583
+ .map((profile) => {
1584
+ const problems: string[] = [];
1585
+ if (!profile.pluginInstalled) problems.push("plugin files missing or incomplete");
1586
+ if (!profile.pluginEnabled) problems.push("plugin not enabled");
1587
+ if (profile.legacyShellHookPresent) problems.push("legacy shell hook also present");
1588
+ return [
1589
+ "hermes/" + profile.name,
1590
+ problems.length === 0 ? "native plugin enabled" : "UNHEALTHY — " + problems.join("; "),
1591
+ ];
1592
+ });
1593
+ }
1594
+
1382
1595
  export const hermes: Integration = {
1383
1596
  id: "hermes",
1384
1597
  displayName: "Hermes",
1598
+ requiresHealthyDaemon: true,
1385
1599
  scopes: HERMES_HOOK_SCOPES,
1386
1600
  eventTypes: HERMES_HOOK_EVENT_TYPES,
1387
1601
 
@@ -1410,75 +1624,66 @@ export const hermes: Integration = {
1410
1624
  writeYamlDoc(settingsPath, settings as unknown as Document);
1411
1625
  },
1412
1626
 
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`;
1627
+ prepareInstall(settingsPath) {
1628
+ installHermesPlugin(settingsPath);
1629
+ },
1630
+
1631
+ buildHookEntry(_binaryPath, _eventType, _scope) {
1632
+ // Native plugins are package-level registrations. This sentinel preserves
1633
+ // the common Integration interface; writeHookEntries does the real work.
1421
1634
  return {
1422
- command,
1423
- timeout: 30,
1424
1635
  [FAILPROOFAI_HOOK_MARKER]: true,
1636
+ _hermesPluginPath: getHermesPluginSourcePath(),
1425
1637
  };
1426
1638
  },
1427
1639
 
1428
1640
  isFailproofaiHook: isMarkedHook,
1429
1641
 
1430
- writeHookEntries(settings, binaryPath, scope) {
1642
+ writeHookEntries(settings) {
1431
1643
  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;
1644
+ // A native plugin and the legacy shell bridge running together would
1645
+ // evaluate every event twice. Remove only failproofai-owned shell entries;
1646
+ // unrelated operator hooks remain intact.
1647
+ removeLegacyHermesHooks(doc);
1648
+
1649
+ const js = (doc.toJS() ?? {}) as { plugins?: HermesPluginsConfig };
1650
+ const plugins: HermesPluginsConfig =
1651
+ js.plugins && typeof js.plugins === "object" ? js.plugins : {};
1652
+ const enabled = Array.isArray(plugins.enabled) ? [...plugins.enabled] : [];
1653
+ if (!enabled.includes(HERMES_PLUGIN_ID)) enabled.push(HERMES_PLUGIN_ID);
1654
+ plugins.enabled = enabled;
1655
+ if (Array.isArray(plugins.disabled)) {
1656
+ plugins.disabled = plugins.disabled.filter((name) => name !== HERMES_PLUGIN_ID);
1657
+ if (plugins.disabled.length === 0) delete plugins.disabled;
1445
1658
  }
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);
1659
+ doc.set("plugins", plugins);
1452
1660
  },
1453
1661
 
1454
1662
  removeHooksFromFile(settingsPath) {
1455
- if (!existsSync(settingsPath)) return 0;
1456
1663
  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;
1664
+ let removed = removeLegacyHermesHooks(doc);
1665
+ const js = (doc.toJS() ?? {}) as { plugins?: HermesPluginsConfig };
1666
+ const plugins = js.plugins;
1667
+ let configChanged = removed > 0;
1668
+ if (plugins && typeof plugins === "object") {
1669
+ if (Array.isArray(plugins.enabled)) {
1670
+ const before = plugins.enabled.length;
1671
+ plugins.enabled = plugins.enabled.filter((name) => name !== HERMES_PLUGIN_ID);
1672
+ if (plugins.enabled.length !== before) {
1673
+ removed += 1;
1674
+ configChanged = true;
1675
+ }
1676
+ if (plugins.enabled.length === 0) delete plugins.enabled;
1470
1677
  }
1471
- if (removed > 0) {
1472
- if (Object.keys(hooks).length === 0) doc.delete("hooks");
1473
- else doc.set("hooks", hooks);
1678
+ if (plugins.entries && HERMES_PLUGIN_ID in plugins.entries) {
1679
+ delete plugins.entries[HERMES_PLUGIN_ID];
1680
+ configChanged = true;
1474
1681
  }
1682
+ if (Object.keys(plugins).length === 0) doc.delete("plugins");
1683
+ else doc.set("plugins", plugins);
1475
1684
  }
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);
1685
+ if (configChanged) writeYamlDoc(settingsPath, doc);
1686
+ if (removeManagedHermesPlugin(settingsPath)) removed += 1;
1482
1687
  return removed;
1483
1688
  },
1484
1689
 
@@ -1497,15 +1702,9 @@ export const hermes: Integration = {
1497
1702
  };
1498
1703
 
1499
1704
  function hermesConfigHasHooks(settingsPath: string): boolean {
1500
- if (!existsSync(settingsPath)) return false;
1501
1705
  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
- }
1706
+ const state = hermesConfigState(settingsPath);
1707
+ return state.pluginEnabled && hasManagedHermesPlugin(settingsPath) && !state.legacyShellHookPresent;
1509
1708
  } catch {
1510
1709
  // Corrupt config — treat as not installed.
1511
1710
  }
@@ -1523,10 +1722,9 @@ function hermesConfigHasHooks(settingsPath: string): boolean {
1523
1722
  * warning.
1524
1723
  */
1525
1724
  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);
1725
+ return hermesProfileHealth()
1726
+ .filter((profile) => existsSync(profile.home) && !profile.healthy)
1727
+ .map((profile) => profile.name);
1530
1728
  }
1531
1729
 
1532
1730
  // ── OpenClaw integration ────────────────────────────────────────────────────
@@ -29,6 +29,7 @@ import { CORE_SOURCE, addPack, setPackPolicyEnabled } from "./pack-store";
29
29
  import type { ResolvedPack } from "./pack-manifest";
30
30
  import { hasInstalledPacks, readInstalledPacks } from "./pack-manifest";
31
31
  import { packPolicyParamKey } from "./policy-evaluator";
32
+ import { probeDaemonEndToEnd } from "./daemon-service";
32
33
  import {
33
34
  chip,
34
35
  note,
@@ -446,9 +447,13 @@ async function installHooksImpl(
446
447
  // the user for multi-CLI selection before reaching here when --cli is omitted.
447
448
  const selectedClis: IntegrationType[] = cli && cli.length > 0 ? [...new Set(cli)] : ["claude"];
448
449
 
450
+ const selectedIntegrations = selectedClis.map((cliId) => ({
451
+ cliId,
452
+ integration: getIntegration(cliId),
453
+ }));
454
+
449
455
  // Per-CLI scope validation: Codex doesn't have a "local" scope.
450
- for (const cliId of selectedClis) {
451
- const integration = getIntegration(cliId);
456
+ for (const { cliId, integration } of selectedIntegrations) {
452
457
  if (!integration.scopes.includes(scope)) {
453
458
  try {
454
459
  await trackHookEvent(getInstanceId(), "scope_validation_failed", {
@@ -464,6 +469,23 @@ async function installHooksImpl(
464
469
  }
465
470
  }
466
471
 
472
+ // Daemon-only native integrations fail closed when their evaluator cannot be
473
+ // reached. Never enable one until a real policy request succeeds; otherwise
474
+ // a direct `policies --install` can lock every tool call. Shell-hook and CLI-
475
+ // backed integrations retain their local evaluator fallback and therefore do
476
+ // not opt into this requirement.
477
+ const daemonRequiredBy = selectedIntegrations
478
+ .map(({ integration }) => integration)
479
+ .filter((integration) => integration.requiresHealthyDaemon);
480
+ if (daemonRequiredBy.length > 0 && !(await probeDaemonEndToEnd())) {
481
+ const names = daemonRequiredBy.map((integration) => integration.displayName).join(", ");
482
+ const verb = daemonRequiredBy.length === 1 ? "requires" : "require";
483
+ throw new CliError(
484
+ `${names} ${verb} a healthy failproofaid daemon before FailproofAI enforcement can be enabled.\n` +
485
+ "Run `failproofai config` to install and configure the daemon, then retry.",
486
+ );
487
+ }
488
+
467
489
  const binaryPath = resolveFailproofaiBinary();
468
490
 
469
491
  // Capture existing config before overwriting (used for telemetry diff)
@@ -672,6 +694,7 @@ async function installHooksImpl(
672
694
  const settingsPaths = settingsPathsFor(integration, scope, cwd);
673
695
  try {
674
696
  for (const settingsPath of settingsPaths) {
697
+ integration.prepareInstall?.(settingsPath);
675
698
  const settings = integration.readSettings(settingsPath);
676
699
  integration.writeHookEntries(settings, binaryPath, scope);
677
700
  integration.writeSettings(settingsPath, settings);
@@ -738,10 +761,17 @@ async function installHooksImpl(
738
761
 
739
762
  for (const { cli: cliId, path } of writtenSettingsPaths) {
740
763
  const integration = getIntegration(cliId);
741
- console.log(
742
- `Failproof AI hooks installed for ${integration.displayName} ` +
743
- `(${integration.eventTypes.length} event types, scope: ${scope}).`
744
- );
764
+ if (cliId === "hermes") {
765
+ console.log(
766
+ `Failproof AI native plugin installed for ${integration.displayName} ` +
767
+ `(8 registered hooks, scope: ${scope}).`
768
+ );
769
+ } else {
770
+ console.log(
771
+ `Failproof AI hooks installed for ${integration.displayName} ` +
772
+ `(${integration.eventTypes.length} event types, scope: ${scope}).`
773
+ );
774
+ }
745
775
  console.log(`Settings: ${path}`);
746
776
  }
747
777
  if (scope === "project") {
@@ -876,7 +906,11 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
876
906
  for (const s of scopesToRemove) {
877
907
  // Usually one path; Hermes returns one per profile.
878
908
  const settingsPaths = settingsPathsFor(integration, s, cwd);
879
- const existing = settingsPaths.filter((p) => existsSync(p));
909
+ // A Hermes install copies its managed plugin before updating config.yaml.
910
+ // If the config write is interrupted, uninstall must still call the
911
+ // integration so it can remove that orphaned managed directory.
912
+ const existing =
913
+ cliId === "hermes" ? settingsPaths : settingsPaths.filter((p) => existsSync(p));
880
914
 
881
915
  if (existing.length === 0) {
882
916
  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
@@ -1,3 +0,0 @@
1
- module.exports=[44376,(a,b,c)=>{b.exports=a.x("node:module",()=>require("node:module"))},36119,81243,a=>{"use strict";var b=a.i(37936),c=a.i(21572),d=a.i(2157),e=a.i(75818),f=a.i(60526),g=a.i(50227),h=a.i(74533);function i(){return(0,f.userInfo)().username}function j(a=i()){return`failproofaid@${a}.service`}function k(a=i()){return(0,g.resolve)(process.env.FAILPROOFAI_SYSTEMD_DIR||"/etc/systemd/system",j(a))}function l(){try{var a;return a=(0,h.execFileSync)("systemctl",["show",j(),"--property=ConditionResult","--value"],{stdio:["ignore","pipe","ignore"],timeout:1e4}).toString(),"no"===a.trim()?"condition-failed":"stopped"}catch{return"stopped"}}a.i(10695),a.i(66680),a.i(44376),a.i(27028);var m=a.i(15501),n=a.i(8674),o=a.i(93328);function p(){return(process.env.FAILPROOF_API_URL??process.env.FAILPROOFAI_API_URL??"https://api.befailproof.ai").replace(/\/+$/,"")}class q extends Error{status;code;retryAfterSecs;constructor(a,b,c,d){super(c),this.status=a,this.code=b,this.retryAfterSecs=d,this.name="AuthApiError"}}async function r(a){let b={};try{b=await a.json()}catch{}let c=b.code??`http_${a.status}`,d=b.message??b.detail??a.statusText??"request failed",e=b.retry_after_secs;if(void 0===e){let b=a.headers.get("retry-after");if(b){let a=Number(b);Number.isFinite(a)&&(e=a)}}return void 0!==e&&(e=Math.max(0,Math.min(86400,e))),new q(a.status,c,d,e)}async function s(a,b){try{return await fetch(a,{...b,signal:function(a){let b=AbortSignal.timeout(1e4);if(!a)return b;let c=AbortSignal.any;if(c)return c([b,a]);let d=new AbortController;if(b.aborted)d.abort(b.reason);else b.addEventListener("abort",()=>d.abort(b.reason),{once:!0});if(a.aborted)d.abort(a.reason);else a.addEventListener("abort",()=>d.abort(a.reason),{once:!0});return d.signal}(b.signal??void 0)})}catch(d){let c=function(a){if(null===a||"object"!=typeof a)return!1;let b=a.name;return"AbortError"===b||"TimeoutError"===b}(d);if((0,o.trackEvent)("api_server_unreachable",{kind:c?"timeout":"network",path:function(a){try{return new URL(a).pathname}catch{return a}}(a),method:"string"==typeof b.method?b.method:"GET"}),c)throw new q(0,"timeout",`request to ${a} timed out after 10000ms`);throw d}}async function t(a,b,c){let d={"content-type":"application/json"};c?.accessToken&&(d.authorization=`Bearer ${c.accessToken}`);let e=await s(`${p()}${a}`,{method:"POST",headers:d,body:JSON.stringify(b)});if(204!==e.status){if(!e.ok)throw await r(e);return await e.json()}}async function u(a,b){let c=await s(`${p()}${a}`,{method:"GET",headers:{authorization:`Bearer ${b}`}});if(!c.ok)throw await r(c);return await c.json()}async function v(a){return t("/v0/auth/token/refresh",{refresh_token:a})}async function w(a){return u("/v0/auth/me",a)}function x(){let a=process.env.FAILPROOFAI_AUTH_DIR;return a?(0,g.join)(a,"session.json"):(0,e.auditSessionFile)()}function y(){let a=x();if(!(0,d.existsSync)(a))return null;try{let b=(0,d.readFileSync)(a,"utf-8"),c=JSON.parse(b);if("string"!=typeof c.access_token||"string"!=typeof c.refresh_token||"number"!=typeof c.access_expires_at||"object"!=typeof c.user||!c.user||"string"!=typeof c.user.id||"string"!=typeof c.user.email)return null;return{access_token:c.access_token,refresh_token:c.refresh_token,access_expires_at:c.access_expires_at,refresh_expires_at:"number"==typeof c.refresh_expires_at?c.refresh_expires_at:c.access_expires_at,user:{id:c.user.id,email:c.user.email}}}catch{return null}}function z(){let a=x();(0,d.existsSync)(a)&&(0,d.rmSync)(a,{force:!0});try{let a=(0,e.migrationsDir)();if(!(0,d.existsSync)(a))return;for(let b of(0,d.readdirSync)(a)){if(!b.startsWith("backup-layout"))continue;let c=(0,g.resolve)(a,b,"auth.json");(0,d.existsSync)(c)&&(0,d.rmSync)(c,{force:!0})}}catch{}}let A=new Map;async function B(a){let b=A.get(a.refresh_token);if(b)return b;let c=(async()=>{let b=function(a,b){let c=Math.floor(Date.now()/1e3),d=a.user??b;if(!d)throw Error("authFromTokenResponse: missing user identity");return{access_token:a.access_token,refresh_token:a.refresh_token,access_expires_at:c+a.access_expires_in,refresh_expires_at:c+a.refresh_expires_in,user:d}}(await v(a.refresh_token),a.user);return(0,n.writeJsonAtomically)(x(),b),b})();A.set(a.refresh_token,c);try{return await c}finally{A.delete(a.refresh_token)}}async function C(){let a=y();if(!a)return null;let b=Math.floor(Date.now()/1e3);if(a.access_expires_at-b>60)return a;try{return await B(a)}catch(a){return a instanceof q&&401===a.status&&z(),null}}async function D(){let a=await C();if(!a)return null;try{return{me:await w(a.access_token),auth:a}}catch(a){if(a instanceof q&&401===a.status){let a=y();if(!a)return null;try{let b=await B(a);return{me:await w(b.access_token),auth:b}}catch(a){a instanceof q&&401===a.status&&z()}}return null}}async function E(){let a=(0,c.readConfig)(),b=function(){let a,b;try{a=(0,d.readFileSync)((0,e.auditScheduleFile)(void 0),"utf8")}catch{return null}try{b=JSON.parse(a)}catch{return null}if(!b||"object"!=typeof b||Array.isArray(b))return null;let c=b,f=a=>"number"==typeof a&&Number.isFinite(a)?a:null,g=f(c.schema);return{schema:g,nextDueAtMs:f(c.next_due_at_ms),lastAttemptAtMs:f(c.last_attempt_at_ms),lastRunAtMs:f(c.last_run_at_ms),lastExitCode:f(c.last_exit_code),schemaAhead:null!==g&&g>1}}(),f=(0,m.readDashboardCacheMeta)(),g=y(),i=function(){if("linux"!==process.platform&&process.platform,!(0,d.existsSync)(k()))return"not-installed";try{let a=(0,h.execFileSync)("systemctl",["is-active",j()],{stdio:["ignore","pipe","ignore"],timeout:1e4}).toString().trim();return"active"===a?"running":l()}catch{return l()}}();return{auto:a.audit.auto,intervalDays:a.audit.intervalDays,signedInAs:g?{id:g.user.id,email:g.user.email}:null,daemon:i,schedule:b?{nextDueAtMs:b.nextDueAtMs,lastAttemptAtMs:b.lastAttemptAtMs,lastRunAtMs:b.lastRunAtMs,lastExitCode:b.lastExitCode,schemaAhead:b.schemaAhead}:null,lastResultAt:f?.cachedAt??null,lastScan:f?{finishedAt:f.cachedAt,findings:f.findings,sessionsScanned:f.sessionsScanned,eventsScanned:f.eventsScanned}:null,daemonStartedAtMs:"running"===i?function(){if(!(0,d.existsSync)(k()))return null;try{let a=(0,h.execFileSync)("systemctl",["show",j(),"-p","ActiveEnterTimestampMonotonic","--value"],{stdio:["ignore","pipe","ignore"],timeout:1e4}).toString().trim();return function(a,b,c){if(!Number.isFinite(a)||a<=0||!Number.isFinite(b)||b<=0)return null;let d=(b-a)/1e3;return d<0?null:Math.round(c-d)}(Number(a),Math.floor(Number(process.hrtime.bigint())/1e3),Date.now())}catch{return null}}():null}}a.s(["readAuth",0,y,"whoAmI",0,D],81243),(0,a.i(13095).ensureServerEntryExports)([E]),(0,b.registerServerReference)(E,"00063bb359b039d550f793409319d65c11c5b57371",null),a.s(["getScheduledAuditAction",0,E],36119)}];
2
-
3
- //# sourceMappingURL=%5Broot-of-the-server%5D__0oa1lav._.js.map