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
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.5",
3
+ "version": "1.0.6-beta.0",
4
4
  "description": "Observability and enforcement for AI agent harnesses. 39 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -12,6 +12,7 @@
12
12
  "scripts/",
13
13
  "lib/",
14
14
  "pi-extension/",
15
+ "hermes-plugin/",
15
16
  "openclaw-plugin/",
16
17
  ".next/standalone/",
17
18
  "dist/",
@@ -80,15 +81,15 @@
80
81
  "access": "public"
81
82
  },
82
83
  "devDependencies": {
83
- "@anthropic-ai/sdk": "^0.123.0",
84
+ "@anthropic-ai/sdk": "^0.125.0",
84
85
  "@mdx-js/mdx": "^3.1.1",
85
86
  "@tailwindcss/postcss": "^4.3.1",
86
87
  "@tanstack/react-virtual": "^3.14.10",
87
88
  "@testing-library/jest-dom": "^7.0.0",
88
89
  "@testing-library/react": "^16.3.2",
89
90
  "@testing-library/user-event": "^14.6.5",
90
- "@types/node": "26.4.1",
91
- "@types/react": "19.2.18",
91
+ "@types/node": "26.5.1",
92
+ "@types/react": "19.3.0",
92
93
  "@types/react-dom": "^19.2.3",
93
94
  "@vitejs/plugin-react": "^6.1.0",
94
95
  "clsx": "^2.1.1",
@@ -121,15 +122,15 @@
121
122
  "browserslist": "4.28.8"
122
123
  },
123
124
  "optionalDependencies": {
124
- "@failproofai/failproofaid-linux-x64": "1.0.5",
125
- "@failproofai/failproofaid-linux-arm64": "1.0.5",
126
- "@failproofai/failproofaid-darwin-x64": "1.0.5",
127
- "@failproofai/failproofaid-darwin-arm64": "1.0.5"
125
+ "@failproofai/failproofaid-linux-x64": "1.0.6-beta.0",
126
+ "@failproofai/failproofaid-linux-arm64": "1.0.6-beta.0",
127
+ "@failproofai/failproofaid-darwin-x64": "1.0.6-beta.0",
128
+ "@failproofai/failproofaid-darwin-arm64": "1.0.6-beta.0"
128
129
  },
129
130
  "failproofaidBinaries": {
130
- "linux-x64": "5fb4c8a993af9279e46306803c68d6650ea57290bbd7ec92795a7f7b696bb506",
131
- "linux-arm64": "21c502073649d9c12710fc2707b9dee08b7ac1528b8544b85a755017c8fdff0e",
132
- "darwin-x64": "5e02c2f4887793c64f5553d8d00fd21507f88dce1a6869ca1aca2f6468129373",
133
- "darwin-arm64": "65be288a3485ca86869580a036e6c7e9b7b5660a6589143b375586b6c28b5c84"
131
+ "linux-x64": "38637a91db9a0e5683bbfbfffbba3d65e421960c3135510f23541df5f9d60478",
132
+ "linux-arm64": "b9a2d9126fb9816b7743ec5b068d36d6515fa3a93fc2e3801fbe3a7a1b2143bf",
133
+ "darwin-x64": "a06675cd28b3d2f54d45a28f527643841a69006dd68f989dad654806b190948b",
134
+ "darwin-arm64": "6ef65b956ebff944c38cf2d65b6dc1066384aba7952d7d19c4805c1f1ba593ba"
134
135
  }
135
136
  }
@@ -11,6 +11,11 @@
11
11
  * - Frontmatter YAML (class A): the model re-emits an inner `"` unescaped into
12
12
  * a double-quoted `title:`/`description:` value, breaking the YAML. This is
13
13
  * the class that failed run 29575781632; `findFrontmatterError` catches it.
14
+ * - Empty block (class A2): the model emits a second opening `---`, so the
15
+ * block Mintlify reads closes before any key is inside it and the whole
16
+ * frontmatter renders as body text. YAML calls a leading `---` a
17
+ * document-start marker and parses it clean, so class A is blind to this;
18
+ * `mintlifyFrontmatterBlock` is the second view that is not.
14
19
  * - Key parity (class B): the model drops the frontmatter block entirely, or
15
20
  * renames a key. A dropped block is still *valid YAML* (mintlify tolerates
16
21
  * it, deriving the title from the slug), so only comparing against the
@@ -52,6 +57,34 @@ function frontmatterKeys(page: string): string[] | null {
52
57
  }
53
58
  }
54
59
 
60
+ /**
61
+ * The frontmatter block as MINTLIFY reads it: opened by a first line that is
62
+ * exactly `---`, closed by the FIRST line after it that is `---` — trailing
63
+ * spaces or tabs allowed, as `FRONTMATTER_RE` allows them, but never leading
64
+ * ones: an indented `---` is content, not a delimiter. `null` when the page
65
+ * does not open with a delimiter at all.
66
+ *
67
+ * Deliberately NOT reusing `FRONTMATTER_RE`. That matcher exists to feed
68
+ * `YAML.parse`, and YAML reads a bare `---` as a document-START marker rather
69
+ * than as a terminator — so a page beginning `---\n---\ntitle: …` parses there
70
+ * as a perfectly clean `{title: …}` while Mintlify closes the block on line 2
71
+ * and renders every key as body text. The reader gets a literal `title: "…"` at
72
+ * the top of the page and no title on it.
73
+ *
74
+ * That is not hypothetical: 102 published pages across seven locales shipped
75
+ * exactly that way, invisible to every other check in this file, because both
76
+ * of them asked YAML. The two views have to be compared, never shared.
77
+ */
78
+ function mintlifyFrontmatterBlock(page: string): string | null {
79
+ const lines = page.split(/\r?\n/);
80
+ if (lines[0] !== "---") return null;
81
+ const close = lines.findIndex(
82
+ (line, i) => i > 0 && /^---[ \t]*$/.test(line),
83
+ );
84
+ if (close < 0) return null;
85
+ return lines.slice(1, close).join("\n");
86
+ }
87
+
55
88
  /**
56
89
  * A ±2-line window around `line` (1-based), the failing line prefixed `> ` and
57
90
  * its neighbours ` `. Empty string when `line` is undefined.
@@ -93,6 +126,21 @@ export async function findTranslationError(
93
126
  );
94
127
  }
95
128
 
129
+ // Class A2: a block that is EMPTY as Mintlify delimits it. A stray opening
130
+ // delimiter (`---\n---\ntitle: …`) is the shape that does this, and it is
131
+ // invisible to everything above — see `mintlifyFrontmatterBlock`. Checked for
132
+ // every source shape, like the YAML above: the model adding this to a
133
+ // frontmatter-less page breaks that page just as thoroughly.
134
+ const mintlifyBlock = mintlifyFrontmatterBlock(rendered);
135
+ if (mintlifyBlock !== null && mintlifyBlock.trim() === "") {
136
+ return (
137
+ "The YAML frontmatter block is empty. The page opens with `---` and the " +
138
+ "very next line is `---` again, which closes the block before any key is " +
139
+ "in it — every `title:`/`description:`/`icon:` line below then renders as " +
140
+ "body text and the page has no title. Emit exactly one opening `---`."
141
+ );
142
+ }
143
+
96
144
  const sourceKeys = frontmatterKeys(source);
97
145
  if (sourceKeys) {
98
146
  // The source has frontmatter, so the translation must carry the same keys.
@@ -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