failproofai 1.0.8 → 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 +4 -4
- 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 +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/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 +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/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 +4 -4
- 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 +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/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/src_hooks_1aveq0u._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03m2iw6._.js +3 -0
- 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/_11ilmst._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{_0-vcssj._.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 +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/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/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.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/{0wbjy0zo-m7is.js → 08ckontevw-0w.js} +1 -1
- package/.next/standalone/.next/static/chunks/0pefjgvu4x-sx.js +1 -0
- package/.next/standalone/.next/static/chunks/{21uv-uusw329x.js → 1-d48e9k-7e1a.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0m-9d6yn9hx4j.js → 1r74hmr1wm_sd.js} +1 -1
- package/.next/standalone/.next/static/chunks/1uec0n0lcx5h1.js +1 -0
- package/.next/standalone/.next/static/chunks/21-pqc3-l9ak2.js +6 -0
- package/.next/standalone/.next/static/chunks/21qlo00jis4w-.js +1 -0
- package/.next/standalone/.next/static/chunks/2n00c3zqyloy9.js +1 -0
- package/.next/standalone/.next/static/chunks/{0bhidk90e-07f.css → 3_e_wg5_oay87.css} +1 -1
- package/.next/standalone/.next/static/chunks/{3c3qbmosdjl6w.js → 3tv107_for_n7.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2qdpj67x6ifk_.js → 3vp54x0dyrrm6.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-38nzuvp95_2rv.js → turbopack-11dn2pegmg9_9.js} +1 -1
- 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/fp-cloud-cli/uv.lock +3 -3
- package/.next/standalone/hermes-plugin/README.md +18 -8
- 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 +12 -12
- package/.next/standalone/sdk/python/uv.lock +18 -18
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +33 -7
- package/dist/cli.mjs +1385 -1158
- package/dist/worker.mjs +59 -375
- package/hermes-plugin/README.md +18 -8
- package/package.json +12 -12
- 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 +472 -68
- package/src/hooks/jev-activity.ts +8 -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 +30 -12
- package/src/hooks/policy-types.ts +4 -4
- package/src/hooks/safe-config-write.ts +119 -0
- 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 → ozuWoyIwOTqeUUjKqOKiH}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{5yKM8NoTOpUI0MoQl8eo5 → ozuWoyIwOTqeUUjKqOKiH}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{5yKM8NoTOpUI0MoQl8eo5 → ozuWoyIwOTqeUUjKqOKiH}/_ssgManifest.js +0 -0
|
@@ -61,8 +61,8 @@ 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 {
|
|
65
|
-
import { resolvePolicyAuthority
|
|
64
|
+
import { hasInstalledRegexPacks, readInstalledPacks } from "./pack-manifest";
|
|
65
|
+
import { resolvePolicyAuthority } from "./policy-authority";
|
|
66
66
|
import { POLICY_CATALOG } from "./policy-catalog";
|
|
67
67
|
import { normalizePolicyName } from "./policy-registry";
|
|
68
68
|
import type { HooksConfig } from "./policy-types";
|
|
@@ -87,11 +87,23 @@ export interface ReviewableCoverage {
|
|
|
87
87
|
* user convention files — whose policies are not in the counts above.
|
|
88
88
|
*/
|
|
89
89
|
customFiles: number;
|
|
90
|
+
/**
|
|
91
|
+
* Jev checks the installed packs give it to ask. Zero on a machine with no
|
|
92
|
+
* pack declaring any — the vanilla install — and then Jev is never called.
|
|
93
|
+
*/
|
|
94
|
+
jevChecks: number;
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
/** The command that replaces a pack with one built by this release. */
|
|
93
98
|
export const RETAKE_PACK_COMMAND = "failproofai policies add FailproofAI/policies";
|
|
94
99
|
|
|
100
|
+
/** The command that gives Jev its checks. The package ships none. */
|
|
101
|
+
export const JEV_POLICIES_ADD_COMMAND = "failproofai policies add FailproofAI/jev-policies";
|
|
102
|
+
|
|
103
|
+
/** Said wherever Jev is configured but has nothing to ask. Kept short: it is a status line. */
|
|
104
|
+
export const NO_JEV_CHECKS_PROBLEM =
|
|
105
|
+
`Jev has no checks installed, so it asks nothing and clears nothing. To enable it: \`${JEV_POLICIES_ADD_COMMAND}\`.`;
|
|
106
|
+
|
|
95
107
|
/**
|
|
96
108
|
* Count a policy set by the authority it would REGISTER with —
|
|
97
109
|
* `resolvePolicyAuthority`, the rule `registerPolicy` applies — so this cannot
|
|
@@ -112,11 +124,9 @@ export const RETAKE_PACK_COMMAND = "failproofai policies add FailproofAI/policie
|
|
|
112
124
|
export function countReviewable(
|
|
113
125
|
policies: Iterable<AuthorityRecord>,
|
|
114
126
|
/**
|
|
115
|
-
* The semantic checks that can be asked on this machine
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* reviewable" on exactly the machines the feature was built for — the ones
|
|
119
|
-
* running a pack that carries both tiers.
|
|
127
|
+
* The semantic checks that can be asked on this machine — the ones installed
|
|
128
|
+
* packs declare. Defaults to none, which is what a machine with no such pack
|
|
129
|
+
* has: nothing is reviewable there.
|
|
120
130
|
*/
|
|
121
131
|
knownReviewers?: ReadonlySet<string>,
|
|
122
132
|
): {
|
|
@@ -157,7 +167,9 @@ export function surveyReviewableCoverage(cwd?: string): ReviewableCoverage {
|
|
|
157
167
|
*/
|
|
158
168
|
let reviewers: ReadonlySet<string> | undefined;
|
|
159
169
|
try {
|
|
160
|
-
|
|
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();
|
|
161
173
|
const packs = readInstalledPacks().packs;
|
|
162
174
|
for (const pack of packs) {
|
|
163
175
|
const selected = pack.enabled;
|
|
@@ -169,10 +181,11 @@ export function surveyReviewableCoverage(cwd?: string): ReviewableCoverage {
|
|
|
169
181
|
// hook path's manifest-only `reviewerNamesFor` cannot see. Anything else and
|
|
170
182
|
// the panel and `jev status` promise a clear that cannot happen.
|
|
171
183
|
const asked = semanticPoliciesFromPacks(jevPacks(packs));
|
|
172
|
-
reviewers =
|
|
184
|
+
reviewers = new Set(asked.policies.map((p) => p.name));
|
|
173
185
|
} catch {
|
|
174
|
-
// An unreadable manifest enforces nothing
|
|
175
|
-
// reports that to the hook log on the path
|
|
186
|
+
// An unreadable manifest enforces nothing and gives Jev nothing to ask;
|
|
187
|
+
// `readInstalledPacks` already reports that to the hook log on the path
|
|
188
|
+
// that cares.
|
|
176
189
|
}
|
|
177
190
|
|
|
178
191
|
let config: HooksConfig;
|
|
@@ -212,7 +225,7 @@ export function surveyReviewableCoverage(cwd?: string): ReviewableCoverage {
|
|
|
212
225
|
customFiles = 0;
|
|
213
226
|
}
|
|
214
227
|
|
|
215
|
-
return { ...countReviewable(records, reviewers), customFiles };
|
|
228
|
+
return { ...countReviewable(records, reviewers), customFiles, jevChecks: reviewers?.size ?? 0 };
|
|
216
229
|
}
|
|
217
230
|
|
|
218
231
|
/**
|
|
@@ -244,8 +257,13 @@ export function reviewableSummary(coverage: ReviewableCoverage): string {
|
|
|
244
257
|
* Nothing is diagnosed for an empty policy set: there is no clear to lose, and
|
|
245
258
|
* an authority warning to someone who enforces nothing answers a question they
|
|
246
259
|
* did not ask.
|
|
260
|
+
*
|
|
261
|
+
* Except the one thing that comes first: no installed pack gives Jev any checks.
|
|
262
|
+
* Then Jev asks nothing whatever the policy set is, and the fix is to install
|
|
263
|
+
* them, not to re-mark anything.
|
|
247
264
|
*/
|
|
248
265
|
export function reviewableProblem(coverage: ReviewableCoverage): string | null {
|
|
266
|
+
if (coverage.jevChecks === 0) return NO_JEV_CHECKS_PROBLEM;
|
|
249
267
|
// Policies from the user's own files were not counted and may be reviewable,
|
|
250
268
|
// so "never" cannot be said honestly; the summary already says what was skipped.
|
|
251
269
|
if (coverage.enabled === 0 || coverage.reviewable > 0 || coverage.customFiles > 0) return null;
|
|
@@ -46,9 +46,9 @@ export interface RegisteredPolicy {
|
|
|
46
46
|
/** Whether Jev may clear this policy's deny/instruct; see {@link effectiveAuthority}. */
|
|
47
47
|
authority?: PolicyAuthority;
|
|
48
48
|
/**
|
|
49
|
-
* The
|
|
50
|
-
* asked, and each clear (see {@link PolicyAuthority}),
|
|
51
|
-
* policy.
|
|
49
|
+
* The Jev checks (declared by an installed pack — the package ships none)
|
|
50
|
+
* that must all be asked, and each clear (see {@link PolicyAuthority}),
|
|
51
|
+
* before Jev clears this policy.
|
|
52
52
|
*/
|
|
53
53
|
reviewedBy?: string[];
|
|
54
54
|
}
|
|
@@ -188,7 +188,7 @@ export interface SemanticProbeDeclaration {
|
|
|
188
188
|
/**
|
|
189
189
|
* A semantic (Jev) policy as its author DECLARES it — a question set, not code.
|
|
190
190
|
*
|
|
191
|
-
* The
|
|
191
|
+
* The runtime form is `SemanticPolicy` in `semantic/types.ts`, and
|
|
192
192
|
* the one difference is the whole reason both exist: there, `precondition` is a
|
|
193
193
|
* function over the computed facts; here it is the NAME of one, because a
|
|
194
194
|
* declaration has to survive a trip through a JSON manifest and because a
|
|
@@ -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
|
+
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* | Jev answered | reviewable denies/instructs Jev covered are cleared; final =|
|
|
23
23
|
* | | the most severe of {remaining regex results, Jev's verdict} |
|
|
24
24
|
*
|
|
25
|
-
* `
|
|
25
|
+
* `observe` mode computes and records all of it, and still returns the regex
|
|
26
26
|
* result.
|
|
27
27
|
*
|
|
28
28
|
* ## Where this departs from plan §4
|
|
@@ -232,14 +232,14 @@ import type { PolicyAuthority } from "../policy-types";
|
|
|
232
232
|
// The only value imports, and deliberately from the activity vocabulary rather
|
|
233
233
|
// than local literals: the fallback reason this module writes itself has to
|
|
234
234
|
// be a code the activity store's closed list names, or it is stored and
|
|
235
|
-
// shipped as `other`, and
|
|
235
|
+
// shipped as `other`, and an observe-mode verdict's model id has to pass the same
|
|
236
236
|
// shape check the row's own `jevModel` does. Importing them makes a rename a
|
|
237
237
|
// compile error here. `jev-activity.ts` is pure (no node imports, no semantic
|
|
238
238
|
// modules), so this costs the hook path nothing.
|
|
239
239
|
import { JEV_MODEL_RE, JEV_REASON_REQUEST_CUT } from "../jev-activity";
|
|
240
240
|
|
|
241
241
|
export type Decision = "allow" | "deny" | "instruct";
|
|
242
|
-
export type JevMode = "
|
|
242
|
+
export type JevMode = "observe" | "enforce";
|
|
243
243
|
|
|
244
244
|
/** One regex policy's verdict, in evaluation order. */
|
|
245
245
|
export interface RegexVerdict {
|
|
@@ -383,13 +383,13 @@ export interface JevActivityFields {
|
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
/**
|
|
386
|
-
* Jev's own deny or instruct in
|
|
386
|
+
* Jev's own deny or instruct in OBSERVE mode — the verdict enforce mode would
|
|
387
387
|
* have applied, recorded rather than applied. The handler files it in the
|
|
388
388
|
* activity row's `observed` list, the "would have" record observe-mode cloud
|
|
389
389
|
* and pack policies already use, so FailproofAI Cloud's policy page counts it
|
|
390
390
|
* with no change on its side.
|
|
391
391
|
*/
|
|
392
|
-
export interface
|
|
392
|
+
export interface ObserveVerdict {
|
|
393
393
|
/** `semantic/<check>` — the name enforce mode would have attributed it to. */
|
|
394
394
|
policyName: string;
|
|
395
395
|
decision: "deny" | "instruct";
|
|
@@ -401,7 +401,7 @@ export interface ShadowVerdict {
|
|
|
401
401
|
|
|
402
402
|
export interface CombineOutcome {
|
|
403
403
|
final: FinalVerdict;
|
|
404
|
-
/** Reviewable regex policies Jev cleared (in
|
|
404
|
+
/** Reviewable regex policies Jev cleared (in observe mode: would have cleared). */
|
|
405
405
|
cleared: string[];
|
|
406
406
|
/**
|
|
407
407
|
* True when the first entry of `final` came from THIS TIER rather than from
|
|
@@ -414,11 +414,11 @@ export interface CombineOutcome {
|
|
|
414
414
|
decidedByJev: boolean;
|
|
415
415
|
activity: JevActivityFields;
|
|
416
416
|
/**
|
|
417
|
-
*
|
|
417
|
+
* Observe mode only, and only when Jev's own verdict was deny or instruct.
|
|
418
418
|
* Absent otherwise — including every enforce outcome, where the verdict was
|
|
419
419
|
* APPLIED and `decidedByJev` / the final entries already say so.
|
|
420
420
|
*/
|
|
421
|
-
|
|
421
|
+
observeVerdict?: ObserveVerdict;
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
/**
|
|
@@ -495,7 +495,7 @@ export function combineTwoTier(
|
|
|
495
495
|
const notDenied = new Set(review.notDenied);
|
|
496
496
|
const cleared = wholePicture && !review.unclearableWarned ? verdicts.filter((v) => clears(v, asked, notDenied)).map((v) => v.policyName) : [];
|
|
497
497
|
|
|
498
|
-
// Built once, for both modes: enforce applies it,
|
|
498
|
+
// Built once, for both modes: enforce applies it, observe records it, and the
|
|
499
499
|
// two must never disagree about what the verdict WAS.
|
|
500
500
|
const jevEntry = { policyName: review.policyName, reason: review.reason ?? `Flagged by semantic review (${review.policyName})` };
|
|
501
501
|
const enforced = resolveEnforce(verdicts, cleared, review.decision, jevEntry);
|
|
@@ -510,7 +510,7 @@ export function combineTwoTier(
|
|
|
510
510
|
evaluator: review.requestCut ? "jev-fallback" : "jev",
|
|
511
511
|
...(review.requestCut ? { jevFallbackReason: JEV_REASON_REQUEST_CUT } : {}),
|
|
512
512
|
jevDecision: review.decision,
|
|
513
|
-
// Only a clear that SOFTENED the call (in
|
|
513
|
+
// Only a clear that SOFTENED the call (in observe mode: would have). One that
|
|
514
514
|
// Jev's own deny, or another regex deny, still decided over changed
|
|
515
515
|
// nothing — and `jev status` and the policy page's "Cleared by Jev" both
|
|
516
516
|
// read `jevCleared` as calls Jev let through.
|
|
@@ -520,11 +520,11 @@ export function combineTwoTier(
|
|
|
520
520
|
jevMode: mode,
|
|
521
521
|
};
|
|
522
522
|
|
|
523
|
-
if (mode === "
|
|
523
|
+
if (mode === "observe") {
|
|
524
524
|
// Jev's own deny or instruct, exactly as enforce mode would have applied it
|
|
525
525
|
// (upward only: a cut or injected call keeps it, as the enforce branch
|
|
526
526
|
// below does). A clear is recorded in `jevCleared`, not here.
|
|
527
|
-
const
|
|
527
|
+
const observeVerdict: ObserveVerdict | undefined =
|
|
528
528
|
review.decision === "deny" || review.decision === "instruct"
|
|
529
529
|
? {
|
|
530
530
|
policyName: jevEntry.policyName,
|
|
@@ -535,7 +535,7 @@ export function combineTwoTier(
|
|
|
535
535
|
version: review.model && JEV_MODEL_RE.test(review.model) ? review.model : "jev",
|
|
536
536
|
}
|
|
537
537
|
: undefined;
|
|
538
|
-
return { final: legacy, cleared, decidedByJev: false, activity, ...(
|
|
538
|
+
return { final: legacy, cleared, decidedByJev: false, activity, ...(observeVerdict ? { observeVerdict } : {}) };
|
|
539
539
|
}
|
|
540
540
|
|
|
541
541
|
return { ...enforced, cleared, activity };
|
|
@@ -98,7 +98,7 @@ export interface SemanticOptions {
|
|
|
98
98
|
*/
|
|
99
99
|
signal?: AbortSignal;
|
|
100
100
|
model?: string;
|
|
101
|
-
/** Overrides the resolved set (
|
|
101
|
+
/** Overrides the resolved set (the installed packs' checks; empty with none). */
|
|
102
102
|
policies?: ReadonlyArray<SemanticPolicy>;
|
|
103
103
|
/** The agent the call is for: a pack scoped to other agents contributes no checks. */
|
|
104
104
|
cli?: string;
|
|
@@ -306,8 +306,8 @@ export function prepareSemantic(input: SemanticInput, opts: SemanticOptions = {}
|
|
|
306
306
|
scanned,
|
|
307
307
|
input.projectRoot ?? null,
|
|
308
308
|
);
|
|
309
|
-
// The
|
|
310
|
-
//
|
|
309
|
+
// The checks the installed packs declare — none when no pack declares any,
|
|
310
|
+
// and then nothing is asked (see `pack-policies.ts`). Resolved
|
|
311
311
|
// here rather than by the caller because this is the one place the policy set
|
|
312
312
|
// is read, and a second resolution site is a second answer to "what does this
|
|
313
313
|
// machine ask Jev". A caller that supplies `policies` (a replay, an ablation,
|
|
@@ -496,7 +496,7 @@ export interface VerdictLogMeta {
|
|
|
496
496
|
eventType: string;
|
|
497
497
|
/**
|
|
498
498
|
* What the handler did with the outcome: combined it with the regex results
|
|
499
|
-
* (`two-tier`), logged it while enforcing the regex result (`
|
|
499
|
+
* (`two-tier`), logged it while enforcing the regex result (`observe`), or
|
|
500
500
|
* kept the regex result because Jev never answered (`legacy-fallback`).
|
|
501
501
|
*
|
|
502
502
|
* A TRUNCATED call is `two-tier`, not `legacy-fallback`: its clears were
|
|
@@ -504,7 +504,7 @@ export interface VerdictLogMeta {
|
|
|
504
504
|
* `combine.ts`). `truncated` on the same row is what says the clearing half
|
|
505
505
|
* was off for it; the activity row records `jev-fallback` / `truncated`.
|
|
506
506
|
*/
|
|
507
|
-
applied: "two-tier" | "
|
|
507
|
+
applied: "two-tier" | "observe" | "legacy-fallback";
|
|
508
508
|
}
|
|
509
509
|
|
|
510
510
|
export function verdictLogRow(input: SemanticInput, outcome: SemanticOutcome, meta: VerdictLogMeta): Record<string, unknown> {
|
|
@@ -469,7 +469,7 @@ async function postJson(url: string, bearer: string, body: unknown, signal: Abor
|
|
|
469
469
|
body: JSON.stringify(body),
|
|
470
470
|
signal,
|
|
471
471
|
// Never followed. The configured URL is the one `validateBaseUrl` checked
|
|
472
|
-
// (https, or loopback http in
|
|
472
|
+
// (https, or loopback http in observe mode only); a redirect would hand the
|
|
473
473
|
// answer — the thing that can clear a deny — to an origin nobody checked,
|
|
474
474
|
// plain http included. No provider redirects this POST.
|
|
475
475
|
redirect: "manual",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
* The one provider whose key is NOT in this file. `failproofai config --token`
|
|
56
56
|
* with a key carrying `jev:evaluate` stores that key in the `jev` slot of
|
|
57
57
|
* `credentials.json` and, when there is no `jev.json` yet, writes one naming
|
|
58
|
-
* this provider, the Cloud origin + `/enforcement/v1/jev` and `mode: "
|
|
58
|
+
* this provider, the Cloud origin + `/enforcement/v1/jev` and `mode: "observe"`.
|
|
59
59
|
* So for this provider:
|
|
60
60
|
*
|
|
61
61
|
* - the key comes from `credentials.json` (`readJevCloudCredential`), read with
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
*
|
|
81
81
|
* # `mode: "off"`
|
|
82
82
|
*
|
|
83
|
-
* Every provider accepts `off |
|
|
83
|
+
* Every provider accepts `off | observe | enforce`. `off` keeps the file — the
|
|
84
84
|
* endpoint, and for BYOK the key — while Jev does not run at all:
|
|
85
85
|
* `loadJevConfig` returns null exactly as for an absent file. It exists so the
|
|
86
86
|
* dashboard can switch the Cloud route off without deleting the file that
|
|
@@ -96,8 +96,16 @@ export type { JevCloudCredential } from "../fp-config";
|
|
|
96
96
|
|
|
97
97
|
export type JevProviderKind = "typesafe" | "openrouter" | "vercel" | "cloudflare" | "custom" | "failproofai";
|
|
98
98
|
|
|
99
|
-
/** `off` does not run Jev at all; `
|
|
100
|
-
export type JevConfigMode = "off" | "
|
|
99
|
+
/** `off` does not run Jev at all; `observe` logs Jev and enforces regex; `enforce` applies the combine rules. */
|
|
100
|
+
export type JevConfigMode = "off" | "observe" | "enforce";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A mode as read from a file, a flag or a request: `off`, `observe` or
|
|
104
|
+
* `enforce`. Null for anything else.
|
|
105
|
+
*/
|
|
106
|
+
export function parseJevMode(raw: unknown): JevConfigMode | null {
|
|
107
|
+
return raw === "off" || raw === "observe" || raw === "enforce" ? raw : null;
|
|
108
|
+
}
|
|
101
109
|
|
|
102
110
|
export interface JevConfig {
|
|
103
111
|
provider: JevProviderKind;
|
|
@@ -130,7 +138,7 @@ export interface JevConfig {
|
|
|
130
138
|
credentialOrigin?: string;
|
|
131
139
|
}
|
|
132
140
|
|
|
133
|
-
export const DEFAULT_JEV_MODE: "
|
|
141
|
+
export const DEFAULT_JEV_MODE: "observe" | "enforce" = "enforce";
|
|
134
142
|
|
|
135
143
|
export const JEV_PROVIDER_KINDS: readonly JevProviderKind[] = ["typesafe", "openrouter", "vercel", "cloudflare", "custom", "failproofai"];
|
|
136
144
|
|
|
@@ -311,7 +319,7 @@ function credentialQueryProblem(url: URL): string | null {
|
|
|
311
319
|
* No credentials in the URL — not as userinfo, not as a query parameter — and no
|
|
312
320
|
* fragment, because a key belongs in the key field, where it is sent as a bearer
|
|
313
321
|
* and never printed. `validateJevConfig` further accepts the loopback http form
|
|
314
|
-
* only in
|
|
322
|
+
* only in observe mode.
|
|
315
323
|
*/
|
|
316
324
|
export function validateBaseUrl(raw: unknown): ValidationResult<string> {
|
|
317
325
|
if (typeof raw !== "string" || raw.trim() === "") return { ok: false, problem: "baseUrl must be a non-empty string" };
|
|
@@ -667,25 +675,26 @@ export function validateJevConfig(
|
|
|
667
675
|
}
|
|
668
676
|
|
|
669
677
|
if (o.mode !== undefined) {
|
|
670
|
-
|
|
671
|
-
|
|
678
|
+
const mode = parseJevMode(o.mode);
|
|
679
|
+
if (mode === null) return { ok: false, problem: 'mode must be "off", "observe" or "enforce"' };
|
|
680
|
+
cfg.mode = mode;
|
|
672
681
|
}
|
|
673
682
|
|
|
674
683
|
// Plain http reaches only a loopback host (`validateBaseUrl`), and nothing
|
|
675
684
|
// authenticates the server there: while the local proxy is down, any process
|
|
676
685
|
// of this user — the agent being judged included — can bind its port and
|
|
677
686
|
// answer "none" to every question. In enforce mode that answer clears
|
|
678
|
-
// reviewable denies; in
|
|
687
|
+
// reviewable denies; in observe mode it changes nothing, and `off` sends
|
|
679
688
|
// nothing at all, so enforce is the one mode it is refused in.
|
|
680
689
|
if (cfg.baseUrl !== undefined && isPlainHttp(cfg.baseUrl) && cfg.mode === "enforce") {
|
|
681
690
|
return {
|
|
682
691
|
ok: false,
|
|
683
692
|
problem:
|
|
684
|
-
"plain http (to localhost) is accepted only with mode
|
|
693
|
+
"plain http (to localhost) is accepted only with mode observe: in enforce mode Jev's answers can clear a deny, " +
|
|
685
694
|
"and while the local proxy is down any process on this machine could take its port and answer. " +
|
|
686
695
|
(cfg.provider === JEV_CLOUD_PROVIDER
|
|
687
|
-
? "Reconnect to an https FailproofAI Cloud URL (failproofai config --token <key> --url https://…), or keep mode
|
|
688
|
-
: "Use https, or mode
|
|
696
|
+
? "Reconnect to an https FailproofAI Cloud URL (failproofai config --token <key> --url https://…), or keep mode observe"
|
|
697
|
+
: "Use https, or mode observe"),
|
|
689
698
|
};
|
|
690
699
|
}
|
|
691
700
|
|
|
@@ -114,7 +114,7 @@ function servedFromCache(response: unknown): boolean {
|
|
|
114
114
|
* `jev.json`, and the request alone does not say who answered it: two
|
|
115
115
|
* providers asked for the same model build byte-identical requests. Unscoped,
|
|
116
116
|
* an answer one config got — from a plain-http loopback proxy T1 allows only in
|
|
117
|
-
*
|
|
117
|
+
* observe mode, say — would clear denies under the next config in enforce mode
|
|
118
118
|
* without that provider ever being asked. The scope is only hashed into the
|
|
119
119
|
* cache key, never stored, so a URL carrying a credential is safe here.
|
|
120
120
|
*/
|
|
@@ -152,7 +152,7 @@ export interface JevCallContext {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
export function resolveMode(cfg: JevConfig): JevMode {
|
|
155
|
-
return cfg.mode === "
|
|
155
|
+
return cfg.mode === "observe" || cfg.mode === "enforce" ? cfg.mode : DEFAULT_JEV_MODE;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/**
|
|
@@ -359,7 +359,7 @@ export function startJevReview(cfg: JevConfig, call: JevCallContext): TwoTierRev
|
|
|
359
359
|
sessionId: call.sessionId,
|
|
360
360
|
cli: call.cli,
|
|
361
361
|
eventType: call.eventType,
|
|
362
|
-
applied: abandoned || review.kind !== "answered" ? "legacy-fallback" : mode === "
|
|
362
|
+
applied: abandoned || review.kind !== "answered" ? "legacy-fallback" : mode === "observe" ? "observe" : "two-tier",
|
|
363
363
|
});
|
|
364
364
|
appendVerdictLog(hit ? { ...row, cached: true } : row);
|
|
365
365
|
}
|
|
@@ -31,9 +31,9 @@ export interface JevStats {
|
|
|
31
31
|
latencyP95Ms: number | null;
|
|
32
32
|
/**
|
|
33
33
|
* Reviewable policies Jev cleared, by name — enforce mode only, where a
|
|
34
|
-
* clear changed the outcome.
|
|
35
|
-
* {@link JevStats.
|
|
36
|
-
* field says "nothing cleared" on
|
|
34
|
+
* clear changed the outcome. Observe mode's would-be clears are in
|
|
35
|
+
* {@link JevStats.observeClearsByPolicy}, so a renderer that prints only this
|
|
36
|
+
* field says "nothing cleared" on an observe-mode machine: print both.
|
|
37
37
|
*/
|
|
38
38
|
clearsByPolicy: Record<string, number>;
|
|
39
39
|
// The fields below are optional in the TYPE only so that a caller written
|
|
@@ -62,9 +62,9 @@ export interface JevStats {
|
|
|
62
62
|
*/
|
|
63
63
|
decisions?: { allow: number; instruct: number; deny: number };
|
|
64
64
|
/** Consulted calls (`total`) per rollout mode; a row written without a mode is counted in neither. */
|
|
65
|
-
modes?: {
|
|
66
|
-
/** What Jev would have cleared in
|
|
67
|
-
|
|
65
|
+
modes?: { observe: number; enforce: number };
|
|
66
|
+
/** What Jev would have cleared in observe mode, where the regex result was enforced instead. */
|
|
67
|
+
observeClearsByPolicy?: Record<string, number>;
|
|
68
68
|
/** Model ids that answered, by count. */
|
|
69
69
|
models?: Record<string, number>;
|
|
70
70
|
}
|
|
@@ -160,14 +160,14 @@ export function computeJevStats(
|
|
|
160
160
|
notConsulted: 0,
|
|
161
161
|
noRequest: 0,
|
|
162
162
|
decisions: { allow: 0, instruct: 0, deny: 0 },
|
|
163
|
-
modes: {
|
|
164
|
-
|
|
163
|
+
modes: { observe: 0, enforce: 0 },
|
|
164
|
+
observeClearsByPolicy: {},
|
|
165
165
|
models: {},
|
|
166
166
|
};
|
|
167
167
|
const latencies: number[] = [];
|
|
168
168
|
const fallbackReasons = new Counter();
|
|
169
169
|
const clearsByPolicy = new Counter();
|
|
170
|
-
const
|
|
170
|
+
const observeClearsByPolicy = new Counter();
|
|
171
171
|
const models = new Counter();
|
|
172
172
|
|
|
173
173
|
for (const raw of entries) {
|
|
@@ -197,13 +197,13 @@ export function computeJevStats(
|
|
|
197
197
|
if (e.jevDecision) stats.decisions[e.jevDecision] += 1;
|
|
198
198
|
if (e.jevLatencyMs !== undefined) latencies.push(e.jevLatencyMs);
|
|
199
199
|
if (e.jevModel) models.bump(e.jevModel);
|
|
200
|
-
const clears = e.jevMode === "
|
|
200
|
+
const clears = e.jevMode === "observe" ? observeClearsByPolicy : clearsByPolicy;
|
|
201
201
|
for (const name of e.jevCleared ?? []) clears.bump(name);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
stats.fallbackReasons = fallbackReasons.toRecord();
|
|
205
205
|
stats.clearsByPolicy = clearsByPolicy.toRecord();
|
|
206
|
-
stats.
|
|
206
|
+
stats.observeClearsByPolicy = observeClearsByPolicy.toRecord();
|
|
207
207
|
stats.models = models.toRecord();
|
|
208
208
|
stats.fallbackRate = stats.total > 0 ? stats.fallbacks / stats.total : 0;
|
|
209
209
|
latencies.sort((a, b) => a - b);
|
|
@@ -217,8 +217,8 @@ export function computeJevStats(
|
|
|
217
217
|
* {@link clampJevStatsWindow}), read from the hook activity store. Never
|
|
218
218
|
* throws: an unreadable store reads as no activity.
|
|
219
219
|
*
|
|
220
|
-
* A renderer must show {@link JevStats.
|
|
221
|
-
* `clearsByPolicy`: in
|
|
220
|
+
* A renderer must show {@link JevStats.observeClearsByPolicy} as well as
|
|
221
|
+
* `clearsByPolicy`: in observe mode every clear Jev would have made is in the
|
|
222
222
|
* former, and the latter is empty. {@link formatJevStats} prints both.
|
|
223
223
|
*/
|
|
224
224
|
export async function jevStats(opts: { windowMs?: number; now?: number } = {}): Promise<JevStatsDetail> {
|
|
@@ -279,11 +279,11 @@ export function formatJevStats(s: JevStats): string {
|
|
|
279
279
|
lines.push(...notAsked);
|
|
280
280
|
if (s.latencyP50Ms !== null) lines.push(` Latency: p50 ${s.latencyP50Ms} ms, p95 ${s.latencyP95Ms} ms`);
|
|
281
281
|
if (Object.keys(s.clearsByPolicy).length > 0) lines.push(` Cleared: ${topCounts(s.clearsByPolicy)}`);
|
|
282
|
-
if (s.
|
|
283
|
-
lines.push(` Would clear: ${topCounts(s.
|
|
282
|
+
if (s.observeClearsByPolicy && Object.keys(s.observeClearsByPolicy).length > 0) {
|
|
283
|
+
lines.push(` Would clear: ${topCounts(s.observeClearsByPolicy)} (observe mode)`);
|
|
284
284
|
}
|
|
285
|
-
if (s.modes && s.modes.
|
|
286
|
-
lines.push(` Modes: enforce ${s.modes.enforce},
|
|
285
|
+
if (s.modes && s.modes.observe > 0 && s.modes.enforce > 0) {
|
|
286
|
+
lines.push(` Modes: enforce ${s.modes.enforce}, observe ${s.modes.observe}`);
|
|
287
287
|
}
|
|
288
288
|
return lines.join("\n");
|
|
289
289
|
}
|