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