failproofai 1.0.8 → 1.0.9-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 +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +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/_not-found/__PAGE__.segment.rsc +14 -14
- 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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- 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.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +3 -3
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +20 -20
- 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/_1ek68ln._.js +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0tmz862._.js +3 -0
- 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/{_0-vcssj._.js → _0n0lja6._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1p3iz2d._.js +23 -0
- 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_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/src_hooks_effective-reviewers_ts_1h4wtvo._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/{3c3qbmosdjl6w.js → 0247hc9ath5t-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0m-9d6yn9hx4j.js → 03001k9jy-kst.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2qdpj67x6ifk_.js → 06k7gsfnb7jkh.js} +1 -1
- package/.next/standalone/.next/static/chunks/0qejem4-zt-hy.js +1 -0
- package/.next/standalone/.next/static/chunks/{21uv-uusw329x.js → 0snylg3y1i47a.js} +1 -1
- package/.next/standalone/.next/static/chunks/1h0if8ky362as.js +1 -0
- package/.next/standalone/.next/static/chunks/{0wbjy0zo-m7is.js → 2qsrrrl2maqsb.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0bhidk90e-07f.css → 3_e_wg5_oay87.css} +1 -1
- package/.next/standalone/.next/static/chunks/3ns-egfh8i8i5.js +6 -0
- package/.next/standalone/.next/static/chunks/3rq7fjj9we0v3.js +1 -0
- package/.next/standalone/.next/static/chunks/42m0f2hh8vfb1.js +1 -0
- package/.next/standalone/app/actions/get-jev-config.ts +2 -2
- package/.next/standalone/app/actions/update-jev-config.ts +12 -9
- package/.next/standalone/app/components/jev-notices.tsx +10 -10
- package/.next/standalone/app/settings/jev-panel.tsx +11 -11
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +2 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +26 -0
- package/.next/standalone/fp-cloud-cli/tests/test_output.py +27 -0
- package/.next/standalone/hermes-plugin/README.md +18 -8
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +33 -7
- package/dist/cli.mjs +556 -521
- package/dist/worker.mjs +56 -375
- package/hermes-plugin/README.md +18 -8
- package/package.json +10 -10
- package/scripts/build-policy-pack.mjs +6 -9
- package/src/hooks/cloud-connection.ts +12 -6
- package/src/hooks/custom-hooks-loader.ts +1 -1
- package/src/hooks/effective-reviewers.ts +50 -43
- package/src/hooks/handler.ts +25 -16
- package/src/hooks/hermes-update.ts +74 -0
- package/src/hooks/hook-activity-store.ts +3 -3
- package/src/hooks/integrations.ts +349 -55
- package/src/hooks/jev-activity.ts +3 -3
- package/src/hooks/jev-cli.ts +17 -17
- package/src/hooks/jev-cloud-connection.ts +4 -4
- package/src/hooks/manager.ts +3 -2
- package/src/hooks/pack-cli.ts +24 -23
- package/src/hooks/pack-manifest.ts +25 -2
- package/src/hooks/pack-store.ts +2 -2
- package/src/hooks/policy-authority.ts +51 -32
- package/src/hooks/policy-evaluator.ts +5 -5
- package/src/hooks/policy-registry.ts +5 -7
- package/src/hooks/policy-reviewability.ts +26 -10
- package/src/hooks/policy-types.ts +4 -4
- package/src/hooks/semantic/combine.ts +13 -13
- package/src/hooks/semantic/evaluator.ts +5 -5
- package/src/hooks/semantic/jev-client.ts +1 -1
- package/src/hooks/semantic/jev-config.ts +21 -12
- package/src/hooks/semantic/jev-review.ts +3 -3
- package/src/hooks/semantic/jev-stats.ts +17 -17
- package/src/hooks/semantic/pack-policies.ts +48 -48
- package/src/hooks/semantic/policies.ts +13 -425
- package/src/hooks/semantic/precondition-names.ts +1 -1
- package/src/hooks/semantic/preconditions.ts +1 -1
- package/src/hooks/semantic/types.ts +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13t1zkw._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_13kfn90._.js +0 -23
- package/.next/standalone/.next/static/chunks/06dnzbolj00mc.js +0 -6
- package/.next/standalone/.next/static/chunks/16f3fa-lx38hk.js +0 -1
- package/.next/standalone/.next/static/chunks/2g2tki08kdhie.js +0 -1
- package/.next/standalone/.next/static/chunks/3-nbtkhg9y-1j.js +0 -1
- package/.next/standalone/.next/static/chunks/355km0ihuqo1p.js +0 -1
- /package/.next/standalone/.next/static/{5yKM8NoTOpUI0MoQl8eo5 → z1C0kJVF0NCtWn5R88hW3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{5yKM8NoTOpUI0MoQl8eo5 → z1C0kJVF0NCtWn5R88hW3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{5yKM8NoTOpUI0MoQl8eo5 → z1C0kJVF0NCtWn5R88hW3}/_ssgManifest.js +0 -0
|
@@ -14,11 +14,14 @@ import {
|
|
|
14
14
|
existsSync,
|
|
15
15
|
lstatSync,
|
|
16
16
|
mkdirSync,
|
|
17
|
+
readlinkSync,
|
|
18
|
+
realpathSync,
|
|
17
19
|
renameSync,
|
|
18
20
|
rmSync,
|
|
21
|
+
symlinkSync,
|
|
19
22
|
unlinkSync,
|
|
20
23
|
} from "node:fs";
|
|
21
|
-
import { resolve, dirname } from "node:path";
|
|
24
|
+
import { resolve, dirname, basename } from "node:path";
|
|
22
25
|
import { fileURLToPath } from "node:url";
|
|
23
26
|
import { homedir } from "node:os";
|
|
24
27
|
import { parseDocument, type Document } from "yaml";
|
|
@@ -1388,9 +1391,13 @@ function makePiProjectRelativeEntry(extPath: string): string {
|
|
|
1388
1391
|
// Hermes loads trusted Python plugins from `<HERMES_HOME>/plugins/<name>/`.
|
|
1389
1392
|
// The shipped plugin registers Hermes-native hooks and talks directly to the
|
|
1390
1393
|
// local failproofaid socket, avoiding a fresh CLI process per event. Config is
|
|
1391
|
-
// YAML and profile-scoped; install
|
|
1394
|
+
// YAML and profile-scoped; install LINKS every profile's `plugins/failproofai`
|
|
1395
|
+
// to the package's `hermes-plugin/` (so npm upgrades apply with no reinstall,
|
|
1396
|
+
// like OpenClaw's `plugins.load.paths`), falling back to a marked copy, and
|
|
1392
1397
|
// adds `failproofai` to `plugins.enabled`. Legacy shell-hook entries are removed
|
|
1393
|
-
// during migration so one tool call is never evaluated twice
|
|
1398
|
+
// during migration so one tool call is never evaluated twice — and because
|
|
1399
|
+
// config.yaml shell hooks never reach Hermes cron jobs: each cron fire builds
|
|
1400
|
+
// its own hook scope, which discovered plugins join and config hooks do not.
|
|
1394
1401
|
|
|
1395
1402
|
/** One hook entry as stored under a `hooks:` event key in config.yaml. */
|
|
1396
1403
|
interface HermesHookEntry {
|
|
@@ -1409,38 +1416,137 @@ interface HermesPluginsConfig {
|
|
|
1409
1416
|
const HERMES_PLUGIN_ID = "failproofai";
|
|
1410
1417
|
const HERMES_PLUGIN_MARKER = ".failproofai-managed";
|
|
1411
1418
|
const HERMES_PLUGIN_FILES = ["plugin.yaml", "__init__.py", "client.py", "ledger.py"] as const;
|
|
1419
|
+
/** Basename of the plugin directory inside the npm package. */
|
|
1420
|
+
const HERMES_PLUGIN_SOURCE_DIR = "hermes-plugin";
|
|
1412
1421
|
|
|
1413
1422
|
function getHermesPluginSourcePath(): string {
|
|
1414
1423
|
const fromEnv = process.env.FAILPROOFAI_PACKAGE_ROOT;
|
|
1415
|
-
if (fromEnv) return resolve(fromEnv,
|
|
1416
|
-
return resolve(fileURLToPath(import.meta.url), "..", "..", "..",
|
|
1424
|
+
if (fromEnv) return resolve(fromEnv, HERMES_PLUGIN_SOURCE_DIR);
|
|
1425
|
+
return resolve(fileURLToPath(import.meta.url), "..", "..", "..", HERMES_PLUGIN_SOURCE_DIR);
|
|
1417
1426
|
}
|
|
1418
1427
|
|
|
1419
1428
|
export function hermesPluginPathForSettings(settingsPath: string): string {
|
|
1420
1429
|
return resolve(dirname(settingsPath), "plugins", HERMES_PLUGIN_ID);
|
|
1421
1430
|
}
|
|
1422
1431
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1432
|
+
/** `existsSync` follows symlinks, so a dangling link reads as absent. This does not. */
|
|
1433
|
+
function pathPresent(path: string): boolean {
|
|
1425
1434
|
try {
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
return HERMES_PLUGIN_FILES.every((file) => existsSync(resolve(pluginPath, file)));
|
|
1435
|
+
lstatSync(path);
|
|
1436
|
+
return true;
|
|
1429
1437
|
} catch {
|
|
1430
1438
|
return false;
|
|
1431
1439
|
}
|
|
1432
1440
|
}
|
|
1433
1441
|
|
|
1434
|
-
function
|
|
1435
|
-
|
|
1442
|
+
function hasAllHermesPluginFiles(dir: string): boolean {
|
|
1443
|
+
return HERMES_PLUGIN_FILES.every((file) => existsSync(resolve(dir, file)));
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* Whether `dir` is a FailproofAI `hermes-plugin` directory (or was one: a link
|
|
1448
|
+
* whose target vanished during an npm upgrade or uninstall is still ours).
|
|
1449
|
+
* The basename is the package layout; the manifest name confirms it when the
|
|
1450
|
+
* target is readable, so a link to someone else's plugin is never claimed.
|
|
1451
|
+
*/
|
|
1452
|
+
function isFailproofaiHermesPluginSource(dir: string): boolean {
|
|
1453
|
+
if (basename(dir) !== HERMES_PLUGIN_SOURCE_DIR) return false;
|
|
1454
|
+
if (!existsSync(dir)) return true;
|
|
1436
1455
|
try {
|
|
1437
|
-
|
|
1456
|
+
const manifest = readFileSync(resolve(dir, "plugin.yaml"), "utf8");
|
|
1457
|
+
return /^name:\s*["']?failproofai["']?\s*$/m.test(manifest);
|
|
1438
1458
|
} catch {
|
|
1439
1459
|
return false;
|
|
1440
1460
|
}
|
|
1441
1461
|
}
|
|
1442
1462
|
|
|
1443
|
-
function
|
|
1463
|
+
function samePath(a: string, b: string): boolean {
|
|
1464
|
+
if (resolve(a) === resolve(b)) return true;
|
|
1465
|
+
try {
|
|
1466
|
+
return realpathSync(a) === realpathSync(b);
|
|
1467
|
+
} catch {
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* What occupies `<profile>/plugins/failproofai`, and whether FailproofAI owns it.
|
|
1474
|
+
*
|
|
1475
|
+
* • `link` — a symlink into a FailproofAI package's `hermes-plugin/` (the
|
|
1476
|
+
* current install shape: npm upgrades apply with no reinstall).
|
|
1477
|
+
* • `copy` — a copied directory carrying the `.failproofai-managed`
|
|
1478
|
+
* marker (1.0.6–1.0.8 installs, and the fallback where a
|
|
1479
|
+
* symlink cannot be created).
|
|
1480
|
+
* • `foreign` — anything else: an unmarked directory, a file, or a symlink
|
|
1481
|
+
* somewhere that is not a FailproofAI plugin. Never touched.
|
|
1482
|
+
*/
|
|
1483
|
+
type HermesPluginState =
|
|
1484
|
+
| { kind: "absent" }
|
|
1485
|
+
| { kind: "link"; target: string; complete: boolean; current: boolean }
|
|
1486
|
+
| { kind: "copy"; complete: boolean }
|
|
1487
|
+
| { kind: "foreign" };
|
|
1488
|
+
|
|
1489
|
+
function hermesPluginState(settingsPath: string): HermesPluginState {
|
|
1490
|
+
const pluginPath = hermesPluginPathForSettings(settingsPath);
|
|
1491
|
+
let isLink: boolean;
|
|
1492
|
+
let isDirectory: boolean;
|
|
1493
|
+
try {
|
|
1494
|
+
const stat = lstatSync(pluginPath);
|
|
1495
|
+
isLink = stat.isSymbolicLink();
|
|
1496
|
+
isDirectory = stat.isDirectory();
|
|
1497
|
+
} catch {
|
|
1498
|
+
return { kind: "absent" };
|
|
1499
|
+
}
|
|
1500
|
+
if (isLink) {
|
|
1501
|
+
let target: string;
|
|
1502
|
+
try {
|
|
1503
|
+
target = resolve(dirname(pluginPath), readlinkSync(pluginPath));
|
|
1504
|
+
} catch {
|
|
1505
|
+
return { kind: "foreign" };
|
|
1506
|
+
}
|
|
1507
|
+
if (!isFailproofaiHermesPluginSource(target)) return { kind: "foreign" };
|
|
1508
|
+
return {
|
|
1509
|
+
kind: "link",
|
|
1510
|
+
target,
|
|
1511
|
+
complete: hasAllHermesPluginFiles(target),
|
|
1512
|
+
current: samePath(target, getHermesPluginSourcePath()),
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
if (isDirectory && existsSync(resolve(pluginPath, HERMES_PLUGIN_MARKER))) {
|
|
1516
|
+
return { kind: "copy", complete: hasAllHermesPluginFiles(pluginPath) };
|
|
1517
|
+
}
|
|
1518
|
+
return { kind: "foreign" };
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function hasManagedHermesPlugin(settingsPath: string): boolean {
|
|
1522
|
+
const state = hermesPluginState(settingsPath);
|
|
1523
|
+
return (state.kind === "link" || state.kind === "copy") && state.complete;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/** Test seam: lets a test make symlink creation fail to exercise the copy fallback. */
|
|
1527
|
+
export interface HermesPluginInstallDeps {
|
|
1528
|
+
symlink?: (target: string, path: string) => void;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function defaultHermesSymlink(target: string, path: string): void {
|
|
1532
|
+
// A junction needs no privilege on Windows; the type is ignored elsewhere.
|
|
1533
|
+
symlinkSync(target, path, process.platform === "win32" ? "junction" : "dir");
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Put the plugin at `<profile>/plugins/failproofai`, preferring a symlink to
|
|
1538
|
+
* the package's own `hermes-plugin/` so every npm upgrade updates it in place —
|
|
1539
|
+
* the same reason OpenClaw's config points into the package. Hermes' plugin
|
|
1540
|
+
* scan uses `Path.is_dir()`, which follows symlinks, so the link is discovered
|
|
1541
|
+
* like a directory. Falls back to a marked copy when a link cannot be created.
|
|
1542
|
+
*
|
|
1543
|
+
* Only a path FailproofAI owns (a marked copy, or a link into a FailproofAI
|
|
1544
|
+
* `hermes-plugin/`) is ever replaced; anything else throws.
|
|
1545
|
+
*/
|
|
1546
|
+
export function installHermesPlugin(
|
|
1547
|
+
settingsPath: string,
|
|
1548
|
+
deps: HermesPluginInstallDeps = {},
|
|
1549
|
+
): "linked" | "copied" | "unchanged" {
|
|
1444
1550
|
const source = getHermesPluginSourcePath();
|
|
1445
1551
|
for (const file of HERMES_PLUGIN_FILES) {
|
|
1446
1552
|
if (!existsSync(resolve(source, file))) {
|
|
@@ -1449,50 +1555,88 @@ function installHermesPlugin(settingsPath: string): void {
|
|
|
1449
1555
|
}
|
|
1450
1556
|
|
|
1451
1557
|
const destination = hermesPluginPathForSettings(settingsPath);
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
}
|
|
1558
|
+
const existing = hermesPluginState(settingsPath);
|
|
1559
|
+
if (existing.kind === "foreign") {
|
|
1560
|
+
throw new Error(
|
|
1561
|
+
`Refusing to overwrite an unmanaged Hermes plugin at ${destination}. ` +
|
|
1562
|
+
`Move it aside or install FailproofAI under a different Hermes profile.`,
|
|
1563
|
+
);
|
|
1459
1564
|
}
|
|
1565
|
+
if (existing.kind === "link" && existing.current) return "unchanged";
|
|
1460
1566
|
|
|
1461
1567
|
mkdirSync(dirname(destination), { recursive: true });
|
|
1462
1568
|
const suffix = `${process.pid}-${Date.now()}`;
|
|
1463
1569
|
const temporary = `${destination}.install-${suffix}`;
|
|
1464
|
-
|
|
1465
|
-
let
|
|
1570
|
+
|
|
1571
|
+
let linked = false;
|
|
1466
1572
|
try {
|
|
1573
|
+
(deps.symlink ?? defaultHermesSymlink)(source, temporary);
|
|
1574
|
+
linked = true;
|
|
1575
|
+
} catch {
|
|
1576
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
if (!linked) {
|
|
1467
1580
|
mkdirSync(temporary, { recursive: true });
|
|
1468
|
-
|
|
1469
|
-
|
|
1581
|
+
try {
|
|
1582
|
+
for (const file of HERMES_PLUGIN_FILES) {
|
|
1583
|
+
cpSync(resolve(source, file), resolve(temporary, file));
|
|
1584
|
+
}
|
|
1585
|
+
writeFileSync(
|
|
1586
|
+
resolve(temporary, HERMES_PLUGIN_MARKER),
|
|
1587
|
+
"Managed by failproofai. Remove with `failproofai policies --uninstall --cli hermes`.\n",
|
|
1588
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
1589
|
+
);
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
1592
|
+
throw err;
|
|
1470
1593
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// A link replacing a link is one atomic rename. Anything replacing a
|
|
1597
|
+
// directory (or a link replaced by a directory) cannot rename over it, so
|
|
1598
|
+
// the old entry is moved aside first and restored if the swap fails.
|
|
1599
|
+
if (linked && (existing.kind === "absent" || existing.kind === "link")) {
|
|
1600
|
+
try {
|
|
1601
|
+
renameSync(temporary, destination);
|
|
1602
|
+
} catch (err) {
|
|
1603
|
+
rmSync(temporary, { force: true });
|
|
1604
|
+
throw err;
|
|
1605
|
+
}
|
|
1606
|
+
return "linked";
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const backup = `${destination}.backup-${suffix}`;
|
|
1610
|
+
let movedExisting = false;
|
|
1611
|
+
try {
|
|
1612
|
+
if (pathPresent(destination)) {
|
|
1477
1613
|
renameSync(destination, backup);
|
|
1478
1614
|
movedExisting = true;
|
|
1479
1615
|
}
|
|
1480
1616
|
renameSync(temporary, destination);
|
|
1617
|
+
// fs.rm never follows a symlink, so a backed-up link removes only itself.
|
|
1481
1618
|
if (movedExisting) rmSync(backup, { recursive: true, force: true });
|
|
1482
1619
|
} catch (err) {
|
|
1483
1620
|
rmSync(temporary, { recursive: true, force: true });
|
|
1484
|
-
if (movedExisting && !
|
|
1621
|
+
if (movedExisting && !pathPresent(destination) && pathPresent(backup)) {
|
|
1485
1622
|
renameSync(backup, destination);
|
|
1486
1623
|
}
|
|
1487
1624
|
throw err;
|
|
1488
1625
|
}
|
|
1626
|
+
return linked ? "linked" : "copied";
|
|
1489
1627
|
}
|
|
1490
1628
|
|
|
1491
1629
|
function removeManagedHermesPlugin(settingsPath: string): boolean {
|
|
1492
1630
|
const pluginPath = hermesPluginPathForSettings(settingsPath);
|
|
1631
|
+
const state = hermesPluginState(settingsPath);
|
|
1632
|
+
if (state.kind === "link") {
|
|
1633
|
+
// Only the link goes; the package directory it points at is npm's.
|
|
1634
|
+
unlinkSync(pluginPath);
|
|
1635
|
+
return true;
|
|
1636
|
+
}
|
|
1493
1637
|
// The marker is the ownership boundary. Remove even an incomplete managed
|
|
1494
|
-
//
|
|
1495
|
-
if (
|
|
1638
|
+
// copy so an interrupted install can always be repaired or uninstalled.
|
|
1639
|
+
if (state.kind !== "copy") return false;
|
|
1496
1640
|
rmSync(pluginPath, { recursive: true, force: true });
|
|
1497
1641
|
return true;
|
|
1498
1642
|
}
|
|
@@ -1549,13 +1693,163 @@ function hermesConfigState(settingsPath: string): {
|
|
|
1549
1693
|
};
|
|
1550
1694
|
}
|
|
1551
1695
|
|
|
1696
|
+
/**
|
|
1697
|
+
* Enable the native plugin in one profile's config and drop FailproofAI's
|
|
1698
|
+
* legacy shell hooks. A native plugin and the legacy shell bridge running
|
|
1699
|
+
* together would evaluate every event twice. Only failproofai-owned shell
|
|
1700
|
+
* entries are removed; unrelated operator hooks remain intact.
|
|
1701
|
+
*/
|
|
1702
|
+
function enableHermesPluginInDoc(doc: Document): void {
|
|
1703
|
+
removeLegacyHermesHooks(doc);
|
|
1704
|
+
|
|
1705
|
+
const js = (doc.toJS() ?? {}) as { plugins?: HermesPluginsConfig };
|
|
1706
|
+
const plugins: HermesPluginsConfig =
|
|
1707
|
+
js.plugins && typeof js.plugins === "object" ? js.plugins : {};
|
|
1708
|
+
const enabled = Array.isArray(plugins.enabled) ? [...plugins.enabled] : [];
|
|
1709
|
+
if (!enabled.includes(HERMES_PLUGIN_ID)) enabled.push(HERMES_PLUGIN_ID);
|
|
1710
|
+
plugins.enabled = enabled;
|
|
1711
|
+
if (Array.isArray(plugins.disabled)) {
|
|
1712
|
+
plugins.disabled = plugins.disabled.filter((name) => name !== HERMES_PLUGIN_ID);
|
|
1713
|
+
if (plugins.disabled.length === 0) delete plugins.disabled;
|
|
1714
|
+
}
|
|
1715
|
+
doc.set("plugins", plugins);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
export type HermesMigrationStatus =
|
|
1719
|
+
/** Brought to the linked-plugin state by this run. */
|
|
1720
|
+
| "migrated"
|
|
1721
|
+
/** Already linked to this package, enabled, and free of shell hooks. */
|
|
1722
|
+
| "current"
|
|
1723
|
+
/** No FailproofAI integration in this profile — left alone. */
|
|
1724
|
+
| "untouched"
|
|
1725
|
+
/** Needs migrating, but the daemon cannot serve the native plugin. Nothing changed. */
|
|
1726
|
+
| "blocked"
|
|
1727
|
+
/** Could not be migrated (unmanaged plugin in the way, unreadable config, I/O error). */
|
|
1728
|
+
| "failed";
|
|
1729
|
+
|
|
1730
|
+
export interface HermesProfileMigration {
|
|
1731
|
+
name: string;
|
|
1732
|
+
home: string;
|
|
1733
|
+
status: HermesMigrationStatus;
|
|
1734
|
+
detail: string;
|
|
1735
|
+
/** Legacy config.yaml shell hooks are still in place after this run. */
|
|
1736
|
+
legacyShellHooksRemain: boolean;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
/**
|
|
1740
|
+
* The upgrade half `failproofai update` owes Hermes: bring every profile that
|
|
1741
|
+
* already uses FailproofAI (legacy shell hooks, a copied plugin, or a link) to
|
|
1742
|
+
* the linked-plugin state. Profiles with no FailproofAI integration are never
|
|
1743
|
+
* touched — an update must not opt a profile in.
|
|
1744
|
+
*
|
|
1745
|
+
* `daemonSupportsPolicyEvaluation` is asked at most once, and only when some
|
|
1746
|
+
* profile needs changing. When it says no, NOTHING is changed: removing the
|
|
1747
|
+
* shell hooks in favour of a plugin whose daemon cannot answer would fail every
|
|
1748
|
+
* tool call closed (the same gate `policies --install` applies).
|
|
1749
|
+
*/
|
|
1750
|
+
export async function migrateHermesProfiles(opts: {
|
|
1751
|
+
daemonSupportsPolicyEvaluation: () => Promise<boolean>;
|
|
1752
|
+
installDeps?: HermesPluginInstallDeps;
|
|
1753
|
+
}): Promise<HermesProfileMigration[]> {
|
|
1754
|
+
const results: HermesProfileMigration[] = [];
|
|
1755
|
+
let daemonOk: boolean | undefined;
|
|
1756
|
+
for (const profile of listHermesProfiles()) {
|
|
1757
|
+
if (!existsSync(profile.home)) continue;
|
|
1758
|
+
const settingsPath = resolve(profile.home, "config.yaml");
|
|
1759
|
+
const pluginPath = hermesPluginPathForSettings(settingsPath);
|
|
1760
|
+
const config = hermesConfigState(settingsPath);
|
|
1761
|
+
const plugin = hermesPluginState(settingsPath);
|
|
1762
|
+
const ours = plugin.kind === "link" || plugin.kind === "copy";
|
|
1763
|
+
const base = { name: profile.name, home: profile.home };
|
|
1764
|
+
const legacy = config.legacyShellHookPresent;
|
|
1765
|
+
|
|
1766
|
+
if (!legacy && !ours && !config.pluginEnabled) {
|
|
1767
|
+
results.push({ ...base, status: "untouched", detail: "no failproofai integration", legacyShellHooksRemain: false });
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
if (plugin.kind === "link" && plugin.current && plugin.complete && config.pluginEnabled && !legacy) {
|
|
1771
|
+
results.push({ ...base, status: "current", detail: "already linked", legacyShellHooksRemain: false });
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
if (plugin.kind === "foreign") {
|
|
1775
|
+
results.push({
|
|
1776
|
+
...base,
|
|
1777
|
+
status: "failed",
|
|
1778
|
+
detail:
|
|
1779
|
+
`an unmanaged plugin occupies ${pluginPath}` +
|
|
1780
|
+
(legacy ? "; shell hooks left in place (cron jobs are not checked)" : ""),
|
|
1781
|
+
legacyShellHooksRemain: legacy,
|
|
1782
|
+
});
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
if (existsSync(settingsPath)) {
|
|
1786
|
+
const errors = parseDocument(readFileSync(settingsPath, "utf8")).errors;
|
|
1787
|
+
if (errors.length > 0) {
|
|
1788
|
+
results.push({
|
|
1789
|
+
...base,
|
|
1790
|
+
status: "failed",
|
|
1791
|
+
detail: `${settingsPath} does not parse as YAML; left untouched`,
|
|
1792
|
+
legacyShellHooksRemain: legacy,
|
|
1793
|
+
});
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
if (daemonOk === undefined) daemonOk = await opts.daemonSupportsPolicyEvaluation();
|
|
1799
|
+
if (!daemonOk) {
|
|
1800
|
+
results.push({
|
|
1801
|
+
...base,
|
|
1802
|
+
status: "blocked",
|
|
1803
|
+
detail: legacy
|
|
1804
|
+
? "daemon lacks native policy evaluation; shell hooks left in place (cron jobs are not checked)"
|
|
1805
|
+
: "daemon lacks native policy evaluation; the plugin here cannot get verdicts",
|
|
1806
|
+
legacyShellHooksRemain: legacy,
|
|
1807
|
+
});
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
try {
|
|
1812
|
+
// Plugin first, config second: the shell hooks are only removed once the
|
|
1813
|
+
// plugin that replaces them is on disk.
|
|
1814
|
+
const mode = installHermesPlugin(settingsPath, opts.installDeps);
|
|
1815
|
+
const doc = readYamlDoc(settingsPath);
|
|
1816
|
+
enableHermesPluginInDoc(doc);
|
|
1817
|
+
writeYamlDoc(settingsPath, doc);
|
|
1818
|
+
const from = legacy
|
|
1819
|
+
? "shell hooks"
|
|
1820
|
+
: plugin.kind === "copy"
|
|
1821
|
+
? "copied plugin"
|
|
1822
|
+
: plugin.kind === "link"
|
|
1823
|
+
? "old plugin link"
|
|
1824
|
+
: "enabled but missing plugin";
|
|
1825
|
+
const to = mode === "copied" ? "plugin copy (symlink not possible here)" : "linked plugin";
|
|
1826
|
+
results.push({ ...base, status: "migrated", detail: `${from} → ${to}`, legacyShellHooksRemain: false });
|
|
1827
|
+
} catch (err) {
|
|
1828
|
+
results.push({
|
|
1829
|
+
...base,
|
|
1830
|
+
status: "failed",
|
|
1831
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
1832
|
+
legacyShellHooksRemain: hermesConfigState(settingsPath).legacyShellHookPresent,
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
return results;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1552
1839
|
export interface HermesProfileHealth {
|
|
1553
1840
|
name: string;
|
|
1554
1841
|
home: string;
|
|
1555
1842
|
settingsPath: string;
|
|
1556
1843
|
pluginInstalled: boolean;
|
|
1844
|
+
/** How the plugin is installed: a link into the package, a copy, or neither. */
|
|
1845
|
+
pluginMode: "link" | "copy" | null;
|
|
1557
1846
|
pluginEnabled: boolean;
|
|
1558
1847
|
legacyShellHookPresent: boolean;
|
|
1848
|
+
/**
|
|
1849
|
+
* Still enforced only by legacy config.yaml shell hooks. Hermes cron jobs run
|
|
1850
|
+
* with none of those, so their tool calls go unchecked.
|
|
1851
|
+
*/
|
|
1852
|
+
cronUnchecked: boolean;
|
|
1559
1853
|
healthy: boolean;
|
|
1560
1854
|
}
|
|
1561
1855
|
|
|
@@ -1564,15 +1858,19 @@ export function hermesProfileHealth(): HermesProfileHealth[] {
|
|
|
1564
1858
|
return listHermesProfiles().map((profile) => {
|
|
1565
1859
|
const settingsPath = resolve(profile.home, "config.yaml");
|
|
1566
1860
|
const config = hermesConfigState(settingsPath);
|
|
1567
|
-
const
|
|
1861
|
+
const plugin = hermesPluginState(settingsPath);
|
|
1862
|
+
const pluginInstalled = (plugin.kind === "link" || plugin.kind === "copy") && plugin.complete;
|
|
1863
|
+
const pluginWorks = pluginInstalled && config.pluginEnabled;
|
|
1568
1864
|
return {
|
|
1569
1865
|
name: profile.name,
|
|
1570
1866
|
home: profile.home,
|
|
1571
1867
|
settingsPath,
|
|
1572
1868
|
pluginInstalled,
|
|
1869
|
+
pluginMode: plugin.kind === "link" || plugin.kind === "copy" ? plugin.kind : null,
|
|
1573
1870
|
pluginEnabled: config.pluginEnabled,
|
|
1574
1871
|
legacyShellHookPresent: config.legacyShellHookPresent,
|
|
1575
|
-
|
|
1872
|
+
cronUnchecked: config.legacyShellHookPresent && !pluginWorks,
|
|
1873
|
+
healthy: pluginWorks && !config.legacyShellHookPresent,
|
|
1576
1874
|
};
|
|
1577
1875
|
});
|
|
1578
1876
|
}
|
|
@@ -1582,13 +1880,25 @@ export function hermesProfileStatusRows(): Array<[string, string]> {
|
|
|
1582
1880
|
return hermesProfileHealth()
|
|
1583
1881
|
.filter((profile) => existsSync(profile.home))
|
|
1584
1882
|
.map((profile) => {
|
|
1883
|
+
const label = "hermes/" + profile.name;
|
|
1884
|
+
if (profile.cronUnchecked) {
|
|
1885
|
+
// Not a cosmetic leftover: the shell-hook integration does not reach
|
|
1886
|
+
// cron jobs at all, so this profile has an unenforced path.
|
|
1887
|
+
return [
|
|
1888
|
+
label,
|
|
1889
|
+
"UNHEALTHY — legacy shell hooks: Hermes cron jobs are not checked. Run `failproofai update`",
|
|
1890
|
+
];
|
|
1891
|
+
}
|
|
1585
1892
|
const problems: string[] = [];
|
|
1586
1893
|
if (!profile.pluginInstalled) problems.push("plugin files missing or incomplete");
|
|
1587
1894
|
if (!profile.pluginEnabled) problems.push("plugin not enabled");
|
|
1588
1895
|
if (profile.legacyShellHookPresent) problems.push("legacy shell hook also present");
|
|
1896
|
+
if (problems.length > 0) return [label, "UNHEALTHY — " + problems.join("; ")];
|
|
1589
1897
|
return [
|
|
1590
|
-
|
|
1591
|
-
|
|
1898
|
+
label,
|
|
1899
|
+
profile.pluginMode === "copy"
|
|
1900
|
+
? "native plugin enabled (copied — `failproofai update` links it so upgrades apply)"
|
|
1901
|
+
: "native plugin enabled",
|
|
1592
1902
|
];
|
|
1593
1903
|
});
|
|
1594
1904
|
}
|
|
@@ -1641,23 +1951,7 @@ export const hermes: Integration = {
|
|
|
1641
1951
|
isFailproofaiHook: isMarkedHook,
|
|
1642
1952
|
|
|
1643
1953
|
writeHookEntries(settings) {
|
|
1644
|
-
|
|
1645
|
-
// A native plugin and the legacy shell bridge running together would
|
|
1646
|
-
// evaluate every event twice. Remove only failproofai-owned shell entries;
|
|
1647
|
-
// unrelated operator hooks remain intact.
|
|
1648
|
-
removeLegacyHermesHooks(doc);
|
|
1649
|
-
|
|
1650
|
-
const js = (doc.toJS() ?? {}) as { plugins?: HermesPluginsConfig };
|
|
1651
|
-
const plugins: HermesPluginsConfig =
|
|
1652
|
-
js.plugins && typeof js.plugins === "object" ? js.plugins : {};
|
|
1653
|
-
const enabled = Array.isArray(plugins.enabled) ? [...plugins.enabled] : [];
|
|
1654
|
-
if (!enabled.includes(HERMES_PLUGIN_ID)) enabled.push(HERMES_PLUGIN_ID);
|
|
1655
|
-
plugins.enabled = enabled;
|
|
1656
|
-
if (Array.isArray(plugins.disabled)) {
|
|
1657
|
-
plugins.disabled = plugins.disabled.filter((name) => name !== HERMES_PLUGIN_ID);
|
|
1658
|
-
if (plugins.disabled.length === 0) delete plugins.disabled;
|
|
1659
|
-
}
|
|
1660
|
-
doc.set("plugins", plugins);
|
|
1954
|
+
enableHermesPluginInDoc(settings as unknown as Document);
|
|
1661
1955
|
},
|
|
1662
1956
|
|
|
1663
1957
|
removeHooksFromFile(settingsPath) {
|
|
@@ -57,7 +57,7 @@ export const JEV_ACTIVITY_KEYS = [
|
|
|
57
57
|
|
|
58
58
|
const EVALUATORS = new Set(["jev", "jev-fallback"]);
|
|
59
59
|
const DECISIONS = new Set(["allow", "instruct", "deny"]);
|
|
60
|
-
const MODES = new Set(["
|
|
60
|
+
const MODES = new Set(["observe", "enforce"]);
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* A reason's leading code — lowercase kebab-case — either alone or followed by
|
|
@@ -374,9 +374,9 @@ export function describeJevActivity(raw: JevActivityFields): string[] | null {
|
|
|
374
374
|
if (e.jevDecision) facts.push(`Jev verdict: ${e.jevDecision}`);
|
|
375
375
|
const cleared = e.jevCleared ?? [];
|
|
376
376
|
if (cleared.length > 0) {
|
|
377
|
-
facts.push(`${e.jevMode === "
|
|
377
|
+
facts.push(`${e.jevMode === "observe" ? "would have cleared" : "cleared"} ${cleared.join(", ")}`);
|
|
378
378
|
}
|
|
379
|
-
if (e.jevMode === "
|
|
379
|
+
if (e.jevMode === "observe") facts.push("observe mode: the regex result was enforced");
|
|
380
380
|
if (e.jevLatencyMs !== undefined) facts.push(`${e.jevLatencyMs} ms`);
|
|
381
381
|
if (e.jevModel) facts.push(e.jevModel);
|
|
382
382
|
// Answered, but nothing this build can show (a verdict in a shape it cannot read).
|
package/src/hooks/jev-cli.ts
CHANGED
|
@@ -186,7 +186,7 @@ export const JEV_USAGE = [
|
|
|
186
186
|
"Usage:",
|
|
187
187
|
" failproofai jev --url <url> [--key-stdin | --token <token>] [options]",
|
|
188
188
|
" failproofai jev setup --provider <kind> [--key-stdin | --key-from-env] [options]",
|
|
189
|
-
" failproofai jev setup --provider failproofai [--mode off|
|
|
189
|
+
" failproofai jev setup --provider failproofai [--mode off|observe|enforce] (FailproofAI Cloud: no key, no URL)",
|
|
190
190
|
" failproofai jev status [--json]",
|
|
191
191
|
" failproofai jev test [--json]",
|
|
192
192
|
" failproofai jev models [--provider <kind>] [--url <base>] [--json]",
|
|
@@ -269,7 +269,7 @@ function modeLine(mode: NonNullable<JevConfig["mode"]>): string {
|
|
|
269
269
|
if (mode === "off") return "off — Jev is not asked at all; the regex policies decide alone";
|
|
270
270
|
return mode === "enforce"
|
|
271
271
|
? "enforce — Jev's verdicts apply: it may clear a reviewable policy's deny and add its own"
|
|
272
|
-
: "
|
|
272
|
+
: "observe — Jev is asked and logged; the regex result is what is enforced";
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
/**
|
|
@@ -458,12 +458,11 @@ export function jevStatsLines(stats: JevStats | null, opts: RenderOpts = {}): st
|
|
|
458
458
|
.join(", ");
|
|
459
459
|
const clears = byPolicy(stats.clearsByPolicy);
|
|
460
460
|
// `clearsByPolicy` counts clears that CHANGED an outcome, which only enforce
|
|
461
|
-
// mode can do; in
|
|
462
|
-
// `
|
|
463
|
-
// alone would tell
|
|
464
|
-
// turned
|
|
465
|
-
|
|
466
|
-
const shadowClears = byPolicy((stats as { shadowClearsByPolicy?: Record<string, number> }).shadowClearsByPolicy);
|
|
461
|
+
// mode can do; in observe mode every clear Jev would have made is in
|
|
462
|
+
// `observeClearsByPolicy` instead and this one is empty. Printing the first
|
|
463
|
+
// alone would tell an observe-mode user "cleared nothing" — the one number
|
|
464
|
+
// they turned observe mode on to watch.
|
|
465
|
+
const observeClears = byPolicy(stats.observeClearsByPolicy);
|
|
467
466
|
const ms = (v: number | null) => (v === null ? "—" : `${Math.round(v)} ms`);
|
|
468
467
|
return stack(
|
|
469
468
|
heading,
|
|
@@ -473,7 +472,7 @@ export function jevStatsLines(stats: JevStats | null, opts: RenderOpts = {}): st
|
|
|
473
472
|
["fell back to regex", `${pct(stats.fallbackRate)}${reasons ? ` (${reasons})` : ""}`],
|
|
474
473
|
["latency", `p50 ${ms(stats.latencyP50Ms)} · p95 ${ms(stats.latencyP95Ms)}`],
|
|
475
474
|
["cleared", clears || "nothing"],
|
|
476
|
-
...(
|
|
475
|
+
...(observeClears ? ([["would have cleared (observe)", observeClears]] as Array<[string, string]>) : []),
|
|
477
476
|
],
|
|
478
477
|
opts,
|
|
479
478
|
),
|
|
@@ -1206,11 +1205,10 @@ async function cloudSetup(values: Map<string, string>, bools: Set<string>, opts:
|
|
|
1206
1205
|
delete next.apiKey;
|
|
1207
1206
|
delete next.accountId;
|
|
1208
1207
|
if (!sameProvider && existing?.raw.timeoutMs !== undefined) next.timeoutMs = existing.raw.timeoutMs;
|
|
1209
|
-
//
|
|
1208
|
+
// Observe unless told otherwise, as `config --token` starts it: Jev on a new
|
|
1210
1209
|
// route is logged before it is allowed to clear anything. A mode already in a
|
|
1211
1210
|
// Cloud file is kept.
|
|
1212
|
-
|
|
1213
|
-
next.mode = mode;
|
|
1211
|
+
next.mode = values.get("--mode") ?? (sameProvider && typeof next.mode === "string" ? next.mode : "observe");
|
|
1214
1212
|
if (values.has("--timeout-ms")) {
|
|
1215
1213
|
const rawTimeout = values.get("--timeout-ms") as string;
|
|
1216
1214
|
const n = Number(rawTimeout);
|
|
@@ -1247,7 +1245,7 @@ async function cloudSetup(values: Map<string, string>, bools: Set<string>, opts:
|
|
|
1247
1245
|
]
|
|
1248
1246
|
: [
|
|
1249
1247
|
"Not saved: this machine is not connected to FailproofAI Cloud with a key that carries jev:evaluate.",
|
|
1250
|
-
"Connect it with one that does (the \"machine\" preset) — that also turns Jev on, in
|
|
1248
|
+
"Connect it with one that does (the \"machine\" preset) — that also turns Jev on, in observe mode, when there is no jev.json yet:",
|
|
1251
1249
|
]),
|
|
1252
1250
|
" failproofai config --token <key>",
|
|
1253
1251
|
"",
|
|
@@ -1332,7 +1330,7 @@ async function cloudSetup(values: Map<string, string>, bools: Set<string>, opts:
|
|
|
1332
1330
|
/**
|
|
1333
1331
|
* Where a file would send requests, for display, whether or not it loads. Only
|
|
1334
1332
|
* the routing fields are used — the key and model do not decide where requests
|
|
1335
|
-
* go — and
|
|
1333
|
+
* go — and observe mode lets a loopback http URL through validation. Null when
|
|
1336
1334
|
* it names nothing recognisable.
|
|
1337
1335
|
*/
|
|
1338
1336
|
/**
|
|
@@ -1366,7 +1364,7 @@ function displayOnlyCloudOrigin(routing: { provider?: unknown; baseUrl?: unknown
|
|
|
1366
1364
|
|
|
1367
1365
|
function namedEndpoint(raw: Record<string, unknown> | null): string | null {
|
|
1368
1366
|
if (!raw || typeof raw.provider !== "string" || !(JEV_PROVIDER_KINDS as readonly string[]).includes(raw.provider)) return null;
|
|
1369
|
-
const routing: Record<string, unknown> = { provider: raw.provider, apiKey: ENV_KEY_STAND_IN, mode: "
|
|
1367
|
+
const routing: Record<string, unknown> = { provider: raw.provider, apiKey: ENV_KEY_STAND_IN, mode: "observe", ...displayOnlyCloudOrigin(raw) };
|
|
1370
1368
|
if (raw.baseUrl !== undefined) routing.baseUrl = raw.baseUrl;
|
|
1371
1369
|
if (raw.accountId !== undefined) routing.accountId = raw.accountId;
|
|
1372
1370
|
try {
|
|
@@ -1499,6 +1497,8 @@ async function status(argv: string[], opts: RenderOpts): Promise<JevCliResult> {
|
|
|
1499
1497
|
enabled: coverage.enabled,
|
|
1500
1498
|
reviewable: coverage.reviewable,
|
|
1501
1499
|
customPolicyFiles: coverage.customFiles,
|
|
1500
|
+
// Checks an installed pack gives Jev to ask; 0 means Jev is inert.
|
|
1501
|
+
jevChecks: coverage.jevChecks,
|
|
1502
1502
|
problem: coverageProblem,
|
|
1503
1503
|
},
|
|
1504
1504
|
}
|
|
@@ -1599,7 +1599,7 @@ async function status(argv: string[], opts: RenderOpts): Promise<JevCliResult> {
|
|
|
1599
1599
|
],
|
|
1600
1600
|
opts,
|
|
1601
1601
|
),
|
|
1602
|
-
nextStep("failproofai jev setup --mode
|
|
1602
|
+
nextStep("failproofai jev setup --mode observe", "Switch it back on (observe logs Jev and keeps enforcing regex; enforce lets it clear), here or from the dashboard:", opts),
|
|
1603
1603
|
legacyNote,
|
|
1604
1604
|
jevStatsLines(stats, opts),
|
|
1605
1605
|
),
|
|
@@ -1786,7 +1786,7 @@ async function test(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promise
|
|
|
1786
1786
|
: inspection.status === "key-missing"
|
|
1787
1787
|
? "failproofai jev setup --key-stdin < key-file"
|
|
1788
1788
|
: inspection.status === "off"
|
|
1789
|
-
? "failproofai jev setup --mode
|
|
1789
|
+
? "failproofai jev setup --mode observe"
|
|
1790
1790
|
: inspection.status === "not-connected" || inspection.status === "key-lacks-jev"
|
|
1791
1791
|
? "failproofai config --token <key>"
|
|
1792
1792
|
: "failproofai jev setup --provider <kind> --key-stdin";
|