failproofai 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
- 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 +4 -4
- 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 +8 -7
- 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 +9 -7
- 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 +6 -6
- 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 +11 -10
- 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 +8 -7
- 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 +6 -6
- 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 +7 -6
- 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 +52 -8
- package/.next/standalone/.next/server/app/settings/page.js +12 -7
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__20kzpkf._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
- 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 +16 -16
- package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
- package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
- package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__056wjo4._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06pflha._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0n0xg95._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__0rwtwpm._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__11mayhe._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12e7nhs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13t1zkw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__1pprgri._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__1q4p5b8._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_166t73i._.js → _0-vcssj._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_13kfn90._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
- package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
- 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 +13 -13
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +4 -2
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
- 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 +67 -23
- package/.next/standalone/.next/static/chunks/06dnzbolj00mc.js +6 -0
- package/.next/standalone/.next/static/chunks/0bhidk90e-07f.css +2 -0
- package/.next/standalone/.next/static/chunks/{2mdh397ghgnvv.js → 0m-9d6yn9hx4j.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0__8a7m868fvf.js → 0wbjy0zo-m7is.js} +1 -1
- package/.next/standalone/.next/static/chunks/16f3fa-lx38hk.js +1 -0
- package/.next/standalone/.next/static/chunks/{0o6qlkgubtoex.js → 21uv-uusw329x.js} +1 -1
- package/.next/standalone/.next/static/chunks/2g2tki08kdhie.js +1 -0
- package/.next/standalone/.next/static/chunks/2qdpj67x6ifk_.js +69 -0
- package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
- package/.next/standalone/.next/static/chunks/3-nbtkhg9y-1j.js +1 -0
- package/.next/standalone/.next/static/chunks/355km0ihuqo1p.js +1 -0
- package/.next/standalone/.next/static/chunks/{13i7-9is-vhys.js → 3c3qbmosdjl6w.js} +1 -1
- package/.next/standalone/PROBE-FOLLOWUP.md +186 -0
- package/.next/standalone/app/actions/get-jev-config.ts +604 -0
- package/.next/standalone/app/actions/pack-actions.ts +12 -0
- package/.next/standalone/app/actions/update-jev-config.ts +569 -0
- package/.next/standalone/app/components/jev-notices.tsx +96 -0
- package/.next/standalone/app/policies/hooks-client.tsx +14 -3
- package/.next/standalone/app/settings/jev-panel.tsx +630 -0
- package/.next/standalone/app/settings/page.tsx +20 -1
- package/.next/standalone/app/settings/settings-client.tsx +27 -1
- package/.next/standalone/app/settings/settings.css +79 -0
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
- package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- package/.next/standalone/sdk/typescript/CHANGELOG.md +18 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +0 -274
- package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +3 -2
- package/bin/failproofai.mjs +148 -4
- package/dist/cli.mjs +18599 -9478
- package/dist/index.js +19 -1
- package/dist/worker.mjs +10500 -3773
- package/package.json +10 -10
- package/pi-extension/index.ts +11 -0
- package/scripts/build-policy-pack.mjs +53 -2
- package/src/audit/features.ts +3 -2
- package/src/hooks/builtin-policies.ts +195 -8
- package/src/hooks/cloud-connection.ts +190 -1
- package/src/hooks/cloud-enrollment-cli.ts +57 -8
- package/src/hooks/cloud-introspect.ts +6 -0
- package/src/hooks/cloud-managed-policies.ts +22 -0
- package/src/hooks/configure-wizard.ts +26 -6
- package/src/hooks/custom-hooks-loader.ts +98 -10
- package/src/hooks/custom-hooks-registry.ts +45 -1
- package/src/hooks/effective-reviewers.ts +243 -0
- package/src/hooks/first-run-gate.ts +5 -0
- package/src/hooks/flush-cli.ts +35 -8
- package/src/hooks/fp-config.ts +266 -1
- package/src/hooks/fp-home.ts +23 -0
- package/src/hooks/fp-reset.ts +22 -4
- package/src/hooks/handler.ts +303 -7
- package/src/hooks/hook-activity-store.ts +118 -6
- package/src/hooks/hook-telemetry.ts +41 -0
- package/src/hooks/jev-activity.ts +385 -0
- package/src/hooks/jev-cli.ts +2172 -0
- package/src/hooks/jev-cloud-connection.ts +392 -0
- package/src/hooks/loader-utils.ts +6 -0
- package/src/hooks/manager.ts +45 -7
- package/src/hooks/pack-cli.ts +568 -37
- package/src/hooks/pack-failclosed.ts +3 -0
- package/src/hooks/pack-manifest.ts +489 -7
- package/src/hooks/pack-store.ts +194 -11
- package/src/hooks/policy-authority.ts +386 -0
- package/src/hooks/policy-catalog.ts +206 -0
- package/src/hooks/policy-evaluator.ts +963 -796
- package/src/hooks/policy-registry.ts +27 -1
- package/src/hooks/policy-reviewability.ts +258 -0
- package/src/hooks/policy-types.ts +128 -0
- package/src/hooks/semantic/combine.ts +581 -0
- package/src/hooks/semantic/compile.ts +176 -0
- package/src/hooks/semantic/decide.ts +513 -0
- package/src/hooks/semantic/envelope.ts +1255 -0
- package/src/hooks/semantic/evaluator.ts +556 -0
- package/src/hooks/semantic/facts.ts +387 -0
- package/src/hooks/semantic/intent.ts +1190 -0
- package/src/hooks/semantic/jev-client.ts +1155 -0
- package/src/hooks/semantic/jev-config.ts +1142 -0
- package/src/hooks/semantic/jev-review.ts +389 -0
- package/src/hooks/semantic/jev-stats.ts +289 -0
- package/src/hooks/semantic/jev-throttle.ts +427 -0
- package/src/hooks/semantic/pack-policies.ts +294 -0
- package/src/hooks/semantic/policies.ts +597 -0
- package/src/hooks/semantic/precondition-names.ts +60 -0
- package/src/hooks/semantic/preconditions.ts +58 -0
- package/src/hooks/semantic/redact.ts +2937 -0
- package/src/hooks/semantic/session-root.ts +116 -0
- package/src/hooks/semantic/types.ts +162 -0
- package/src/hooks/semver-precedence.ts +128 -0
- package/src/hooks/tui.ts +4 -0
- package/src/hooks/types.ts +1 -1
- package/src/hooks/worker-server.ts +121 -27
- package/src/index.ts +6 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_r2rbg._.js +0 -24
- package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bd3mje._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_0h_douw._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1-i_gzc._.js +0 -23
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
- package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +0 -1
- package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +0 -6
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
- package/.next/standalone/.next/static/chunks/2klitrtzpaoe0.js +0 -1
- package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +0 -2
- package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +0 -1
- package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +0 -69
- package/.next/standalone/.next/static/chunks/43ufqrz8qo3h-.js +0 -1
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_ssgManifest.js +0 -0
package/src/hooks/pack-cli.ts
CHANGED
|
@@ -13,7 +13,20 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
|
|
|
13
13
|
import { INTEGRATION_TYPES } from "./types";
|
|
14
14
|
import { PACK_COMMIT_RE, PACK_VERSION_RE } from "./pack-manifest";
|
|
15
15
|
import { detectInstalledClis } from "./integrations";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
packParamsProblem,
|
|
18
|
+
parsePackIdentity,
|
|
19
|
+
parsePackPolicy,
|
|
20
|
+
parsePackSemanticPolicy,
|
|
21
|
+
readInstalledPacks,
|
|
22
|
+
type SemanticManifestEntry,
|
|
23
|
+
} from "./pack-manifest";
|
|
24
|
+
import { compareVersions, parseSemver } from "./semver-precedence";
|
|
25
|
+
// The build step measures a pack's question set against the envelope's own
|
|
26
|
+
// budget, which means reaching the semantic side. This is a CLI module — loaded
|
|
27
|
+
// by `failproofai publish`, never by a hook — so the rule that keeps those
|
|
28
|
+
// modules off an unconfigured machine's hook path does not apply here.
|
|
29
|
+
import { BUILTIN_QUESTION_CHARS, MAX_PACK_QUESTION_CHARS, questionChars, semanticPoliciesFromPacks } from "./semantic/pack-policies";
|
|
17
30
|
import {
|
|
18
31
|
AmbiguousPackId,
|
|
19
32
|
PACK_CHECKSUMS_ASSET,
|
|
@@ -27,10 +40,15 @@ import {
|
|
|
27
40
|
removePack,
|
|
28
41
|
setPackPolicyEnabled,
|
|
29
42
|
slugifyCategory,
|
|
43
|
+
type PackPreview,
|
|
30
44
|
} from "./pack-store";
|
|
31
45
|
import type { PolicyEffect } from "./cloud-managed-policies";
|
|
32
46
|
import { loadCustomHooks } from "./custom-hooks-loader";
|
|
33
|
-
import
|
|
47
|
+
import { getSemanticRegistrations } from "./custom-hooks-registry";
|
|
48
|
+
import { isFirstPartyPack, isReservedClaim, jevPacks } from "./effective-reviewers";
|
|
49
|
+
import { authorityFieldsOf, authorityProblem, resolvePolicyAuthority } from "./policy-authority";
|
|
50
|
+
import type { PolicyCatalogEntry, SemanticPolicyDeclaration } from "./policy-types";
|
|
51
|
+
import type { MultiChoice, RenderOpts, TTYIn, TTYOut } from "./tui";
|
|
34
52
|
import {
|
|
35
53
|
chip,
|
|
36
54
|
emptyState,
|
|
@@ -64,8 +82,20 @@ export interface PackBuildMeta {
|
|
|
64
82
|
policies: number;
|
|
65
83
|
defaultOn: number;
|
|
66
84
|
commit?: string;
|
|
85
|
+
/** Jev checks and no regex half: `publish` repeats {@link JEV_ONLY_ROLLBACK_REMINDER}. */
|
|
86
|
+
semanticOnly?: boolean;
|
|
67
87
|
}
|
|
68
88
|
|
|
89
|
+
/**
|
|
90
|
+
* A build from before `semanticOnly` refuses such a pack as blanket-deny, so its
|
|
91
|
+
* author has to tell users. Shared by build's lines and publish's success
|
|
92
|
+
* message, which is the only one an author actually reads.
|
|
93
|
+
*/
|
|
94
|
+
const JEV_ONLY_ROLLBACK_REMINDER = [
|
|
95
|
+
" Jev checks alone: tell users to remove it before rolling a machine back to an older failproofai,",
|
|
96
|
+
" which can deny every tool call over a pack it will not load (see the publish-a-pack docs).",
|
|
97
|
+
];
|
|
98
|
+
|
|
69
99
|
const ok = (lines: string[], meta?: PackBuildMeta): PackCliResult => ({
|
|
70
100
|
lines,
|
|
71
101
|
exitCode: 0,
|
|
@@ -73,12 +103,30 @@ const ok = (lines: string[], meta?: PackBuildMeta): PackCliResult => ({
|
|
|
73
103
|
});
|
|
74
104
|
const fail = (lines: string[]): PackCliResult => ({ lines, exitCode: 1 });
|
|
75
105
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
/**
|
|
107
|
+
* The first failproofai that runs a pack's Jev checks as published: 1.0.7
|
|
108
|
+
* ignores `semantic` (and `minCliVersion`), and 1.0.7-beta.x replaces the
|
|
109
|
+
* built-in checks with a pack's. A pack with checks is held to at least this.
|
|
110
|
+
*/
|
|
111
|
+
const JEV_PACK_MIN_CLI = "1.0.8-beta.0";
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Every occurrence of any of `flags`, comma-split and de-duplicated: a repeated
|
|
115
|
+
* flag adds to the list rather than being silently dropped. `[]` when any
|
|
116
|
+
* occurrence has no value (callers refuse that), undefined when none appears.
|
|
117
|
+
*/
|
|
118
|
+
function parseList(rest: string[], ...flags: string[]): string[] | undefined {
|
|
119
|
+
let found = false;
|
|
120
|
+
const out: string[] = [];
|
|
121
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
122
|
+
const flag = flags.find((f) => rest[i] === f || rest[i].startsWith(`${f}=`));
|
|
123
|
+
if (!flag) continue;
|
|
124
|
+
found = true;
|
|
125
|
+
const raw = rest[i] === flag ? rest[i + 1] : rest[i].slice(flag.length + 1);
|
|
126
|
+
if (!raw || raw.startsWith("--")) return [];
|
|
127
|
+
out.push(...raw.split(",").map((s) => s.trim()).filter(Boolean));
|
|
128
|
+
}
|
|
129
|
+
return found ? [...new Set(out)] : undefined;
|
|
82
130
|
}
|
|
83
131
|
|
|
84
132
|
/**
|
|
@@ -168,17 +216,25 @@ export function packAddSource(rest: string[]): string | undefined {
|
|
|
168
216
|
* a third would be the one that drifts.
|
|
169
217
|
*/
|
|
170
218
|
function parseCliList(rest: string[]): { clis?: string[] } | { error: string[] } {
|
|
171
|
-
|
|
172
|
-
|
|
219
|
+
// Every `--cli`, not just the first: a repeat adds agents. One that names
|
|
220
|
+
// none still refuses the whole selection, below.
|
|
173
221
|
const values: string[] = [];
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
222
|
+
let found = false;
|
|
223
|
+
let emptyOccurrence = false;
|
|
224
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
225
|
+
const before = values.length;
|
|
226
|
+
if (rest[i].startsWith("--cli=")) {
|
|
227
|
+
values.push(...rest[i].slice("--cli=".length).split(",").filter((v) => v.trim()));
|
|
228
|
+
} else if (rest[i] === "--cli") {
|
|
229
|
+
for (let j = i + 1; j < rest.length && looksLikeCliName(rest[j]); j += 1) {
|
|
230
|
+
values.push(...rest[j].split(",").filter((v) => v.trim()));
|
|
231
|
+
}
|
|
232
|
+
} else continue;
|
|
233
|
+
found = true;
|
|
234
|
+
if (values.length === before) emptyOccurrence = true;
|
|
180
235
|
}
|
|
181
|
-
|
|
236
|
+
if (!found) return {};
|
|
237
|
+
const names = emptyOccurrence ? [] : [...new Set(values.map((v) => v.trim()))];
|
|
182
238
|
// `--cli` with nothing after it is a typo, not a scope.
|
|
183
239
|
//
|
|
184
240
|
// It used to become `clis: []`, which `installed.json` stores verbatim and
|
|
@@ -238,7 +294,7 @@ function selectionFrom(rest: string[]): {
|
|
|
238
294
|
} {
|
|
239
295
|
// `--policy` reads right for one ("give me this policy"), `--only` for a set.
|
|
240
296
|
// They are the same switch; taking both means neither is the wrong guess.
|
|
241
|
-
const only = parseList(rest, "--policy"
|
|
297
|
+
const only = parseList(rest, "--policy", "--only");
|
|
242
298
|
const categories = parseList(rest, "--category");
|
|
243
299
|
const parsedClis = parseCliList(rest);
|
|
244
300
|
if ("error" in parsedClis) return { error: parsedClis.error };
|
|
@@ -294,12 +350,27 @@ async function build(rest: string[]): Promise<PackCliResult> {
|
|
|
294
350
|
const version = flag("version");
|
|
295
351
|
const commit = flag("commit");
|
|
296
352
|
const effect = flag("effect") ?? "enforce";
|
|
353
|
+
const minCliVersion = flag("min-cli-version");
|
|
297
354
|
const outDir = resolve(flag("out") ?? "dist-pack");
|
|
355
|
+
// Where it will be installed from, which is what a machine judges a pack by
|
|
356
|
+
// (`isFirstPartyPack` reads `source`, never the id). `publish` passes its
|
|
357
|
+
// repository; `pack build` has only the id to go on.
|
|
358
|
+
const repo = flag("repo");
|
|
298
359
|
|
|
299
360
|
if (!entry || !id || !version) {
|
|
300
361
|
return fail([
|
|
301
362
|
"Usage: failproofai publish <entry.mjs> --repo <owner>/<repo> --version <version>",
|
|
302
|
-
" [--out <dir>] [--effect enforce|observe]",
|
|
363
|
+
" [--out <dir>] [--effect enforce|observe] [--min-cli-version <version>]",
|
|
364
|
+
]);
|
|
365
|
+
}
|
|
366
|
+
// Refused here rather than written and refused by every machine that installs
|
|
367
|
+
// it. A `minCliVersion` nobody can order is a requirement nobody can check,
|
|
368
|
+
// and the loader treats one as grounds to refuse the whole pack — so a typo
|
|
369
|
+
// published here is a pack that installs nowhere.
|
|
370
|
+
if (minCliVersion !== undefined && parseSemver(minCliVersion) === null) {
|
|
371
|
+
return fail([
|
|
372
|
+
`--min-cli-version ${JSON.stringify(minCliVersion)} is not a version that can be compared.`,
|
|
373
|
+
"It must be plain semver — 1.0.7, or 1.0.7-beta.0 for a pre-release.",
|
|
303
374
|
]);
|
|
304
375
|
}
|
|
305
376
|
let identity: { id: string; version: string; effect: PolicyEffect; commit?: string };
|
|
@@ -315,24 +386,34 @@ async function build(rest: string[]): Promise<PackCliResult> {
|
|
|
315
386
|
// Refused rather than bundled: see the note above. A relative specifier is the
|
|
316
387
|
// only kind that would be rewritten by the loader and left outside the digest.
|
|
317
388
|
const source = readFileSync(entryPath, "utf8");
|
|
318
|
-
const localImport =
|
|
389
|
+
const localImport = firstRelativeSpecifier(source);
|
|
319
390
|
if (localImport) {
|
|
320
391
|
return fail([
|
|
321
|
-
`${entryPath} imports ${localImport
|
|
392
|
+
`${entryPath} imports ${localImport}, and only the entry file is digest-pinned.`,
|
|
322
393
|
"Bundle it to a single file first (esbuild, bun build, rollup), then build the pack from that.",
|
|
323
394
|
]);
|
|
324
395
|
}
|
|
325
396
|
|
|
326
397
|
let hooks;
|
|
398
|
+
let semanticDeclarations: SemanticPolicyDeclaration[];
|
|
327
399
|
try {
|
|
328
400
|
hooks = await loadCustomHooks(entryPath, { strict: true });
|
|
401
|
+
// Read AFTER the load, from the same registration pass `clearCustomHooks`
|
|
402
|
+
// resets: a semantic policy has no `fn`, so it never travels back through
|
|
403
|
+
// `loadCustomHooks`'s return value.
|
|
404
|
+
semanticDeclarations = [...getSemanticRegistrations()];
|
|
329
405
|
} catch (err) {
|
|
330
406
|
return fail([`Could not load ${entryPath}: ${err instanceof Error ? err.message : String(err)}`]);
|
|
331
407
|
}
|
|
332
|
-
|
|
408
|
+
// BOTH registries, or a pack that ships only Jev questions — a legitimate
|
|
409
|
+
// thing to publish, since the regex floor may already be somebody else's pack
|
|
410
|
+
// — is refused for registering nothing.
|
|
411
|
+
if (hooks.length === 0 && semanticDeclarations.length === 0) {
|
|
333
412
|
return fail([
|
|
334
413
|
`${entryPath} registered no policies.`,
|
|
335
|
-
"A pack entry calls customPolicies.add({ name, description, match, fn }) for each one
|
|
414
|
+
"A pack entry calls customPolicies.add({ name, description, match, fn }) for each one,",
|
|
415
|
+
"and semanticPolicies.add({ name, title, appliesTo, mode, userCanOverride, probes, guidance })",
|
|
416
|
+
"for each Jev question set.",
|
|
336
417
|
]);
|
|
337
418
|
}
|
|
338
419
|
|
|
@@ -359,6 +440,98 @@ async function build(rest: string[]): Promise<PackCliResult> {
|
|
|
359
440
|
]);
|
|
360
441
|
}
|
|
361
442
|
|
|
443
|
+
// The pack's Jev question sets, validated with the LOADER's own rules — the
|
|
444
|
+
// same deal `parsePackPolicy` gets above, so a semantic policy that could
|
|
445
|
+
// never load fails here, where the author can fix it.
|
|
446
|
+
const semantic: SemanticManifestEntry[] = [];
|
|
447
|
+
const packSource = { source: `github:${repo ?? identity.id}` };
|
|
448
|
+
for (const [index, declaration] of semanticDeclarations.entries()) {
|
|
449
|
+
try {
|
|
450
|
+
const parsed = parsePackSemanticPolicy(identity.id, declaration, index);
|
|
451
|
+
// Same reason two regex policies cannot share a name, one layer over: the
|
|
452
|
+
// answer map is keyed `<policy>.<probe>`, so a duplicate would overwrite
|
|
453
|
+
// the first policy's questions in the compiled request.
|
|
454
|
+
if (semantic.some((s) => s.name === parsed.name)) {
|
|
455
|
+
throw new Error(`two semantic policies are called ${JSON.stringify(parsed.name)}`);
|
|
456
|
+
}
|
|
457
|
+
// The loader's own rule: a built-in check name from anyone but FailproofAI
|
|
458
|
+
// is void on every machine, never asked and never a reviewer.
|
|
459
|
+
if (isReservedClaim(packSource, parsed.name)) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`${JSON.stringify(parsed.name)} is a name reserved for FailproofAI's own Jev checks, so a pack from ` +
|
|
462
|
+
`${repo ?? identity.id} would never have it asked. Pick a name of your own.`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
semantic.push(parsed);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
return fail([err instanceof Error ? err.message : String(err)]);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// A pack with Jev checks needs a build that runs them, so its minimum is at
|
|
472
|
+
// least the first one — written when the author gave none, refused when lower.
|
|
473
|
+
if (semantic.length > 0 && minCliVersion !== undefined && (compareVersions(minCliVersion, JEV_PACK_MIN_CLI) ?? 0) < 0) {
|
|
474
|
+
return fail([
|
|
475
|
+
`--min-cli-version ${minCliVersion} is older than the first failproofai that runs a pack's Jev checks (${JEV_PACK_MIN_CLI}):`,
|
|
476
|
+
"1.0.7 ignores them, and 1.0.7-beta.x replaces the built-in checks with them.",
|
|
477
|
+
`Pass --min-cli-version ${JEV_PACK_MIN_CLI} or newer, or leave it out and ${JEV_PACK_MIN_CLI} is written.`,
|
|
478
|
+
]);
|
|
479
|
+
}
|
|
480
|
+
const requiredCli = semantic.length > 0 ? (minCliVersion ?? JEV_PACK_MIN_CLI) : minCliVersion;
|
|
481
|
+
|
|
482
|
+
// The question budget, checked against what one Jev request can carry. A pack
|
|
483
|
+
// over it installs and then has its overflow policies dropped one by one on
|
|
484
|
+
// the user's machine, in manifest order — a pack that enforces less than it
|
|
485
|
+
// says, which is the failure this whole lane is built to avoid.
|
|
486
|
+
//
|
|
487
|
+
// A pack from outside FailproofAI is ADDED to the built-in checks, which every
|
|
488
|
+
// machine spends the budget on first (`semanticPoliciesFromPacks`), so it gets
|
|
489
|
+
// only what they leave.
|
|
490
|
+
const questionCost = semantic.reduce((total, entry) => total + questionChars(entry), 0);
|
|
491
|
+
const firstParty = isFirstPartyPack(packSource);
|
|
492
|
+
const questionBudget = MAX_PACK_QUESTION_CHARS - (firstParty ? 0 : BUILTIN_QUESTION_CHARS);
|
|
493
|
+
if (questionCost > questionBudget) {
|
|
494
|
+
return fail([
|
|
495
|
+
`This pack's ${semantic.length} semantic policies compile to ${questionCost} characters of questions, ` +
|
|
496
|
+
(firstParty
|
|
497
|
+
? `over the ${MAX_PACK_QUESTION_CHARS} one Jev request has room for.`
|
|
498
|
+
: `over the ${questionBudget} a machine leaves a pack from outside FailproofAI: one Jev request has room ` +
|
|
499
|
+
`for ${MAX_PACK_QUESTION_CHARS}, and the 16 built-in checks every machine asks take ${BUILTIN_QUESTION_CHARS} of it first.`),
|
|
500
|
+
"Shorten the probe instructions and criteria, or ship fewer policies per pack.",
|
|
501
|
+
]);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// An authority declaration the manifest would lose, or that registration
|
|
505
|
+
// would quietly downgrade to hard, is refused HERE. Every machine that
|
|
506
|
+
// installs the pack would otherwise make the policy hard with only a warning
|
|
507
|
+
// in its own log — the author's decision, silently not taking effect — while
|
|
508
|
+
// at build time nothing is installed yet and refusing costs nobody anything.
|
|
509
|
+
// The same rule `scripts/build-policy-pack.mjs` applies to the core pack.
|
|
510
|
+
//
|
|
511
|
+
// Judged against the reviewers the PACK SHIPS WITH, not this build's: a pack
|
|
512
|
+
// that carries both tiers replaces the compiled semantic set on every machine
|
|
513
|
+
// that installs it, so `reviewedBy: ["its-own-check"]` is exactly right and
|
|
514
|
+
// the builtin list would call it a name "this build does not have". A pack
|
|
515
|
+
// with no semantic entries still answers to the builtin set, which is what its
|
|
516
|
+
// machines will be running.
|
|
517
|
+
const reviewers = semantic.length > 0 ? new Set(semantic.map((s) => s.name)) : undefined;
|
|
518
|
+
const authorityProblems = hooks.flatMap((hook) => {
|
|
519
|
+
const problem = authorityProblem({ authority: hook.authority, reviewedBy: hook.reviewedBy }, reviewers);
|
|
520
|
+
return problem ? [` ${hook.name}: ${problem}`] : [];
|
|
521
|
+
});
|
|
522
|
+
if (authorityProblems.length > 0) {
|
|
523
|
+
return fail([
|
|
524
|
+
authorityProblems.length === 1
|
|
525
|
+
? "One policy declares an authority this build cannot publish:"
|
|
526
|
+
: `${authorityProblems.length} policies declare an authority this build cannot publish:`,
|
|
527
|
+
...authorityProblems,
|
|
528
|
+
...(reviewers
|
|
529
|
+
? ["This pack declares Jev checks of its own, so reviewedBy may name only those."]
|
|
530
|
+
: []),
|
|
531
|
+
'Fix the declaration, or leave authority out and the policy is "hard". See https://docs.befailproof.ai/policies/authority',
|
|
532
|
+
]);
|
|
533
|
+
}
|
|
534
|
+
|
|
362
535
|
const policies: unknown[] = [];
|
|
363
536
|
for (const [index, hook] of hooks.entries()) {
|
|
364
537
|
// `category` and `defaultEnabled` are pack-manifest fields a plain custom
|
|
@@ -367,13 +540,41 @@ async function build(rest: string[]): Promise<PackCliResult> {
|
|
|
367
540
|
// FALSE: a pack's declared defaults are what `pack add` switches on with no
|
|
368
541
|
// flags, and switching on a stranger's every policy unattended is the
|
|
369
542
|
// installer opinion this lane already refused once.
|
|
370
|
-
const extra = hook as unknown as {
|
|
543
|
+
const extra = hook as unknown as {
|
|
544
|
+
category?: unknown; defaultEnabled?: unknown; params?: unknown; alwaysOn?: unknown;
|
|
545
|
+
};
|
|
546
|
+
// `params` is read off the registration for the same reason, and leaving it
|
|
547
|
+
// out was not cosmetic: `registerPolicy` takes a pack policy's schema from the
|
|
548
|
+
// MANIFEST by name, so a pack published without it evaluates every one of its
|
|
549
|
+
// policies with `ctx.params = {}` — which discards the user's own configured
|
|
550
|
+
// values too, not just the defaults. Of the 39 builtins, 20 carry a schema:
|
|
551
|
+
// `prefer-package-manager` would have been inert, and `block-sudo`'s
|
|
552
|
+
// `allowPatterns`, `block-rm-rf`'s and `block-read-outside-cwd`'s `allowPaths`
|
|
553
|
+
// would all have stopped working — every one of them failing silently
|
|
554
|
+
// STRICTER, which is the direction users work around instead of reporting.
|
|
555
|
+
const paramsProblem = packParamsProblem(extra.params);
|
|
556
|
+
if (paramsProblem) {
|
|
557
|
+
return fail([
|
|
558
|
+
`${hook.name} declares a params schema this build cannot publish: ${paramsProblem}`,
|
|
559
|
+
"A machine reads the schema from the manifest, so an unusable one is a policy",
|
|
560
|
+
"that quietly runs without its parameters. Fix the declaration, or leave params out.",
|
|
561
|
+
]);
|
|
562
|
+
}
|
|
371
563
|
const candidate = {
|
|
372
564
|
name: hook.name,
|
|
373
565
|
description: hook.description ?? "",
|
|
374
566
|
category: typeof extra.category === "string" && extra.category ? extra.category : "General",
|
|
375
567
|
defaultEnabled: extra.defaultEnabled === true,
|
|
376
568
|
match: hook.match ?? {},
|
|
569
|
+
...(extra.params !== undefined ? { params: extra.params } : {}),
|
|
570
|
+
// Carried so parsePackPolicy refuses it as the loader would; left off, an
|
|
571
|
+
// alwaysOn registration published as an ordinary (even reviewable) policy.
|
|
572
|
+
...("alwaysOn" in extra ? { alwaysOn: extra.alwaysOn } : {}),
|
|
573
|
+
// Whether Jev may clear this policy's verdict. A pack's MANIFEST is what
|
|
574
|
+
// a machine reads it from, so a declaration left on the registration
|
|
575
|
+
// alone would be published as nothing — silently hard. Checked above, so
|
|
576
|
+
// everything declared here is shape-valid; absent stays absent.
|
|
577
|
+
...authorityFieldsOf(hook as unknown as Record<string, unknown>),
|
|
377
578
|
};
|
|
378
579
|
try {
|
|
379
580
|
policies.push(parsePackPolicy(identity.id, candidate, index));
|
|
@@ -386,14 +587,21 @@ async function build(rest: string[]): Promise<PackCliResult> {
|
|
|
386
587
|
// The manifest is hashed and the hash is the pin, so every byte in here is
|
|
387
588
|
// part of what a machine verifies — a field carrying "there was nothing to
|
|
388
589
|
// say" earns none of that cost. Readers already treat absence as ordinary.
|
|
590
|
+
//
|
|
591
|
+
// `minCliVersion` and `semantic` follow the same rule, and for `semantic` it is
|
|
592
|
+
// load-bearing rather than tidy: an EMPTY array would still be "a pack that
|
|
593
|
+
// declares semantic entries" to a careless reader, and the replacement rule
|
|
594
|
+
// turns that into "this pack replaced the compiled-in set with nothing".
|
|
389
595
|
const manifest =
|
|
390
596
|
JSON.stringify(
|
|
391
597
|
{
|
|
392
598
|
id: identity.id,
|
|
393
599
|
version: identity.version,
|
|
600
|
+
...(requiredCli ? { minCliVersion: requiredCli } : {}),
|
|
394
601
|
effect: identity.effect,
|
|
395
602
|
...(identity.commit ? { commit: identity.commit } : {}),
|
|
396
603
|
policies,
|
|
604
|
+
...(semantic.length > 0 ? { semantic } : {}),
|
|
397
605
|
},
|
|
398
606
|
null,
|
|
399
607
|
2,
|
|
@@ -411,15 +619,35 @@ async function build(rest: string[]): Promise<PackCliResult> {
|
|
|
411
619
|
);
|
|
412
620
|
|
|
413
621
|
const on = policies.filter((p) => (p as { defaultEnabled?: boolean }).defaultEnabled).length;
|
|
622
|
+
const semanticOnly = policies.length === 0 && semantic.length > 0;
|
|
414
623
|
return ok([
|
|
415
624
|
`Built ${identity.id}@${identity.version} — ${policies.length} policies, ${on} on by default.`,
|
|
625
|
+
// Said separately, and said at all, because this half changes what Jev asks
|
|
626
|
+
// on every machine that installs the pack. An author who did not mean to
|
|
627
|
+
// ship it should find that out here.
|
|
628
|
+
...(semantic.length > 0
|
|
629
|
+
? [
|
|
630
|
+
` ${semantic.length} semantic ${semantic.length === 1 ? "policy" : "policies"} for Jev ` +
|
|
631
|
+
`(${questionCost} characters of questions), ` +
|
|
632
|
+
(identity.effect === "observe"
|
|
633
|
+
? "not asked where it installs: Jev asks only the checks of packs that enforce."
|
|
634
|
+
: `${firstParty ? "replacing" : "added to"} the built-in checks where it installs.`),
|
|
635
|
+
]
|
|
636
|
+
: []),
|
|
637
|
+
...(requiredCli ? [` Requires failproofai ${requiredCli} or newer.`] : []),
|
|
638
|
+
...(semanticOnly ? JEV_ONLY_ROLLBACK_REMINDER : []),
|
|
416
639
|
` ${outDir}/${PACK_MANIFEST_ASSET}`,
|
|
417
640
|
` ${outDir}/${PACK_ENTRY_ASSET}`,
|
|
418
641
|
` ${outDir}/${PACK_CHECKSUMS_ASSET}`,
|
|
419
642
|
"",
|
|
420
643
|
`Publish: attach all three to a GitHub release tagged ${identity.version}, then anyone runs:`,
|
|
421
644
|
` failproofai policies add <owner>/<repo>`,
|
|
422
|
-
], {
|
|
645
|
+
], {
|
|
646
|
+
policies: policies.length,
|
|
647
|
+
defaultOn: on,
|
|
648
|
+
...(identity.commit ? { commit: identity.commit } : {}),
|
|
649
|
+
...(semanticOnly ? { semanticOnly } : {}),
|
|
650
|
+
});
|
|
423
651
|
}
|
|
424
652
|
|
|
425
653
|
/**
|
|
@@ -999,7 +1227,13 @@ function inferTaggedVersion(entryPath: string, sources?: string[]): string | nul
|
|
|
999
1227
|
* Identified by CONTENT, not by filename. A name convention would either miss
|
|
1000
1228
|
* `guards.mjs` or match an unrelated `policies.mjs` that configures something
|
|
1001
1229
|
* else entirely; a file that imports `failproofai` and calls
|
|
1002
|
-
* `customPolicies.add` is a policy file whatever
|
|
1230
|
+
* `customPolicies.add` — or `semanticPolicies.add` — is a policy file whatever
|
|
1231
|
+
* it is called.
|
|
1232
|
+
*
|
|
1233
|
+
* Both registries count, and leaving one out was not a cosmetic gap: a file that
|
|
1234
|
+
* registers only Jev questions was undiscoverable, so `failproofai publish` in
|
|
1235
|
+
* its own directory reported no policy file at all rather than reaching the
|
|
1236
|
+
* "registered no policies" check and explaining itself.
|
|
1003
1237
|
*
|
|
1004
1238
|
* Returns ALL of them. Splitting policies across files is the normal thing to
|
|
1005
1239
|
* do past about three, and they are one pack — so several files is an answer,
|
|
@@ -1008,8 +1242,11 @@ function inferTaggedVersion(entryPath: string, sources?: string[]): string | nul
|
|
|
1008
1242
|
*
|
|
1009
1243
|
* Non-recursive on purpose. Walking the tree finds fixtures, examples and
|
|
1010
1244
|
* anything vendored, and publishing those is the failure this avoids.
|
|
1245
|
+
*
|
|
1246
|
+
* Takes the directory rather than reading `process.cwd()` so the discovery rule
|
|
1247
|
+
* can be tested against a real folder without a `chdir`.
|
|
1011
1248
|
*/
|
|
1012
|
-
function findEntry(dir: string): string[] {
|
|
1249
|
+
export function findEntry(dir: string): string[] {
|
|
1013
1250
|
let names: string[];
|
|
1014
1251
|
try {
|
|
1015
1252
|
names = readdirSync(dir);
|
|
@@ -1025,7 +1262,10 @@ function findEntry(dir: string): string[] {
|
|
|
1025
1262
|
// would be the expensive half of this scan.
|
|
1026
1263
|
if (statSync(full).size > 512 * 1024) continue;
|
|
1027
1264
|
const text = readFileSync(full, "utf8");
|
|
1028
|
-
if (
|
|
1265
|
+
if (
|
|
1266
|
+
/from\s+["']failproofai["']/.test(text) &&
|
|
1267
|
+
/(?:customPolicies|semanticPolicies)\s*\.\s*add\s*\(/.test(text)
|
|
1268
|
+
) {
|
|
1029
1269
|
found.push(full);
|
|
1030
1270
|
}
|
|
1031
1271
|
} catch {
|
|
@@ -1727,8 +1967,46 @@ function bundleEntry(
|
|
|
1727
1967
|
*/
|
|
1728
1968
|
const PUBLISH_VALUE_FLAGS = new Set([
|
|
1729
1969
|
"--repo", "--version", "--id", "--tag", "--notes", "--out", "--effect", "--entry", "--init",
|
|
1730
|
-
"--commit",
|
|
1970
|
+
"--commit", "--min-cli-version",
|
|
1731
1971
|
]);
|
|
1972
|
+
/**
|
|
1973
|
+
* The first relative specifier an entry imports, or null.
|
|
1974
|
+
*
|
|
1975
|
+
* Two callers read this and they must agree, because between them they are the
|
|
1976
|
+
* only two outcomes: `publish` bundles a multi-file entry into the one artifact
|
|
1977
|
+
* a pack has to be, and `build` REFUSES one, since only the entry file is
|
|
1978
|
+
* digest-pinned. A shape that neither recognises gets bundled by neither and
|
|
1979
|
+
* refused by neither, and the author sees whatever the loader's rewrite makes of
|
|
1980
|
+
* it — which is an esbuild parse error naming a token, not a file.
|
|
1981
|
+
*
|
|
1982
|
+
* Both sites previously used `[^;\n]*` between `import` and `from`, which cannot
|
|
1983
|
+
* cross a newline and requires a `from` clause to exist. So both missed:
|
|
1984
|
+
*
|
|
1985
|
+
* import "./policies/regex" // side-effect only: there is no `from`
|
|
1986
|
+
* import {
|
|
1987
|
+
* aLongList,
|
|
1988
|
+
* } from "./shared" // the `from` is on another line
|
|
1989
|
+
*
|
|
1990
|
+
* and a pack entry written either way — the natural way to write one whose
|
|
1991
|
+
* halves register at module scope — failed to publish with `Expected "from" but
|
|
1992
|
+
* found "{"`. Now `[^;]*?` spans newlines and stops at a statement end, and the
|
|
1993
|
+
* bare form is matched on its own. Over-reaching to a later `from` is harmless:
|
|
1994
|
+
* the answer is only ever used as "is there a relative import, and name one".
|
|
1995
|
+
*/
|
|
1996
|
+
export function firstRelativeSpecifier(source: string): string | null {
|
|
1997
|
+
const patterns = [
|
|
1998
|
+
/(?:^|\n)\s*(?:import|export)\b[^;]*?\bfrom\s*["'](\.[^"']*)["']/,
|
|
1999
|
+
/(?:^|\n)\s*import\s*["'](\.[^"']*)["']/,
|
|
2000
|
+
/\bimport\s*\(\s*["'](\.[^"']*)["']\s*\)/,
|
|
2001
|
+
/\brequire\s*\(\s*["'](\.[^"']*)["']\s*\)/,
|
|
2002
|
+
];
|
|
2003
|
+
for (const re of patterns) {
|
|
2004
|
+
const m = re.exec(source);
|
|
2005
|
+
if (m) return m[1];
|
|
2006
|
+
}
|
|
2007
|
+
return null;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1732
2010
|
function publishEntryArg(rest: string[]): string | undefined {
|
|
1733
2011
|
const consumed = new Set<number>();
|
|
1734
2012
|
for (let i = 0; i < rest.length; i += 1) {
|
|
@@ -2106,10 +2384,7 @@ async function publish(rest: string[]): Promise<PackCliResult> {
|
|
|
2106
2384
|
// a bundler for a step this tool already performs for its own pack.
|
|
2107
2385
|
let entryToBuild = entry;
|
|
2108
2386
|
const needsBundle =
|
|
2109
|
-
discovered.length > 1 ||
|
|
2110
|
-
/(?:^|\n)\s*(?:import|export)[^;\n]*from\s+["']\.[^"']*["']/.test(
|
|
2111
|
-
readFileSync(entry, "utf8"),
|
|
2112
|
-
);
|
|
2387
|
+
discovered.length > 1 || firstRelativeSpecifier(readFileSync(entry, "utf8")) !== null;
|
|
2113
2388
|
if (needsBundle) {
|
|
2114
2389
|
const sources = discovered.length > 1 ? discovered : [entry];
|
|
2115
2390
|
const bundled = bundleEntry(sources, outDirEarly);
|
|
@@ -2120,6 +2395,7 @@ async function publish(rest: string[]): Promise<PackCliResult> {
|
|
|
2120
2395
|
const built = await build([
|
|
2121
2396
|
entryToBuild,
|
|
2122
2397
|
"--id", id,
|
|
2398
|
+
...(repo ? ["--repo", repo] : []),
|
|
2123
2399
|
"--version", version,
|
|
2124
2400
|
// NOT when the tree is dirty, even though `--version` let the publish
|
|
2125
2401
|
// through. `commit` claims these bytes came from that commit, and on a
|
|
@@ -2129,6 +2405,7 @@ async function publish(rest: string[]): Promise<PackCliResult> {
|
|
|
2129
2405
|
...(provenance && !provenance.dirty ? ["--commit", provenance.sha] : []),
|
|
2130
2406
|
...outFlagFrom(rest),
|
|
2131
2407
|
...effectFlagFrom(rest),
|
|
2408
|
+
...minCliVersionFlagFrom(rest),
|
|
2132
2409
|
]);
|
|
2133
2410
|
if (built.exitCode !== 0) return built;
|
|
2134
2411
|
|
|
@@ -2143,7 +2420,9 @@ async function publish(rest: string[]): Promise<PackCliResult> {
|
|
|
2143
2420
|
...gitLines,
|
|
2144
2421
|
...(gitLines.length ? [""] : []),
|
|
2145
2422
|
...bundleNote,
|
|
2146
|
-
|
|
2423
|
+
// Everything above build's blank line: a fixed count cut the rollback
|
|
2424
|
+
// reminder mid-sentence and dropped the asset paths.
|
|
2425
|
+
...built.lines.slice(0, built.lines.indexOf("")),
|
|
2147
2426
|
"",
|
|
2148
2427
|
...(repo
|
|
2149
2428
|
? ["Dry run — nothing was published.", `Drop --dry-run to release it on ${repo}.`]
|
|
@@ -2259,6 +2538,7 @@ async function publish(rest: string[]): Promise<PackCliResult> {
|
|
|
2259
2538
|
`Published ${id}@${version} to ${repo} at tag ${tag}.` +
|
|
2260
2539
|
(versionFromSha ? " That names the commit it was built from." : ""),
|
|
2261
2540
|
` ${assets.length} assets attached`,
|
|
2541
|
+
...(built.meta?.semanticOnly ? JEV_ONLY_ROLLBACK_REMINDER : []),
|
|
2262
2542
|
"",
|
|
2263
2543
|
// The install lines are the whole point of the success message, and on a
|
|
2264
2544
|
// private repository they are a lie — `policies add` 404s there. Print the
|
|
@@ -2292,6 +2572,18 @@ function effectFlagFrom(rest: string[]): string[] {
|
|
|
2292
2572
|
if (i === -1) return [];
|
|
2293
2573
|
return rest[i].includes("=") ? [rest[i]] : [rest[i], rest[i + 1]];
|
|
2294
2574
|
}
|
|
2575
|
+
/**
|
|
2576
|
+
* Same again for `--min-cli-version`, and it has to be forwarded explicitly
|
|
2577
|
+
* because `publish` hands `build` a list it assembles rather than its own
|
|
2578
|
+
* `rest`. Left off, the flag parsed, validated, refused an uncomparable value —
|
|
2579
|
+
* and then never reached the manifest, so the field the whole ordering
|
|
2580
|
+
* constraint rests on was silently absent from the published artifact.
|
|
2581
|
+
*/
|
|
2582
|
+
function minCliVersionFlagFrom(rest: string[]): string[] {
|
|
2583
|
+
const i = rest.findIndex((a) => a === "--min-cli-version" || a.startsWith("--min-cli-version="));
|
|
2584
|
+
if (i === -1) return [];
|
|
2585
|
+
return rest[i].includes("=") ? [rest[i]] : [rest[i], rest[i + 1]];
|
|
2586
|
+
}
|
|
2295
2587
|
|
|
2296
2588
|
/** `failproofai publish` — exported for bin/failproofai.mjs and the tests. */
|
|
2297
2589
|
export async function runPublishCommand(rest: string[]): Promise<PackCliResult> {
|
|
@@ -2345,11 +2637,65 @@ async function pickClis(
|
|
|
2345
2637
|
* Returns null when the user cancelled, which must not be confused with an
|
|
2346
2638
|
* empty selection: one means "install nothing", the other means "do nothing".
|
|
2347
2639
|
*/
|
|
2640
|
+
/**
|
|
2641
|
+
* How a pack's semantic half is named to a person: "3 Jev checks", never "3
|
|
2642
|
+
* semantic policies".
|
|
2643
|
+
*
|
|
2644
|
+
* They are not policies in the sense every other number on these screens uses —
|
|
2645
|
+
* nothing switches them on or off, `--policy` cannot name one, and they do not
|
|
2646
|
+
* appear in `failproofai policies`. Calling them policies would make every count
|
|
2647
|
+
* beside them read as the same kind of thing.
|
|
2648
|
+
*/
|
|
2649
|
+
function semanticPhrase(count: number): string {
|
|
2650
|
+
return `${count} Jev ${count === 1 ? "check" : "checks"}`;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
/**
|
|
2654
|
+
* How a pack's Jev checks sit beside this build's, for add, show and the
|
|
2655
|
+
* picker alike: a FailproofAI pack's replace them, anyone else's are added
|
|
2656
|
+
* (`replacesBuiltinChecks`), and only where `jevPacks` lets the pack take part —
|
|
2657
|
+
* never for an observe pack, only for its agents when scoped. One phrase, so
|
|
2658
|
+
* the three cannot drift apart again.
|
|
2659
|
+
*/
|
|
2660
|
+
function besideBuiltinChecks(pack: { source?: string; effect?: PolicyEffect; clis?: string[] | null }): string {
|
|
2661
|
+
if (jevPacks([{ effect: pack.effect ?? "enforce", clis: null }]).length === 0) {
|
|
2662
|
+
return "not asked: this pack only observes, and Jev asks only the checks of packs that enforce";
|
|
2663
|
+
}
|
|
2664
|
+
const how = isFirstPartyPack(pack) ? "replacing this build's own set" : "added to this build's own checks";
|
|
2665
|
+
return pack.clis && pack.clis.length > 0 ? `${how}, for ${pack.clis.join(", ")} only` : how;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
/**
|
|
2669
|
+
* Which of a pack's checks the resolver would leave out beside what is
|
|
2670
|
+
* installed (the shared budget, a reserved or contested name). `preview` is a
|
|
2671
|
+
* pack not installed yet, judged as if it were. A diagnostic: never throws.
|
|
2672
|
+
*/
|
|
2673
|
+
function jevCheckWarnings(id: string, preview?: PackPreview): string[] {
|
|
2674
|
+
try {
|
|
2675
|
+
const installed = readInstalledPacks().packs.filter((p) => p.id !== preview?.id);
|
|
2676
|
+
const packs = preview ? [...installed, { ...preview, clis: null }] : installed;
|
|
2677
|
+
return semanticPoliciesFromPacks(jevPacks(packs)).errors.filter((e) => e.includes(id));
|
|
2678
|
+
} catch {
|
|
2679
|
+
return [];
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2348
2683
|
async function pickFromSource(
|
|
2349
2684
|
source: string,
|
|
2350
2685
|
io: { stdin: TTYIn; stdout: TTYOut },
|
|
2351
2686
|
): Promise<string[] | null> {
|
|
2352
2687
|
const preview = await fetchPackPreview(source);
|
|
2688
|
+
// Nothing to pick. A pack of Jev questions alone has no selectable policies —
|
|
2689
|
+
// its semantic set is not switchable per entry — so the picker would draw an
|
|
2690
|
+
// empty list and ask a question with no answers. `[]` rather than `null`: the
|
|
2691
|
+
// user cancelled nothing, there was simply nothing to choose.
|
|
2692
|
+
if (preview.policies.length === 0) {
|
|
2693
|
+
io.stdout.write(
|
|
2694
|
+
`\n ${preview.id}@${preview.version} carries no selectable policies` +
|
|
2695
|
+
`${preview.semantic.length > 0 ? `, only ${semanticPhrase(preview.semantic.length)}` : ""}.\n\n`,
|
|
2696
|
+
);
|
|
2697
|
+
return [];
|
|
2698
|
+
}
|
|
2353
2699
|
const categories = [...new Set(preview.policies.map((p) => p.category))];
|
|
2354
2700
|
const rows: MultiChoice<string>[] = [];
|
|
2355
2701
|
for (const category of categories) {
|
|
@@ -2364,6 +2710,15 @@ async function pickFromSource(
|
|
|
2364
2710
|
}
|
|
2365
2711
|
}
|
|
2366
2712
|
const on = preview.policies.filter((p) => p.defaultEnabled).length;
|
|
2713
|
+
// Said BEFORE the list, because the list cannot say it: a pack's Jev checks are
|
|
2714
|
+
// not rows here, and they arrive whole. Somebody ticking boxes should know
|
|
2715
|
+
// that is part of what they are agreeing to.
|
|
2716
|
+
if (preview.semantic.length > 0) {
|
|
2717
|
+
io.stdout.write(
|
|
2718
|
+
`\n This pack also brings ${semanticPhrase(preview.semantic.length)} (not selectable), ` +
|
|
2719
|
+
`${besideBuiltinChecks(preview)}.\n See: failproofai policies show ${source}\n\n`,
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2367
2722
|
const picked = await multiSelect<string>({
|
|
2368
2723
|
message: `${preview.id}@${preview.version} — which of these should be on?`,
|
|
2369
2724
|
choices: rows,
|
|
@@ -2463,7 +2818,7 @@ async function add(rest: string[]): Promise<PackCliResult> {
|
|
|
2463
2818
|
];
|
|
2464
2819
|
const skipped = result.available.filter((n) => !result.enabled.includes(n));
|
|
2465
2820
|
|
|
2466
|
-
// Say WHY this set is on. Without it, "10 of
|
|
2821
|
+
// Say WHY this set is on. Without it, "10 of 39 enabled" reads like
|
|
2467
2822
|
// something went wrong rather than like the pack's own recommendation.
|
|
2468
2823
|
const why = {
|
|
2469
2824
|
defaults: "the pack's defaults",
|
|
@@ -2480,9 +2835,34 @@ async function add(rest: string[]): Promise<PackCliResult> {
|
|
|
2480
2835
|
// rendered as if something had gone missing.
|
|
2481
2836
|
lines.push(
|
|
2482
2837
|
result.enabled.length === 0
|
|
2483
|
-
?
|
|
2838
|
+
? // "enforcing nothing" is only true when the pack brought nothing else.
|
|
2839
|
+
// A pack of Jev checks alone enforces through the semantic tier with no
|
|
2840
|
+
// regex policy enabled at all, and telling that user their install does
|
|
2841
|
+
// nothing is worse than saying nothing.
|
|
2842
|
+
` enabled (0/${result.available.length}, ${why}): none` +
|
|
2843
|
+
(result.semantic > 0 ? "" : " — the pack is installed and enforcing nothing")
|
|
2484
2844
|
: ` enabled (${result.enabled.length}/${result.available.length}, ${why}): ${summarise(result.enabled)}`,
|
|
2485
2845
|
);
|
|
2846
|
+
// Counted separately, and without an on/off ratio, because there is none:
|
|
2847
|
+
// a pack's Jev checks arrive whole, and a FailproofAI pack's REPLACE the ones
|
|
2848
|
+
// this build ships (anyone else's are added to them).
|
|
2849
|
+
// Silence here was the shape of the original bug — the manifest's semantic
|
|
2850
|
+
// half was fetched, verified and then never written to `installed.json`, so
|
|
2851
|
+
// the half somebody installed did nothing and nothing said so.
|
|
2852
|
+
if (result.semantic > 0) {
|
|
2853
|
+
lines.push(
|
|
2854
|
+
` ${semanticPhrase(result.semantic)}, ${besideBuiltinChecks(result)}. ` +
|
|
2855
|
+
"They apply only where you configured Jev (`failproofai jev status`).",
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
// Nothing after this screen says it: the record keeps no unreadable minimum.
|
|
2859
|
+
if (result.minCliVersionNote) lines.push(` ${result.minCliVersionNote}`);
|
|
2860
|
+
// Which of its checks the resolver will leave out beside what is already
|
|
2861
|
+
// installed (the shared question budget, a reserved or contested name).
|
|
2862
|
+
// Otherwise said only in the hook log, on the first call that asks Jev.
|
|
2863
|
+
if (result.semantic > 0) {
|
|
2864
|
+
for (const e of jevCheckWarnings(result.id)) lines.push(` ▲ ${e}`);
|
|
2865
|
+
}
|
|
2486
2866
|
|
|
2487
2867
|
if (skipped.length > 0) {
|
|
2488
2868
|
lines.push(` not enabled (${skipped.length}): ${summarise(skipped)}`);
|
|
@@ -2741,6 +3121,149 @@ function relativeAge(iso: string): string {
|
|
|
2741
3121
|
return `${value} ${label}${value === 1 ? "" : "s"} ago`;
|
|
2742
3122
|
}
|
|
2743
3123
|
|
|
3124
|
+
/**
|
|
3125
|
+
* Which of a pack's OWN regex policies each of its Jev checks can clear, keyed
|
|
3126
|
+
* by check name and empty for a check nothing names.
|
|
3127
|
+
*
|
|
3128
|
+
* INVERTED out of the same manifest's `reviewedBy` rather than declared beside
|
|
3129
|
+
* the checks. A semantic entry says nothing about the policies that name it, so
|
|
3130
|
+
* the only other way to show this column is a second list kept by hand — which
|
|
3131
|
+
* is wrong the first time a publisher adds a reviewer to one policy, and wrong
|
|
3132
|
+
* in the direction that overstates what Jev may clear.
|
|
3133
|
+
*
|
|
3134
|
+
* Judged with `resolvePolicyAuthority` against the pack's OWN check names,
|
|
3135
|
+
* because that is the rule the installing machine applies: a pack declaring
|
|
3136
|
+
* semantic entries replaces the compiled-in set (`effectiveReviewerNames`), so
|
|
3137
|
+
* its `reviewedBy` may name only its own checks, and a declaration that names
|
|
3138
|
+
* anything else — or one sitting under `authority: "hard"`, which a manifest may
|
|
3139
|
+
* carry because the two fields are parsed independently — registers HARD. A
|
|
3140
|
+
* policy in either state can never be cleared, so naming it under a check would
|
|
3141
|
+
* promise a clear that cannot happen.
|
|
3142
|
+
*/
|
|
3143
|
+
function reviewersByCheck(
|
|
3144
|
+
policies: ReadonlyArray<PolicyCatalogEntry>,
|
|
3145
|
+
semantic: ReadonlyArray<SemanticManifestEntry>,
|
|
3146
|
+
): Map<string, string[]> {
|
|
3147
|
+
const ownChecks = new Set(semantic.map((s) => s.name));
|
|
3148
|
+
const byCheck = new Map<string, string[]>(semantic.map((s) => [s.name, []]));
|
|
3149
|
+
for (const policy of policies) {
|
|
3150
|
+
const { authority, reviewedBy } = resolvePolicyAuthority(policy, ownChecks);
|
|
3151
|
+
if (authority !== "reviewable") continue;
|
|
3152
|
+
for (const check of reviewedBy ?? []) byCheck.get(check)?.push(policy.name);
|
|
3153
|
+
}
|
|
3154
|
+
return byCheck;
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
/** Reviewers named in full. Past this the cell names two and counts the rest. */
|
|
3158
|
+
const REVIEWERS_LISTED = 3;
|
|
3159
|
+
|
|
3160
|
+
/**
|
|
3161
|
+
* A check's reviewers as one cell.
|
|
3162
|
+
*
|
|
3163
|
+
* The question a reader brings to this column is "can this check clear anything
|
|
3164
|
+
* of mine", which two names and a count answer as well as ten do — and ten
|
|
3165
|
+
* names in a cell push the column past the terminal, where `table` cuts one of
|
|
3166
|
+
* them mid-word and the row stops being readable at all.
|
|
3167
|
+
*/
|
|
3168
|
+
function reviewersCell(names: ReadonlyArray<string>): string {
|
|
3169
|
+
if (names.length <= REVIEWERS_LISTED) return names.join(", ");
|
|
3170
|
+
return `${names.slice(0, 2).join(", ")}, +${names.length - 2}`;
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
/**
|
|
3174
|
+
* Why a check that no policy names is in the pack at all — and the two answers
|
|
3175
|
+
* are different facts, so the row has to tell them apart.
|
|
3176
|
+
*
|
|
3177
|
+
* A `deny` check with no reviewer is a concern the pack's regex half does not
|
|
3178
|
+
* cover: it can only ever ADD a deny, never clear one, and that is a gap in the
|
|
3179
|
+
* regex tier rather than a mistake in the manifest. An `instruct` check can
|
|
3180
|
+
* never answer deny, so every conjunction naming it has one reachable outcome —
|
|
3181
|
+
* a clear — which is why nothing names it and why nothing should (the
|
|
3182
|
+
* `block-work-on-main` entry in CHANGELOG 1.0.7-beta.1 is that bug, shipped).
|
|
3183
|
+
*/
|
|
3184
|
+
function unreviewedCell(mode: SemanticManifestEntry["mode"], policies: number): string {
|
|
3185
|
+
if (mode === "instruct") return "instruct-only — it can never deny";
|
|
3186
|
+
// "the 38" borrows the count from the heading above, where it has just been
|
|
3187
|
+
// read. The other two spellings exist because that sentence has no grammatical
|
|
3188
|
+
// form at one or zero — and a pack of Jev checks alone, with no regex half at
|
|
3189
|
+
// all, is a legitimate thing to publish.
|
|
3190
|
+
if (policies === 0) return "this pack has no policies to clear";
|
|
3191
|
+
if (policies === 1) return "the one policy here does not cover this";
|
|
3192
|
+
return `nothing in the ${policies} covers this`;
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
/**
|
|
3196
|
+
* A pack's Jev checks as a section of rows — the shape the category sections
|
|
3197
|
+
* use, deliberately NOT their columns.
|
|
3198
|
+
*
|
|
3199
|
+
* It replaces a prose paragraph that listed sixteen names comma-separated above
|
|
3200
|
+
* a screen where everything else was a scannable row. The rows are not policy
|
|
3201
|
+
* rows and must not read as ones: there is no on/off chip, because nothing
|
|
3202
|
+
* toggles a check, `--policy` cannot name one, and none of them appears in
|
|
3203
|
+
* `failproofai policies`. What the columns carry instead is the pair of facts
|
|
3204
|
+
* that decide what a check does on a real machine — its mode, and which of this
|
|
3205
|
+
* pack's own policies it can clear.
|
|
3206
|
+
*
|
|
3207
|
+
* Null when the pack declares none, so a pack with no semantic half prints no
|
|
3208
|
+
* section rather than an empty one.
|
|
3209
|
+
*/
|
|
3210
|
+
export function jevChecksSection(
|
|
3211
|
+
pack: {
|
|
3212
|
+
policies: ReadonlyArray<PolicyCatalogEntry>;
|
|
3213
|
+
semantic: ReadonlyArray<SemanticManifestEntry>;
|
|
3214
|
+
/** Where it is (or would be) installed from, which decides replace or add. */
|
|
3215
|
+
source?: string;
|
|
3216
|
+
},
|
|
3217
|
+
opts?: RenderOpts,
|
|
3218
|
+
): string[] | null {
|
|
3219
|
+
if (pack.semantic.length === 0) return null;
|
|
3220
|
+
const reviewers = reviewersByCheck(pack.policies, pack.semantic);
|
|
3221
|
+
const rows = pack.semantic.map((check) => {
|
|
3222
|
+
const named = reviewers.get(check.name) ?? [];
|
|
3223
|
+
// `reviews` and `—` lead the same cell rather than sitting in a column of
|
|
3224
|
+
// their own. A fourth column would pad every `—` out to the width of
|
|
3225
|
+
// `reviews`, which costs nine characters of the only column allowed to
|
|
3226
|
+
// shrink — enough to ellipsize the reason on an 80-column terminal, where
|
|
3227
|
+
// this way everything fits whole.
|
|
3228
|
+
return [
|
|
3229
|
+
check.mode,
|
|
3230
|
+
check.name,
|
|
3231
|
+
named.length > 0
|
|
3232
|
+
? `reviews ${reviewersCell(named)}`
|
|
3233
|
+
: `— ${unreviewedCell(check.mode, pack.policies.length)}`,
|
|
3234
|
+
];
|
|
3235
|
+
});
|
|
3236
|
+
return [
|
|
3237
|
+
// The count is on the heading rather than in a row, the way every other
|
|
3238
|
+
// section on this screen carries its own total.
|
|
3239
|
+
...rule(
|
|
3240
|
+
`Jev checks — ${pack.semantic.length} · not selectable · only where Jev is configured`,
|
|
3241
|
+
opts,
|
|
3242
|
+
),
|
|
3243
|
+
// The reviewers column is the only one allowed to shrink: a mode or a check
|
|
3244
|
+
// name cut in half is a fact nobody can act on, while a cut reviewer list is
|
|
3245
|
+
// one that was already abbreviated.
|
|
3246
|
+
...table({ head: ["", "", ""], rows, protect: [0, 1] }, opts),
|
|
3247
|
+
"",
|
|
3248
|
+
...note(
|
|
3249
|
+
"Nothing toggles them: `--policy` cannot name one and `failproofai policies` never lists them. " +
|
|
3250
|
+
`They arrive whole, ${besideBuiltinChecks(pack)}.`,
|
|
3251
|
+
opts,
|
|
3252
|
+
),
|
|
3253
|
+
...note(
|
|
3254
|
+
// A pack of Jev checks alone has no regex half for the column to be about,
|
|
3255
|
+
// so the sentence says what IS true of it rather than "its own 0 policies".
|
|
3256
|
+
pack.policies.length === 0
|
|
3257
|
+
? "This pack ships no regex policies, so none of these checks clears anything here: a deny " +
|
|
3258
|
+
"check can only ADD a deny, and an instruct check only ever warns."
|
|
3259
|
+
: `\`reviews\` is which of this pack's own ${pack.policies.length} policies each check can clear. ` +
|
|
3260
|
+
"A deny check that reviews nothing can only ever ADD a deny; an instruct check never answers " +
|
|
3261
|
+
"deny, so nothing pairs with one.",
|
|
3262
|
+
opts,
|
|
3263
|
+
),
|
|
3264
|
+
];
|
|
3265
|
+
}
|
|
3266
|
+
|
|
2744
3267
|
async function listRemote(source: string): Promise<PackCliResult> {
|
|
2745
3268
|
const opts = optsFor(process.stdout);
|
|
2746
3269
|
let preview;
|
|
@@ -2774,7 +3297,8 @@ async function listRemote(source: string): Promise<PackCliResult> {
|
|
|
2774
3297
|
stack(
|
|
2775
3298
|
title(
|
|
2776
3299
|
`${preview.id}@${preview.version}`,
|
|
2777
|
-
`${preview.policies.length} policies · ${categories.length} categories
|
|
3300
|
+
`${preview.policies.length} policies · ${categories.length} categories` +
|
|
3301
|
+
(preview.semantic.length > 0 ? ` · ${semanticPhrase(preview.semantic.length)}` : ""),
|
|
2778
3302
|
opts,
|
|
2779
3303
|
),
|
|
2780
3304
|
note(
|
|
@@ -2782,8 +3306,15 @@ async function listRemote(source: string): Promise<PackCliResult> {
|
|
|
2782
3306
|
(preview.effect === "observe" ? " This pack OBSERVES — it records and blocks nothing." : ""),
|
|
2783
3307
|
opts,
|
|
2784
3308
|
),
|
|
3309
|
+
preview.minCliVersion ? note(`Requires failproofai ${preview.minCliVersion} or newer.`, opts) : null,
|
|
3310
|
+
preview.minCliVersionNote ? note(preview.minCliVersionNote, opts) : null,
|
|
2785
3311
|
preview.resolvedFromLatest ? note(`Newest release: ${preview.source}`, opts) : null,
|
|
2786
3312
|
table({ head: ["", "", ""], rows }, opts),
|
|
3313
|
+
// The half the table above cannot hold: its own section, under the
|
|
3314
|
+
// policies rather than above them, because a check is read against the
|
|
3315
|
+
// policies it can clear and those are the rows just passed.
|
|
3316
|
+
jevChecksSection(preview, opts),
|
|
3317
|
+
preview.semantic.length > 0 ? jevCheckWarnings(preview.id, preview).flatMap((e) => warning([e], opts)) : null,
|
|
2787
3318
|
nextStep(`failproofai policies add ${source}`, "Install the defaults with:", opts),
|
|
2788
3319
|
note("Or take part of it: --policy <a,b>, --category <x,y>, --all", opts),
|
|
2789
3320
|
),
|