failproofai 1.0.1 → 1.0.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +6 -6
- 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 +2 -2
- 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 +2 -2
- 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 +3 -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 +5 -4
- 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 +2 -2
- 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 +4 -3
- 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 +4 -4
- 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 +4 -4
- 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 +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- 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 +66 -22
- package/.next/standalone/.next/server/app/policies/page.js +5 -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/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +2 -2
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +1 -1
- 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]__1qzqcio._.js → [externals]__0e32-7c._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__1s462yl._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0neqlu-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t9oqnn._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
- package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -6
- package/.next/standalone/.next/server/chunks/lib_telemetry-id_ts_1k8gj9o._.js +3 -0
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_09d5c79._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1d2g73o._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js} +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0xbw3bu._.js +12 -0
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0spkm68._.js → [root-of-the-server]__07gm3zl._.js} +21 -20
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0l4748x._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__192nhvh._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bwu160._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0u5zauv._.js → _0ns6k3a._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +13 -13
- 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_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_08o7bqo._.js +10 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware.js +1 -1
- 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 +71 -27
- package/.next/standalone/.next/static/chunks/{3c808qrfcq0im.js → 01u1cf8js2yd6.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0k5050ezjvv49.js → 0izgu7nqh2zp-.js} +1 -1
- package/.next/standalone/.next/static/chunks/0u4-org_e7zde.js +1 -0
- package/.next/standalone/.next/static/chunks/1mfzsja6ftim5.css +2 -0
- package/.next/standalone/.next/static/chunks/{2zpp1we2jv6tb.js → 1tynnrrh711lr.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1ide1wl2f7zi1.js → 22rqvlr7p_1mv.js} +13 -13
- package/.next/standalone/.next/static/chunks/{31s1smp-g7gks.js → 2whxqyskpirbk.js} +1 -1
- package/.next/standalone/.next/static/chunks/31mwk8k0q3_3g.js +6 -0
- package/.next/standalone/.next/static/chunks/3ms6gxfm82t6s.js +1 -0
- package/.next/standalone/.next/static/chunks/{37fcpc36x9gpe.js → 3tbbs_d8xrvyp.js} +1 -1
- package/.next/standalone/.next/static/chunks/43nlc236oo898.js +1 -0
- package/.next/standalone/SECURITY.md +5 -4
- package/.next/standalone/app/actions/get-hooks-config.ts +108 -17
- package/.next/standalone/app/actions/pack-actions.ts +163 -0
- package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
- package/.next/standalone/app/audit/_components/share-templates.ts +13 -13
- package/.next/standalone/app/policies/hooks-client.tsx +320 -15
- package/.next/standalone/components/navbar.tsx +2 -28
- package/.next/standalone/components/reach-developers.tsx +8 -1
- package/.next/standalone/docs-old/agenteye/python-sdk-skill.mdx +1 -1
- package/.next/standalone/docs-old/agenteye/python-sdk.mdx +59 -35
- package/.next/standalone/docs-old/i18n/README.ar.md +1 -0
- package/.next/standalone/docs-old/i18n/README.de.md +1 -0
- package/.next/standalone/docs-old/i18n/README.es.md +1 -0
- package/.next/standalone/docs-old/i18n/README.fr.md +1 -0
- package/.next/standalone/docs-old/i18n/README.he.md +1 -0
- package/.next/standalone/docs-old/i18n/README.hi.md +1 -0
- package/.next/standalone/docs-old/i18n/README.it.md +1 -0
- package/.next/standalone/docs-old/i18n/README.ja.md +1 -0
- package/.next/standalone/docs-old/i18n/README.ko.md +1 -0
- package/.next/standalone/docs-old/i18n/README.pt-br.md +1 -0
- package/.next/standalone/docs-old/i18n/README.ru.md +1 -0
- package/.next/standalone/docs-old/i18n/README.tr.md +1 -0
- package/.next/standalone/docs-old/i18n/README.vi.md +1 -0
- package/.next/standalone/docs-old/i18n/README.zh.md +1 -0
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +431 -0
- package/.next/standalone/fp-cloud-cli/LICENSE +42 -0
- package/.next/standalone/fp-cloud-cli/MANIFEST.in +10 -0
- package/.next/standalone/fp-cloud-cli/README.md +259 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/__init__.py +10 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/__main__.py +4 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +64 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/_context.py +332 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/analytics.py +432 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/analytics_config.py +77 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/analytics_registry.py +83 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/app.py +492 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/auth.py +160 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +1694 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/__init__.py +0 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +214 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +407 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +445 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +1054 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/auth_cmds.py +512 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/errors_cmds.py +190 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/evals_cmds.py +161 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +159 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +416 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/guardrails_cmds.py +148 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +472 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +407 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +63 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +319 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +499 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +378 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +151 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +150 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +35 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +404 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/config.py +330 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/dates.py +78 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +345 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +98 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/models.py +891 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/orgs.py +30 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +6593 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +208 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +290 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/py.typed +0 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/select.py +322 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +53 -0
- package/.next/standalone/fp-cloud-cli/pyproject.toml +106 -0
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +302 -0
- package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +8 -0
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +254 -0
- package/.next/standalone/fp-cloud-cli/tests/__init__.py +0 -0
- package/.next/standalone/fp-cloud-cli/tests/conftest.py +92 -0
- package/.next/standalone/fp-cloud-cli/tests/test_alerting.py +644 -0
- package/.next/standalone/fp-cloud-cli/tests/test_analytics.py +385 -0
- package/.next/standalone/fp-cloud-cli/tests/test_audits.py +950 -0
- package/.next/standalone/fp-cloud-cli/tests/test_auth.py +98 -0
- package/.next/standalone/fp-cloud-cli/tests/test_auth_mode.py +272 -0
- package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +118 -0
- package/.next/standalone/fp-cloud-cli/tests/test_client.py +299 -0
- package/.next/standalone/fp-cloud-cli/tests/test_commands.py +814 -0
- package/.next/standalone/fp-cloud-cli/tests/test_config.py +99 -0
- package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +340 -0
- package/.next/standalone/fp-cloud-cli/tests/test_dates.py +76 -0
- package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +481 -0
- package/.next/standalone/fp-cloud-cli/tests/test_facets.py +170 -0
- package/.next/standalone/fp-cloud-cli/tests/test_failproofai_home.py +722 -0
- package/.next/standalone/fp-cloud-cli/tests/test_fp_home_contract.py +198 -0
- package/.next/standalone/fp-cloud-cli/tests/test_hardening.py +186 -0
- package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +127 -0
- package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +496 -0
- package/.next/standalone/fp-cloud-cli/tests/test_list.py +59 -0
- package/.next/standalone/fp-cloud-cli/tests/test_multivalue.py +183 -0
- package/.next/standalone/fp-cloud-cli/tests/test_no_customer_identifiers.py +168 -0
- package/.next/standalone/fp-cloud-cli/tests/test_operator.py +492 -0
- package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +692 -0
- package/.next/standalone/fp-cloud-cli/tests/test_output.py +1732 -0
- package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +191 -0
- package/.next/standalone/fp-cloud-cli/tests/test_readme_matches_reality.py +125 -0
- package/.next/standalone/fp-cloud-cli/tests/test_review_fixes.py +161 -0
- package/.next/standalone/fp-cloud-cli/tests/test_telemetry_completeness.py +90 -0
- package/.next/standalone/fp-cloud-cli/tests/test_usage.py +115 -0
- package/.next/standalone/fp-cloud-cli/tests/test_v1_origin_diagnostic.py +70 -0
- package/.next/standalone/fp-cloud-cli/tests/test_v1_routing.py +214 -0
- package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +104 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +552 -0
- package/.next/standalone/package.json +27 -21
- package/.next/standalone/sdk/python/CHANGELOG.md +70 -0
- package/.next/standalone/sdk/python/LICENSE +42 -0
- package/.next/standalone/sdk/python/MANIFEST.in +21 -0
- package/.next/standalone/sdk/python/README.md +466 -0
- package/.next/standalone/sdk/python/docs/README.md +249 -0
- package/.next/standalone/sdk/python/docs/_shared/README.md +43 -0
- package/.next/standalone/sdk/python/docs/_shared/__init__.py +195 -0
- package/.next/standalone/sdk/python/docs/crewai/README.md +328 -0
- package/.next/standalone/sdk/python/docs/crewai/examples/quickstart.py +61 -0
- package/.next/standalone/sdk/python/docs/crewai/examples/research_crew.py +92 -0
- package/.next/standalone/sdk/python/docs/langgraph/README.md +481 -0
- package/.next/standalone/sdk/python/docs/langgraph/examples/quickstart.py +51 -0
- package/.next/standalone/sdk/python/docs/langgraph/examples/research_agent.py +86 -0
- package/.next/standalone/sdk/python/docs/langgraph/examples/supervisor_handoff.py +83 -0
- package/.next/standalone/sdk/python/docs/llama_index/README.md +332 -0
- package/.next/standalone/sdk/python/docs/llama_index/examples/quickstart.py +62 -0
- package/.next/standalone/sdk/python/docs/llama_index/examples/research_agent.py +93 -0
- package/.next/standalone/sdk/python/docs/manual/README.md +491 -0
- package/.next/standalone/sdk/python/docs/manual/examples/quickstart.py +57 -0
- package/.next/standalone/sdk/python/docs/manual/examples/research_agent.py +133 -0
- package/.next/standalone/sdk/python/docs/pydantic_ai/README.md +323 -0
- package/.next/standalone/sdk/python/docs/pydantic_ai/examples/quickstart.py +48 -0
- package/.next/standalone/sdk/python/docs/pydantic_ai/examples/research_agent.py +94 -0
- package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +131 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_context.py +218 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_environment.py +78 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_events.py +752 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_resolver.py +118 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_runtime.py +21 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_schema.py +317 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_scopes.py +422 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_writer.py +750 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +274 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/_compat.py +247 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/_core.py +1076 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/crewai.py +1669 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/langchain.py +2151 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/llama_index.py +1588 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/pydantic_ai.py +977 -0
- package/.next/standalone/sdk/python/failproofai_sdk/py.typed +0 -0
- package/.next/standalone/sdk/python/pyproject.toml +92 -0
- package/.next/standalone/sdk/python/skill/SKILL.md +399 -0
- package/.next/standalone/sdk/python/skill/agents/openai.yaml +8 -0
- package/.next/standalone/sdk/python/skill/references/events.md +234 -0
- package/.next/standalone/sdk/python/skill/references/frameworks.md +314 -0
- package/.next/standalone/sdk/python/skill/references/install.md +100 -0
- package/.next/standalone/sdk/python/skill/references/integration.md +352 -0
- package/.next/standalone/sdk/python/tests/__init__.py +0 -0
- package/.next/standalone/sdk/python/tests/conftest.py +169 -0
- package/.next/standalone/sdk/python/tests/integrations/__init__.py +6 -0
- package/.next/standalone/sdk/python/tests/integrations/test_crewai.py +1299 -0
- package/.next/standalone/sdk/python/tests/integrations/test_langchain.py +2338 -0
- package/.next/standalone/sdk/python/tests/integrations/test_llama_index.py +1576 -0
- package/.next/standalone/sdk/python/tests/integrations/test_pydantic_ai.py +1102 -0
- package/.next/standalone/sdk/python/tests/test_context.py +410 -0
- package/.next/standalone/sdk/python/tests/test_docs.py +411 -0
- package/.next/standalone/sdk/python/tests/test_durability.py +1352 -0
- package/.next/standalone/sdk/python/tests/test_encoding.py +571 -0
- package/.next/standalone/sdk/python/tests/test_integrations.py +1127 -0
- package/.next/standalone/sdk/python/tests/test_no_customer_identifiers.py +214 -0
- package/.next/standalone/sdk/python/tests/test_packaging.py +185 -0
- package/.next/standalone/sdk/python/tests/test_resolver_umbrella.py +178 -0
- package/.next/standalone/sdk/python/tests/test_scopes.py +487 -0
- package/.next/standalone/sdk/python/tests/test_sdk.py +923 -0
- package/.next/standalone/sdk/python/tests/test_server_contract.py +650 -0
- package/.next/standalone/sdk/python/tests/test_site_docs.py +371 -0
- package/.next/standalone/sdk/python/tests/test_skill_snippets.py +165 -0
- package/.next/standalone/sdk/python/tests/test_spool_contract.py +287 -0
- package/.next/standalone/sdk/python/tests/test_spool_creation.py +360 -0
- package/.next/standalone/sdk/python/tests/test_wire_format.py +279 -0
- package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +378 -0
- package/.next/standalone/sdk/python/uv.lock +6005 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +77 -21
- package/bin/failproofai.mjs +579 -306
- package/dist/cli.mjs +6499 -3662
- package/dist/index.js +5 -5
- package/dist/worker.mjs +2390 -1978
- package/package.json +27 -21
- package/scripts/build-policy-pack.mjs +121 -0
- package/scripts/changelog-section.py +77 -0
- package/scripts/prune-standalone.mjs +3 -0
- package/scripts/python-version.py +232 -0
- package/scripts/translate-docs/cli.ts +21 -0
- package/scripts/translate-docs/mintlify-nav.ts +85 -13
- package/scripts/translate-docs/translator.ts +66 -11
- package/src/audit/cache.ts +40 -3
- package/src/audit/cli.ts +1 -1
- package/src/audit/index.ts +1 -1
- package/src/audit/replay.ts +89 -3
- package/src/audit/report.ts +5 -5
- package/src/audit/schedule-cli.ts +49 -53
- package/src/hooks/builtin-policies.ts +217 -579
- package/src/hooks/cloud-enrollment-cli.ts +66 -27
- package/src/hooks/cloud-managed-policies.ts +6 -1
- package/src/hooks/configure-wizard.ts +56 -369
- package/src/hooks/custom-hooks-loader.ts +89 -6
- package/src/hooks/fp-home.ts +92 -4
- package/src/hooks/fp-reset.ts +34 -2
- package/src/hooks/handler.ts +181 -11
- package/src/hooks/harness-cli.ts +79 -25
- package/src/hooks/hook-activity-store.ts +13 -3
- package/src/hooks/install-prompt.ts +25 -3
- package/src/hooks/manager.ts +424 -172
- package/src/hooks/migrations.ts +2 -2
- package/src/hooks/pack-cli.ts +885 -0
- package/src/hooks/pack-failclosed.ts +189 -0
- package/src/hooks/pack-manifest.ts +352 -0
- package/src/hooks/pack-store.ts +891 -0
- package/src/hooks/policy-catalog.ts +535 -0
- package/src/hooks/policy-evaluator.ts +24 -16
- package/src/hooks/policy-registry.ts +8 -2
- package/src/hooks/policy-types.ts +26 -0
- package/src/hooks/session-pause-cli.ts +33 -9
- package/src/hooks/tui.ts +695 -39
- package/src/hooks/types.ts +15 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1ger0a2._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_1iiwm5e._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07tbbf3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kc8_v5._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mhqk5a._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pltpn8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qkcyf-._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w0x_ok._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bfex62._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +0 -6
- package/.next/standalone/.next/server/chunks/ssr/_0lvyuv4._.js +0 -23
- package/.next/standalone/.next/static/chunks/2_2lhuojq2xop.js +0 -1
- package/.next/standalone/.next/static/chunks/36fw6yk1s6x_f.js +0 -1
- package/.next/standalone/.next/static/chunks/3k7mxbp9hiom0.js +0 -1
- package/.next/standalone/.next/static/chunks/3tzq0260n17ae.js +0 -6
- package/.next/standalone/.next/static/chunks/3wq9x0c-7ld5d.css +0 -2
- package/src/hooks/policy-presets.ts +0 -135
- /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → I5kEb9kSgYS63j4iqfVLO}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → I5kEb9kSgYS63j4iqfVLO}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → I5kEb9kSgYS63j4iqfVLO}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetching and installing policy packs.
|
|
3
|
+
*
|
|
4
|
+
* ## The trust this does and does not give you
|
|
5
|
+
*
|
|
6
|
+
* A pack is verified against a `SHA256SUMS` published in the same release, so a
|
|
7
|
+
* truncated, cached or substituted download is caught. That manifest is NOT a
|
|
8
|
+
* signature: whoever controls the release controls both files, so this proves
|
|
9
|
+
* nothing about the publisher's identity. What it does buy is real and worth
|
|
10
|
+
* being precise about — the digest is RECORDED in `installed.json` at add time,
|
|
11
|
+
* and `pack-manifest.ts` re-verifies it immediately before every import. So a
|
|
12
|
+
* pack cannot change under a machine after it was installed, and a repository
|
|
13
|
+
* that retags or force-pushes an asset stops loading rather than silently
|
|
14
|
+
* running something else. Signing was considered and deliberately deferred.
|
|
15
|
+
*
|
|
16
|
+
* ## Never discovery
|
|
17
|
+
*
|
|
18
|
+
* Asset URLs are CONSTRUCTED from an owner, repo and concrete tag. When the
|
|
19
|
+
* user omits a tag, one `releases/latest` redirect resolves it before any asset
|
|
20
|
+
* is fetched or written. There is no API lookup or rate-limit dependency, and
|
|
21
|
+
* the installed record always names the pinned result — which has to AGREE with
|
|
22
|
+
* the version that pack's manifest declares, or nothing is installed at all
|
|
23
|
+
* (`packTagMatchesVersion`).
|
|
24
|
+
*
|
|
25
|
+
* ## Never on the hook path
|
|
26
|
+
*
|
|
27
|
+
* Everything here touches the network. A hook must never wait on it. This module
|
|
28
|
+
* is reachable only from the `pack` CLI command.
|
|
29
|
+
*/
|
|
30
|
+
import { createHash } from "node:crypto";
|
|
31
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
32
|
+
import { dirname, resolve } from "node:path";
|
|
33
|
+
import { packsRoot, parsePackIdentity, parsePackPolicy, readInstalledPacks } from "./pack-manifest";
|
|
34
|
+
import type { ResolvedPack } from "./pack-manifest";
|
|
35
|
+
import type { InstalledPackRecord } from "./pack-manifest";
|
|
36
|
+
import type { PolicyCatalogEntry } from "./policy-types";
|
|
37
|
+
import type { PolicyEffect } from "./cloud-managed-policies";
|
|
38
|
+
|
|
39
|
+
const DEFAULT_BASE_URL = "https://github.com";
|
|
40
|
+
const FETCH_TIMEOUT_MS = 30_000;
|
|
41
|
+
/** Cap on a pack artifact. A policy file is kilobytes; this is a sanity bound. */
|
|
42
|
+
const MAX_ARTIFACT_BYTES = 8 * 1024 * 1024;
|
|
43
|
+
|
|
44
|
+
/** Asset names a publisher must use. Fixed, so the URL stays constructible. */
|
|
45
|
+
export const PACK_MANIFEST_ASSET = "failproofai-pack.json";
|
|
46
|
+
export const PACK_ENTRY_ASSET = "failproofai-pack.mjs";
|
|
47
|
+
export const PACK_CHECKSUMS_ASSET = "SHA256SUMS";
|
|
48
|
+
|
|
49
|
+
export interface PackSpec {
|
|
50
|
+
owner: string;
|
|
51
|
+
repo: string;
|
|
52
|
+
/** null when the user did not name one — resolved to a concrete tag at add time. */
|
|
53
|
+
tag: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A spec whose tag is known. Everything that builds a URL requires this. */
|
|
57
|
+
export interface PinnedPackSpec extends PackSpec {
|
|
58
|
+
tag: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface AddPackResult {
|
|
62
|
+
id: string;
|
|
63
|
+
version: string;
|
|
64
|
+
source: string;
|
|
65
|
+
/** Ids this install absorbed — the same artifact, previously under another
|
|
66
|
+
* name. Reported so a rename is never a silent deletion of someone's pack. */
|
|
67
|
+
replaced?: string[];
|
|
68
|
+
/** The concrete tag installed, whether typed or resolved. */
|
|
69
|
+
tag: string;
|
|
70
|
+
/**
|
|
71
|
+
* True when the user named no tag and this was the newest release at that
|
|
72
|
+
* moment. Surfaced so the pin is VISIBLE — re-running the same command later
|
|
73
|
+
* can install something different, and that should not be a silent surprise.
|
|
74
|
+
*/
|
|
75
|
+
resolvedFromLatest: boolean;
|
|
76
|
+
/** Names actually registered — the whole pack, or the selected subset. */
|
|
77
|
+
enabled: string[];
|
|
78
|
+
/** Every policy the pack contains, whether taken or not. */
|
|
79
|
+
available: string[];
|
|
80
|
+
/** How the selection was arrived at, so the CLI can explain itself. */
|
|
81
|
+
selection: SelectionReason;
|
|
82
|
+
/** Category slugs the pack offers, for `--category`. */
|
|
83
|
+
categories: string[];
|
|
84
|
+
artifact: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function baseUrl(): string {
|
|
88
|
+
return (process.env.FAILPROOFAI_PACK_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const OWNER_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
92
|
+
const TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parse a pack source into owner, repo and an optional tag.
|
|
96
|
+
*
|
|
97
|
+
* Accepts what a person actually has to hand: the shorthand, and the URL they
|
|
98
|
+
* copied out of the browser.
|
|
99
|
+
*
|
|
100
|
+
* github:acme/finance@v1.2.0
|
|
101
|
+
* acme/finance@v1.2.0
|
|
102
|
+
* acme/finance (tag resolved at add time)
|
|
103
|
+
* https://github.com/acme/finance/releases/tag/v1.2.0
|
|
104
|
+
* https://github.com/acme/finance/releases/download/v1.2.0/failproofai-pack.mjs
|
|
105
|
+
* https://github.com/acme/finance
|
|
106
|
+
*
|
|
107
|
+
* A missing tag is resolved to a CONCRETE one before anything is written, and
|
|
108
|
+
* that concrete tag is what `installed.json` records. So "no moving sources"
|
|
109
|
+
* still holds where it matters: what a machine reinstalls is pinned, even when
|
|
110
|
+
* the person who typed it did not know the version.
|
|
111
|
+
*/
|
|
112
|
+
export function parsePackSpec(source: string): PackSpec {
|
|
113
|
+
const raw = source.trim();
|
|
114
|
+
if (raw.length === 0) throw new Error("pack source is empty");
|
|
115
|
+
|
|
116
|
+
const url = raw.match(/^(?:https?:\/\/)?(?:www\.)?github\.com\/(.+)$/i);
|
|
117
|
+
if (url) {
|
|
118
|
+
const parts = url[1].replace(/\/+$/, "").split("/");
|
|
119
|
+
const [owner, repo, ...rest] = parts;
|
|
120
|
+
// `/releases/tag/<tag>` and `/releases/download/<tag>/<asset>` both carry the
|
|
121
|
+
// tag in the slot after their keyword; a tag may itself contain slashes
|
|
122
|
+
// (`release/2.1`), so take everything up to the asset rather than one segment.
|
|
123
|
+
let tag: string | null = null;
|
|
124
|
+
if (rest[0] === "releases" && (rest[1] === "tag" || rest[1] === "download")) {
|
|
125
|
+
const tail = rest.slice(2);
|
|
126
|
+
tag = (rest[1] === "download" ? tail.slice(0, -1) : tail).join("/") || null;
|
|
127
|
+
} else if (rest[0] === "releases" && rest[1] === "latest") {
|
|
128
|
+
tag = null;
|
|
129
|
+
}
|
|
130
|
+
return validated(owner, repo, tag);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const withoutScheme = raw.replace(/^github:/i, "");
|
|
134
|
+
const at = withoutScheme.lastIndexOf("@");
|
|
135
|
+
const repoPart = at > 0 ? withoutScheme.slice(0, at) : withoutScheme;
|
|
136
|
+
const tag = at > 0 ? withoutScheme.slice(at + 1) : null;
|
|
137
|
+
const slash = repoPart.indexOf("/");
|
|
138
|
+
if (slash <= 0 || slash === repoPart.length - 1) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`pack source must be owner/repo, owner/repo@tag, or a github.com URL — got ${JSON.stringify(source)}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return validated(repoPart.slice(0, slash), repoPart.slice(slash + 1), tag);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validated(owner: string, repo: string, tag: string | null): PackSpec {
|
|
147
|
+
if (!owner || !OWNER_RE.test(owner)) throw new Error(`unsafe owner ${JSON.stringify(owner)}`);
|
|
148
|
+
if (!repo || !OWNER_RE.test(repo)) throw new Error(`unsafe repo ${JSON.stringify(repo)}`);
|
|
149
|
+
if (tag !== null && !TAG_RE.test(tag)) throw new Error(`unsafe tag ${JSON.stringify(tag)}`);
|
|
150
|
+
return { owner, repo, tag };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The concrete tag of a repository's newest release, read from the redirect
|
|
155
|
+
* `releases/latest` issues rather than from the API.
|
|
156
|
+
*
|
|
157
|
+
* Deliberately not `api.github.com`: the redirect is on the host we already
|
|
158
|
+
* fetch the assets from, so it needs no second origin, carries no 60-per-hour
|
|
159
|
+
* unauthenticated rate limit, and stays pointed at whatever
|
|
160
|
+
* `FAILPROOFAI_PACK_BASE_URL` names — which is how a mirror, and the tests,
|
|
161
|
+
* work at all.
|
|
162
|
+
*
|
|
163
|
+
* The price of that, and it is a real one: the redirect is issued only for a
|
|
164
|
+
* PUBLISHED, non-prerelease release. A repository whose newest release is a
|
|
165
|
+
* prerelease or a draft therefore either redirects to an OLDER stable tag —
|
|
166
|
+
* pinning something the publisher has already superseded — or issues no
|
|
167
|
+
* redirect at all. Only the second is detectable from here, so the error names
|
|
168
|
+
* the prerelease case explicitly; the first is why naming a tag is the answer
|
|
169
|
+
* whenever a publisher's newest release is not their newest stable one.
|
|
170
|
+
*/
|
|
171
|
+
export async function resolveLatestTag(spec: PackSpec): Promise<string> {
|
|
172
|
+
const url = `${baseUrl()}/${spec.owner}/${spec.repo}/releases/latest`;
|
|
173
|
+
const response = await fetch(url, {
|
|
174
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
175
|
+
redirect: "manual",
|
|
176
|
+
});
|
|
177
|
+
const location = response.headers.get("location");
|
|
178
|
+
if (!location) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`could not resolve the newest release of ${spec.owner}/${spec.repo} ` +
|
|
181
|
+
`(GET ${url} returned ${response.status} with no redirect). That redirect is issued only ` +
|
|
182
|
+
`for a published, non-prerelease release, so either the repository has no releases at all ` +
|
|
183
|
+
`or every release it has is a prerelease or a draft. Name a tag explicitly.`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const match = location.match(/\/releases\/tag\/(.+)$/);
|
|
187
|
+
if (!match) throw new Error(`unexpected redirect from ${url}: ${location}`);
|
|
188
|
+
const tag = decodeURIComponent(match[1]).replace(/\/+$/, "");
|
|
189
|
+
if (!TAG_RE.test(tag)) throw new Error(`unsafe tag ${JSON.stringify(tag)} from ${url}`);
|
|
190
|
+
return tag;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The canonical spelling recorded in `installed.json`.
|
|
195
|
+
*
|
|
196
|
+
* Always carries the CONCRETE tag, never the tagless form the user may have
|
|
197
|
+
* typed — otherwise re-adding a pack later would resolve to a different release
|
|
198
|
+
* and the recorded source would not describe what is installed.
|
|
199
|
+
*/
|
|
200
|
+
export function formatPackSpec(spec: PinnedPackSpec): string {
|
|
201
|
+
return `github:${spec.owner}/${spec.repo}@${spec.tag}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function packAssetUrl(spec: PinnedPackSpec, asset: string): string {
|
|
205
|
+
return `${baseUrl()}/${spec.owner}/${spec.repo}/releases/download/${spec.tag}/${asset}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Does a release tag describe the version the pack's own manifest declares?
|
|
210
|
+
*
|
|
211
|
+
* These are two independently-typed strings that everything downstream treats as
|
|
212
|
+
* one fact: the TAG builds every asset URL and is what `installed.json` records
|
|
213
|
+
* as the source, while the VERSION is read out of the manifest and is what
|
|
214
|
+
* `pack list` prints and `pack:<id>@<version>:<name>` keys are built from. Left
|
|
215
|
+
* uncompared, a pack tagged `v2` whose manifest still said `1.0.0` installed
|
|
216
|
+
* cleanly and recorded a version no release of that repository answers to.
|
|
217
|
+
*
|
|
218
|
+
* A leading `v` is accepted because both spellings are in live use for the same
|
|
219
|
+
* release — `pack build` tells publishers to tag `<version>` while this repo's
|
|
220
|
+
* own releases are tagged `v<version>` — and refusing that would break packs
|
|
221
|
+
* that are perfectly coherent.
|
|
222
|
+
*
|
|
223
|
+
* Compared on the tag's LAST `/`-delimited segment, so the monorepo shape
|
|
224
|
+
* `parsePackSpec` deliberately supports (`release/2.1`) still installs:
|
|
225
|
+
* `PACK_VERSION_RE` forbids `/`, so such a tag could never equal a version and a
|
|
226
|
+
* whole-string comparison would make the shape uninstallable rather than merely
|
|
227
|
+
* unusual. Everything else is a genuine disagreement — the release was tagged
|
|
228
|
+
* without rebuilding the manifest, or the wrong tag was typed.
|
|
229
|
+
*/
|
|
230
|
+
export function packTagMatchesVersion(tag: string, version: string): boolean {
|
|
231
|
+
const tail = tag.slice(tag.lastIndexOf("/") + 1);
|
|
232
|
+
return tail === version || tail === `v${version}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function fetchBytes(url: string): Promise<Buffer> {
|
|
236
|
+
const response = await fetch(url, {
|
|
237
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
238
|
+
redirect: "follow",
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok) throw new Error(`GET ${url} returned ${response.status}`);
|
|
241
|
+
const contentLength = response.headers.get("content-length");
|
|
242
|
+
if (contentLength !== null) {
|
|
243
|
+
const declared = Number(contentLength);
|
|
244
|
+
if (Number.isFinite(declared) && declared > MAX_ARTIFACT_BYTES) {
|
|
245
|
+
await response.body?.cancel();
|
|
246
|
+
throw new Error(`${url} declares ${declared} bytes, over the ${MAX_ARTIFACT_BYTES} limit`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (!response.body) return Buffer.alloc(0);
|
|
250
|
+
|
|
251
|
+
const chunks: Buffer[] = [];
|
|
252
|
+
const reader = response.body.getReader();
|
|
253
|
+
let total = 0;
|
|
254
|
+
try {
|
|
255
|
+
while (true) {
|
|
256
|
+
const { done, value } = await reader.read();
|
|
257
|
+
if (done) break;
|
|
258
|
+
total += value.byteLength;
|
|
259
|
+
if (total > MAX_ARTIFACT_BYTES) {
|
|
260
|
+
await reader.cancel();
|
|
261
|
+
throw new Error(`${url} is over the ${MAX_ARTIFACT_BYTES} byte limit`);
|
|
262
|
+
}
|
|
263
|
+
chunks.push(Buffer.from(value));
|
|
264
|
+
}
|
|
265
|
+
} finally {
|
|
266
|
+
reader.releaseLock();
|
|
267
|
+
}
|
|
268
|
+
return Buffer.concat(chunks, total);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The digest a `sha256sum`-format manifest records for one asset.
|
|
273
|
+
*
|
|
274
|
+
* A missing line is a refusal, never a skipped check — the manifest is the only
|
|
275
|
+
* thing between a substituted download and a file this process is about to
|
|
276
|
+
* install as policy.
|
|
277
|
+
*/
|
|
278
|
+
export function digestFor(manifest: string, assetName: string): string | null {
|
|
279
|
+
for (const line of manifest.split("\n")) {
|
|
280
|
+
const match = line.trim().match(/^([0-9a-f]{64})\s+\*?(.+)$/i);
|
|
281
|
+
if (match && match[2].trim() === assetName) return match[1].toLowerCase();
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function sha256(bytes: Buffer): string {
|
|
287
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function writeAtomic(target: string, bytes: Buffer | string): void {
|
|
291
|
+
const tempPath = `${target}.${process.pid}.tmp`;
|
|
292
|
+
try {
|
|
293
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
294
|
+
writeFileSync(tempPath, bytes);
|
|
295
|
+
renameSync(tempPath, target);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
rmSync(tempPath, { force: true });
|
|
298
|
+
throw err;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
interface FetchedPack {
|
|
303
|
+
id: string;
|
|
304
|
+
version: string;
|
|
305
|
+
policies: PolicyCatalogEntry[];
|
|
306
|
+
effect?: PolicyEffect;
|
|
307
|
+
artifact: Buffer;
|
|
308
|
+
artifactDigest: string;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Fetch and fully validate a pack, without writing anything. */
|
|
312
|
+
/**
|
|
313
|
+
* The short names for the pack Failproof AI publishes.
|
|
314
|
+
*
|
|
315
|
+
* Lives here, not in the CLI, because the dashboard takes the same typed source
|
|
316
|
+
* and has to resolve it the same way — `core` worked in the terminal and failed
|
|
317
|
+
* in the browser for exactly as long as this list sat in `pack-cli.ts`.
|
|
318
|
+
*/
|
|
319
|
+
/**
|
|
320
|
+
* Where our own policies actually live.
|
|
321
|
+
*
|
|
322
|
+
* They are a GitHub release like anybody else's, and the package carries no
|
|
323
|
+
* copy of them. That is the point: a pack that ships inside the binary is a
|
|
324
|
+
* policy set we chose for you and put on your disk before you asked, and it
|
|
325
|
+
* gives our own policies a delivery path nobody else's pack can use. One lane,
|
|
326
|
+
* the same for everyone — ours are just the pack whose short name we spell.
|
|
327
|
+
*/
|
|
328
|
+
export const CORE_SOURCE = "FailproofAI/policies";
|
|
329
|
+
export const CORE_ALIASES = new Set(["core", "failproofai", "official"]);
|
|
330
|
+
|
|
331
|
+
export interface PackAddOptions {
|
|
332
|
+
only?: string[];
|
|
333
|
+
categories?: string[];
|
|
334
|
+
all?: boolean;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface AddedPack {
|
|
338
|
+
id: string;
|
|
339
|
+
version: string;
|
|
340
|
+
enabled: string[];
|
|
341
|
+
available: string[];
|
|
342
|
+
/** True when it came from the copy inside this package rather than a release. */
|
|
343
|
+
bundled: boolean;
|
|
344
|
+
source?: string;
|
|
345
|
+
tag?: string;
|
|
346
|
+
resolvedFromLatest?: boolean;
|
|
347
|
+
selection?: string;
|
|
348
|
+
categories?: string[];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Install a pack from whatever a person typed — the one entry point for both the
|
|
353
|
+
* CLI and the dashboard, so a source that works in one cannot fail in the other.
|
|
354
|
+
*/
|
|
355
|
+
export async function addPackFromSource(
|
|
356
|
+
source: string,
|
|
357
|
+
opts: PackAddOptions = {},
|
|
358
|
+
): Promise<AddedPack> {
|
|
359
|
+
// `core` is a spelling of CORE_SOURCE, not a second delivery path. Resolved
|
|
360
|
+
// HERE rather than in the CLI so the dashboard and the CLI cannot disagree
|
|
361
|
+
// about what the short name means — they did once, and a name the CLI could
|
|
362
|
+
// resolve failed in the browser.
|
|
363
|
+
const resolved = CORE_ALIASES.has(source.trim().toLowerCase()) ? CORE_SOURCE : source;
|
|
364
|
+
const result = await addPack(resolved, opts);
|
|
365
|
+
return {
|
|
366
|
+
id: result.id,
|
|
367
|
+
version: result.version,
|
|
368
|
+
enabled: result.enabled,
|
|
369
|
+
available: result.available,
|
|
370
|
+
bundled: false,
|
|
371
|
+
source: result.source,
|
|
372
|
+
tag: result.tag,
|
|
373
|
+
resolvedFromLatest: result.resolvedFromLatest,
|
|
374
|
+
selection: result.selection,
|
|
375
|
+
categories: result.categories,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export interface PackPreview {
|
|
380
|
+
id: string;
|
|
381
|
+
version: string;
|
|
382
|
+
effect: PolicyEffect;
|
|
383
|
+
policies: PolicyCatalogEntry[];
|
|
384
|
+
/** The exact source the preview was read from, tag resolved and pinned. */
|
|
385
|
+
source: string;
|
|
386
|
+
/** True when the tag was resolved rather than typed. */
|
|
387
|
+
resolvedFromLatest: boolean;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Read what a pack CONTAINS, without installing it and without running it.
|
|
392
|
+
*
|
|
393
|
+
* The only way to see a stranger's policies was to install them, which is the
|
|
394
|
+
* wrong order: deciding whether to trust a pack should not require already
|
|
395
|
+
* having trusted it. This fetches the MANIFEST and nothing else — the entry
|
|
396
|
+
* artifact is never downloaded and never imported, so a preview cannot execute
|
|
397
|
+
* a line of somebody else's code.
|
|
398
|
+
*
|
|
399
|
+
* The manifest is still verified against the release's own SHA256SUMS and parsed
|
|
400
|
+
* with the loader's rules, so what is shown is what would install — and a pack
|
|
401
|
+
* that could never load says so here rather than after the download.
|
|
402
|
+
*/
|
|
403
|
+
export async function fetchPackPreview(source: string): Promise<PackPreview> {
|
|
404
|
+
if (process.env.FAILPROOFAI_NO_DOWNLOAD === "1") {
|
|
405
|
+
throw new Error("FAILPROOFAI_NO_DOWNLOAD is set, so nothing can be fetched to preview");
|
|
406
|
+
}
|
|
407
|
+
const parsed = parsePackSpec(source);
|
|
408
|
+
const resolvedFromLatest = parsed.tag === null;
|
|
409
|
+
const spec: PinnedPackSpec =
|
|
410
|
+
parsed.tag !== null
|
|
411
|
+
? { ...parsed, tag: parsed.tag }
|
|
412
|
+
: { ...parsed, tag: await resolveLatestTag(parsed) };
|
|
413
|
+
|
|
414
|
+
const checksums = (await fetchBytes(packAssetUrl(spec, PACK_CHECKSUMS_ASSET))).toString("utf8");
|
|
415
|
+
const manifestBytes = await fetchBytes(packAssetUrl(spec, PACK_MANIFEST_ASSET));
|
|
416
|
+
const manifestDigest = digestFor(checksums, PACK_MANIFEST_ASSET);
|
|
417
|
+
if (!manifestDigest) {
|
|
418
|
+
throw new Error(`${PACK_CHECKSUMS_ASSET} has no entry for ${PACK_MANIFEST_ASSET}`);
|
|
419
|
+
}
|
|
420
|
+
if (sha256(manifestBytes) !== manifestDigest) {
|
|
421
|
+
throw new Error(`${PACK_MANIFEST_ASSET} failed integrity verification`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
let raw: unknown;
|
|
425
|
+
try {
|
|
426
|
+
raw = JSON.parse(manifestBytes.toString("utf8"));
|
|
427
|
+
} catch (err) {
|
|
428
|
+
throw new Error(`${PACK_MANIFEST_ASSET} is not valid JSON: ${errText(err)}`);
|
|
429
|
+
}
|
|
430
|
+
if (!raw || typeof raw !== "object") throw new Error(`${PACK_MANIFEST_ASSET} is not an object`);
|
|
431
|
+
const value = raw as { id?: unknown; version?: unknown; policies?: unknown; effect?: unknown };
|
|
432
|
+
const identity = parsePackIdentity(value);
|
|
433
|
+
if (!Array.isArray(value.policies) || value.policies.length === 0) {
|
|
434
|
+
throw new Error("pack manifest declares no policies");
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
id: identity.id,
|
|
438
|
+
version: identity.version,
|
|
439
|
+
effect: identity.effect,
|
|
440
|
+
policies: value.policies.map((policy, i) => parsePackPolicy(identity.id, policy, i)),
|
|
441
|
+
source: formatPackSpec(spec),
|
|
442
|
+
resolvedFromLatest,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The record this install is really an update OF — matched by id, or failing
|
|
448
|
+
* that by artifact digest.
|
|
449
|
+
*
|
|
450
|
+
* The digest half matters because an id can be renamed under a user: the
|
|
451
|
+
* published release calls this set `failproofai/builtins` and the vendored copy
|
|
452
|
+
* calls it `failproofai/core`. Looking up by id alone found nothing, so the
|
|
453
|
+
* install fell back to the pack's DEFAULTS and quietly discarded whatever the
|
|
454
|
+
* user had chosen — while reporting that their selection was kept.
|
|
455
|
+
*/
|
|
456
|
+
function priorRecordFor(id: string, sha256: string): ResolvedPack | undefined {
|
|
457
|
+
const installed = readInstalledPacks().packs;
|
|
458
|
+
return installed.find((p) => p.id === id) ?? installed.find((p) => p.sha256 === sha256);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function fetchPack(spec: PinnedPackSpec): Promise<FetchedPack> {
|
|
462
|
+
const checksums = (await fetchBytes(packAssetUrl(spec, PACK_CHECKSUMS_ASSET))).toString("utf8");
|
|
463
|
+
|
|
464
|
+
const manifestBytes = await fetchBytes(packAssetUrl(spec, PACK_MANIFEST_ASSET));
|
|
465
|
+
const manifestDigest = digestFor(checksums, PACK_MANIFEST_ASSET);
|
|
466
|
+
if (!manifestDigest) throw new Error(`${PACK_CHECKSUMS_ASSET} has no entry for ${PACK_MANIFEST_ASSET}`);
|
|
467
|
+
if (sha256(manifestBytes) !== manifestDigest) {
|
|
468
|
+
throw new Error(`${PACK_MANIFEST_ASSET} failed integrity verification`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const artifact = await fetchBytes(packAssetUrl(spec, PACK_ENTRY_ASSET));
|
|
472
|
+
const entryDigest = digestFor(checksums, PACK_ENTRY_ASSET);
|
|
473
|
+
if (!entryDigest) throw new Error(`${PACK_CHECKSUMS_ASSET} has no entry for ${PACK_ENTRY_ASSET}`);
|
|
474
|
+
const artifactDigest = sha256(artifact);
|
|
475
|
+
if (artifactDigest !== entryDigest) {
|
|
476
|
+
throw new Error(`${PACK_ENTRY_ASSET} failed integrity verification`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
let parsed: unknown;
|
|
480
|
+
try {
|
|
481
|
+
parsed = JSON.parse(manifestBytes.toString("utf8"));
|
|
482
|
+
} catch (err) {
|
|
483
|
+
throw new Error(`${PACK_MANIFEST_ASSET} is not valid JSON: ${errText(err)}`);
|
|
484
|
+
}
|
|
485
|
+
if (!parsed || typeof parsed !== "object") throw new Error(`${PACK_MANIFEST_ASSET} is not an object`);
|
|
486
|
+
const raw = parsed as { id?: unknown; version?: unknown; policies?: unknown; effect?: unknown };
|
|
487
|
+
const identity = parsePackIdentity(raw);
|
|
488
|
+
if (!Array.isArray(raw.policies) || raw.policies.length === 0) {
|
|
489
|
+
throw new Error("pack manifest declares no policies");
|
|
490
|
+
}
|
|
491
|
+
// Validated with the SAME rules the loader applies, so a pack that could never
|
|
492
|
+
// load is refused here — while nothing has been written — rather than
|
|
493
|
+
// installing cleanly and failing silently on the next tool call.
|
|
494
|
+
const policies = raw.policies.map((p, i) => parsePackPolicy(identity.id, p, i));
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
id: identity.id,
|
|
498
|
+
version: identity.version,
|
|
499
|
+
policies,
|
|
500
|
+
...(raw.effect !== undefined ? { effect: identity.effect } : {}),
|
|
501
|
+
artifact,
|
|
502
|
+
artifactDigest,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Fetch, verify and install a pack, then activate it.
|
|
508
|
+
*
|
|
509
|
+
* `only` takes a subset of the pack's policies. Omitted, the whole pack is
|
|
510
|
+
* taken — and re-adding a pack that already had a selection CARRIES IT FORWARD,
|
|
511
|
+
* because upgrading a pack should not quietly switch on the policies someone
|
|
512
|
+
* chose to leave off.
|
|
513
|
+
*/
|
|
514
|
+
export function slugifyCategory(category: string): string {
|
|
515
|
+
return category.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Which of a pack's policies to take.
|
|
520
|
+
*
|
|
521
|
+
* The default is the pack's OWN `defaultEnabled` set, not everything. A pack
|
|
522
|
+
* carries an opinion about which of its policies are safe to switch on
|
|
523
|
+
* unattended — for the builtins that is 10 of 38 — and installing the whole
|
|
524
|
+
* thing overrode that opinion with one nobody held. `block-kubectl`,
|
|
525
|
+
* `block-terraform` and `require-ci-green-before-stop` are off by default in the
|
|
526
|
+
* npm package precisely because they interrupt legitimate work.
|
|
527
|
+
*/
|
|
528
|
+
function resolveSelection(
|
|
529
|
+
policies: PolicyCatalogEntry[],
|
|
530
|
+
opts: { only?: string[]; categories?: string[]; all?: boolean } | undefined,
|
|
531
|
+
previous: string[] | null | undefined,
|
|
532
|
+
previouslyInstalled: boolean,
|
|
533
|
+
): { enabled: string[] | null; reason: SelectionReason } {
|
|
534
|
+
const available = policies.map((p) => p.name);
|
|
535
|
+
if (opts?.all) return { enabled: null, reason: "all" };
|
|
536
|
+
|
|
537
|
+
const picked = new Set<string>();
|
|
538
|
+
if (opts?.categories?.length) {
|
|
539
|
+
const wanted = new Set(opts.categories.map(slugifyCategory));
|
|
540
|
+
const known = new Map(policies.map((p) => [slugifyCategory(p.category), p.category]));
|
|
541
|
+
const unknown = [...wanted].filter((c) => !known.has(c));
|
|
542
|
+
if (unknown.length > 0) {
|
|
543
|
+
throw new Error(
|
|
544
|
+
`no such categor${unknown.length === 1 ? "y" : "ies"}: ${unknown.join(", ")} ` +
|
|
545
|
+
`(has: ${[...new Set(policies.map((p) => slugifyCategory(p.category)))].join(", ")})`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
for (const p of policies) if (wanted.has(slugifyCategory(p.category))) picked.add(p.name);
|
|
549
|
+
}
|
|
550
|
+
if (opts?.only?.length) {
|
|
551
|
+
const unknown = opts.only.filter((n) => !available.includes(n));
|
|
552
|
+
if (unknown.length > 0) {
|
|
553
|
+
throw new Error(`pack does not contain ${unknown.join(", ")}`);
|
|
554
|
+
}
|
|
555
|
+
for (const n of opts.only) picked.add(n);
|
|
556
|
+
}
|
|
557
|
+
if (picked.size > 0) {
|
|
558
|
+
// Kept in the pack's declared order, which is the order everything else
|
|
559
|
+
// presents them in.
|
|
560
|
+
return { enabled: available.filter((n) => picked.has(n)), reason: "selected" };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// No flags. An upgrade keeps whatever the machine already had — switching a
|
|
564
|
+
// policy back on because the user did not repeat themselves is not an upgrade.
|
|
565
|
+
if (previouslyInstalled) {
|
|
566
|
+
return { enabled: previous === null ? null : (previous ?? null), reason: "carried" };
|
|
567
|
+
}
|
|
568
|
+
return { enabled: policies.filter((p) => p.defaultEnabled).map((p) => p.name), reason: "defaults" };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export type SelectionReason = "all" | "selected" | "carried" | "defaults";
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Import the artifact and check it registers what the manifest promised.
|
|
575
|
+
*
|
|
576
|
+
* `pack add` used to verify digests and manifest SHAPE and then install without
|
|
577
|
+
* ever executing the file. Two failures got through, both reproduced: an
|
|
578
|
+
* artifact that does not parse installed at exit 0 and bricked every later tool
|
|
579
|
+
* call, and a one-name typo between manifest and artifact — the exact slip a
|
|
580
|
+
* publisher hand-maintaining two files makes — installed reporting "2/2
|
|
581
|
+
* enabled" and then converted into a MACHINE-WIDE DENY, because a declared
|
|
582
|
+
* policy that never registers is precisely what the fail-closed guard denies
|
|
583
|
+
* for.
|
|
584
|
+
*
|
|
585
|
+
* The right place to catch that is here, where nothing has been activated yet
|
|
586
|
+
* and the publisher's own mistake can be named. This runs only on the CLI path;
|
|
587
|
+
* the hook path never imports anything it has not already recorded.
|
|
588
|
+
*/
|
|
589
|
+
async function verifyArtifactRegisters(
|
|
590
|
+
artifactPath: string,
|
|
591
|
+
declared: readonly string[],
|
|
592
|
+
): Promise<void> {
|
|
593
|
+
const { loadCustomHooks } = await import("./custom-hooks-loader");
|
|
594
|
+
let registered: string[];
|
|
595
|
+
try {
|
|
596
|
+
registered = (await loadCustomHooks(artifactPath, { strict: true })).map((h) => h.name);
|
|
597
|
+
} catch (err) {
|
|
598
|
+
throw new Error(
|
|
599
|
+
`its artifact could not be loaded: ${err instanceof Error ? err.message : String(err)}`,
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
const missing = declared.filter((name) => !registered.includes(name));
|
|
603
|
+
const extra = registered.filter((name) => !declared.includes(name));
|
|
604
|
+
if (missing.length > 0) {
|
|
605
|
+
throw new Error(
|
|
606
|
+
`its manifest declares ${missing.join(", ")} but the artifact does not register ` +
|
|
607
|
+
`${missing.length === 1 ? "it" : "them"} — that would install as protection that never runs`,
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
if (extra.length > 0) {
|
|
611
|
+
throw new Error(
|
|
612
|
+
`its artifact registers undeclared ${extra.join(", ")} — a policy that enforces but ` +
|
|
613
|
+
`appears in no listing cannot be reviewed or switched off`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* The same check, as a question rather than a refusal — for `pack list`, which
|
|
620
|
+
* reported a pack whose artifact does not load as fully healthy, on the very
|
|
621
|
+
* command the fail-closed deny message tells the human to run.
|
|
622
|
+
*/
|
|
623
|
+
export async function checkPackArtifact(
|
|
624
|
+
artifactPath: string,
|
|
625
|
+
declared: readonly string[],
|
|
626
|
+
): Promise<string | null> {
|
|
627
|
+
try {
|
|
628
|
+
await verifyArtifactRegisters(artifactPath, declared);
|
|
629
|
+
return null;
|
|
630
|
+
} catch (err) {
|
|
631
|
+
return err instanceof Error ? err.message : String(err);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export async function addPack(
|
|
636
|
+
source: string,
|
|
637
|
+
opts?: { only?: string[]; categories?: string[]; all?: boolean },
|
|
638
|
+
): Promise<AddPackResult> {
|
|
639
|
+
if (process.env.FAILPROOFAI_NO_DOWNLOAD) {
|
|
640
|
+
throw new Error(
|
|
641
|
+
"pack downloads are disabled (FAILPROOFAI_NO_DOWNLOAD). Already-installed packs keep enforcing.",
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
const parsed = parsePackSpec(source);
|
|
645
|
+
// Resolve BEFORE anything is written, and pin the concrete result. A tagless
|
|
646
|
+
// source is a convenience for the person typing; what the machine records must
|
|
647
|
+
// always name one release.
|
|
648
|
+
const spec: PinnedPackSpec = parsed.tag
|
|
649
|
+
? { ...parsed, tag: parsed.tag }
|
|
650
|
+
: { ...parsed, tag: await resolveLatestTag(parsed) };
|
|
651
|
+
const resolvedFromLatest = parsed.tag === null;
|
|
652
|
+
const fetched = await fetchPack(spec);
|
|
653
|
+
// Checked here, before a byte is written and before the artifact is imported,
|
|
654
|
+
// so a refusal leaves the machine exactly as it was — and on the RESOLVED tag
|
|
655
|
+
// as well as a typed one, because a tagless add pins whatever
|
|
656
|
+
// `releases/latest` pointed at and lands the same incoherent pair in
|
|
657
|
+
// `installed.json` with nobody having typed it. Deliberately NOT in
|
|
658
|
+
// `fetchPackPreview`: looking at a pack has to stay possible precisely so a
|
|
659
|
+
// publisher can SEE the two values disagree.
|
|
660
|
+
if (!packTagMatchesVersion(spec.tag, fetched.version)) {
|
|
661
|
+
const fromTag = spec.tag.slice(spec.tag.lastIndexOf("/") + 1).replace(/^v/, "");
|
|
662
|
+
const fix =
|
|
663
|
+
`re-tag the release ${fetched.version} (a leading v is fine), or rebuild the pack with ` +
|
|
664
|
+
`--version ${fromTag}`;
|
|
665
|
+
throw new Error(
|
|
666
|
+
`${fetched.id} was not installed: release tag ${spec.tag} does not match the version its ` +
|
|
667
|
+
`manifest declares (${fetched.version}). ` +
|
|
668
|
+
(resolvedFromLatest
|
|
669
|
+
? `That is the newest release of ${spec.owner}/${spec.repo}, so the release and the pack ` +
|
|
670
|
+
`disagree at the source — ask the publisher to ${fix}.`
|
|
671
|
+
: `Name the tag whose manifest says ${fetched.version}, or fix the pack: ${fix}.`),
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
const available = fetched.policies.map((p) => p.name);
|
|
675
|
+
|
|
676
|
+
const prior = priorRecordFor(fetched.id, fetched.artifactDigest);
|
|
677
|
+
const { enabled, reason } = resolveSelection(
|
|
678
|
+
fetched.policies,
|
|
679
|
+
opts,
|
|
680
|
+
prior?.enabled ? prior.enabled.filter((n) => available.includes(n)) : prior?.enabled,
|
|
681
|
+
Boolean(prior),
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
const root = packsRoot();
|
|
685
|
+
const artifactRel = `artifacts/${fetched.artifactDigest}.mjs`;
|
|
686
|
+
const artifactAbs = resolve(root, artifactRel);
|
|
687
|
+
// Content-addressed, so a file that already exists has these exact bytes and
|
|
688
|
+
// rewriting it could only ever disturb a pack currently importing it.
|
|
689
|
+
if (!existsSync(artifactAbs)) writeAtomic(artifactAbs, fetched.artifact);
|
|
690
|
+
|
|
691
|
+
// Before `installed.json` — the file that activates it — is written. The
|
|
692
|
+
// artifact on disk is inert until something points at it, so a refusal here
|
|
693
|
+
// leaves the machine exactly as it was.
|
|
694
|
+
try {
|
|
695
|
+
await verifyArtifactRegisters(artifactAbs, available);
|
|
696
|
+
} catch (err) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
`${fetched.id}@${fetched.version} was not installed: ${err instanceof Error ? err.message : String(err)}`,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// An id is self-declared and nothing owns it, while `upsertInstalled` replaces
|
|
703
|
+
// by id — so a pack served from any repository could declare
|
|
704
|
+
// `failproofai/builtins` and silently take the place of the one already
|
|
705
|
+
// installed. Binding the id to the source it first came from is the cheap half
|
|
706
|
+
// of the fix: a second source for the same id has to be an explicit removal.
|
|
707
|
+
// Compared on the REPOSITORY, not the whole source: `github:acme/pack@v1.2.0`
|
|
708
|
+
// and `@v1.3.0` are the same publisher shipping an upgrade, which is the
|
|
709
|
+
// ordinary case this must not break.
|
|
710
|
+
const repoOf = (source: string): string => {
|
|
711
|
+
const at = source.lastIndexOf("@");
|
|
712
|
+
return at > source.indexOf(":") ? source.slice(0, at) : source;
|
|
713
|
+
};
|
|
714
|
+
if (
|
|
715
|
+
prior &&
|
|
716
|
+
!prior.source.startsWith("bundled:") &&
|
|
717
|
+
repoOf(prior.source) !== repoOf(formatPackSpec(spec))
|
|
718
|
+
) {
|
|
719
|
+
throw new Error(
|
|
720
|
+
`pack id ${fetched.id} is already installed from ${prior.source}. ` +
|
|
721
|
+
`Refusing to replace it with ${formatPackSpec(spec)} — remove it first if that is what you mean.`,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const record: InstalledPackRecord = {
|
|
726
|
+
id: fetched.id,
|
|
727
|
+
version: fetched.version,
|
|
728
|
+
source: formatPackSpec(spec),
|
|
729
|
+
entry: artifactRel,
|
|
730
|
+
sha256: fetched.artifactDigest,
|
|
731
|
+
policies: fetched.policies,
|
|
732
|
+
...(fetched.effect ? { effect: fetched.effect } : {}),
|
|
733
|
+
...(enabled ? { enabled } : {}),
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
const absorbed = upsertInstalled(record);
|
|
737
|
+
return {
|
|
738
|
+
replaced: absorbed,
|
|
739
|
+
id: fetched.id,
|
|
740
|
+
version: fetched.version,
|
|
741
|
+
source: record.source,
|
|
742
|
+
tag: spec.tag,
|
|
743
|
+
resolvedFromLatest,
|
|
744
|
+
enabled: enabled ?? available,
|
|
745
|
+
available,
|
|
746
|
+
selection: reason,
|
|
747
|
+
categories: [...new Set(fetched.policies.map((p) => slugifyCategory(p.category)))],
|
|
748
|
+
artifact: artifactAbs,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* The vendored pack directory, when a working tree happens to have one.
|
|
754
|
+
*
|
|
755
|
+
* The published package does NOT carry `policy-pack/` and there is no install
|
|
756
|
+
* path through here. Our policies are fetched from their GitHub release like
|
|
757
|
+
* anybody else's — a pack that ships inside the binary is a policy set chosen
|
|
758
|
+
* for the user and written to their disk before they asked, and it gave our own
|
|
759
|
+
* policies a delivery route no third-party pack could use.
|
|
760
|
+
*
|
|
761
|
+
* The one remaining caller is `audit/replay.ts`, which prefers the pack's own
|
|
762
|
+
* functions when a dev tree has run `build:pack` and falls back to the compiled
|
|
763
|
+
* implementations when it returns null. Both produce identical text, so the two
|
|
764
|
+
* paths score the same; the fallback is the normal case now.
|
|
765
|
+
*
|
|
766
|
+
* Anchored at `FAILPROOFAI_PACKAGE_ROOT` for the reason `npmPlatformBinaryPath`
|
|
767
|
+
* documents: `import.meta.url` does not survive the CJS bundle.
|
|
768
|
+
*/
|
|
769
|
+
export function bundledPackDir(): string | null {
|
|
770
|
+
const root = process.env.FAILPROOFAI_PACKAGE_ROOT;
|
|
771
|
+
if (!root) return null;
|
|
772
|
+
const dir = resolve(root, "policy-pack");
|
|
773
|
+
return existsSync(resolve(dir, PACK_MANIFEST_ASSET)) ? dir : null;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export function setPackPolicyEnabled(
|
|
777
|
+
packId: string,
|
|
778
|
+
name: string,
|
|
779
|
+
on: boolean,
|
|
780
|
+
): { ok: boolean; reason?: string } {
|
|
781
|
+
const manifestPath = resolve(packsRoot(), "installed.json");
|
|
782
|
+
if (!existsSync(manifestPath)) return { ok: false, reason: "no packs are installed" };
|
|
783
|
+
let raw: { schemaVersion?: number; packs?: InstalledPackRecord[] };
|
|
784
|
+
try {
|
|
785
|
+
raw = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
786
|
+
} catch {
|
|
787
|
+
return { ok: false, reason: `pack manifest at ${manifestPath} is unreadable` };
|
|
788
|
+
}
|
|
789
|
+
const packs = Array.isArray(raw.packs) ? raw.packs : [];
|
|
790
|
+
const pack = packs.find((p) => p?.id === packId);
|
|
791
|
+
if (!pack) return { ok: false, reason: `no installed pack with id ${packId}` };
|
|
792
|
+
// `policies` is `unknown` on the record — it is validated by the loader, not
|
|
793
|
+
// here — so read the names defensively rather than trusting the shape.
|
|
794
|
+
const declared = Array.isArray(pack.policies) ? (pack.policies as Array<{ name?: unknown }>) : [];
|
|
795
|
+
const available = declared
|
|
796
|
+
.map((p) => (typeof p?.name === "string" ? p.name : null))
|
|
797
|
+
.filter((n): n is string => n !== null);
|
|
798
|
+
if (!available.includes(name)) {
|
|
799
|
+
return { ok: false, reason: `pack ${packId} declares no policy named ${name}` };
|
|
800
|
+
}
|
|
801
|
+
// `enabled` absent means "the whole pack", which is what `--all` records so a
|
|
802
|
+
// later version's new policies are included. Turning one off has to make that
|
|
803
|
+
// implicit set explicit, or there is nothing to subtract from.
|
|
804
|
+
const current = pack.enabled ?? available;
|
|
805
|
+
const next: string[] = on
|
|
806
|
+
? [...new Set([...current, name])]
|
|
807
|
+
: current.filter((n: string) => n !== name);
|
|
808
|
+
// Kept in the pack's declared order rather than the order things were toggled.
|
|
809
|
+
pack.enabled = available.filter((n) => next.includes(n));
|
|
810
|
+
writeAtomic(manifestPath, JSON.stringify({ schemaVersion: 1, packs }, null, 2) + "\n");
|
|
811
|
+
return { ok: true };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export function removePack(id: string): boolean {
|
|
815
|
+
const manifestPath = resolve(packsRoot(), "installed.json");
|
|
816
|
+
if (!existsSync(manifestPath)) return false;
|
|
817
|
+
let raw: { schemaVersion?: number; packs?: InstalledPackRecord[] };
|
|
818
|
+
try {
|
|
819
|
+
raw = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
820
|
+
} catch {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
const packs = Array.isArray(raw.packs) ? raw.packs : [];
|
|
824
|
+
const remaining = packs.filter((p) => p?.id !== id);
|
|
825
|
+
if (remaining.length === packs.length) return false;
|
|
826
|
+
// The artifact is left on disk deliberately: it is content-addressed and inert
|
|
827
|
+
// once nothing points at it, so keeping it makes a re-add offline-safe. Same
|
|
828
|
+
// choice the cloud reconciler makes.
|
|
829
|
+
writeAtomic(manifestPath, JSON.stringify({ schemaVersion: 1, packs: remaining }, null, 2) + "\n");
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Replace or append one pack in `installed.json`, written LAST and atomically so
|
|
835
|
+
* activation is a single flip: until this rename lands, the artifact on disk is
|
|
836
|
+
* a file nothing points at.
|
|
837
|
+
*/
|
|
838
|
+
/**
|
|
839
|
+
* Install or update one pack record, collapsing anything that is the SAME PACK
|
|
840
|
+
* under a different id.
|
|
841
|
+
*
|
|
842
|
+
* Two ids for one artifact is not hypothetical: the published release calls this
|
|
843
|
+
* set `failproofai/builtins` and the copy vendored in the package calls it
|
|
844
|
+
* `failproofai/core`, so following the help's own two examples installed both.
|
|
845
|
+
* The result claimed "2 packs · 76 policies" over ONE artifact, warned on stderr
|
|
846
|
+
* on every hook event, and left policies showing ON that did not block.
|
|
847
|
+
*
|
|
848
|
+
* Matched on the artifact DIGEST, which is what actually decides whether two
|
|
849
|
+
* records describe the same code. Returns the ids it absorbed so the caller can
|
|
850
|
+
* say so — an earlier version deleted the old row silently, taking the user's
|
|
851
|
+
* selection and the enforcement it gave them with it.
|
|
852
|
+
*/
|
|
853
|
+
function upsertInstalled(record: InstalledPackRecord): string[] {
|
|
854
|
+
const manifestPath = resolve(packsRoot(), "installed.json");
|
|
855
|
+
let packs: InstalledPackRecord[] = [];
|
|
856
|
+
if (existsSync(manifestPath)) {
|
|
857
|
+
try {
|
|
858
|
+
const raw = JSON.parse(readFileSync(manifestPath, "utf8")) as { packs?: InstalledPackRecord[] };
|
|
859
|
+
if (Array.isArray(raw.packs)) packs = raw.packs;
|
|
860
|
+
} catch {
|
|
861
|
+
// A manifest we cannot parse is one we must not append to: merging into
|
|
862
|
+
// it would either drop the packs it held or write them back malformed.
|
|
863
|
+
throw new Error(
|
|
864
|
+
`existing pack manifest at ${manifestPath} is unreadable; move it aside and re-add`,
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
// Anything with these bytes under another name is this pack, renamed.
|
|
869
|
+
const absorbed = packs
|
|
870
|
+
.filter((p) => p?.sha256 === record.sha256 && p?.id !== record.id)
|
|
871
|
+
.map((p) => p.id);
|
|
872
|
+
// The selection the user had under the old id is theirs — carry it, rather
|
|
873
|
+
// than resetting them to defaults because a publisher renamed something.
|
|
874
|
+
if (!record.enabled) {
|
|
875
|
+
const prior = packs.find((p) => absorbed.includes(p?.id));
|
|
876
|
+
const carried = prior?.enabled?.filter((n) =>
|
|
877
|
+
(record.policies as Array<{ name?: unknown }> | undefined)?.some((rp) => rp?.name === n),
|
|
878
|
+
);
|
|
879
|
+
if (carried && carried.length > 0) record.enabled = carried;
|
|
880
|
+
}
|
|
881
|
+
const remaining = packs.filter((p) => !absorbed.includes(p?.id));
|
|
882
|
+
const idx = remaining.findIndex((p) => p?.id === record.id);
|
|
883
|
+
if (idx >= 0) remaining[idx] = record;
|
|
884
|
+
else remaining.push(record);
|
|
885
|
+
writeAtomic(manifestPath, JSON.stringify({ schemaVersion: 1, packs: remaining }, null, 2) + "\n");
|
|
886
|
+
return absorbed;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function errText(err: unknown): string {
|
|
890
|
+
return err instanceof Error ? err.message : String(err);
|
|
891
|
+
}
|