failproofai 0.0.13-beta.0 → 0.0.13-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- 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 +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 +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- 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 +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 +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -2
- 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 +3 -2
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +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 +2 -2
- 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 +2 -2
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +2 -2
- 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 +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 +1 -1
- 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 +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +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 +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- 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 +3 -2
- 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 +3 -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 +2 -2
- 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/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cefjd9._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ekj92q._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hv08mx._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0obaznk._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xvsnvt._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12v-lma._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__136y39g._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__13h8pzr._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hkdkmz._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1i9rknc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0cycwg6._.js → [root-of-the-server]__1tvn4wf._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_1z062h5._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +5 -5
- 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]__015_i4t._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04bqvos._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1-m1rhq._.js → [root-of-the-server]__099afy6._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dqgs96._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1uvfwgr._.js → [root-of-the-server]__0hdsupo._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0q5rfxg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qukut7._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ehe24g._.js → [root-of-the-server]__0ynf7tx._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +245 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d8omgc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ql-mch._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0bbpv2d._.js → [root-of-the-server]__1qlh4jx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0ul5rf_._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_11v0g3z._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.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/lib_hermes-projects_ts_05b8-it._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1u91ynr.js → node_modules_html-to-image_es_index_0pv57nx.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 +10 -10
- package/.next/standalone/.next/static/chunks/{0azkpcy8eb1gr.js → 0b6rdtcge_3ec.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1zxtyef-iqede.js → 0pj_6lbbjd000.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1h569gia53ant.js → 0xnrgonfdeivm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{39rn602wszcbd.js → 105w_oznq-van.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0byua6ng1f_ka.js → 13tbm6i1swiru.js} +1 -1
- package/.next/standalone/.next/static/chunks/{41mhnhfkmglpp.js → 1fhuj4fjovzp5.js} +1 -1
- package/.next/standalone/.next/static/chunks/209ful1jrphvy.js +1 -0
- package/.next/standalone/.next/static/chunks/{3jka0hvhqouyv.js → 23odnajn2ltwu.js} +1 -1
- package/.next/standalone/.next/static/chunks/25s52un3k_wht.css +2 -0
- package/.next/standalone/.next/static/chunks/36r2vgi6854za.js +6 -0
- package/.next/standalone/.next/static/chunks/3b77kulbdbrqw.js +1 -0
- package/.next/standalone/.next/static/chunks/3rjg-1s5ouu5r.js +1 -0
- package/.next/standalone/app/components/sessions-list.tsx +19 -0
- package/.next/standalone/app/project/[name]/page.tsx +9 -4
- package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +26 -5
- package/.next/standalone/assets/logos/hermes-dark.svg +1 -0
- package/.next/standalone/assets/logos/hermes-light.svg +1 -0
- package/.next/standalone/lib/cli-registry.ts +6 -1
- package/.next/standalone/lib/download-session.ts +16 -0
- package/.next/standalone/lib/hermes-projects.ts +164 -0
- package/.next/standalone/lib/hermes-sessions.ts +273 -0
- package/.next/standalone/lib/projects.ts +14 -3
- package/.next/standalone/lib/sqlite-reader.ts +102 -0
- package/.next/standalone/package.json +5 -3
- package/.next/standalone/server.js +1 -1
- package/README.md +10 -1
- package/bin/failproofai.mjs +17 -16
- package/dist/cli.mjs +7762 -226
- package/lib/cli-registry.ts +6 -1
- package/lib/download-session.ts +16 -0
- package/lib/hermes-projects.ts +164 -0
- package/lib/hermes-sessions.ts +273 -0
- package/lib/projects.ts +14 -3
- package/lib/sql-js-asm.d.ts +26 -0
- package/lib/sqlite-reader.ts +102 -0
- package/package.json +5 -3
- package/src/audit/cli-adapters/hermes.ts +58 -0
- package/src/audit/cli-adapters/index.ts +6 -0
- package/src/hooks/handler.ts +8 -0
- package/src/hooks/install-prompt.ts +7 -3
- package/src/hooks/integrations.ts +179 -5
- package/src/hooks/policy-evaluator.ts +42 -0
- package/src/hooks/resolve-transcript-path.ts +4 -0
- package/src/hooks/tool-name-canonicalize.ts +6 -0
- package/src/hooks/types.ts +67 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dwpg-h._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0lnenda._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__13i_sva._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_mqemn._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m2_4t0._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_0-tu4ot._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_0ttxbz7._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_1bnh1y0._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_1epycqa._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_1wpdcgo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0808sha._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e4-6d8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0f62vu9._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g253ve._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0k65l27._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vxf0_g._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12mcauo._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e-x7j4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mt35_w._.js +0 -221
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pcxxwg._.js +0 -4
- package/.next/standalone/.next/static/chunks/03zg3ms7uaoq3.js +0 -1
- package/.next/standalone/.next/static/chunks/0f7d7hnbh4djs.js +0 -1
- package/.next/standalone/.next/static/chunks/1o0xa47736gi9.css +0 -2
- package/.next/standalone/.next/static/chunks/2ks4pzu5b-l_q.js +0 -6
- package/.next/standalone/.next/static/chunks/2wbuxnsvux4di.js +0 -1
- /package/.next/standalone/.next/static/{W6ELw5VwHTFy2amS2px6e → 31ZoDDYKHEIN_TrgKv85t}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{W6ELw5VwHTFy2amS2px6e → 31ZoDDYKHEIN_TrgKv85t}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{W6ELw5VwHTFy2amS2px6e → 31ZoDDYKHEIN_TrgKv85t}/_ssgManifest.js +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "0.0.13-beta.
|
|
3
|
+
"version": "0.0.13-beta.1",
|
|
4
4
|
"description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"predev": "bun run build:cli && bun link",
|
|
24
24
|
"dev": "FAILPROOFAI_TELEMETRY_DISABLED=1 bun scripts/dev.ts --port 8020",
|
|
25
|
-
"build:cli": "bun build --target=node --format=esm --outfile=dist/cli.mjs bin/failproofai.mjs --external posthog-node && node -e \"const fs=require('fs');const c=fs.readFileSync('dist/cli.mjs','utf8');fs.writeFileSync('dist/cli.mjs',c.replace('#!/usr/bin/env bun','#!/usr/bin/env node').replace('// @bun\\n',''))\"",
|
|
25
|
+
"build:cli": "bun build --target=node --format=esm --outfile=dist/cli.mjs bin/failproofai.mjs --external posthog-node --external sql.js && node -e \"const fs=require('fs');const c=fs.readFileSync('dist/cli.mjs','utf8');fs.writeFileSync('dist/cli.mjs',c.replace('#!/usr/bin/env bun','#!/usr/bin/env node').replace('// @bun\\n',''))\"",
|
|
26
26
|
"build": "bun build --target=node --format=cjs --outfile=dist/index.js src/index.ts && bun run build:cli && bun --bun next build && node -e \"const {cpSync}=require('fs');cpSync('.next/static','.next/standalone/.next/static',{recursive:true});\" && node scripts/prune-standalone.mjs",
|
|
27
27
|
"prestart": "bun run build:cli && bun link",
|
|
28
28
|
"start": "FAILPROOFAI_TELEMETRY_DISABLED=1 bun scripts/start.ts",
|
|
@@ -99,7 +99,9 @@
|
|
|
99
99
|
"dependencies": {
|
|
100
100
|
"html-to-image": "^1.11.13",
|
|
101
101
|
"html2canvas": "^1.4.1",
|
|
102
|
-
"posthog-node": "^5.37.1"
|
|
102
|
+
"posthog-node": "^5.37.1",
|
|
103
|
+
"sql.js": "^1.14.1",
|
|
104
|
+
"yaml": "^2.9.0"
|
|
103
105
|
},
|
|
104
106
|
"overrides": {
|
|
105
107
|
"postcss": "8.5.14",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermes (hermes-agent) transcript adapter — AUDIT-ONLY (Pillar 2).
|
|
3
|
+
*
|
|
4
|
+
* Hermes keeps every gateway user's sessions in one SQLite DB. We read it
|
|
5
|
+
* directly via the bundled sql.js reader (lib/hermes-projects.ts enumerates the
|
|
6
|
+
* `sessions` table; lib/hermes-sessions.ts parses each session's `messages`),
|
|
7
|
+
* producing the same LogEntry[] shape the other adapters do — so
|
|
8
|
+
* `logEntriesToEvents` handles the rest.
|
|
9
|
+
*
|
|
10
|
+
* Gateway sessions have no `cwd` (Slack/Telegram runs aren't in a repo), so they
|
|
11
|
+
* group by `source` (slack/telegram/cli/cron) instead of working directory.
|
|
12
|
+
*/
|
|
13
|
+
import { getHermesSessions } from "../../../lib/hermes-projects";
|
|
14
|
+
import { getHermesSessionLog } from "../../../lib/hermes-sessions";
|
|
15
|
+
import type { NormalizedToolEvent, TranscriptMetadata } from "../types";
|
|
16
|
+
import type { ListOpts } from "./claude";
|
|
17
|
+
import { logEntriesToEvents } from "./shared";
|
|
18
|
+
|
|
19
|
+
export async function listHermesTranscriptMetadata(
|
|
20
|
+
opts: ListOpts = {},
|
|
21
|
+
): Promise<TranscriptMetadata[]> {
|
|
22
|
+
// `audit --project <cwd>` filters on working directory; gateway sessions have
|
|
23
|
+
// none, so Hermes contributes nothing to a cwd-scoped audit.
|
|
24
|
+
if (opts.projects && opts.projects.length > 0) return [];
|
|
25
|
+
|
|
26
|
+
const sinceMs = opts.sinceMs ?? 0;
|
|
27
|
+
const sessions = await getHermesSessions();
|
|
28
|
+
const out: TranscriptMetadata[] = [];
|
|
29
|
+
for (const s of sessions) {
|
|
30
|
+
if (s.mtimeMs < sinceMs) continue;
|
|
31
|
+
if (s.messageCount <= 0 && !s.hasMessages) continue; // empty → no events (message_count can lag; trust real messages)
|
|
32
|
+
out.push({
|
|
33
|
+
cli: "hermes",
|
|
34
|
+
// Group by channel; gateway sessions are cwd-less.
|
|
35
|
+
projectName: s.source ? `hermes:${s.source}` : "hermes",
|
|
36
|
+
sessionId: s.sessionId,
|
|
37
|
+
transcriptPath: `hermes://${s.sessionId}`,
|
|
38
|
+
mtimeMs: s.mtimeMs,
|
|
39
|
+
// message_count is stable for an ended session, so (mtime, size) forms a
|
|
40
|
+
// real per-transcript cache key — an ended session is parsed once.
|
|
41
|
+
sizeBytes: s.messageCount,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function streamHermesEvents(
|
|
48
|
+
meta: TranscriptMetadata,
|
|
49
|
+
): Promise<NormalizedToolEvent[]> {
|
|
50
|
+
const log = await getHermesSessionLog(meta.sessionId);
|
|
51
|
+
if (!log) return [];
|
|
52
|
+
return logEntriesToEvents(log.entries, {
|
|
53
|
+
cli: "hermes",
|
|
54
|
+
sessionId: meta.sessionId,
|
|
55
|
+
transcriptPath: meta.transcriptPath,
|
|
56
|
+
cwd: log.cwd ?? "",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -18,6 +18,7 @@ import { listCursorTranscriptMetadata, streamCursorEvents } from "./cursor";
|
|
|
18
18
|
import { listOpenCodeTranscriptMetadata, streamOpenCodeEvents } from "./opencode";
|
|
19
19
|
import { listPiTranscriptMetadata, streamPiEvents } from "./pi";
|
|
20
20
|
import { listGeminiTranscriptMetadata, streamGeminiEvents } from "./gemini";
|
|
21
|
+
import { listHermesTranscriptMetadata, streamHermesEvents } from "./hermes";
|
|
21
22
|
|
|
22
23
|
export type { ListOpts };
|
|
23
24
|
|
|
@@ -63,6 +64,11 @@ export const ADAPTERS: Record<IntegrationType, CliAdapter> = {
|
|
|
63
64
|
listTranscripts: listGeminiTranscriptMetadata,
|
|
64
65
|
streamEvents: streamGeminiEvents,
|
|
65
66
|
},
|
|
67
|
+
hermes: {
|
|
68
|
+
cli: "hermes",
|
|
69
|
+
listTranscripts: listHermesTranscriptMetadata,
|
|
70
|
+
streamEvents: streamHermesEvents,
|
|
71
|
+
},
|
|
66
72
|
};
|
|
67
73
|
|
|
68
74
|
export function getAdapter(cli: IntegrationType): CliAdapter {
|
package/src/hooks/handler.ts
CHANGED
|
@@ -13,12 +13,14 @@ import type {
|
|
|
13
13
|
CursorHookEventType,
|
|
14
14
|
PiHookEventType,
|
|
15
15
|
GeminiHookEventType,
|
|
16
|
+
HermesHookEventType,
|
|
16
17
|
} from "./types";
|
|
17
18
|
import {
|
|
18
19
|
CODEX_EVENT_MAP,
|
|
19
20
|
CURSOR_EVENT_MAP,
|
|
20
21
|
PI_EVENT_MAP,
|
|
21
22
|
GEMINI_EVENT_MAP,
|
|
23
|
+
HERMES_EVENT_MAP,
|
|
22
24
|
} from "./types";
|
|
23
25
|
import { canonicalizeToolName, canonicalizeToolInput } from "./tool-name-canonicalize";
|
|
24
26
|
import type { PolicyFunction, PolicyResult } from "./policy-types";
|
|
@@ -65,6 +67,12 @@ function canonicalizeEventType(raw: string, cli: IntegrationType): HookEventType
|
|
|
65
67
|
const mapped = GEMINI_EVENT_MAP[raw as GeminiHookEventType];
|
|
66
68
|
if (mapped) return mapped;
|
|
67
69
|
}
|
|
70
|
+
if (cli === "hermes") {
|
|
71
|
+
// Hermes sends snake_case event names (pre_tool_call, on_session_start, …);
|
|
72
|
+
// map to PascalCase. Has no turn-end Stop event, so no Stop mapping exists.
|
|
73
|
+
const mapped = HERMES_EVENT_MAP[raw as HermesHookEventType];
|
|
74
|
+
if (mapped) return mapped;
|
|
75
|
+
}
|
|
68
76
|
// claude / copilot / unknown — already PascalCase, pass through.
|
|
69
77
|
// HOOK_EVENT_TYPES type-checks downstream.
|
|
70
78
|
return raw as HookEventType;
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as readline from "node:readline";
|
|
13
13
|
import { BUILTIN_POLICIES } from "./builtin-policies";
|
|
14
|
-
import { detectInstalledClis, getIntegration } from "./integrations";
|
|
15
|
-
import {
|
|
14
|
+
import { detectInstalledClis, getIntegration, listInstallableIds } from "./integrations";
|
|
15
|
+
import { type IntegrationType } from "./types";
|
|
16
16
|
import { trackHookEvent } from "./hook-telemetry";
|
|
17
17
|
import { getInstanceId } from "../../lib/telemetry-id";
|
|
18
18
|
|
|
@@ -139,7 +139,11 @@ export function buildCliMenuOptions(
|
|
|
139
139
|
): { options: CliMenuOption[]; undetected: IntegrationType[] } {
|
|
140
140
|
const undetected: IntegrationType[] =
|
|
141
141
|
action === "install"
|
|
142
|
-
?
|
|
142
|
+
? // Only CLIs that support live-hook install — a future audit-only CLI
|
|
143
|
+
// (one with no INTEGRATIONS entry) has no install path and must not
|
|
144
|
+
// appear as a forward-install option. (hermes IS installable, so it
|
|
145
|
+
// correctly appears here.)
|
|
146
|
+
listInstallableIds().filter((id) => !detected.includes(id))
|
|
143
147
|
: [];
|
|
144
148
|
|
|
145
149
|
const options: CliMenuOption[] = [];
|
|
@@ -11,6 +11,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "
|
|
|
11
11
|
import { resolve, dirname } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
|
+
import { parseDocument, type Document } from "yaml";
|
|
14
15
|
import {
|
|
15
16
|
HOOK_EVENT_TYPES,
|
|
16
17
|
HOOK_SCOPES,
|
|
@@ -27,6 +28,8 @@ import {
|
|
|
27
28
|
PI_HOOK_SCOPES,
|
|
28
29
|
GEMINI_HOOK_EVENT_TYPES,
|
|
29
30
|
GEMINI_HOOK_SCOPES,
|
|
31
|
+
HERMES_HOOK_EVENT_TYPES,
|
|
32
|
+
HERMES_HOOK_SCOPES,
|
|
30
33
|
FAILPROOFAI_HOOK_MARKER,
|
|
31
34
|
INTEGRATION_TYPES,
|
|
32
35
|
type IntegrationType,
|
|
@@ -50,6 +53,23 @@ function writeJsonFile(path: string, data: Record<string, unknown>): void {
|
|
|
50
53
|
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
/** Read a YAML file as a `Document` so writes round-trip the user's other keys +
|
|
57
|
+
* comments (a plain parse→stringify would strip comments). Empty / missing /
|
|
58
|
+
* corrupt → an empty Document. Used by the Hermes integration, whose config
|
|
59
|
+
* lives in `~/.hermes/config.yaml`. */
|
|
60
|
+
function readYamlDoc(path: string): Document {
|
|
61
|
+
try {
|
|
62
|
+
return existsSync(path) ? parseDocument(readFileSync(path, "utf8")) : parseDocument("");
|
|
63
|
+
} catch {
|
|
64
|
+
return parseDocument("");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeYamlDoc(path: string, doc: Document): void {
|
|
69
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
70
|
+
writeFileSync(path, doc.toString(), "utf8");
|
|
71
|
+
}
|
|
72
|
+
|
|
53
73
|
function isMarkedHook(hook: unknown): boolean {
|
|
54
74
|
if (!hook || typeof hook !== "object") return false;
|
|
55
75
|
const h = hook as Record<string, unknown>;
|
|
@@ -1423,9 +1443,146 @@ export const gemini: Integration = {
|
|
|
1423
1443
|
},
|
|
1424
1444
|
};
|
|
1425
1445
|
|
|
1446
|
+
// ── Hermes (hermes-agent) — live hooks (Pillar 1) ────────────────────────────
|
|
1447
|
+
//
|
|
1448
|
+
// External-command CLI like codex/cursor, but its config is YAML
|
|
1449
|
+
// (`~/.hermes/config.yaml`) under a `hooks:` map, so the I/O layer uses the yaml
|
|
1450
|
+
// Document API (comment-preserving). Flat per-event arrays like cursor:
|
|
1451
|
+
// `hooks: { pre_tool_call: [ { command, timeout, <marker> } ], … }`. Hermes
|
|
1452
|
+
// reads a `{"decision":"block",…}` JSON response on stdout (see
|
|
1453
|
+
// policy-evaluator.ts); exit codes are ignored. User-scope only.
|
|
1454
|
+
|
|
1455
|
+
/** One hook entry as stored under a `hooks:` event key in config.yaml. */
|
|
1456
|
+
interface HermesHookEntry {
|
|
1457
|
+
command: string;
|
|
1458
|
+
timeout?: number;
|
|
1459
|
+
[key: string]: unknown;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
export const hermes: Integration = {
|
|
1463
|
+
id: "hermes",
|
|
1464
|
+
displayName: "Hermes",
|
|
1465
|
+
scopes: HERMES_HOOK_SCOPES,
|
|
1466
|
+
eventTypes: HERMES_HOOK_EVENT_TYPES,
|
|
1467
|
+
|
|
1468
|
+
// Hermes config is USER-scope only (`~/.hermes/config.yaml`); there is no
|
|
1469
|
+
// project/local file, so scope/cwd are irrelevant here.
|
|
1470
|
+
getSettingsPath() {
|
|
1471
|
+
return resolve(homedir(), ".hermes", "config.yaml");
|
|
1472
|
+
},
|
|
1473
|
+
|
|
1474
|
+
readSettings(settingsPath) {
|
|
1475
|
+
// Return the yaml Document (cast) so writeHookEntries/writeSettings can
|
|
1476
|
+
// round-trip it while preserving the user's other keys + comments.
|
|
1477
|
+
return readYamlDoc(settingsPath) as unknown as Record<string, unknown>;
|
|
1478
|
+
},
|
|
1479
|
+
|
|
1480
|
+
writeSettings(settingsPath, settings) {
|
|
1481
|
+
writeYamlDoc(settingsPath, settings as unknown as Document);
|
|
1482
|
+
},
|
|
1483
|
+
|
|
1484
|
+
buildHookEntry(binaryPath, eventType, scope) {
|
|
1485
|
+
// No matcher → fires for ALL tools / all platforms (slack/telegram/cli/cron)
|
|
1486
|
+
// and internal subagents. `timeout` is in seconds; Hermes runs the command
|
|
1487
|
+
// via shlex.split (shell=false).
|
|
1488
|
+
const command =
|
|
1489
|
+
scope === "project"
|
|
1490
|
+
? `npx -y failproofai --hook ${eventType} --cli hermes`
|
|
1491
|
+
: `"${binaryPath}" --hook ${eventType} --cli hermes`;
|
|
1492
|
+
return {
|
|
1493
|
+
command,
|
|
1494
|
+
timeout: 30,
|
|
1495
|
+
[FAILPROOFAI_HOOK_MARKER]: true,
|
|
1496
|
+
};
|
|
1497
|
+
},
|
|
1498
|
+
|
|
1499
|
+
isFailproofaiHook: isMarkedHook,
|
|
1500
|
+
|
|
1501
|
+
writeHookEntries(settings, binaryPath, scope) {
|
|
1502
|
+
const doc = settings as unknown as Document;
|
|
1503
|
+
// Read the current hooks map as plain JS, then re-set ONLY the `hooks` key —
|
|
1504
|
+
// preserving comments on every other part of config.yaml.
|
|
1505
|
+
const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
|
|
1506
|
+
const hooks: Record<string, HermesHookEntry[]> =
|
|
1507
|
+
js.hooks && typeof js.hooks === "object" ? js.hooks : {};
|
|
1508
|
+
|
|
1509
|
+
for (const eventType of HERMES_HOOK_EVENT_TYPES) {
|
|
1510
|
+
const entry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as HermesHookEntry;
|
|
1511
|
+
const arr = Array.isArray(hooks[eventType]) ? hooks[eventType] : [];
|
|
1512
|
+
const idx = arr.findIndex((h) => isMarkedHook(h));
|
|
1513
|
+
if (idx >= 0) arr[idx] = entry;
|
|
1514
|
+
else arr.push(entry);
|
|
1515
|
+
hooks[eventType] = arr;
|
|
1516
|
+
}
|
|
1517
|
+
doc.set("hooks", hooks);
|
|
1518
|
+
// The headless gateway has no TTY to answer Hermes's first-use hook-consent
|
|
1519
|
+
// prompt, so auto-accept declared hooks. Tradeoff: also auto-accepts any
|
|
1520
|
+
// other hook the operator adds; a targeted `shell-hooks-allowlist.json`
|
|
1521
|
+
// pre-seed is a future refinement.
|
|
1522
|
+
doc.set("hooks_auto_accept", true);
|
|
1523
|
+
},
|
|
1524
|
+
|
|
1525
|
+
removeHooksFromFile(settingsPath) {
|
|
1526
|
+
if (!existsSync(settingsPath)) return 0;
|
|
1527
|
+
const doc = readYamlDoc(settingsPath);
|
|
1528
|
+
const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
|
|
1529
|
+
const hooks = js.hooks;
|
|
1530
|
+
|
|
1531
|
+
let removed = 0;
|
|
1532
|
+
if (hooks && typeof hooks === "object") {
|
|
1533
|
+
for (const eventType of Object.keys(hooks)) {
|
|
1534
|
+
const entries = hooks[eventType];
|
|
1535
|
+
if (!Array.isArray(entries)) continue;
|
|
1536
|
+
const before = entries.length;
|
|
1537
|
+
const filtered = entries.filter((h) => !isMarkedHook(h));
|
|
1538
|
+
removed += before - filtered.length;
|
|
1539
|
+
if (filtered.length === 0) delete hooks[eventType];
|
|
1540
|
+
else hooks[eventType] = filtered;
|
|
1541
|
+
}
|
|
1542
|
+
if (removed > 0) {
|
|
1543
|
+
if (Object.keys(hooks).length === 0) doc.delete("hooks");
|
|
1544
|
+
else doc.set("hooks", hooks);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Always drop our headless-consent flag on uninstall — even if the hooks were
|
|
1549
|
+
// already removed manually — so it can't silently auto-accept future operator
|
|
1550
|
+
// hooks. `doc.delete` returns true iff the key was present.
|
|
1551
|
+
const droppedAutoAccept = doc.delete("hooks_auto_accept");
|
|
1552
|
+
if (removed > 0 || droppedAutoAccept) writeYamlDoc(settingsPath, doc);
|
|
1553
|
+
return removed;
|
|
1554
|
+
},
|
|
1555
|
+
|
|
1556
|
+
hooksInstalledInSettings(scope, cwd) {
|
|
1557
|
+
const settingsPath = this.getSettingsPath(scope, cwd);
|
|
1558
|
+
if (!existsSync(settingsPath)) return false;
|
|
1559
|
+
try {
|
|
1560
|
+
const doc = readYamlDoc(settingsPath);
|
|
1561
|
+
const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
|
|
1562
|
+
const hooks = js.hooks;
|
|
1563
|
+
if (!hooks || typeof hooks !== "object") return false;
|
|
1564
|
+
for (const entries of Object.values(hooks)) {
|
|
1565
|
+
if (Array.isArray(entries) && entries.some((h) => isMarkedHook(h))) return true;
|
|
1566
|
+
}
|
|
1567
|
+
} catch {
|
|
1568
|
+
// Corrupt config — treat as not installed.
|
|
1569
|
+
}
|
|
1570
|
+
return false;
|
|
1571
|
+
},
|
|
1572
|
+
|
|
1573
|
+
detectInstalled() {
|
|
1574
|
+
return binaryExists("hermes");
|
|
1575
|
+
},
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1426
1578
|
// ── Registry ────────────────────────────────────────────────────────────────
|
|
1427
1579
|
|
|
1428
|
-
|
|
1580
|
+
// `Partial` is kept (not every IntegrationType is guaranteed installable for
|
|
1581
|
+
// LIVE hooks / Pillar 1) so a future audit-only CLI can omit its entry without a
|
|
1582
|
+
// type error. `hermes` now has BOTH an audit adapter
|
|
1583
|
+
// (src/audit/cli-adapters/hermes.ts) AND live-hook install support, so it is
|
|
1584
|
+
// registered here.
|
|
1585
|
+
const INTEGRATIONS: Partial<Record<IntegrationType, Integration>> = {
|
|
1429
1586
|
claude: claudeCode,
|
|
1430
1587
|
codex,
|
|
1431
1588
|
copilot,
|
|
@@ -1433,21 +1590,38 @@ const INTEGRATIONS: Record<IntegrationType, Integration> = {
|
|
|
1433
1590
|
opencode,
|
|
1434
1591
|
pi,
|
|
1435
1592
|
gemini,
|
|
1593
|
+
hermes,
|
|
1436
1594
|
};
|
|
1437
1595
|
|
|
1438
1596
|
export function getIntegration(id: IntegrationType): Integration {
|
|
1439
1597
|
const integration = INTEGRATIONS[id];
|
|
1440
1598
|
if (!integration) {
|
|
1441
|
-
|
|
1599
|
+
// A future audit-only CLI (one with an audit adapter but no INTEGRATIONS
|
|
1600
|
+
// entry) reaches here when someone tries to install live hooks for it. Be
|
|
1601
|
+
// explicit rather than "unknown integration". (hermes is NOT audit-only — it
|
|
1602
|
+
// has a live-hook entry — so it never reaches this branch.)
|
|
1603
|
+
throw new Error(
|
|
1604
|
+
`"${id}" is audit-only — live-hook install is not supported for it yet. Installable: ${listInstallableIds().join(", ")}`,
|
|
1605
|
+
);
|
|
1442
1606
|
}
|
|
1443
1607
|
return integration;
|
|
1444
1608
|
}
|
|
1445
1609
|
|
|
1610
|
+
/** IntegrationTypes that support live-hook install (i.e. have an INTEGRATIONS
|
|
1611
|
+
* entry). Any audit-only CLI (one with an audit adapter but no hook install)
|
|
1612
|
+
* is excluded so it never appears in install menus. */
|
|
1613
|
+
export function listInstallableIds(): IntegrationType[] {
|
|
1614
|
+
return INTEGRATION_TYPES.filter((id) => INTEGRATIONS[id] !== undefined);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1446
1617
|
export function listIntegrations(): Integration[] {
|
|
1447
|
-
return INTEGRATION_TYPES.map((id) => INTEGRATIONS[id])
|
|
1618
|
+
return INTEGRATION_TYPES.map((id) => INTEGRATIONS[id]).filter(
|
|
1619
|
+
(i): i is Integration => i !== undefined,
|
|
1620
|
+
);
|
|
1448
1621
|
}
|
|
1449
1622
|
|
|
1450
|
-
/** Detect which agent CLIs are installed on PATH.
|
|
1623
|
+
/** Detect which agent CLIs are installed on PATH. Only considers CLIs that
|
|
1624
|
+
* support live-hook install (audit-only CLIs have no INTEGRATIONS entry). */
|
|
1451
1625
|
export function detectInstalledClis(): IntegrationType[] {
|
|
1452
|
-
return INTEGRATION_TYPES.filter((id) => INTEGRATIONS[id]
|
|
1626
|
+
return INTEGRATION_TYPES.filter((id) => INTEGRATIONS[id]?.detectInstalled());
|
|
1453
1627
|
}
|
|
@@ -256,6 +256,25 @@ export async function evaluatePolicies(
|
|
|
256
256
|
};
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
// Hermes: the block contract is `{"decision":"block","reason"}` on stdout;
|
|
260
|
+
// Hermes IGNORES exit codes, so the JSON is the only channel. This one
|
|
261
|
+
// return covers every Hermes event we install (PreToolUse / PostToolUse /
|
|
262
|
+
// SubagentStop) — there is no Stop/turn-end event to special-case (Hermes
|
|
263
|
+
// has no turn boundary; the 5 require-*-before-stop builtins never fire for
|
|
264
|
+
// it — see CLAUDE.md / the audit plan). A block on PreToolUse stops the
|
|
265
|
+
// tool before it runs, regardless of the originating platform
|
|
266
|
+
// (slack/telegram/cli/cron) or subagent.
|
|
267
|
+
if (session?.cli === "hermes") {
|
|
268
|
+
return {
|
|
269
|
+
exitCode: 0,
|
|
270
|
+
stdout: JSON.stringify({ decision: "block", reason: blockedMessage }),
|
|
271
|
+
stderr: "",
|
|
272
|
+
policyName: policy.name,
|
|
273
|
+
reason,
|
|
274
|
+
decision: "deny",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
259
278
|
// OpenCode: `session.idle` is a notification-only bus event — by the
|
|
260
279
|
// time the plugin handler fires, OpenCode has already gone idle and
|
|
261
280
|
// throwing from the handler does not force-retry. The only working
|
|
@@ -552,6 +571,29 @@ export async function evaluatePolicies(
|
|
|
552
571
|
};
|
|
553
572
|
}
|
|
554
573
|
|
|
574
|
+
// Hermes: no additional-context channel on any event (the only actionable
|
|
575
|
+
// response is `{"decision":"block"}`). So instruct degrades to allow +
|
|
576
|
+
// log — we emit a non-blocking `{decision:"allow", reason}` (Hermes lets
|
|
577
|
+
// the tool run) and surface the note on stderr for the operator's logs.
|
|
578
|
+
// Documented limitation; there is no Stop event to force-retry into.
|
|
579
|
+
if (session?.cli === "hermes") {
|
|
580
|
+
const stderrMsg = instructEntries
|
|
581
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
582
|
+
.join("\n");
|
|
583
|
+
return {
|
|
584
|
+
exitCode: 0,
|
|
585
|
+
stdout: JSON.stringify({
|
|
586
|
+
decision: "allow",
|
|
587
|
+
reason: `Instruction from failproofai: ${combined}`,
|
|
588
|
+
}),
|
|
589
|
+
stderr: stderrMsg + "\n",
|
|
590
|
+
policyName: policyNames[0],
|
|
591
|
+
policyNames,
|
|
592
|
+
reason: combined,
|
|
593
|
+
decision: "instruct",
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
555
597
|
// OpenCode: same rationale as the deny branch above — emit
|
|
556
598
|
// additionalContext so the shim submits a follow-up via
|
|
557
599
|
// client.session.prompt instead of throwing into a dead handler.
|
|
@@ -63,6 +63,10 @@ export function resolveTranscriptPath(
|
|
|
63
63
|
return findGeminiTranscript(sessionId) ?? undefined;
|
|
64
64
|
case "opencode":
|
|
65
65
|
return `opencode-db://${sessionId}`;
|
|
66
|
+
case "hermes":
|
|
67
|
+
// Hermes live-hook payloads carry no transcript file; the audit path
|
|
68
|
+
// reads sessions straight from ~/.hermes/state.db (see hermes-sessions.ts).
|
|
69
|
+
return undefined;
|
|
66
70
|
default:
|
|
67
71
|
return undefined;
|
|
68
72
|
}
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
PI_TOOL_MAP,
|
|
16
16
|
PI_TOOL_INPUT_MAP,
|
|
17
17
|
GEMINI_TOOL_MAP,
|
|
18
|
+
HERMES_TOOL_MAP,
|
|
19
|
+
HERMES_TOOL_INPUT_MAP,
|
|
18
20
|
} from "./types";
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -33,6 +35,7 @@ export function canonicalizeToolName(
|
|
|
33
35
|
if (cli === "gemini") return GEMINI_TOOL_MAP[raw] ?? raw;
|
|
34
36
|
if (cli === "opencode") return OPENCODE_TOOL_MAP[raw] ?? raw;
|
|
35
37
|
if (cli === "pi") return PI_TOOL_MAP[raw] ?? raw;
|
|
38
|
+
if (cli === "hermes") return HERMES_TOOL_MAP[raw] ?? raw;
|
|
36
39
|
return raw;
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -56,6 +59,9 @@ export function canonicalizeToolInput(
|
|
|
56
59
|
let perToolMap: Record<string, string> | undefined;
|
|
57
60
|
if (cli === "opencode") perToolMap = OPENCODE_TOOL_INPUT_MAP[toolName];
|
|
58
61
|
else if (cli === "pi") perToolMap = PI_TOOL_INPUT_MAP[toolName];
|
|
62
|
+
// Hermes read_file/write_file/patch deliver the file path as `path`; map it to
|
|
63
|
+
// `file_path` so path/content builtins fire (verified against a live state.db).
|
|
64
|
+
else if (cli === "hermes") perToolMap = HERMES_TOOL_INPUT_MAP[toolName];
|
|
59
65
|
if (!perToolMap) return rawInput;
|
|
60
66
|
const out: Record<string, unknown> = {};
|
|
61
67
|
for (const [k, v] of Object.entries(rawInput as Record<string, unknown>)) {
|
package/src/hooks/types.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export const HOOK_SCOPES = ["user", "project", "local"] as const;
|
|
6
6
|
export type HookScope = (typeof HOOK_SCOPES)[number];
|
|
7
7
|
|
|
8
|
-
export const INTEGRATION_TYPES = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"] as const;
|
|
8
|
+
export const INTEGRATION_TYPES = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"] as const;
|
|
9
9
|
export type IntegrationType = (typeof INTEGRATION_TYPES)[number];
|
|
10
10
|
|
|
11
11
|
export const CODEX_HOOK_SCOPES = ["user", "project"] as const;
|
|
@@ -61,6 +61,72 @@ export const CODEX_TOOL_MAP: Record<string, string> = {
|
|
|
61
61
|
write_stdin: "Bash",
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
// ── Hermes (hermes-agent) ───────────────────────────────────────────────────
|
|
65
|
+
//
|
|
66
|
+
// Hermes supports BOTH audit (Pillar 2) and live hooks (Pillar 1). This tool
|
|
67
|
+
// map is consumed by the audit adapter (via `logEntriesToEvents`) AND the
|
|
68
|
+
// live-hook handler, both through `canonicalizeToolName`. Tool names are the
|
|
69
|
+
// granular toolset tools verified
|
|
70
|
+
// against a live ~/.hermes/state.db (frequency in a real gateway session:
|
|
71
|
+
// terminal 574, read_file 124, patch 94, write_file 54, web_search 42, …).
|
|
72
|
+
// Names with a Claude canonical are mapped so builtin policies fire; Hermes-
|
|
73
|
+
// specific tools (skill_view, cronjob, browser_*, memory, session_search,
|
|
74
|
+
// clarify, process) pass through unchanged so they still appear in the audit,
|
|
75
|
+
// just unmatched by builtin policies.
|
|
76
|
+
export const HERMES_TOOL_MAP: Record<string, string> = {
|
|
77
|
+
terminal: "Bash",
|
|
78
|
+
bash: "Bash",
|
|
79
|
+
read_file: "Read",
|
|
80
|
+
write_file: "Write",
|
|
81
|
+
patch: "Edit",
|
|
82
|
+
web_search: "WebSearch",
|
|
83
|
+
web_extract: "WebFetch",
|
|
84
|
+
search_files: "Grep",
|
|
85
|
+
todo: "TodoWrite",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Hermes tool-INPUT key canonicalization, keyed by the *canonical* tool name
|
|
89
|
+
// (the handler canonicalizes the name before calling canonicalizeToolInput).
|
|
90
|
+
// Verified against a live ~/.hermes/state.db: read_file / write_file / patch
|
|
91
|
+
// deliver the file path as `path`, but Claude builtins read `file_path`
|
|
92
|
+
// (block-env-files, block-secrets-write, block-read-outside-cwd) — so map it.
|
|
93
|
+
// write_file's `content`, patch's `old_string`/`new_string`, and search_files'
|
|
94
|
+
// `pattern`/`path` are already canonical, so Grep needs no entry. Mirrors
|
|
95
|
+
// PI_TOOL_INPUT_MAP (Pi has the same `path` → `file_path` shape).
|
|
96
|
+
export const HERMES_TOOL_INPUT_MAP: Record<string, Record<string, string>> = {
|
|
97
|
+
Read: { path: "file_path" },
|
|
98
|
+
Write: { path: "file_path" },
|
|
99
|
+
Edit: { path: "file_path" },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Hermes live-hook (Pillar 1) events + scopes. Hermes fires these snake_case
|
|
103
|
+
// events with a JSON payload on stdin; the command we install runs
|
|
104
|
+
// `failproofai --hook <event> --cli hermes`. Config is USER-scope only
|
|
105
|
+
// (`~/.hermes/config.yaml`; Hermes has no project scope). `pre_tool_call` is the
|
|
106
|
+
// core deny point — it fires for tool calls from every source
|
|
107
|
+
// (slack/telegram/cli/cron) and internal subagents, so a single install
|
|
108
|
+
// intercepts all platforms. Hermes has NO turn-end `Stop` event, so the
|
|
109
|
+
// `require-*-before-stop` builtins never fire for it (see the audit plan).
|
|
110
|
+
export const HERMES_HOOK_SCOPES = ["user"] as const;
|
|
111
|
+
export type HermesHookScope = (typeof HERMES_HOOK_SCOPES)[number];
|
|
112
|
+
|
|
113
|
+
export const HERMES_HOOK_EVENT_TYPES = [
|
|
114
|
+
"pre_tool_call",
|
|
115
|
+
"post_tool_call",
|
|
116
|
+
"on_session_start",
|
|
117
|
+
"on_session_end",
|
|
118
|
+
"subagent_stop",
|
|
119
|
+
] as const;
|
|
120
|
+
export type HermesHookEventType = (typeof HERMES_HOOK_EVENT_TYPES)[number];
|
|
121
|
+
|
|
122
|
+
export const HERMES_EVENT_MAP: Record<HermesHookEventType, HookEventType> = {
|
|
123
|
+
pre_tool_call: "PreToolUse",
|
|
124
|
+
post_tool_call: "PostToolUse",
|
|
125
|
+
on_session_start: "SessionStart",
|
|
126
|
+
on_session_end: "SessionEnd",
|
|
127
|
+
subagent_stop: "SubagentStop",
|
|
128
|
+
};
|
|
129
|
+
|
|
64
130
|
// ── GitHub Copilot CLI ─────────────────────────────────────────────────────
|
|
65
131
|
//
|
|
66
132
|
// Copilot CLI accepts two payload formats. We install with PascalCase event
|