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
@@ -0,0 +1,150 @@
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
+ import time
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Any, Mapping, Sequence
13
+
14
+ PROTOCOL_VERSION = 1
15
+ MAX_FRAME_BYTES = 16 * 1024 * 1024
16
+ MAX_REQUEST_BYTES = 1024 * 1024
17
+
18
+
19
+ class EvaluationError(RuntimeError):
20
+ """The daemon could not return a trustworthy policy verdict."""
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class PolicyVerdict:
25
+ decision: str
26
+ policy_names: tuple[str, ...]
27
+ reason: str | None
28
+ matched_policies: tuple[str, ...]
29
+ duration_ms: int
30
+ tool_name: str | None
31
+
32
+
33
+ def daemon_socket_path() -> Path:
34
+ override = os.environ.get("FAILPROOFAI_DAEMON_SOCKET", "").strip()
35
+ if override:
36
+ return Path(override).expanduser()
37
+ root = os.environ.get("FAILPROOFAI_HOME", "").strip()
38
+ home = Path(root).expanduser() if root else Path.home() / ".failproofai"
39
+ return home / "run" / "failproofaid.sock"
40
+
41
+
42
+ def _remaining_timeout(deadline: float) -> float:
43
+ remaining = deadline - time.monotonic()
44
+ if remaining <= 0:
45
+ raise EvaluationError("failproofaid evaluation timed out")
46
+ return remaining
47
+
48
+
49
+ def _read_exact(sock: socket.socket, length: int, deadline: float) -> bytes:
50
+ chunks: list[bytes] = []
51
+ remaining = length
52
+ while remaining:
53
+ sock.settimeout(_remaining_timeout(deadline))
54
+ try:
55
+ chunk = sock.recv(remaining)
56
+ except TimeoutError as exc:
57
+ raise EvaluationError("failproofaid evaluation timed out") from exc
58
+ if not chunk:
59
+ raise EvaluationError("failproofaid closed the connection before returning a verdict")
60
+ chunks.append(chunk)
61
+ remaining -= len(chunk)
62
+ return b"".join(chunks)
63
+
64
+
65
+ def _string_list(value: object, field: str) -> tuple[str, ...]:
66
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)):
67
+ raise EvaluationError(f"failproofaid returned an invalid {field}")
68
+ if not all(isinstance(item, str) for item in value):
69
+ raise EvaluationError(f"failproofaid returned an invalid {field}")
70
+ return tuple(value)
71
+
72
+
73
+ def evaluate_policy(
74
+ *,
75
+ event: str,
76
+ payload: Mapping[str, Any],
77
+ cwd: str | None,
78
+ connect_timeout_ms: int = 250,
79
+ evaluation_timeout_ms: int = 12_000,
80
+ ) -> PolicyVerdict:
81
+ request = {
82
+ "type": "policyEvaluation",
83
+ "protocolVersion": PROTOCOL_VERSION,
84
+ "integration": "hermes",
85
+ "event": event,
86
+ "payload": dict(payload),
87
+ "cwd": cwd,
88
+ }
89
+ try:
90
+ body = json.dumps(
91
+ request,
92
+ ensure_ascii=False,
93
+ separators=(",", ":"),
94
+ default=lambda value: repr(value),
95
+ ).encode("utf-8")
96
+ except (TypeError, ValueError) as exc:
97
+ raise EvaluationError(f"could not encode policy request: {exc}") from exc
98
+ if len(body) > MAX_REQUEST_BYTES:
99
+ raise EvaluationError("policy request exceeds the 1 MiB limit")
100
+
101
+ try:
102
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
103
+ sock.settimeout(max(connect_timeout_ms, 1) / 1000)
104
+ sock.connect(str(daemon_socket_path()))
105
+ deadline = time.monotonic() + max(evaluation_timeout_ms, 1) / 1000
106
+ sock.settimeout(_remaining_timeout(deadline))
107
+ try:
108
+ sock.sendall(struct.pack(">I", len(body)) + body)
109
+ except TimeoutError as exc:
110
+ raise EvaluationError("failproofaid evaluation timed out") from exc
111
+ declared_length = struct.unpack(">I", _read_exact(sock, 4, deadline))[0]
112
+ if declared_length > MAX_FRAME_BYTES:
113
+ raise EvaluationError("failproofaid response exceeds the 16 MiB limit")
114
+ response = json.loads(_read_exact(sock, declared_length, deadline).decode("utf-8"))
115
+ except EvaluationError:
116
+ raise
117
+ except (OSError, UnicodeError, ValueError, struct.error) as exc:
118
+ raise EvaluationError(f"failproofaid evaluation failed: {exc}") from exc
119
+
120
+ if not isinstance(response, dict):
121
+ raise EvaluationError("failproofaid returned a non-object response")
122
+ if response.get("protocolVersion") != PROTOCOL_VERSION:
123
+ raise EvaluationError("failproofaid protocol version mismatch")
124
+ if response.get("type") == "error":
125
+ message = response.get("message")
126
+ raise EvaluationError(str(message or "failproofaid could not evaluate the policy"))
127
+ if response.get("type") != "policyResult":
128
+ raise EvaluationError("failproofaid does not support native policy evaluation")
129
+
130
+ decision = response.get("decision")
131
+ if decision not in {"allow", "deny", "instruct"}:
132
+ raise EvaluationError("failproofaid returned an invalid policy decision")
133
+ reason = response.get("reason")
134
+ if reason is not None and not isinstance(reason, str):
135
+ raise EvaluationError("failproofaid returned an invalid policy reason")
136
+ duration_ms = response.get("durationMs", 0)
137
+ if not isinstance(duration_ms, int) or duration_ms < 0:
138
+ raise EvaluationError("failproofaid returned an invalid evaluation duration")
139
+ tool_name = response.get("toolName")
140
+ if tool_name is not None and not isinstance(tool_name, str):
141
+ raise EvaluationError("failproofaid returned an invalid tool name")
142
+
143
+ return PolicyVerdict(
144
+ decision=decision,
145
+ policy_names=_string_list(response.get("policyNames", []), "policyNames"),
146
+ reason=reason,
147
+ matched_policies=_string_list(response.get("matchedPolicies", []), "matchedPolicies"),
148
+ duration_ms=duration_ms,
149
+ tool_name=tool_name,
150
+ )
@@ -0,0 +1,187 @@
1
+ """Persistent, bounded delivery state for FailproofAI instruct decisions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import sqlite3
8
+ import threading
9
+ import time
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Sequence
13
+
14
+
15
+ class LedgerError(RuntimeError):
16
+ """Instruction state could not be read or updated safely."""
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class InstructionAction:
21
+ block: bool
22
+ status: str
23
+
24
+
25
+ class InstructionLedger:
26
+ def __init__(self, path: Path, *, ttl_seconds: int = 3600, max_rounds: int = 2) -> None:
27
+ self.path = path
28
+ self.ttl_seconds = max(60, int(ttl_seconds))
29
+ self.max_rounds = max(0, int(max_rounds))
30
+ self._init_lock = threading.Lock()
31
+ self._initialized = False
32
+
33
+ def _connect(self) -> sqlite3.Connection:
34
+ self.path.parent.mkdir(parents=True, exist_ok=True)
35
+ connection = sqlite3.connect(self.path, timeout=0.25, isolation_level=None)
36
+ connection.execute("PRAGMA busy_timeout = 250")
37
+ connection.execute("PRAGMA journal_mode = WAL")
38
+ connection.execute("PRAGMA synchronous = NORMAL")
39
+ return connection
40
+
41
+ def _ensure_schema(self) -> None:
42
+ if self._initialized:
43
+ return
44
+ with self._init_lock:
45
+ if self._initialized:
46
+ return
47
+ try:
48
+ with self._connect() as connection:
49
+ connection.execute(
50
+ """
51
+ CREATE TABLE IF NOT EXISTS instruction_deliveries (
52
+ delivery_key TEXT PRIMARY KEY,
53
+ profile TEXT NOT NULL,
54
+ session_id TEXT NOT NULL,
55
+ task_id TEXT NOT NULL,
56
+ turn_id TEXT NOT NULL,
57
+ policy_fingerprint TEXT NOT NULL,
58
+ scope_key TEXT NOT NULL,
59
+ blocked_api_request_id TEXT NOT NULL,
60
+ acknowledged_api_request_id TEXT,
61
+ state TEXT NOT NULL,
62
+ created_at_ms INTEGER NOT NULL,
63
+ expires_at_ms INTEGER NOT NULL
64
+ )
65
+ """
66
+ )
67
+ connection.execute(
68
+ """
69
+ CREATE INDEX IF NOT EXISTS instruction_deliveries_turn
70
+ ON instruction_deliveries(profile, session_id, task_id, turn_id, expires_at_ms)
71
+ """
72
+ )
73
+ except sqlite3.Error as exc:
74
+ raise LedgerError(f"could not initialize instruction state: {exc}") from exc
75
+ self._initialized = True
76
+
77
+ @staticmethod
78
+ def _digest(parts: Sequence[str]) -> str:
79
+ encoded = json.dumps(list(parts), ensure_ascii=False, separators=(",", ":")).encode("utf-8")
80
+ return hashlib.sha256(encoded).hexdigest()
81
+
82
+ def decide(
83
+ self,
84
+ *,
85
+ profile: str,
86
+ session_id: str,
87
+ task_id: str,
88
+ turn_id: str,
89
+ api_request_id: str,
90
+ policy_names: Sequence[str],
91
+ reason: str,
92
+ scope_key: str,
93
+ now_ms: int | None = None,
94
+ ) -> InstructionAction:
95
+ if not session_id or not turn_id or not api_request_id:
96
+ return InstructionAction(False, "missing-correlation-id")
97
+
98
+ self._ensure_schema()
99
+ now = int(time.time() * 1000) if now_ms is None else int(now_ms)
100
+ expires_at = now + self.ttl_seconds * 1000
101
+ task = task_id or "root"
102
+ policy_fingerprint = self._digest([*sorted(policy_names), reason])
103
+ delivery_key = self._digest(
104
+ [profile, session_id, task, turn_id, policy_fingerprint, scope_key]
105
+ )
106
+
107
+ try:
108
+ with self._connect() as connection:
109
+ connection.execute("BEGIN IMMEDIATE")
110
+ connection.execute(
111
+ "DELETE FROM instruction_deliveries WHERE expires_at_ms <= ?", (now,)
112
+ )
113
+ row = connection.execute(
114
+ """
115
+ SELECT blocked_api_request_id, state
116
+ FROM instruction_deliveries
117
+ WHERE delivery_key = ?
118
+ """,
119
+ (delivery_key,),
120
+ ).fetchone()
121
+ if row is not None:
122
+ blocked_request, state = row
123
+ if state == "issued" and blocked_request == api_request_id:
124
+ connection.commit()
125
+ return InstructionAction(True, "same-api-request")
126
+ if state == "issued":
127
+ connection.execute(
128
+ """
129
+ UPDATE instruction_deliveries
130
+ SET state = 'acknowledged', acknowledged_api_request_id = ?
131
+ WHERE delivery_key = ?
132
+ """,
133
+ (api_request_id, delivery_key),
134
+ )
135
+ connection.commit()
136
+ return InstructionAction(False, "acknowledged")
137
+
138
+ rounds = connection.execute(
139
+ """
140
+ SELECT COUNT(*)
141
+ FROM instruction_deliveries
142
+ WHERE profile = ? AND session_id = ? AND task_id = ? AND turn_id = ?
143
+ AND expires_at_ms > ?
144
+ """,
145
+ (profile, session_id, task, turn_id, now),
146
+ ).fetchone()[0]
147
+ if rounds >= self.max_rounds:
148
+ connection.commit()
149
+ return InstructionAction(False, "turn-cap")
150
+
151
+ connection.execute(
152
+ """
153
+ INSERT INTO instruction_deliveries (
154
+ delivery_key, profile, session_id, task_id, turn_id,
155
+ policy_fingerprint, scope_key, blocked_api_request_id,
156
+ acknowledged_api_request_id, state, created_at_ms, expires_at_ms
157
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'issued', ?, ?)
158
+ """,
159
+ (
160
+ delivery_key,
161
+ profile,
162
+ session_id,
163
+ task,
164
+ turn_id,
165
+ policy_fingerprint,
166
+ scope_key,
167
+ api_request_id,
168
+ now,
169
+ expires_at,
170
+ ),
171
+ )
172
+ connection.commit()
173
+ return InstructionAction(True, "issued")
174
+ except (sqlite3.Error, OSError) as exc:
175
+ raise LedgerError(f"could not update instruction state: {exc}") from exc
176
+
177
+ def clear_session(self, session_id: str) -> None:
178
+ if not session_id:
179
+ return
180
+ self._ensure_schema()
181
+ try:
182
+ with self._connect() as connection:
183
+ connection.execute(
184
+ "DELETE FROM instruction_deliveries WHERE session_id = ?", (session_id,)
185
+ )
186
+ except (sqlite3.Error, OSError) as exc:
187
+ raise LedgerError(f"could not clear instruction state: {exc}") from exc
@@ -0,0 +1,37 @@
1
+ name: failproofai
2
+ version: 1.0.6-beta.0
3
+ description: Real-time FailproofAI policy enforcement for Hermes
4
+ manifest_version: 2
5
+ api_version: 1
6
+ homepage: https://github.com/FailproofAI/failproofai
7
+ tags: [security, policy, observability]
8
+ provides_hooks:
9
+ - pre_tool_call
10
+ - post_tool_call
11
+ - pre_llm_call
12
+ - on_session_start
13
+ - on_session_end
14
+ - on_session_reset
15
+ - on_session_finalize
16
+ - subagent_stop
17
+ config_schema:
18
+ failure_mode:
19
+ type: str
20
+ default: deny
21
+ description: Block or allow when the local FailproofAI evaluator is unavailable.
22
+ connect_timeout_ms:
23
+ type: int
24
+ default: 250
25
+ description: Maximum time to connect to the local FailproofAI daemon.
26
+ evaluation_timeout_ms:
27
+ type: int
28
+ default: 12000
29
+ description: Maximum total time allowed for one policy evaluation.
30
+ instruction_ttl_seconds:
31
+ type: int
32
+ default: 3600
33
+ description: Maximum lifetime of an instruction-delivery record.
34
+ max_instruction_rounds:
35
+ type: int
36
+ default: 2
37
+ description: Maximum distinct instruction interruptions in one Hermes turn.
@@ -3,10 +3,11 @@
3
3
  *
4
4
  * A Hermes "profile" is not a column or a flag: it is a whole separate Hermes
5
5
  * home directory, each with its own `config.yaml`, `.env`, `SOUL.md`, and
6
- * `state.db`. The default profile lives at `~/.hermes`; every other profile at
7
- * `~/.hermes/profiles/<name>/`. Selection is `hermes -p <name>`, a generated
8
- * `~/.local/bin/<name>` alias that exports `HERMES_HOME`, or a sticky default
9
- * recorded in `<root>/active_profile`.
6
+ * `state.db`. The default profile lives at `~/.hermes`; upstream named profiles
7
+ * live at `~/.hermes/profiles/<name>/`, while older/custom multi-install setups
8
+ * may use sibling homes such as `~/.hermes-work`. Selection is `hermes -p
9
+ * <name>`, a generated `~/.local/bin/<name>` alias that exports `HERMES_HOME`,
10
+ * or a sticky default recorded in `<root>/active_profile`.
10
11
  *
11
12
  * Upstream's own contributor guide warns that hardcoding `~/.hermes` breaks
12
13
  * profiles — which is exactly what both pillars used to do:
@@ -21,12 +22,14 @@
21
22
  * Home override: set `HERMES_HOME` (Hermes's own env var — respected here so a
22
23
  * profile-scoped shell and failproofai agree on what "all profiles" means).
23
24
  */
24
- import { readdirSync } from "node:fs";
25
+ import { existsSync, readdirSync } from "node:fs";
25
26
  import { homedir } from "node:os";
26
27
  import { basename, dirname, join, resolve } from "node:path";
27
28
 
28
29
  /** Directory under the Hermes root that holds non-default profiles. */
29
30
  const PROFILES_DIR = "profiles";
31
+ const STANDARD_HOME = ".hermes";
32
+ const SIBLING_HOME_PREFIX = ".hermes-";
30
33
 
31
34
  /** Name we give the root home (`~/.hermes`), which Hermes itself leaves unnamed. */
32
35
  export const HERMES_DEFAULT_PROFILE = "default";
@@ -41,28 +44,45 @@ export interface HermesProfile {
41
44
  /**
42
45
  * The Hermes ROOT home — the directory that owns `profiles/`.
43
46
  *
44
- * `HERMES_HOME` may point AT a profile (`<root>/profiles/<name>`), because
45
- * that's what the per-profile alias wrapper exports. We climb back to `<root>`
46
- * in that case so discovery still sees every sibling profile — mirroring what
47
- * upstream does so `profile list` can see them all.
47
+ * `HERMES_HOME` may point AT an upstream profile (`<root>/profiles/<name>`) or
48
+ * at a sibling installation (`~/.hermes-<name>`). We normalize either standard
49
+ * layout back to `~/.hermes` so discovery covers every installation. A custom
50
+ * non-standard path remains authoritative; scanning arbitrary sibling paths
51
+ * would risk treating backups and unrelated directories as live agents.
48
52
  */
49
53
  export function hermesRoot(): string {
50
54
  const env = (process.env.HERMES_HOME || "").trim();
51
55
  if (env) {
52
- const home = resolve(env);
56
+ let home = resolve(env);
53
57
  const parent = dirname(home);
54
- if (basename(parent) === PROFILES_DIR) return dirname(parent);
58
+ if (basename(parent) === PROFILES_DIR) home = dirname(parent);
59
+ const base = basename(home);
60
+ if (base === STANDARD_HOME || base.startsWith(SIBLING_HOME_PREFIX)) {
61
+ return join(dirname(home), STANDARD_HOME);
62
+ }
55
63
  return home;
56
64
  }
57
- return join(homedir(), ".hermes");
65
+ return join(homedir(), STANDARD_HOME);
66
+ }
67
+
68
+ function uniqueProfileName(preferred: string, seen: Set<string>): string {
69
+ if (!seen.has(preferred)) return preferred;
70
+ const base = `${preferred}-home`;
71
+ if (!seen.has(base)) return base;
72
+ let suffix = 2;
73
+ while (seen.has(`${base}-${suffix}`)) suffix += 1;
74
+ return `${base}-${suffix}`;
58
75
  }
59
76
 
60
77
  /**
61
78
  * Every Hermes profile on disk: the root home first (as `"default"`), then each
62
- * `<root>/profiles/<name>/` in name order.
79
+ * `<root>/profiles/<name>/`, then valid `~/.hermes-<name>` sibling homes, in
80
+ * name order within each layout.
63
81
  *
64
- * Fail-open a missing or unreadable `profiles/` dir just means "default only",
65
- * which is the single-profile install everyone starts with.
82
+ * A sibling home counts only when it contains `config.yaml`, matching OpenClaw
83
+ * discovery and avoiding arbitrary `.hermes-*` backup directories. Upstream
84
+ * `profiles/` directories are authoritative and remain discoverable before
85
+ * their config is first written.
66
86
  *
67
87
  * A profile directory literally named `default` would collide with the root's
68
88
  * reserved name; the root wins and the directory is skipped (dedup by name keeps
@@ -73,22 +93,50 @@ export function listHermesProfiles(): HermesProfile[] {
73
93
  const out: HermesProfile[] = [{ name: HERMES_DEFAULT_PROFILE, home: root }];
74
94
  const seen = new Set<string>([HERMES_DEFAULT_PROFILE]);
75
95
 
76
- let names: string[];
96
+ let names: string[] = [];
77
97
  try {
78
98
  names = readdirSync(join(root, PROFILES_DIR), { withFileTypes: true })
79
99
  // Symlinked profile dirs are legitimate, and `isDirectory()` is false for them.
80
100
  .filter((e) => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith("."))
81
101
  .map((e) => e.name)
82
102
  .sort();
83
- } catch {
84
- return out;
85
- }
103
+ } catch {}
86
104
 
87
105
  for (const name of names) {
88
106
  if (seen.has(name)) continue;
89
107
  seen.add(name);
90
108
  out.push({ name, home: join(root, PROFILES_DIR, name) });
91
109
  }
110
+
111
+ // Only the standard ~/.hermes layout defines `.hermes-*` sibling homes.
112
+ // A custom HERMES_HOME is intentionally not used as a prefix convention.
113
+ if (basename(root) !== STANDARD_HOME) return out;
114
+
115
+ let siblings: HermesProfile[] = [];
116
+ const parent = dirname(root);
117
+ try {
118
+ siblings = readdirSync(parent, { withFileTypes: true })
119
+ .filter(
120
+ (entry) =>
121
+ (entry.isDirectory() || entry.isSymbolicLink()) &&
122
+ entry.name.startsWith(SIBLING_HOME_PREFIX) &&
123
+ entry.name.length > SIBLING_HOME_PREFIX.length &&
124
+ existsSync(join(parent, entry.name, "config.yaml")),
125
+ )
126
+ .map((entry) => ({
127
+ name: entry.name.slice(SIBLING_HOME_PREFIX.length),
128
+ home: join(parent, entry.name),
129
+ }))
130
+ .sort((a, b) => a.name.localeCompare(b.name));
131
+ } catch {
132
+ return out;
133
+ }
134
+
135
+ for (const sibling of siblings) {
136
+ const name = uniqueProfileName(sibling.name, seen);
137
+ seen.add(name);
138
+ out.push({ name, home: sibling.home });
139
+ }
92
140
  return out;
93
141
  }
94
142
 
@@ -14,8 +14,9 @@
14
14
  *
15
15
  * PROFILES: Hermes profiles are separate home dirs, each with its OWN state.db
16
16
  * (`~/.hermes/state.db` for the default, `~/.hermes/profiles/<name>/state.db`
17
- * otherwise) see lib/hermes-profiles.ts. So every read here fans out across
18
- * profiles instead of assuming one DB.
17
+ * for upstream profiles, and `~/.hermes-<name>/state.db` for sibling homes)
18
+ * see lib/hermes-profiles.ts. So every read here fans out across profiles
19
+ * instead of assuming one DB.
19
20
  *
20
21
  * DB path override: set `HERMES_DB_PATH` (used by tests and to point at a copied
21
22
  * or remote state.db) — it collapses discovery to that single file. Set
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/env",
3
- "version": "16.3.4",
3
+ "version": "16.3.5",
4
4
  "keywords": [
5
5
  "react",
6
6
  "next",
@@ -139,7 +139,7 @@ var HmrTarget = /*#__PURE__*/ function(HmrTarget) {
139
139
  HmrTarget["Server"] = "server";
140
140
  return HmrTarget;
141
141
  }({});
142
- const nextVersion = "16.3.4";
142
+ const nextVersion = "16.3.5";
143
143
  const ArchName = (0, _os.arch)();
144
144
  const PlatformName = (0, _os.platform)();
145
145
  function infoLog(...args) {