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
package/dist/worker.mjs CHANGED
@@ -3483,7 +3483,7 @@ function parsePackPolicyName(canonicalName) {
3483
3483
  }
3484
3484
 
3485
3485
  // package.json
3486
- var version = "1.0.5";
3486
+ var version = "1.0.6-beta.0";
3487
3487
  var init_package = () => {};
3488
3488
 
3489
3489
  // src/posthog-key.ts
@@ -10605,7 +10605,20 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
10605
10605
  }
10606
10606
  } catch {}
10607
10607
  }
10608
- return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
10608
+ return {
10609
+ exitCode: result.exitCode,
10610
+ stdout: result.stdout,
10611
+ stderr: result.stderr,
10612
+ evaluation: {
10613
+ decision: result.decision,
10614
+ policyName: result.policyName,
10615
+ policyNames: result.policyNames ?? (result.policyName ? [result.policyName] : []),
10616
+ reason: result.reason,
10617
+ matchedPolicies,
10618
+ durationMs,
10619
+ toolName: parsed.tool_name ?? null
10620
+ }
10621
+ };
10609
10622
  } finally {
10610
10623
  if (opts?.awaitTelemetryFlush ?? true) {
10611
10624
  await flushHookTelemetry();
@@ -10709,7 +10722,8 @@ function handleConnection(socket, shutdown) {
10709
10722
  type: "hookResult",
10710
10723
  exitCode: result.exitCode,
10711
10724
  stdout: result.stdout,
10712
- stderr: result.stderr
10725
+ stderr: result.stderr,
10726
+ evaluation: result.evaluation
10713
10727
  }));
