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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ec94jt._.js → [root-of-the-server]__0oqrl71._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1g7s7di._.js → _0yq5_3a._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0xq-p5o._.js → _16pa9ht._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/{3fbsuyflx-2qi.js → 03h3b-uomi7_d.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03madt33xbsx2.js → 078dofaxvjgpd.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1-n1o5wh4wfy6.js → 1fqukzzowjflt.js} +1 -1
- package/.next/standalone/.next/static/chunks/1lyg_puttohbc.js +1 -0
- package/.next/standalone/.next/static/chunks/{28z6u4u7hdrnd.js → 2j92xul6y24_v.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1t6tmwe_1myeo.js → 2kj826h1w9r47.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3jffngwvgxm5q.js → 3mjfvng11xnoo.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0zlixskj11-yh.js → 3nkc2jlgyygda.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2lczelmvvc2yg.js → 3ob2161ap37ul.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2dj6xq9mueqb6.js → 3u1s8dsqiean5.js} +1 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/app.py +4 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/auth.py +29 -4
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +57 -14
- package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +17 -3
- package/.next/standalone/fp-cloud-cli/tests/test_request_id.py +111 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +3 -3
- package/.next/standalone/hermes-plugin/README.md +88 -10
- package/.next/standalone/hermes-plugin/__init__.py +813 -28
- package/.next/standalone/hermes-plugin/client.py +4 -2
- package/.next/standalone/hermes-plugin/ledger.py +9 -3
- package/.next/standalone/hermes-plugin/plugin.yaml +2 -2
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +8 -1
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +82 -7
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +23 -4
- package/.next/standalone/sdk/python/tests/test_evaluator_request_id.py +197 -0
- package/.next/standalone/sdk/python/uv.lock +9 -9
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +15 -12
- package/dist/worker.mjs +5 -2
- package/hermes-plugin/README.md +88 -10
- package/hermes-plugin/__init__.py +813 -28
- package/hermes-plugin/client.py +4 -2
- package/hermes-plugin/ledger.py +9 -3
- package/hermes-plugin/plugin.yaml +2 -2
- package/package.json +10 -10
- package/src/hooks/types.ts +7 -0
- package/.next/standalone/.next/static/chunks/0w8_n30wmnorq.js +0 -1
- /package/.next/standalone/.next/static/{Q2PyL8ZdGGdvVnn-3FrL2 → ECs8__hcNabYV58WfDh-3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{Q2PyL8ZdGGdvVnn-3FrL2 → ECs8__hcNabYV58WfDh-3}/_clientMiddlewareManifest.js +0 -0
- /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__(
|
|
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.
|
|
544
|
+
version = "2.8.0"
|
|
545
545
|
source = { registry = "https://pypi.org/simple" }
|
|
546
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
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/
|
|
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`:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|