failproofai 1.0.9-beta.0 → 1.0.9-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 +5 -5
- 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/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 +1 -1
- 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.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 +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/_09dz7xv._.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/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]__0tmz862._.js → [root-of-the-server]__03m2iw6._.js} +2 -2
- 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]__0rwtwpm._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__12e7nhs._.js → [root-of-the-server]__1-bm5zf._.js} +1 -1
- 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/{_1p3iz2d._.js → _11ilmst._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{_0n0lja6._.js → _1jatlmf._.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 +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/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +1 -1
- 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 +23 -23
- package/.next/standalone/.next/static/chunks/{06jxjnw8bfczc.js → 0542cuk_va8g1.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2qsrrrl2maqsb.js → 08ckontevw-0w.js} +1 -1
- package/.next/standalone/.next/static/chunks/{42m0f2hh8vfb1.js → 0pefjgvu4x-sx.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0snylg3y1i47a.js → 1-d48e9k-7e1a.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03001k9jy-kst.js → 1r74hmr1wm_sd.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0qejem4-zt-hy.js → 1uec0n0lcx5h1.js} +1 -1
- package/.next/standalone/.next/static/chunks/21-pqc3-l9ak2.js +6 -0
- package/.next/standalone/.next/static/chunks/{1h0if8ky362as.js → 21qlo00jis4w-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3rq7fjj9we0v3.js → 2n00c3zqyloy9.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0247hc9ath5t-.js → 3tv107_for_n7.js} +1 -1
- package/.next/standalone/.next/static/chunks/{06k7gsfnb7jkh.js → 3vp54x0dyrrm6.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-38nzuvp95_2rv.js → turbopack-11dn2pegmg9_9.js} +1 -1
- package/.next/standalone/fp-cloud-cli/uv.lock +3 -3
- 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/pages-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/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/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -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/package.json +11 -11
- package/.next/standalone/sdk/python/uv.lock +18 -18
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +911 -719
- package/dist/worker.mjs +4 -1
- package/package.json +11 -11
- package/src/hooks/integrations.ts +128 -18
- package/src/hooks/jev-activity.ts +5 -0
- package/src/hooks/policy-reviewability.ts +4 -2
- package/src/hooks/safe-config-write.ts +119 -0
- package/.next/standalone/.next/static/chunks/3ns-egfh8i8i5.js +0 -6
- /package/.next/standalone/.next/static/{z1C0kJVF0NCtWn5R88hW3 → ozuWoyIwOTqeUUjKqOKiH}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{z1C0kJVF0NCtWn5R88hW3 → ozuWoyIwOTqeUUjKqOKiH}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{z1C0kJVF0NCtWn5R88hW3 → ozuWoyIwOTqeUUjKqOKiH}/_ssgManifest.js +0 -0
package/dist/worker.mjs
CHANGED
|
@@ -1162,7 +1162,7 @@ function instruct(reason) {
|
|
|
1162
1162
|
}
|
|
1163
1163
|
|
|
1164
1164
|
// package.json
|
|
1165
|
-
var version = "1.0.9-beta.
|
|
1165
|
+
var version = "1.0.9-beta.1";
|
|
1166
1166
|
var init_package = () => {};
|
|
1167
1167
|
|
|
1168
1168
|
// src/hooks/policy-types.ts
|
|
@@ -4172,8 +4172,11 @@ function sanitizeJevActivity(entry) {
|
|
|
4172
4172
|
drop("evaluator");
|
|
4173
4173
|
if (!(typeof out.jevDecision === "string" && DECISIONS.has(out.jevDecision)))
|
|
4174
4174
|
drop("jevDecision");
|
|
4175
|
+
const unknownMode = out.jevMode !== undefined && !(typeof out.jevMode === "string" && MODES.has(out.jevMode));
|
|
4175
4176
|
if (!(typeof out.jevMode === "string" && MODES.has(out.jevMode)))
|
|
4176
4177
|
drop("jevMode");
|
|
4178
|
+
if (unknownMode)
|
|
4179
|
+
drop("jevCleared");
|
|
4177
4180
|
if (Array.isArray(out.jevCleared)) {
|
|
4178
4181
|
const names = [
|
|
4179
4182
|
...new Set(out.jevCleared.filter(isJevPolicyName))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "1.0.9-beta.
|
|
3
|
+
"version": "1.0.9-beta.1",
|
|
4
4
|
"description": "Observability and enforcement for AI agent harnesses. 40 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs",
|
|
@@ -81,14 +81,14 @@
|
|
|
81
81
|
"access": "public"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@anthropic-ai/sdk": "^0.
|
|
84
|
+
"@anthropic-ai/sdk": "^0.128.0",
|
|
85
85
|
"@mdx-js/mdx": "^3.1.1",
|
|
86
86
|
"@tailwindcss/postcss": "^4.3.1",
|
|
87
87
|
"@tanstack/react-virtual": "^3.14.10",
|
|
88
88
|
"@testing-library/jest-dom": "^7.0.0",
|
|
89
89
|
"@testing-library/react": "^16.3.2",
|
|
90
90
|
"@testing-library/user-event": "^14.6.5",
|
|
91
|
-
"@types/node": "26.6.
|
|
91
|
+
"@types/node": "26.6.2",
|
|
92
92
|
"@types/react": "19.3.0",
|
|
93
93
|
"@types/react-dom": "^19.2.3",
|
|
94
94
|
"@vitejs/plugin-react": "^6.1.0",
|
|
@@ -122,15 +122,15 @@
|
|
|
122
122
|
"browserslist": "4.28.8"
|
|
123
123
|
},
|
|
124
124
|
"optionalDependencies": {
|
|
125
|
-
"@failproofai/failproofaid-linux-x64": "1.0.9-beta.
|
|
126
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.9-beta.
|
|
127
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.9-beta.
|
|
128
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.9-beta.
|
|
125
|
+
"@failproofai/failproofaid-linux-x64": "1.0.9-beta.1",
|
|
126
|
+
"@failproofai/failproofaid-linux-arm64": "1.0.9-beta.1",
|
|
127
|
+
"@failproofai/failproofaid-darwin-x64": "1.0.9-beta.1",
|
|
128
|
+
"@failproofai/failproofaid-darwin-arm64": "1.0.9-beta.1"
|
|
129
129
|
},
|
|
130
130
|
"failproofaidBinaries": {
|
|
131
|
-
"linux-x64": "
|
|
132
|
-
"linux-arm64": "
|
|
133
|
-
"darwin-x64": "
|
|
134
|
-
"darwin-arm64": "
|
|
131
|
+
"linux-x64": "a0db1ad28bc0584bb4554b1dc60b55353fad9d982354880f9cc1e1ee567e5d36",
|
|
132
|
+
"linux-arm64": "b8d2797db8d93ce0deec86707093dd22ecd2d3fe2b137944938f4851d31718f1",
|
|
133
|
+
"darwin-x64": "94dc7ea33121c884736eadafc8281b0b752824b5c7fbe1726745bd1fa2b2a577",
|
|
134
|
+
"darwin-arm64": "8da18bbede18b8c112e8c46b6f0de798e84445c163a91a80edeef6d9e21da8ad"
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -25,6 +25,7 @@ import { resolve, dirname, basename } from "node:path";
|
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { homedir } from "node:os";
|
|
27
27
|
import { parseDocument, type Document } from "yaml";
|
|
28
|
+
import { writeConfigFileAtomic } from "./safe-config-write";
|
|
28
29
|
import { listHermesProfiles, hermesRoot } from "../../lib/hermes-profiles";
|
|
29
30
|
import { listOpenClawProfiles } from "../../lib/openclaw-profiles";
|
|
30
31
|
import {
|
|
@@ -65,32 +66,82 @@ import {
|
|
|
65
66
|
|
|
66
67
|
// ── Generic helpers ─────────────────────────────────────────────────────────
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The error every config reader throws for a file it cannot read faithfully.
|
|
71
|
+
*
|
|
72
|
+
* These files belong to the user's agent. A config that exists but does not
|
|
73
|
+
* parse is never treated as empty: writing our entries into an "empty" document
|
|
74
|
+
* would replace every other setting in it, and the agent would come back without
|
|
75
|
+
* its model, keys or channels. Refusing leaves the file exactly as the user has
|
|
76
|
+
* it and says which file and why.
|
|
77
|
+
*/
|
|
78
|
+
export class UnreadableAgentConfigError extends Error {
|
|
79
|
+
constructor(readonly path: string, reason: string) {
|
|
80
|
+
super(
|
|
81
|
+
`Refusing to modify ${path}: ${reason}. ` +
|
|
82
|
+
`failproofai never rewrites a config it could not parse — fix or move the file, then retry.`,
|
|
83
|
+
);
|
|
84
|
+
this.name = "UnreadableAgentConfigError";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
function readJsonFile(path: string): Record<string, unknown> {
|
|
69
89
|
if (!existsSync(path)) return {};
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
let raw: string;
|
|
91
|
+
try {
|
|
92
|
+
raw = readFileSync(path, "utf8");
|
|
93
|
+
} catch (err) {
|
|
94
|
+
throw new UnreadableAgentConfigError(path, `it could not be read (${err instanceof Error ? err.message : String(err)})`);
|
|
95
|
+
}
|
|
96
|
+
if (raw.trim() === "") return {};
|
|
97
|
+
let parsed: unknown;
|
|
98
|
+
try {
|
|
99
|
+
parsed = JSON.parse(raw);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
throw new UnreadableAgentConfigError(path, `it is not valid JSON (${err instanceof Error ? err.message : String(err)})`);
|
|
102
|
+
}
|
|
103
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
104
|
+
throw new UnreadableAgentConfigError(path, "its top level is not a JSON object");
|
|
105
|
+
}
|
|
106
|
+
return parsed as Record<string, unknown>;
|
|
72
107
|
}
|
|
73
108
|
|
|
74
109
|
function writeJsonFile(path: string, data: Record<string, unknown>): void {
|
|
75
|
-
|
|
76
|
-
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
110
|
+
writeConfigFileAtomic(path, JSON.stringify(data, null, 2) + "\n");
|
|
77
111
|
}
|
|
78
112
|
|
|
79
113
|
/** Read a YAML file as a `Document` so writes round-trip the user's other keys +
|
|
80
|
-
* comments (a plain parse→stringify would strip comments).
|
|
81
|
-
*
|
|
82
|
-
*
|
|
114
|
+
* comments (a plain parse→stringify would strip comments). Missing or empty →
|
|
115
|
+
* an empty Document; unreadable or unparseable → {@link UnreadableAgentConfigError},
|
|
116
|
+
* never an empty Document that the next write would put in the user's place.
|
|
117
|
+
* Used by the Hermes integration, whose config lives in `~/.hermes/config.yaml`. */
|
|
83
118
|
function readYamlDoc(path: string): Document {
|
|
119
|
+
if (!existsSync(path)) return parseDocument("");
|
|
120
|
+
let text: string;
|
|
121
|
+
try {
|
|
122
|
+
text = readFileSync(path, "utf8");
|
|
123
|
+
} catch (err) {
|
|
124
|
+
throw new UnreadableAgentConfigError(path, `it could not be read (${err instanceof Error ? err.message : String(err)})`);
|
|
125
|
+
}
|
|
126
|
+
const doc = parseDocument(text);
|
|
127
|
+
if (doc.errors.length > 0) {
|
|
128
|
+
const first = doc.errors[0]?.message.split("\n")[0] ?? "syntax error";
|
|
129
|
+
throw new UnreadableAgentConfigError(path, `it does not parse as YAML (${first})`);
|
|
130
|
+
}
|
|
131
|
+
return doc;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** {@link readYamlDoc} for read-only callers (status, health): an unreadable file is `null`. */
|
|
135
|
+
function inspectYamlDoc(path: string): Document | null {
|
|
84
136
|
try {
|
|
85
|
-
return
|
|
137
|
+
return readYamlDoc(path);
|
|
86
138
|
} catch {
|
|
87
|
-
return
|
|
139
|
+
return null;
|
|
88
140
|
}
|
|
89
141
|
}
|
|
90
142
|
|
|
91
143
|
function writeYamlDoc(path: string, doc: Document): void {
|
|
92
|
-
|
|
93
|
-
writeFileSync(path, doc.toString(), "utf8");
|
|
144
|
+
writeConfigFileAtomic(path, doc.toString());
|
|
94
145
|
}
|
|
95
146
|
|
|
96
147
|
function isMarkedHook(hook: unknown): boolean {
|
|
@@ -1127,7 +1178,7 @@ export const opencode: Integration = {
|
|
|
1127
1178
|
// (a) Write the shim file. mkdirSync is recursive so the plugins/ dir
|
|
1128
1179
|
// is created on first install.
|
|
1129
1180
|
mkdirSync(dirname(pluginPath), { recursive: true });
|
|
1130
|
-
|
|
1181
|
+
writeConfigFileAtomic(pluginPath, buildOpenCodePluginShim(binaryPath, effectiveScope));
|
|
1131
1182
|
|
|
1132
1183
|
// (b) Merge our entry into the plugin array idempotently. Replace any
|
|
1133
1184
|
// existing failproofai-marked entry; otherwise append.
|
|
@@ -1422,7 +1473,30 @@ const HERMES_PLUGIN_SOURCE_DIR = "hermes-plugin";
|
|
|
1422
1473
|
function getHermesPluginSourcePath(): string {
|
|
1423
1474
|
const fromEnv = process.env.FAILPROOFAI_PACKAGE_ROOT;
|
|
1424
1475
|
if (fromEnv) return resolve(fromEnv, HERMES_PLUGIN_SOURCE_DIR);
|
|
1425
|
-
return
|
|
1476
|
+
return findHermesPluginSourcePath(dirname(fileURLToPath(import.meta.url)));
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* The package's `hermes-plugin/`, found by walking up from `startDir`.
|
|
1481
|
+
*
|
|
1482
|
+
* The CLI entry point sets `FAILPROOFAI_PACKAGE_ROOT`, so this is only the
|
|
1483
|
+
* fallback — but a fixed number of parents is right for exactly one layout:
|
|
1484
|
+
* three from `src/hooks/` is the package root, three from the bundled
|
|
1485
|
+
* `dist/cli.mjs` is the package's PARENT. Walking up to the first directory
|
|
1486
|
+
* that really holds the plugin's manifest works from source, from `dist/`, and
|
|
1487
|
+
* from the dashboard's standalone build alike. When none is found the old
|
|
1488
|
+
* answer is returned, so the install still fails loudly on a missing asset.
|
|
1489
|
+
*/
|
|
1490
|
+
export function findHermesPluginSourcePath(startDir: string): string {
|
|
1491
|
+
let dir = resolve(startDir);
|
|
1492
|
+
for (let i = 0; i < 8; i++) {
|
|
1493
|
+
const candidate = resolve(dir, HERMES_PLUGIN_SOURCE_DIR);
|
|
1494
|
+
if (existsSync(resolve(candidate, "plugin.yaml"))) return candidate;
|
|
1495
|
+
const parent = dirname(dir);
|
|
1496
|
+
if (parent === dir) break;
|
|
1497
|
+
dir = parent;
|
|
1498
|
+
}
|
|
1499
|
+
return resolve(startDir, "..", "..", HERMES_PLUGIN_SOURCE_DIR);
|
|
1426
1500
|
}
|
|
1427
1501
|
|
|
1428
1502
|
export function hermesPluginPathForSettings(settingsPath: string): string {
|
|
@@ -1599,11 +1673,16 @@ export function installHermesPlugin(
|
|
|
1599
1673
|
if (linked && (existing.kind === "absent" || existing.kind === "link")) {
|
|
1600
1674
|
try {
|
|
1601
1675
|
renameSync(temporary, destination);
|
|
1676
|
+
return "linked";
|
|
1602
1677
|
} catch (err) {
|
|
1603
|
-
|
|
1604
|
-
|
|
1678
|
+
// Windows cannot always rename a junction over another junction; the
|
|
1679
|
+
// backup swap below moves the old link aside first, so try that before
|
|
1680
|
+
// giving up. With nothing there to move aside, the rename is the error.
|
|
1681
|
+
if (existing.kind !== "link") {
|
|
1682
|
+
rmSync(temporary, { force: true });
|
|
1683
|
+
throw err;
|
|
1684
|
+
}
|
|
1605
1685
|
}
|
|
1606
|
-
return "linked";
|
|
1607
1686
|
}
|
|
1608
1687
|
|
|
1609
1688
|
const backup = `${destination}.backup-${suffix}`;
|
|
@@ -1671,11 +1750,13 @@ function removeLegacyHermesHooks(doc: Document): number {
|
|
|
1671
1750
|
function hermesConfigState(settingsPath: string): {
|
|
1672
1751
|
pluginEnabled: boolean;
|
|
1673
1752
|
legacyShellHookPresent: boolean;
|
|
1753
|
+
unreadable?: boolean;
|
|
1674
1754
|
} {
|
|
1675
1755
|
if (!existsSync(settingsPath)) {
|
|
1676
1756
|
return { pluginEnabled: false, legacyShellHookPresent: false };
|
|
1677
1757
|
}
|
|
1678
|
-
const doc =
|
|
1758
|
+
const doc = inspectYamlDoc(settingsPath);
|
|
1759
|
+
if (!doc) return { pluginEnabled: false, legacyShellHookPresent: false, unreadable: true };
|
|
1679
1760
|
const js = (doc.toJS() ?? {}) as {
|
|
1680
1761
|
hooks?: Record<string, HermesHookEntry[]>;
|
|
1681
1762
|
plugins?: HermesPluginsConfig;
|
|
@@ -1763,6 +1844,29 @@ export async function migrateHermesProfiles(opts: {
|
|
|
1763
1844
|
const base = { name: profile.name, home: profile.home };
|
|
1764
1845
|
const legacy = config.legacyShellHookPresent;
|
|
1765
1846
|
|
|
1847
|
+
// A config.yaml that does not parse is never edited. Whether it is ours to
|
|
1848
|
+
// report is read from the raw text: it cannot be parsed to find out.
|
|
1849
|
+
if (config.unreadable) {
|
|
1850
|
+
let raw = "";
|
|
1851
|
+
try {
|
|
1852
|
+
raw = readFileSync(settingsPath, "utf8");
|
|
1853
|
+
} catch {
|
|
1854
|
+
raw = "";
|
|
1855
|
+
}
|
|
1856
|
+
const mentionsUs = ours || raw.includes(FAILPROOFAI_HOOK_MARKER) || /failproofai/.test(raw);
|
|
1857
|
+
results.push(
|
|
1858
|
+
mentionsUs
|
|
1859
|
+
? {
|
|
1860
|
+
...base,
|
|
1861
|
+
status: "failed",
|
|
1862
|
+
detail: `${settingsPath} does not parse as YAML; left untouched`,
|
|
1863
|
+
legacyShellHooksRemain: /--hook\s+\S+\s+--cli\s+hermes|__failproofai_hook__/.test(raw),
|
|
1864
|
+
}
|
|
1865
|
+
: { ...base, status: "untouched", detail: "no failproofai integration", legacyShellHooksRemain: false },
|
|
1866
|
+
);
|
|
1867
|
+
continue;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1766
1870
|
if (!legacy && !ours && !config.pluginEnabled) {
|
|
1767
1871
|
results.push({ ...base, status: "untouched", detail: "no failproofai integration", legacyShellHooksRemain: false });
|
|
1768
1872
|
continue;
|
|
@@ -1850,6 +1954,8 @@ export interface HermesProfileHealth {
|
|
|
1850
1954
|
* with none of those, so their tool calls go unchecked.
|
|
1851
1955
|
*/
|
|
1852
1956
|
cronUnchecked: boolean;
|
|
1957
|
+
/** config.yaml exists but does not parse; failproofai will not touch it. */
|
|
1958
|
+
configUnreadable: boolean;
|
|
1853
1959
|
healthy: boolean;
|
|
1854
1960
|
}
|
|
1855
1961
|
|
|
@@ -1870,7 +1976,8 @@ export function hermesProfileHealth(): HermesProfileHealth[] {
|
|
|
1870
1976
|
pluginEnabled: config.pluginEnabled,
|
|
1871
1977
|
legacyShellHookPresent: config.legacyShellHookPresent,
|
|
1872
1978
|
cronUnchecked: config.legacyShellHookPresent && !pluginWorks,
|
|
1873
|
-
|
|
1979
|
+
configUnreadable: config.unreadable === true,
|
|
1980
|
+
healthy: pluginWorks && !config.legacyShellHookPresent && config.unreadable !== true,
|
|
1874
1981
|
};
|
|
1875
1982
|
});
|
|
1876
1983
|
}
|
|
@@ -1881,6 +1988,9 @@ export function hermesProfileStatusRows(): Array<[string, string]> {
|
|
|
1881
1988
|
.filter((profile) => existsSync(profile.home))
|
|
1882
1989
|
.map((profile) => {
|
|
1883
1990
|
const label = "hermes/" + profile.name;
|
|
1991
|
+
if (profile.configUnreadable) {
|
|
1992
|
+
return [label, `UNHEALTHY — ${profile.settingsPath} does not parse; failproofai leaves it untouched until it is fixed`];
|
|
1993
|
+
}
|
|
1884
1994
|
if (profile.cronUnchecked) {
|
|
1885
1995
|
// Not a cosmetic leftover: the shell-hook integration does not reach
|
|
1886
1996
|
// cron jobs at all, so this profile has an unenforced path.
|
|
@@ -236,7 +236,12 @@ export function sanitizeJevActivity<T extends JevActivityFields>(entry: T): T {
|
|
|
236
236
|
|
|
237
237
|
if (!(typeof out.evaluator === "string" && EVALUATORS.has(out.evaluator))) drop("evaluator");
|
|
238
238
|
if (!(typeof out.jevDecision === "string" && DECISIONS.has(out.jevDecision))) drop("jevDecision");
|
|
239
|
+
// A mode this build does not know (a row written before the mode was named
|
|
240
|
+
// `observe`) cannot say whether its clears were enforced, so they are not
|
|
241
|
+
// counted as anything: dropping only the mode would file them as enforced.
|
|
242
|
+
const unknownMode = out.jevMode !== undefined && !(typeof out.jevMode === "string" && MODES.has(out.jevMode));
|
|
239
243
|
if (!(typeof out.jevMode === "string" && MODES.has(out.jevMode))) drop("jevMode");
|
|
244
|
+
if (unknownMode) drop("jevCleared");
|
|
240
245
|
|
|
241
246
|
if (Array.isArray(out.jevCleared)) {
|
|
242
247
|
const names = [
|
|
@@ -61,7 +61,7 @@ import { resolve } from "node:path";
|
|
|
61
61
|
import { discoverPolicyFiles } from "./custom-hooks-loader";
|
|
62
62
|
import { customPoliciesDir } from "./fp-home";
|
|
63
63
|
import { configuredCustomPolicyPaths, findProjectConfigDir, readMergedHooksConfig } from "./hooks-config";
|
|
64
|
-
import {
|
|
64
|
+
import { hasInstalledRegexPacks, readInstalledPacks } from "./pack-manifest";
|
|
65
65
|
import { resolvePolicyAuthority } from "./policy-authority";
|
|
66
66
|
import { POLICY_CATALOG } from "./policy-catalog";
|
|
67
67
|
import { normalizePolicyName } from "./policy-registry";
|
|
@@ -167,7 +167,9 @@ export function surveyReviewableCoverage(cwd?: string): ReviewableCoverage {
|
|
|
167
167
|
*/
|
|
168
168
|
let reviewers: ReadonlySet<string> | undefined;
|
|
169
169
|
try {
|
|
170
|
-
|
|
170
|
+
// Only a pack with regex policies retires the shim, exactly as handler.ts
|
|
171
|
+
// decides it: a Jev-only pack (FailproofAI/jev-policies) replaces none.
|
|
172
|
+
packsInstalled = hasInstalledRegexPacks();
|
|
171
173
|
const packs = readInstalledPacks().packs;
|
|
172
174
|
for (const pack of packs) {
|
|
173
175
|
const selected = pack.enabled;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash-safe writes for configuration files that belong to someone else.
|
|
3
|
+
*
|
|
4
|
+
* Every agent integration edits a file the user's agent reads on startup:
|
|
5
|
+
* `~/.claude/settings.json`, `~/.hermes/config.yaml`, `~/.openclaw/openclaw.json`
|
|
6
|
+
* and the rest. A plain `writeFileSync` truncates that file first and fills it
|
|
7
|
+
* second, so a crash, a full disk or a killed process in between leaves the agent
|
|
8
|
+
* with a half-written config — for Hermes that is a gateway that will not start.
|
|
9
|
+
* And a later install then refuses to touch the file because it no longer parses,
|
|
10
|
+
* so nothing repairs it.
|
|
11
|
+
*
|
|
12
|
+
* So the new content never goes near the live file until it is complete and on
|
|
13
|
+
* disk:
|
|
14
|
+
*
|
|
15
|
+
* 1. write it to a uniquely named temporary file in the SAME directory (a
|
|
16
|
+
* rename is only atomic within one filesystem),
|
|
17
|
+
* 2. fsync that file,
|
|
18
|
+
* 3. copy the current file to `<name>.failproofai-backup` — the last version
|
|
19
|
+
* before failproofai changed it, for a person to restore by hand,
|
|
20
|
+
* 4. rename the temporary file over the live one (atomic on POSIX, and a
|
|
21
|
+
* replacing move on Windows), then fsync the directory so the rename
|
|
22
|
+
* itself survives a power cut.
|
|
23
|
+
*
|
|
24
|
+
* Any failure before step 4 leaves the live file exactly as it was and removes
|
|
25
|
+
* the temporary file. The file keeps its permission bits (Hermes keeps
|
|
26
|
+
* `config.yaml` at 0600), and a config that is a symlink — a dotfiles checkout —
|
|
27
|
+
* is written through to its target rather than replaced by a regular file.
|
|
28
|
+
*/
|
|
29
|
+
import {
|
|
30
|
+
chmodSync,
|
|
31
|
+
closeSync,
|
|
32
|
+
copyFileSync,
|
|
33
|
+
existsSync,
|
|
34
|
+
fsyncSync,
|
|
35
|
+
lstatSync,
|
|
36
|
+
mkdirSync,
|
|
37
|
+
openSync,
|
|
38
|
+
realpathSync,
|
|
39
|
+
renameSync,
|
|
40
|
+
rmSync,
|
|
41
|
+
statSync,
|
|
42
|
+
writeSync,
|
|
43
|
+
} from "node:fs";
|
|
44
|
+
import { randomBytes } from "node:crypto";
|
|
45
|
+
import { basename, dirname, join } from "node:path";
|
|
46
|
+
|
|
47
|
+
/** Where the copy of the previous version is kept, beside the file itself. */
|
|
48
|
+
export function configBackupPath(path: string): string {
|
|
49
|
+
return `${resolveWriteTarget(path)}.failproofai-backup`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Hooks for tests to simulate an interruption at the one step that matters. */
|
|
53
|
+
export interface AtomicWriteDeps {
|
|
54
|
+
rename?: (from: string, to: string) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A symlinked config is written through to what it points at. */
|
|
58
|
+
function resolveWriteTarget(path: string): string {
|
|
59
|
+
try {
|
|
60
|
+
if (lstatSync(path).isSymbolicLink()) return realpathSync(path);
|
|
61
|
+
} catch {
|
|
62
|
+
// Absent or unreadable: write to the path as given.
|
|
63
|
+
}
|
|
64
|
+
return path;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function writeConfigFileAtomic(path: string, content: string, deps: AtomicWriteDeps = {}): void {
|
|
68
|
+
const target = resolveWriteTarget(path);
|
|
69
|
+
const dir = dirname(target);
|
|
70
|
+
mkdirSync(dir, { recursive: true });
|
|
71
|
+
|
|
72
|
+
let mode: number | undefined;
|
|
73
|
+
try {
|
|
74
|
+
mode = statSync(target).mode & 0o777;
|
|
75
|
+
} catch {
|
|
76
|
+
mode = undefined; // a new file: the process umask decides, as writeFileSync would
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const temporary = join(dir, `.${basename(target)}.failproofai-${process.pid}-${randomBytes(4).toString("hex")}.tmp`);
|
|
80
|
+
let fd: number | undefined;
|
|
81
|
+
try {
|
|
82
|
+
fd = openSync(temporary, "wx", mode ?? 0o666);
|
|
83
|
+
const bytes = Buffer.from(content, "utf8");
|
|
84
|
+
let written = 0;
|
|
85
|
+
while (written < bytes.length) written += writeSync(fd, bytes, written, bytes.length - written);
|
|
86
|
+
fsyncSync(fd);
|
|
87
|
+
closeSync(fd);
|
|
88
|
+
fd = undefined;
|
|
89
|
+
// `open` applies the umask; the live file's own bits are what should survive.
|
|
90
|
+
if (mode !== undefined) chmodSync(temporary, mode);
|
|
91
|
+
|
|
92
|
+
if (existsSync(target)) copyFileSync(target, `${target}.failproofai-backup`);
|
|
93
|
+
|
|
94
|
+
(deps.rename ?? renameSync)(temporary, target);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (fd !== undefined) {
|
|
97
|
+
try {
|
|
98
|
+
closeSync(fd);
|
|
99
|
+
} catch {
|
|
100
|
+
// already closed or never opened
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
rmSync(temporary, { force: true });
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Make the rename itself durable. Not supported on every platform (Windows
|
|
108
|
+
// cannot open a directory); the file content is already safe either way.
|
|
109
|
+
try {
|
|
110
|
+
const dirFd = openSync(dir, "r");
|
|
111
|
+
try {
|
|
112
|
+
fsyncSync(dirFd);
|
|
113
|
+
} finally {
|
|
114
|
+
closeSync(dirFd);
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// best effort
|
|
118
|
+
}
|
|
119
|
+
}
|