10714
10728
  } catch (err) {
10715
10729
  const msg = err instanceof Error ? err.message : String(err);
@@ -0,0 +1,133 @@
1
+ # FailproofAI for Hermes
2
+
3
+ This directory is the native Hermes adapter shipped inside the `failproofai`
4
+ npm package. It keeps policy evaluation in FailproofAI's TypeScript worker and
5
+ translates structured verdicts into Hermes-native hook behavior.
6
+
7
+ Validated contract: Hermes 0.21.3 at
8
+ `4d55ca91656ac5f83e1506679b7f81e0238e5e16`. The plugin uses only documented
9
+ manifest v2 fields, `PluginContext` methods/state, and keyword hook payloads.
10
+
11
+ ## Installation
12
+
13
+ Run:
14
+
15
+ ```bash
16
+ failproofai policies --install --cli hermes --scope user
17
+ ```
18
+
19
+ FailproofAI copies this directory to every discovered profile at
20
+ `<HERMES_HOME>/plugins/failproofai/` and adds `failproofai` to
21
+ `plugins.enabled` in that profile's `config.yaml`. Reinstall replaces only a
22
+ directory carrying `.failproofai-managed`; an unrelated plugin with the same
23
+ directory name is never overwritten.
24
+
25
+ Legacy FailproofAI shell hooks are removed during migration. Operator-owned
26
+ hooks and unrelated plugin settings are preserved. No dashboard deployment or
27
+ backtest is part of installation.
28
+
29
+ ## Runtime path
30
+
31
+ ```text
32
+ Hermes pre_tool_call
33
+ -> native Python plugin
34
+ -> owner-only failproofaid Unix socket
35
+ -> warm TypeScript policy worker
36
+ -> structured allow | deny | instruct verdict
37
+ -> Hermes-native return value
38
+ ```
39
+
40
+ There is no cloud request and no new CLI process in the tool-call path.
41
+
42
+ ## Decision behavior
43
+
44
+ - `allow`: return no directive; Hermes runs the tool.
45
+ - `deny`: always return `{"action":"block","message":"..."}`.
46
+ - `instruct`: persist delivery state, block the first attempt with a
47
+ model-visible `FAILPROOF INSTRUCTION`, keep the same API request blocked, then
48
+ allow a later API iteration for the same instruction scope.
49
+
50
+ The default instruction scope is profile + session + task + turn + policy
51
+ fingerprint + canonical tool. The persistent SQLite ledger lives below Hermes'
52
+ profile-scoped plugin data directory. Two distinct instruction interruptions
53
+ are allowed per turn by default; after that, advisory instructions fail open so
54
+ they cannot create an infinite retry loop. A real deny is never bypassed by the
55
+ ledger.
56
+
57
+ ## Configuration
58
+
59
+ Settings live under `plugins.entries.failproofai.settings`:
60
+
61
+ ```yaml
62
+ plugins:
63
+ enabled:
64
+ - failproofai
65
+ entries:
66
+ failproofai:
67
+ settings:
68
+ failure_mode: deny
69
+ connect_timeout_ms: 250
70
+ evaluation_timeout_ms: 12000
71
+ instruction_ttl_seconds: 3600
72
+ max_instruction_rounds: 2
73
+ ```
74
+
75
+ `failure_mode` controls evaluator/protocol failures only. `deny` is the safe
76
+ default. `allow` favors availability when the local daemon cannot return a
77
+ trusted verdict. Ledger failures always allow only the advisory `instruct`
78
+ decision, because otherwise corrupted retry state could block a turn forever.
79
+
80
+ ## Local protocol
81
+
82
+ Requests and responses use the existing length-prefixed failproofaid Unix
83
+ socket protocol, version 1.
84
+
85
+ ```json
86
+ {
87
+ "type": "policyEvaluation",
88
+ "protocolVersion": 1,
89
+ "integration": "hermes",
90
+ "event": "pre_tool_call",
91
+ "payload": {},
92
+ "cwd": "/workspace/project"
93
+ }
94
+ ```
95
+
96
+ ```json
97
+ {
98
+ "type": "policyResult",
99
+ "protocolVersion": 1,
100
+ "decision": "instruct",
101
+ "policyNames": ["custom/approved-write-route"],
102
+ "reason": "Use the approved write route.",
103
+ "matchedPolicies": ["custom/approved-write-route"],
104
+ "durationMs": 3,
105
+ "toolName": "Write"
106
+ }
107
+ ```
108
+
109
+ Requests are limited to 1 MiB and responses to 16 MiB. Invalid JSON, unknown
110
+ message types, version mismatch, timeout, and socket failure are treated as
111
+ untrusted evaluation failures.
112
+
113
+ ## Diagnostics and rollback
114
+
115
+ `failproofai config --status` reports each existing Hermes profile as healthy,
116
+ disabled, incomplete, or duplicated with a legacy shell hook. Hermes-side load
117
+ errors are available through:
118
+
119
+ ```bash
120
+ HERMES_PLUGINS_DEBUG=1 hermes plugins list
121
+ hermes plugins doctor ~/.hermes/plugins/failproofai --ci
122
+ hermes logs --level WARNING
123
+ ```
124
+
125
+ Use a temporary `HERMES_HOME` for development and compatibility tests. Remove
126
+ the integration with:
127
+
128
+ ```bash
129
+ failproofai policies --uninstall --cli hermes --scope user
130
+ ```
131
+
132
+ Uninstall removes the config registration and only the plugin directory marked
133
+ as FailproofAI-managed.
@@ -0,0 +1,248 @@
1
+ """Native Hermes bridge for FailproofAI policy enforcement."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any, Mapping
9
+
10
+ from .client import PolicyVerdict, evaluate_policy
11
+ from .ledger import InstructionLedger, LedgerError
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ _PROTOCOL_CONTEXT = (
16
+ "When a tool result starts with FAILPROOF INSTRUCTION, treat it as policy guidance. "
17
+ "Reconsider the attempted action before retrying. Do not evade or ignore the instruction; "
18
+ "change tools, arguments, targets, or side effects only when the instruction requires it. "
19
+ "If you cannot follow the instruction, stop and explain why."
20
+ )
21
+
22
+
23
+ def _bounded_int(value: object, default: int, minimum: int, maximum: int) -> int:
24
+ try:
25
+ parsed = int(value)
26
+ except (TypeError, ValueError):
27
+ return default
28
+ return min(max(parsed, minimum), maximum)
29
+
30
+
31
+ def _profile_name() -> str:
32
+ explicit = os.environ.get("HERMES_PROFILE", "").strip()
33
+ if explicit:
34
+ return explicit
35
+ configured_home = os.environ.get("HERMES_HOME", "").strip()
36
+ home = Path(configured_home).expanduser() if configured_home else Path.home() / ".hermes"
37
+ if home.parent.name == "profiles":
38
+ return home.name
39
+ return "default"
40
+
41
+
42
+ def _string(value: object) -> str:
43
+ return value if isinstance(value, str) else ""
44
+
45
+
46
+ class FailproofAIPlugin:
47
+ def __init__(self, ctx: Any) -> None:
48
+ self.ctx = ctx
49
+ self.profile = _profile_name()
50
+ self.failure_mode = str(ctx.get_config("failure_mode", "deny")).strip().lower()
51
+ self.connect_timeout_ms = _bounded_int(
52
+ ctx.get_config("connect_timeout_ms", 250), 250, 25, 5_000
53
+ )
54
+ self.evaluation_timeout_ms = _bounded_int(
55
+ ctx.get_config("evaluation_timeout_ms", 12_000), 12_000, 100, 29_000
56
+ )
57
+ ttl_seconds = _bounded_int(
58
+ ctx.get_config("instruction_ttl_seconds", 3_600), 3_600, 60, 86_400
59
+ )
60
+ max_rounds = _bounded_int(
61
+ ctx.get_config("max_instruction_rounds", 2), 2, 0, 10
62
+ )
63
+ self.ledger = InstructionLedger(
64
+ Path(ctx.state.data_dir) / "instructions.db",
65
+ ttl_seconds=ttl_seconds,
66
+ max_rounds=max_rounds,
67
+ )
68
+
69
+ def _fallback(self, error: Exception) -> dict[str, str] | None:
70
+ logger.error("FailproofAI evaluator unavailable: %s", error)
71
+ if self.failure_mode == "allow":
72
+ return None
73
+ return {
74
+ "action": "block",
75
+ "message": (
76
+ "FailproofAI could not verify this tool call because the local policy "
77
+ "evaluator is unavailable. Check `failproofai status` before retrying."
78
+ ),
79
+ }
80
+
81
+ def _evaluate(self, event: str, payload: Mapping[str, Any]) -> PolicyVerdict:
82
+ cwd = _string(payload.get("cwd")) or os.getcwd()
83
+ return evaluate_policy(
84
+ event=event,
85
+ payload=payload,
86
+ cwd=cwd,
87
+ connect_timeout_ms=self.connect_timeout_ms,
88
+ evaluation_timeout_ms=self.evaluation_timeout_ms,
89
+ )
90
+
91
+ def pre_tool_call(
92
+ self,
93
+ tool_name: str = "",
94
+ args: Mapping[str, Any] | None = None,
95
+ session_id: str = "",
96
+ task_id: str = "",
97
+ tool_call_id: str = "",
98
+ turn_id: str = "",
99
+ api_request_id: str = "",
100
+ **kwargs: Any,
101
+ ) -> dict[str, str] | None:
102
+ payload = {
103
+ "hook_event_name": "pre_tool_call",
104
+ "tool_name": tool_name,
105
+ "tool_input": dict(args or {}),
106
+ "session_id": session_id,
107
+ "cwd": _string(kwargs.get("cwd")) or os.getcwd(),
108
+ "hermes": {
109
+ "profile": self.profile,
110
+ "task_id": task_id,
111
+ "tool_call_id": tool_call_id,
112
+ "turn_id": turn_id,
113
+ "api_request_id": api_request_id,
114
+ },
115
+ }
116
+ try:
117
+ verdict = self._evaluate("pre_tool_call", payload)
118
+ except Exception as exc:
119
+ return self._fallback(exc)
120
+
121
+ if verdict.decision == "allow":
122
+ return None
123
+ if verdict.decision == "deny":
124
+ return {
125
+ "action": "block",
126
+ "message": verdict.reason or "Blocked by FailproofAI policy.",
127
+ }
128
+
129
+ reason = verdict.reason or "Reconsider this tool call before retrying."
130
+ try:
131
+ action = self.ledger.decide(
132
+ profile=self.profile,
133
+ session_id=session_id,
134
+ task_id=task_id,
135
+ turn_id=turn_id,
136
+ api_request_id=api_request_id,
137
+ policy_names=verdict.policy_names,
138
+ reason=reason,
139
+ scope_key=verdict.tool_name or tool_name or "unknown-tool",
140
+ )
141
+ except LedgerError as exc:
142
+ # An advisory instruction must never become an indefinite denial
143
+ # because its local retry state is unavailable.
144
+ logger.error("FailproofAI instruction state unavailable; allowing: %s", exc)
145
+ return None
146
+
147
+ if not action.block:
148
+ if action.status in {"missing-correlation-id", "turn-cap"}:
149
+ logger.warning("FailproofAI instruction allowed in degraded state: %s", action.status)
150
+ return None
151
+
152
+ policies = ", ".join(verdict.policy_names) or "unknown policy"
153
+ return {
154
+ "action": "block",
155
+ "message": (
156
+ f"FAILPROOF INSTRUCTION ({policies})\n\n{reason}\n\n"
157
+ "Reconsider the attempted action, then make a corrected tool call. "
158
+ "Do not route around this instruction with an equivalent ungoverned action."
159
+ ),
160
+ }
161
+
162
+ def _observe(self, event: str, payload: Mapping[str, Any]) -> None:
163
+ try:
164
+ self._evaluate(event, payload)
165
+ except Exception as exc:
166
+ logger.warning("FailproofAI observation failed for %s: %s", event, exc)
167
+
168
+ def post_tool_call(
169
+ self,
170
+ tool_name: str = "",
171
+ args: Mapping[str, Any] | None = None,
172
+ result: Any = None,
173
+ session_id: str = "",
174
+ **kwargs: Any,
175
+ ) -> None:
176
+ self._observe(
177
+ "post_tool_call",
178
+ {
179
+ "hook_event_name": "post_tool_call",
180
+ "tool_name": tool_name,
181
+ "tool_input": dict(args or {}),
182
+ "tool_response": result,
183
+ "session_id": session_id,
184
+ "cwd": _string(kwargs.get("cwd")) or os.getcwd(),
185
+ "hermes": {"profile": self.profile, **kwargs},
186
+ },
187
+ )
188
+
189
+ def on_session_start(self, session_id: str = "", **kwargs: Any) -> None:
190
+ self._observe(
191
+ "on_session_start",
192
+ {
193
+ "hook_event_name": "on_session_start",
194
+ "session_id": session_id,
195
+ "cwd": _string(kwargs.get("cwd")) or os.getcwd(),
196
+ "hermes": {"profile": self.profile, **kwargs},
197
+ },
198
+ )
199
+
200
+ def on_session_end(self, session_id: str = "", **kwargs: Any) -> None:
201
+ self._observe(
202
+ "on_session_end",
203
+ {
204
+ "hook_event_name": "on_session_end",
205
+ "session_id": session_id,
206
+ "cwd": _string(kwargs.get("cwd")) or os.getcwd(),
207
+ "hermes": {"profile": self.profile, **kwargs},
208
+ },
209
+ )
210
+ self._clear_session(session_id)
211
+
212
+ def subagent_stop(self, session_id: str = "", **kwargs: Any) -> None:
213
+ self._observe(
214
+ "subagent_stop",
215
+ {
216
+ "hook_event_name": "subagent_stop",
217
+ "session_id": session_id,
218
+ "cwd": _string(kwargs.get("cwd")) or os.getcwd(),
219
+ "hermes": {"profile": self.profile, **kwargs},
220
+ },
221
+ )
222
+
223
+ def pre_llm_call(self, **kwargs: Any) -> dict[str, str]:
224
+ return {"context": _PROTOCOL_CONTEXT}
225
+
226
+ def on_session_reset(self, session_id: str = "", **kwargs: Any) -> None:
227
+ self._clear_session(session_id)
228
+
229
+ def on_session_finalize(self, session_id: str = "", **kwargs: Any) -> None:
230
+ self._clear_session(session_id)
231
+
232
+ def _clear_session(self, session_id: str) -> None:
233
+ try:
234
+ self.ledger.clear_session(session_id)
235
+ except LedgerError as exc:
236
+ logger.warning("FailproofAI instruction cleanup failed: %s", exc)
237
+
238
+
239
+ def register(ctx: Any) -> None:
240
+ plugin = FailproofAIPlugin(ctx)
241
+ ctx.register_hook("pre_tool_call", plugin.pre_tool_call)
242
+ ctx.register_hook("post_tool_call", plugin.post_tool_call)
243
+ ctx.register_hook("pre_llm_call", plugin.pre_llm_call)
244
+ ctx.register_hook("on_session_start", plugin.on_session_start)
245
+ ctx.register_hook("on_session_end", plugin.on_session_end)
246
+ ctx.register_hook("on_session_reset", plugin.on_session_reset)
247
+ ctx.register_hook("on_session_finalize", plugin.on_session_finalize)
248
+ ctx.register_hook("subagent_stop", plugin.subagent_stop)
@@ -0,0 +1,134 @@
1
+ """Versioned Unix-socket client for failproofaid policy evaluation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import socket
8
+ import struct
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any, Mapping, Sequence
12
+
13
+ PROTOCOL_VERSION = 1
14
+ MAX_FRAME_BYTES = 16 * 1024 * 1024
15
+ MAX_REQUEST_BYTES = 1024 * 1024
16
+
17
+
18
+ class EvaluationError(RuntimeError):
19
+ """The daemon could not return a trustworthy policy verdict."""
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class PolicyVerdict:
24
+ decision: str
25
+ policy_names: tuple[str, ...]
26
+ reason: str | None
27
+ matched_policies: tuple[str, ...]
28
+ duration_ms: int
29
+ tool_name: str | None
30
+
31
+
32
+ def daemon_socket_path() -> Path:
33
+ override = os.environ.get("FAILPROOFAI_DAEMON_SOCKET", "").strip()
34
+ if override:
35
+ return Path(override).expanduser()
36
+ root = os.environ.get("FAILPROOFAI_HOME", "").strip()
37
+ home = Path(root).expanduser() if root else Path.home() / ".failproofai"
38
+ return home / "run" / "failproofaid.sock"
39
+
40
+
41
+ def _read_exact(sock: socket.socket, length: int) -> bytes:
42
+ chunks: list[bytes] = []
43
+ remaining = length
44
+ while remaining:
45
+ chunk = sock.recv(remaining)
46
+ if not chunk:
47
+ raise EvaluationError("failproofaid closed the connection before returning a verdict")
48
+ chunks.append(chunk)
49
+ remaining -= len(chunk)
50
+ return b"".join(chunks)
51
+
52
+
53
+ def _string_list(value: object, field: str) -> tuple[str, ...]:
54
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)):
55
+ raise EvaluationError(f"failproofaid returned an invalid {field}")
56
+ if not all(isinstance(item, str) for item in value):
57
+ raise EvaluationError(f"failproofaid returned an invalid {field}")
58
+ return tuple(value)
59
+
60
+
61
+ def evaluate_policy(
62
+ *,
63
+ event: str,
64
+ payload: Mapping[str, Any],
65
+ cwd: str | None,
66
+ connect_timeout_ms: int = 250,
67
+ evaluation_timeout_ms: int = 12_000,
68
+ ) -> PolicyVerdict:
69
+ request = {
70
+ "type": "policyEvaluation",
71
+ "protocolVersion": PROTOCOL_VERSION,
72
+ "integration": "hermes",
73
+ "event": event,
74
+ "payload": dict(payload),
75
+ "cwd": cwd,
76
+ }
77
+ try:
78
+ body = json.dumps(
79
+ request,
80
+ ensure_ascii=False,
81
+ separators=(",", ":"),
82
+ default=lambda value: repr(value),
83
+ ).encode("utf-8")
84
+ except (TypeError, ValueError) as exc:
85
+ raise EvaluationError(f"could not encode policy request: {exc}") from exc
86
+ if len(body) > MAX_REQUEST_BYTES:
87
+ raise EvaluationError("policy request exceeds the 1 MiB limit")
88
+
89
+ try:
90
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
91
+ sock.settimeout(max(connect_timeout_ms, 1) / 1000)
92
+ sock.connect(str(daemon_socket_path()))
93
+ sock.settimeout(max(evaluation_timeout_ms, 1) / 1000)
94
+ sock.sendall(struct.pack(">I", len(body)) + body)
95
+ declared_length = struct.unpack(">I", _read_exact(sock, 4))[0]
96
+ if declared_length > MAX_FRAME_BYTES:
97
+ raise EvaluationError("failproofaid response exceeds the 16 MiB limit")
98
+ response = json.loads(_read_exact(sock, declared_length).decode("utf-8"))
99
+ except EvaluationError:
100
+ raise
101
+ except (OSError, UnicodeError, ValueError, struct.error) as exc:
102
+ raise EvaluationError(f"failproofaid evaluation failed: {exc}") from exc
103
+
104
+ if not isinstance(response, dict):
105
+ raise EvaluationError("failproofaid returned a non-object response")
106
+ if response.get("protocolVersion") != PROTOCOL_VERSION:
107
+ raise EvaluationError("failproofaid protocol version mismatch")
108
+ if response.get("type") == "error":
109
+ message = response.get("message")
110
+ raise EvaluationError(str(message or "failproofaid could not evaluate the policy"))
111
+ if response.get("type") != "policyResult":
112
+ raise EvaluationError("failproofaid does not support native policy evaluation")
113
+
114
+ decision = response.get("decision")
115
+ if decision not in {"allow", "deny", "instruct"}:
116
+ raise EvaluationError("failproofaid returned an invalid policy decision")
117
+ reason = response.get("reason")
118
+ if reason is not None and not isinstance(reason, str):
119
+ raise EvaluationError("failproofaid returned an invalid policy reason")
120
+ duration_ms = response.get("durationMs", 0)
121
+ if not isinstance(duration_ms, int) or duration_ms < 0:
122
+ raise EvaluationError("failproofaid returned an invalid evaluation duration")
123
+ tool_name = response.get("toolName")
124
+ if tool_name is not None and not isinstance(tool_name, str):
125
+ raise EvaluationError("failproofaid returned an invalid tool name")
126
+
127
+ return PolicyVerdict(
128
+ decision=decision,
129
+ policy_names=_string_list(response.get("policyNames", []), "policyNames"),
130
+ reason=reason,
131
+ matched_policies=_string_list(response.get("matchedPolicies", []), "matchedPolicies"),
132
+ duration_ms=duration_ms,
133
+ tool_name=tool_name,
134
+ )