failproofai 1.0.9 → 1.0.10

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 (128) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  30. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +14 -14
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  36. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  37. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  38. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  41. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  51. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  54. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  57. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  60. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
  64. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ec94jt._.js → [root-of-the-server]__0oqrl71._.js} +2 -2
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/{_1g7s7di._.js → _0yq5_3a._.js} +1 -1
  75. package/.next/standalone/.next/server/chunks/ssr/{_0xq-p5o._.js → _16pa9ht._.js} +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
  83. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  84. package/.next/standalone/.next/server/pages/404.html +1 -1
  85. package/.next/standalone/.next/server/pages/500.html +1 -1
  86. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  87. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  88. package/.next/standalone/.next/static/chunks/{3fbsuyflx-2qi.js → 03h3b-uomi7_d.js} +1 -1
  89. package/.next/standalone/.next/static/chunks/{03madt33xbsx2.js → 078dofaxvjgpd.js} +1 -1
  90. package/.next/standalone/.next/static/chunks/{1-n1o5wh4wfy6.js → 1fqukzzowjflt.js} +1 -1
  91. package/.next/standalone/.next/static/chunks/1lyg_puttohbc.js +1 -0
  92. package/.next/standalone/.next/static/chunks/{28z6u4u7hdrnd.js → 2j92xul6y24_v.js} +1 -1
  93. package/.next/standalone/.next/static/chunks/{1t6tmwe_1myeo.js → 2kj826h1w9r47.js} +1 -1
  94. package/.next/standalone/.next/static/chunks/{3jffngwvgxm5q.js → 3mjfvng11xnoo.js} +1 -1
  95. package/.next/standalone/.next/static/chunks/{0zlixskj11-yh.js → 3nkc2jlgyygda.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{2lczelmvvc2yg.js → 3ob2161ap37ul.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/{2dj6xq9mueqb6.js → 3u1s8dsqiean5.js} +1 -1
  98. package/.next/standalone/fp-cloud-cli/fp_cli/app.py +4 -0
  99. package/.next/standalone/fp-cloud-cli/fp_cli/auth.py +29 -4
  100. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +57 -14
  101. package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +17 -3
  102. package/.next/standalone/fp-cloud-cli/tests/test_request_id.py +111 -0
  103. package/.next/standalone/fp-cloud-cli/uv.lock +3 -3
  104. package/.next/standalone/hermes-plugin/README.md +88 -10
  105. package/.next/standalone/hermes-plugin/__init__.py +813 -28
  106. package/.next/standalone/hermes-plugin/client.py +4 -2
  107. package/.next/standalone/hermes-plugin/ledger.py +9 -3
  108. package/.next/standalone/hermes-plugin/plugin.yaml +2 -2
  109. package/.next/standalone/package.json +10 -10
  110. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +8 -1
  111. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +82 -7
  112. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +23 -4
  113. package/.next/standalone/sdk/python/tests/test_evaluator_request_id.py +197 -0
  114. package/.next/standalone/sdk/python/uv.lock +9 -9
  115. package/.next/standalone/server.js +1 -1
  116. package/dist/cli.mjs +15 -12
  117. package/dist/worker.mjs +5 -2
  118. package/hermes-plugin/README.md +88 -10
  119. package/hermes-plugin/__init__.py +813 -28
  120. package/hermes-plugin/client.py +4 -2
  121. package/hermes-plugin/ledger.py +9 -3
  122. package/hermes-plugin/plugin.yaml +2 -2
  123. package/package.json +10 -10
  124. package/src/hooks/types.ts +7 -0
  125. package/.next/standalone/.next/static/chunks/0w8_n30wmnorq.js +0 -1
  126. /package/.next/standalone/.next/static/{Q2PyL8ZdGGdvVnn-3FrL2 → ECs8__hcNabYV58WfDh-3}/_buildManifest.js +0 -0
  127. /package/.next/standalone/.next/static/{Q2PyL8ZdGGdvVnn-3FrL2 → ECs8__hcNabYV58WfDh-3}/_clientMiddlewareManifest.js +0 -0
  128. /package/.next/standalone/.next/static/{Q2PyL8ZdGGdvVnn-3FrL2 → ECs8__hcNabYV58WfDh-3}/_ssgManifest.js +0 -0
@@ -26,9 +26,24 @@ class FpCliError(click.ClickException):
26
26
 
27
27
  exit_code = 1
28
28
 
29
- def __init__(self, message: str, *, hint: Optional[str] = None) -> None:
29
+ def __init__(
30
+ self,
31
+ message: str,
32
+ *,
33
+ hint: Optional[str] = None,
34
+ request_id: Optional[str] = None,
35
+ ) -> None:
30
36
  super().__init__(message)
31
37
  self.hint = hint
38
+ # The id of the HTTP request that failed — the server's echo, else the
39
+ # one this CLI sent. On every typed error, not only ApiError, so a 403 or
40
+ # a 404 is as traceable in the server's logs as a 500.
41
+ self.request_id = request_id
42
+
43
+ @property
44
+ def ref(self) -> Optional[str]:
45
+ """What a person reads and pastes: the first 8 chars of the request id."""
46
+ return self.request_id[:8] if self.request_id else None
32
47
 
33
48
 
34
49
  class KeyModeUnsupportedError(FpCliError):
@@ -85,9 +100,8 @@ class ApiError(FpCliError):
85
100
  request_id: Optional[str] = None,
86
101
  hint: Optional[str] = None,
87
102
  ) -> None:
88
- super().__init__(message, hint=hint)
103
+ super().__init__(message, hint=hint, request_id=request_id)
89
104
  self.status = status
90
- self.request_id = request_id
91
105
 
92
106
  def format_message(self) -> str:
93
107
  parts = [self.message]
@@ -0,0 +1,111 @@
1
+ """Every CLI request carries its own id, and every failure names it.
2
+
3
+ A customer pastes `ref 4bf92f35`; support finds every server log line of that
4
+ request with one query. That only works if (a) each HTTP request has a distinct
5
+ id, (b) every typed error — not just ApiError — carries it, and (c) the id is
6
+ still there when something in front of the API answered without echoing one.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+
14
+ import httpx
15
+ import pytest
16
+ import respx
17
+
18
+ from fp_cli import client as api
19
+ from fp_cli.app import app
20
+ from fp_cli.client import ClientContext
21
+ from fp_cli.errors import ApiError, AuthError, ForbiddenError, NetworkError, NotFoundError
22
+
23
+ BASE = "http://dash.test"
24
+ TRACE_ID = re.compile(r"^[0-9a-f]{32}$")
25
+ SERVER_ID = "4bf92f3577b34da6a3ce929d0e0e4736"
26
+
27
+
28
+ def ctx() -> ClientContext:
29
+ return ClientContext(base_url=BASE, token="tok")
30
+
31
+
32
+ @respx.mock
33
+ def test_each_request_gets_its_own_trace_id():
34
+ route = respx.get(f"{BASE}/api/sessions").mock(
35
+ return_value=httpx.Response(200, json={"sessions": [], "next_cursor": None})
36
+ )
37
+ api.list_sessions(ctx())
38
+ api.list_sessions(ctx())
39
+ ids = [call.request.headers["x-request-id"] for call in route.calls]
40
+ assert len(ids) == 2
41
+ assert all(TRACE_ID.match(i) for i in ids), ids
42
+ assert ids[0] != ids[1]
43
+
44
+
45
+ @pytest.mark.parametrize(
46
+ ("status", "error_type"),
47
+ [(401, AuthError), (403, ForbiddenError), (404, NotFoundError), (500, ApiError)],
48
+ )
49
+ @respx.mock
50
+ def test_every_typed_error_carries_the_servers_request_id(status, error_type):
51
+ respx.get(f"{BASE}/api/sessions").mock(
52
+ return_value=httpx.Response(status, json={"error": "x"}, headers={"x-request-id": SERVER_ID})
53
+ )
54
+ with pytest.raises(error_type) as excinfo:
55
+ api.list_sessions(ctx())
56
+ assert excinfo.value.request_id == SERVER_ID
57
+ assert excinfo.value.ref == SERVER_ID[:8]
58
+
59
+
60
+ @respx.mock
61
+ def test_without_an_echo_the_error_names_the_id_we_sent():
62
+ # A proxy or front door answered: no x-request-id on the response. The id we
63
+ # sent is still the one in the dashboard's access logs.
64
+ route = respx.get(f"{BASE}/api/sessions").mock(return_value=httpx.Response(502, text="bad gateway"))
65
+ with pytest.raises(ApiError) as excinfo:
66
+ api.list_sessions(ctx())
67
+ sent = route.calls.last.request.headers["x-request-id"]
68
+ assert excinfo.value.request_id == sent
69
+
70
+
71
+ @respx.mock
72
+ def test_an_unsane_echo_is_not_put_on_the_error():
73
+ route = respx.get(f"{BASE}/api/sessions").mock(
74
+ return_value=httpx.Response(500, json={"error": "x"}, headers={"x-request-id": "a b c"})
75
+ )
76
+ with pytest.raises(ApiError) as excinfo:
77
+ api.list_sessions(ctx())
78
+ assert excinfo.value.request_id == route.calls.last.request.headers["x-request-id"]
79
+
80
+
81
+ @respx.mock
82
+ def test_a_network_error_names_the_id_it_sent():
83
+ route = respx.get(f"{BASE}/api/sessions").mock(side_effect=httpx.ConnectError("refused"))
84
+ with pytest.raises(NetworkError) as excinfo:
85
+ api.list_sessions(ctx())
86
+ assert excinfo.value.request_id == route.calls.last.request.headers["x-request-id"]
87
+ # Exit code contract unchanged.
88
+ assert excinfo.value.exit_code == 3
89
+
90
+
91
+ @respx.mock
92
+ def test_the_human_error_line_shows_the_ref(logged_in, runner):
93
+ respx.get(f"{BASE}/api/sessions").mock(
94
+ return_value=httpx.Response(403, json={"error": "forbidden"}, headers={"x-request-id": SERVER_ID})
95
+ )
96
+ result = runner.invoke(app, ["sessions"])
97
+ assert result.exit_code == 5
98
+ assert f"ref {SERVER_ID[:8]}" in result.stderr
99
+
100
+
101
+ @respx.mock
102
+ def test_the_json_envelope_carries_the_full_id_for_non_api_errors(logged_in, runner):
103
+ respx.get(f"{BASE}/api/sessions").mock(
104
+ return_value=httpx.Response(403, json={"error": "forbidden"}, headers={"x-request-id": SERVER_ID})
105
+ )
106
+ result = runner.invoke(app, ["--json", "sessions"])
107
+ assert result.exit_code == 5
108
+ data = json.loads(result.stdout)
109
+ assert data["request_id"] == SERVER_ID
110
+ # The ref is for humans; the envelope's message stays clean.
111
+ assert "ref " not in data["error"]
@@ -541,9 +541,9 @@ wheels = [
541
541
 
542
542
  [[package]]
543
543
  name = "urllib3"
544
- version = "2.7.0"
544
+ version = "2.8.0"
545
545
  source = { registry = "https://pypi.org/simple" }
546
- sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
546
+ sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" }
547
547
  wheels = [
548
- { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
548
+ { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
549
549
  ]
@@ -7,6 +7,11 @@ translates structured verdicts into Hermes-native hook behavior.
7
7
  Validated contract: Hermes 0.21.3 at
8
8
  `4d55ca91656ac5f83e1506679b7f81e0238e5e16`. The plugin uses only documented
9
9
  manifest v2 fields, `PluginContext` methods/state, and keyword hook payloads.
10
+ Hermes 0.20.0 is also supported: its `PluginContext` has no `get_config` or
11
+ `state` (both arrived in 0.20.1), so the plugin reads the same
12
+ `plugins.entries.failproofai` settings through Hermes' config loader and keeps
13
+ its ledger in the directory `state.data_dir` would name, which a later Hermes
14
+ upgrade reuses.
10
15
 
11
16
  ## Installation
12
17
 
@@ -40,6 +45,14 @@ Legacy FailproofAI shell hooks are removed during migration. Operator-owned
40
45
  hooks and unrelated plugin settings are preserved. No dashboard deployment or
41
46
  backtest is part of installation.
42
47
 
48
+ ## Supported Hermes versions
49
+
50
+ Hermes 0.20.0 and later. Tested on 0.20.0 (v2026.8.3), 0.20.1 (v2026.8.13),
51
+ 0.20.6 (v2026.8.27), 0.21.0 (v2026.8.31), 0.21.3 (v2026.9.14), 0.21.5
52
+ (v2026.9.24, the latest release) and upstream main (2026-10-05).
53
+ Hermes 0.20.0 has no `ctx.get_config` or `ctx.state`; the plugin detects that and
54
+ reads the same settings and state directory another way.
55
+
43
56
  ## Runtime path
44
57
 
45
58
  ```text
@@ -49,6 +62,8 @@ Hermes pre_tool_call
49
62
  -> warm TypeScript policy worker
50
63
  -> structured allow | deny | instruct verdict
51
64
  -> Hermes-native return value
65
+ Hermes tool_execution middleware (wraps the real call)
66
+ -> instruct reminder placed at the start of the call's own result
52
67
  ```
53
68
 
54
69
  There is no cloud request and no new CLI process in the tool-call path.
@@ -57,16 +72,72 @@ There is no cloud request and no new CLI process in the tool-call path.
57
72
 
58
73
  - `allow`: return no directive; Hermes runs the tool.
59
74
  - `deny`: always return `{"action":"block","message":"..."}`.
60
- - `instruct`: persist delivery state, block the first attempt with a
61
- model-visible `FAILPROOF INSTRUCTION`, keep the same API request blocked, then
62
- allow a later API iteration for the same instruction scope.
63
-
64
- The default instruction scope is profile + session + task + turn + policy
65
- fingerprint + canonical tool. The persistent SQLite ledger lives below Hermes'
66
- profile-scoped plugin data directory. Two distinct instruction interruptions
67
- are allowed per turn by default; after that, advisory instructions fail open so
68
- they cannot create an infinite retry loop. A real deny is never bypassed by the
69
- ledger.
75
+ - `instruct`: the call runs, and the reminder is placed at the **start** of its
76
+ result, so it survives the 1,500-character preview Hermes keeps of a large
77
+ result:
78
+ - a JSON object result gets `"failproof_policy_reminder"` as its first key
79
+ (the rest is unchanged, so `json.loads` consumers such as `execute_code`
80
+ scripts keep working). A field of that name the tool returned itself is
81
+ moved to `"tool_failproof_policy_reminder"`, never merged into the
82
+ reminder;
83
+ - other text gets a leading `[FailproofAI policy reminder (<policies>)] ...`
84
+ line (an `Error...` result keeps that prefix and gets the line at its end);
85
+ - a list of content blocks or a multimodal envelope gets a leading text block,
86
+ leaving image blocks intact.
87
+
88
+ Tool calls made by an `execute_code` script hand their reminder to the outer
89
+ `execute_code` result, which is the one the model reads, and the script gets
90
+ its own result untouched, so nothing it prints, writes or sends carries the
91
+ reminder. (With no open outer call to carry it, the inner result keeps it.)
92
+ A result Hermes reports as blocked (`{"error": ...}` only) is never
93
+ annotated. This uses Hermes' `tool_execution` middleware
94
+ (`ctx.register_middleware`, present unchanged in 0.20.0, 0.21.x and main).
95
+
96
+ A policy's reminder (same policy and reason) is attached once per Hermes
97
+ session + turn, also when calls run in parallel; a later turn gets it again.
98
+ Hermes saves a result to a file once it passes 15% of the model's context
99
+ window (4 characters per token, clamped to 8,000–100,000 characters;
100
+ `read_file` is never saved) and shows the model a 1,500-character preview.
101
+ When the full reminder would take the result past 7,500 characters it is
102
+ compacted: at most three reminders, each policy name cut to 80 characters and
103
+ each reason to 160, so it always fits inside that preview. It is always
104
+ attached: on a model whose threshold is the 8,000 floor, a result just under
105
+ it may be saved because of the reminder, which is then the head of the
106
+ preview.
107
+
108
+ `instruct` therefore no longer stops the action first on Hermes: use `deny`
109
+ when an action must not happen at all.
110
+
111
+ Where Hermes offers no `tool_execution` middleware, `instruct` falls back to
112
+ holding the call once: persist delivery state, return model-visible
113
+ `FailproofAI policy guidance (...)` ("apply it, then continue; repeating the
114
+ same call is allowed"), keep the same API request held, then allow a later API
115
+ iteration. That fallback's scope is profile + session + task + turn + policy
116
+ fingerprint — not the tool, so moving the same work to another tool (for
117
+ example `execute_code`) is not held again.
118
+ The persistent SQLite ledger lives below Hermes' profile-scoped plugin data
119
+ directory. Two distinct instruction interruptions are allowed per turn by
120
+ default; after that, advisory instructions fail open so they cannot create an
121
+ infinite retry loop. A real deny is never bypassed by the ledger.
122
+
123
+ Every model turn also receives a short protocol note (`pre_llm_call`): a
124
+ `failproof_policy_reminder` is an operator rule added by FailproofAI (not tool
125
+ data) about a call that ran normally and is never to be quoted, repeated or
126
+ mentioned in replies, messages or files, a FailproofAI block means that action
127
+ must not run by any route, and `FailproofAI policy guidance` held a call only once.
128
+
129
+ No hook raises: `pre_tool_call` blocks on any internal error (Hermes 0.20.0 and
130
+ 0.21.x would treat a raised exception as allow), observer hooks log and return,
131
+ and the middleware falls back to the plain result. One evaluation, connect
132
+ included, is capped at 25 seconds (`evaluation_timeout_ms`, default 12000):
133
+ Hermes 0.21.x abandons a `pre_tool_call` after 30 seconds and then blocks every
134
+ call for 60. Observer hooks wait at most 2 seconds, so against a hung daemon a
135
+ blocked call costs the evaluation deadline plus 2 seconds, not twice the
136
+ deadline.
137
+
138
+ Tool names are canonicalized in the daemon: Hermes 0.21's `process_manage`,
139
+ `cronjob_manage` and `todo_list` match policies written for 0.20's `process`,
140
+ `cronjob` and `todo`.
70
141
 
71
142
  ## Configuration
72
143
 
@@ -137,6 +208,13 @@ hermes plugins doctor ~/.hermes/plugins/failproofai --ci
137
208
  hermes logs --level WARNING
138
209
  ```
139
210
 
211
+ Each `register()` writes `heartbeat.json` beside `instructions.db` in the
212
+ profile's plugin data directory (`plugin-data/agent-plugin-failproofai-5296f299/`):
213
+ pid, Hermes version, `HERMES_HOME`, profile, the plugin's real path and version,
214
+ the hooks and middleware Hermes accepted, `register_ok` and a timestamp. It is
215
+ the proof of what a running Hermes process actually loaded, which
216
+ `hermes plugins list` (config only) cannot show; Hermes' log gets one INFO line.
217
+
140
218
  Use a temporary `HERMES_HOME` for development and compatibility tests. Remove
141
219
  the integration with:
142
220