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
package/dist/worker.mjs
CHANGED
|
@@ -370,7 +370,7 @@ function failproofaiHome(home) {
|
|
|
370
370
|
return resolve(home, ".failproofai");
|
|
371
371
|
return process.env.FAILPROOFAI_HOME || resolve(homedir(), ".failproofai");
|
|
372
372
|
}
|
|
373
|
-
var LAYOUT_VERSION = 4, at = (...parts) => resolve(failproofaiHome(), ...parts), atHome = (home, ...parts) => home ? resolve(failproofaiHome(home), ...parts) : at(...parts), versionFile = (home) => atHome(home, "VERSION"), configFile = (home) => atHome(home, "config.json"), policiesDir = (home) => atHome(home, "policies"), customPoliciesDir, globalPolicyConfigFile = (home) => atHome(home, "policies-config.json"), cloudPoliciesDir = (home) => resolve(policiesDir(home), "cloud-policies"), hookActivityDir = (home) => atHome(home, "hook-activity"), stateDir = (home) => atHome(home, "state"), shimsDir = (home) => resolve(stateDir(home), "shims"), sessionPauseDir = () => resolve(stateDir(), "sessions"), telemetryIdFile = (home) => resolve(stateDir(home), "telemetry-id"), codexSessionPathsFile = (home) => resolve(stateDir(home), "codex-session-paths.json"), logsDir = (home) => atHome(home, "logs"), legacy;
|
|
373
|
+
var LAYOUT_VERSION = 4, at = (...parts) => resolve(failproofaiHome(), ...parts), atHome = (home, ...parts) => home ? resolve(failproofaiHome(home), ...parts) : at(...parts), versionFile = (home) => atHome(home, "VERSION"), configFile = (home) => atHome(home, "config.json"), policiesDir = (home) => atHome(home, "policies"), customPoliciesDir, globalPolicyConfigFile = (home) => atHome(home, "policies-config.json"), cloudPoliciesDir = (home) => resolve(policiesDir(home), "cloud-policies"), packsDir = (home) => resolve(policiesDir(home), "packs"), packsInstalledFile = (home) => resolve(packsDir(home), "installed.json"), hookActivityDir = (home) => atHome(home, "hook-activity"), stateDir = (home) => atHome(home, "state"), shimsDir = (home) => resolve(stateDir(home), "shims"), sessionPauseDir = () => resolve(stateDir(), "sessions"), telemetryIdFile = (home) => resolve(stateDir(home), "telemetry-id"), codexSessionPathsFile = (home) => resolve(stateDir(home), "codex-session-paths.json"), logsDir = (home) => atHome(home, "logs"), legacy;
|
|
374
374
|
var init_fp_home = __esm(() => {
|
|
375
375
|
customPoliciesDir = policiesDir;
|
|
376
376
|
legacy = {
|
|
@@ -566,1963 +566,2000 @@ var init_hooks_config = __esm(() => {
|
|
|
566
566
|
init_fp_home();
|
|
567
567
|
});
|
|
568
568
|
|
|
569
|
-
// src/hooks/policy-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
function registerPolicy(name, description, fn, match, priority = 0) {
|
|
598
|
-
const canonical = normalizePolicyName(name);
|
|
599
|
-
const registry = getRegistry();
|
|
600
|
-
const idx = registry.findIndex((p) => p.name === canonical);
|
|
601
|
-
const entry = { name: canonical, description, fn, match, priority };
|
|
602
|
-
if (idx >= 0) {
|
|
603
|
-
registry[idx] = entry;
|
|
604
|
-
} else {
|
|
605
|
-
registry.push(entry);
|
|
606
|
-
}
|
|
607
|
-
setIndexCache(null);
|
|
608
|
-
}
|
|
609
|
-
function getPoliciesForEvent(eventType, toolName) {
|
|
610
|
-
let cache = getIndexCache();
|
|
611
|
-
if (!cache) {
|
|
612
|
-
cache = new Map;
|
|
613
|
-
setIndexCache(cache);
|
|
614
|
-
}
|
|
615
|
-
const key = `${eventType}:${toolName ?? ""}`;
|
|
616
|
-
const cached = cache.get(key);
|
|
617
|
-
if (cached)
|
|
618
|
-
return cached;
|
|
619
|
-
const result = getRegistry().filter((p) => {
|
|
620
|
-
if (p.match.events && p.match.events.length > 0) {
|
|
621
|
-
if (!p.match.events.includes(eventType))
|
|
622
|
-
return false;
|
|
623
|
-
}
|
|
624
|
-
if (p.match.toolNames && p.match.toolNames.length > 0) {
|
|
625
|
-
if (!toolName || !p.match.toolNames.includes(toolName))
|
|
626
|
-
return false;
|
|
627
|
-
}
|
|
628
|
-
return true;
|
|
629
|
-
}).sort((a, b) => b.priority - a.priority);
|
|
630
|
-
cache.set(key, result);
|
|
631
|
-
return result;
|
|
632
|
-
}
|
|
633
|
-
function clearPolicies() {
|
|
634
|
-
const g = globalThis;
|
|
635
|
-
g[REGISTRY_KEY] = [];
|
|
636
|
-
setIndexCache(null);
|
|
637
|
-
}
|
|
638
|
-
var REGISTRY_KEY = "__FAILPROOFAI_POLICY_REGISTRY__", INDEX_CACHE_KEY = "__FAILPROOFAI_POLICY_INDEX_CACHE__", DEFAULT_POLICY_NAMESPACE = "failproofai";
|
|
639
|
-
|
|
640
|
-
// src/hooks/builtin-policies.ts
|
|
641
|
-
import { resolve as resolve3, join as join2 } from "node:path";
|
|
642
|
-
import { statSync as statSync3 } from "node:fs";
|
|
643
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
644
|
-
import { execSync, execFileSync } from "node:child_process";
|
|
645
|
-
import { homedir as homedir3 } from "node:os";
|
|
646
|
-
function isAgentInternalPath(resolved2) {
|
|
647
|
-
const normResolved = resolved2.replaceAll("\\", "/");
|
|
648
|
-
for (const dir of [".claude", ".codex", ".copilot", ".cursor", ".opencode", ".pi", ".gemini"]) {
|
|
649
|
-
const root = join2(homedir3(), dir).replaceAll("\\", "/");
|
|
650
|
-
if (normResolved === root || normResolved.startsWith(root + "/"))
|
|
651
|
-
return true;
|
|
652
|
-
}
|
|
653
|
-
for (const sub of [join2(".config", "opencode"), join2(".local", "share", "opencode")]) {
|
|
654
|
-
const root = join2(homedir3(), sub).replaceAll("\\", "/");
|
|
655
|
-
if (normResolved === root || normResolved.startsWith(root + "/"))
|
|
656
|
-
return true;
|
|
657
|
-
}
|
|
658
|
-
return false;
|
|
659
|
-
}
|
|
660
|
-
function isAgentSettingsFile(resolved2) {
|
|
661
|
-
if (/[\\/]\.claude[\\/]settings(?:\.[^/\\]+)?\.json$/.test(resolved2))
|
|
662
|
-
return true;
|
|
663
|
-
if (/[\\/]\.codex[\\/]hooks\.json$/.test(resolved2))
|
|
664
|
-
return true;
|
|
665
|
-
if (/[\\/]\.copilot[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved2))
|
|
666
|
-
return true;
|
|
667
|
-
if (/[\\/]\.github[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved2))
|
|
668
|
-
return true;
|
|
669
|
-
if (/[\\/]\.cursor[\\/]hooks\.json$/.test(resolved2))
|
|
670
|
-
return true;
|
|
671
|
-
if (/[\\/]\.opencode[\\/]opencode\.jsonc?$/.test(resolved2))
|
|
672
|
-
return true;
|
|
673
|
-
if (/[\\/]\.opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved2))
|
|
674
|
-
return true;
|
|
675
|
-
if (/[\\/]\.config[\\/]opencode[\\/]opencode\.jsonc?$/.test(resolved2))
|
|
676
|
-
return true;
|
|
677
|
-
if (/[\\/]\.config[\\/]opencode[\\/]config\.json$/.test(resolved2))
|
|
678
|
-
return true;
|
|
679
|
-
if (/[\\/]\.config[\\/]opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved2))
|
|
680
|
-
return true;
|
|
681
|
-
if (/[\\/]\.pi[\\/](?:agent[\\/])?settings\.json$/.test(resolved2))
|
|
682
|
-
return true;
|
|
683
|
-
if (/[\\/]\.pi[\\/](?:agent[\\/])?extensions[\\/]/.test(resolved2))
|
|
684
|
-
return true;
|
|
685
|
-
if (/[\\/]\.gemini[\\/]settings\.json$/.test(resolved2))
|
|
686
|
-
return true;
|
|
687
|
-
if (/[\\/]\.gemini[\\/]config[\\/]hooks\.json$/.test(resolved2))
|
|
688
|
-
return true;
|
|
689
|
-
return false;
|
|
690
|
-
}
|
|
691
|
-
function getCommand(ctx) {
|
|
692
|
-
return ctx.toolInput?.command ?? "";
|
|
693
|
-
}
|
|
694
|
-
function getFilePath(ctx) {
|
|
695
|
-
return ctx.toolInput?.file_path ?? "";
|
|
696
|
-
}
|
|
697
|
-
function parseArgvTokens(cmd) {
|
|
698
|
-
return cmd.trim().split(/\s+/).map((t) => t.replace(/^['"]|['"]$/g, ""));
|
|
699
|
-
}
|
|
700
|
-
function namesSelfPause(command) {
|
|
701
|
-
for (const segment of command.split(SEGMENT_SEPARATORS)) {
|
|
702
|
-
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
703
|
-
let i = 0;
|
|
704
|
-
while (i < tokens.length) {
|
|
705
|
-
const token = tokens[i];
|
|
706
|
-
const isSkippable = ENV_ASSIGNMENT_RE.test(token) || token.startsWith("-") || token.startsWith(">") || token.startsWith("<") || RUNNER_OPERAND_RE.test(token) || COMMAND_PREFIX_TOKENS.has(token.slice(token.lastIndexOf("/") + 1));
|
|
707
|
-
if (!isSkippable)
|
|
708
|
-
break;
|
|
709
|
-
i++;
|
|
710
|
-
}
|
|
711
|
-
if (i >= tokens.length)
|
|
712
|
-
continue;
|
|
713
|
-
if (!SELF_BINARY_TOKEN_RE.test(tokens[i]))
|
|
714
|
-
continue;
|
|
715
|
-
const args = tokens.slice(i + 1);
|
|
716
|
-
const configAt = args.findIndex((a) => CONFIG_SUBCOMMAND_RE.test(a));
|
|
717
|
-
if (configAt === -1)
|
|
718
|
-
continue;
|
|
719
|
-
if (args.slice(configAt + 1).some((a) => PAUSE_FLAG_RE.test(a)))
|
|
720
|
-
return true;
|
|
721
|
-
}
|
|
722
|
-
return false;
|
|
723
|
-
}
|
|
724
|
-
function decodeAnsiC(body) {
|
|
725
|
-
return body.replace(/\\(x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{1,4}|U[0-9A-Fa-f]{1,8}|[0-7]{1,3}|.)/g, (_, seq) => {
|
|
726
|
-
try {
|
|
727
|
-
if (seq[0] === "x")
|
|
728
|
-
return String.fromCharCode(parseInt(seq.slice(1), 16));
|
|
729
|
-
if (seq[0] === "u" || seq[0] === "U")
|
|
730
|
-
return String.fromCodePoint(parseInt(seq.slice(1), 16));
|
|
731
|
-
if (/^[0-7]+$/.test(seq))
|
|
732
|
-
return String.fromCharCode(parseInt(seq, 8));
|
|
733
|
-
} catch {
|
|
734
|
-
return seq;
|
|
735
|
-
}
|
|
736
|
-
const named = {
|
|
737
|
-
a: "\x07",
|
|
738
|
-
b: "\b",
|
|
739
|
-
e: "\x1B",
|
|
740
|
-
f: "\f",
|
|
741
|
-
n: `
|
|
742
|
-
`,
|
|
743
|
-
r: "\r",
|
|
744
|
-
t: "\t",
|
|
745
|
-
v: "\v",
|
|
746
|
-
"\\": "\\",
|
|
747
|
-
"'": "'",
|
|
748
|
-
'"': '"',
|
|
749
|
-
"?": "?"
|
|
750
|
-
};
|
|
751
|
-
return named[seq] ?? seq;
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
function stripShellQuoting(command) {
|
|
755
|
-
const joined = command.replace(/\\\r?\n/g, "");
|
|
756
|
-
const ansiDecoded = joined.replace(/\$'((?:[^'\\]|\\.)*)'/g, (_, body) => decodeAnsiC(body));
|
|
757
|
-
return ansiDecoded.replace(/\\(.)/g, "$1").replace(/['"]/g, "");
|
|
758
|
-
}
|
|
759
|
-
function statGitHeadMtimeMs(cwd) {
|
|
760
|
-
try {
|
|
761
|
-
return statSync3(join2(cwd, ".git", "HEAD")).mtimeMs;
|
|
762
|
-
} catch {
|
|
763
|
-
return null;
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
function getCurrentBranch(cwd) {
|
|
767
|
-
try {
|
|
768
|
-
const headMtimeMs = statGitHeadMtimeMs(cwd);
|
|
769
|
-
const cached = gitBranchCache.get(cwd);
|
|
770
|
-
if (cached && headMtimeMs !== null && cached.headMtimeMs === headMtimeMs) {
|
|
771
|
-
return cached.branch || null;
|
|
772
|
-
}
|
|
773
|
-
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
774
|
-
cwd,
|
|
775
|
-
encoding: "utf8",
|
|
776
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
777
|
-
timeout: 3000
|
|
778
|
-
}).trim();
|
|
779
|
-
if (headMtimeMs !== null) {
|
|
780
|
-
if (gitBranchCache.size >= GIT_BRANCH_CACHE_MAX_ENTRIES)
|
|
781
|
-
gitBranchCache.clear();
|
|
782
|
-
gitBranchCache.set(cwd, { branch, headMtimeMs });
|
|
783
|
-
}
|
|
784
|
-
return branch || null;
|
|
785
|
-
} catch {
|
|
786
|
-
return null;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
function getHeadSha(cwd) {
|
|
790
|
-
try {
|
|
791
|
-
const sha = execSync("git rev-parse HEAD", {
|
|
792
|
-
cwd,
|
|
793
|
-
encoding: "utf8",
|
|
794
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
795
|
-
timeout: 3000
|
|
796
|
-
}).trim();
|
|
797
|
-
return sha || null;
|
|
798
|
-
} catch {
|
|
799
|
-
return null;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
function getThirdPartyCheckRuns(cwd, sha) {
|
|
803
|
-
try {
|
|
804
|
-
const json = execFileSync("gh", [
|
|
805
|
-
"api",
|
|
806
|
-
`repos/{owner}/{repo}/commits/${sha}/check-runs`,
|
|
807
|
-
"--jq",
|
|
808
|
-
'.check_runs | map(select(.app.slug != "github-actions")) | map({name: .name, status: .status, conclusion: (.conclusion // "")})'
|
|
809
|
-
], {
|
|
810
|
-
cwd,
|
|
811
|
-
encoding: "utf8",
|
|
812
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
813
|
-
timeout: 15000
|
|
814
|
-
}).trim();
|
|
815
|
-
if (!json || json === "[]")
|
|
816
|
-
return [];
|
|
817
|
-
return JSON.parse(json);
|
|
818
|
-
} catch {
|
|
819
|
-
return [];
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
function getCommitStatuses(cwd, sha) {
|
|
823
|
-
try {
|
|
824
|
-
const json = execFileSync("gh", [
|
|
825
|
-
"api",
|
|
826
|
-
`repos/{owner}/{repo}/commits/${sha}/statuses`,
|
|
827
|
-
"--jq",
|
|
828
|
-
"map({name: .context, state: .state}) | unique_by(.name)"
|
|
829
|
-
], {
|
|
830
|
-
cwd,
|
|
831
|
-
encoding: "utf8",
|
|
832
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
833
|
-
timeout: 15000
|
|
834
|
-
}).trim();
|
|
835
|
-
if (!json || json === "[]")
|
|
836
|
-
return [];
|
|
837
|
-
const statuses = JSON.parse(json);
|
|
838
|
-
return statuses.map((s) => ({
|
|
839
|
-
name: s.name,
|
|
840
|
-
status: s.state === "pending" ? "in_progress" : "completed",
|
|
841
|
-
conclusion: s.state === "pending" ? "" : s.state === "success" ? "success" : "failure"
|
|
842
|
-
}));
|
|
843
|
-
} catch {
|
|
844
|
-
return [];
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
function matchesAllowedPattern(cmd, pattern) {
|
|
848
|
-
const cmdTokens = parseArgvTokens(cmd);
|
|
849
|
-
const patTokens = parseArgvTokens(pattern);
|
|
850
|
-
if (cmdTokens.length < patTokens.length)
|
|
851
|
-
return false;
|
|
852
|
-
if (cmdTokens.some((tok) => SHELL_OPERATORS.has(tok)))
|
|
853
|
-
return false;
|
|
854
|
-
if (cmdTokens.some((tok) => SHELL_METACHAR_RE.test(tok)))
|
|
855
|
-
return false;
|
|
856
|
-
return patTokens.every((tok, i) => tok === "*" || tok === cmdTokens[i]);
|
|
857
|
-
}
|
|
858
|
-
function sanitizeJwt(ctx) {
|
|
859
|
-
const output = JSON.stringify(ctx.payload);
|
|
860
|
-
if (JWT_RE.test(output)) {
|
|
861
|
-
return {
|
|
862
|
-
decision: "deny",
|
|
863
|
-
reason: "JWT token detected in tool output",
|
|
864
|
-
message: "[REDACTED: JWT token removed by failproofai]"
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
return allow();
|
|
868
|
-
}
|
|
869
|
-
function sanitizeApiKeys(ctx) {
|
|
870
|
-
const output = JSON.stringify(ctx.payload);
|
|
871
|
-
for (const [pattern, label] of API_KEY_PATTERNS) {
|
|
872
|
-
if (pattern.test(output)) {
|
|
873
|
-
return {
|
|
874
|
-
decision: "deny",
|
|
875
|
-
reason: `${label} detected in tool output`,
|
|
876
|
-
message: `[REDACTED: ${label} removed by failproofai]`
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
const additional = ctx.params?.additionalPatterns ?? [];
|
|
881
|
-
for (const { regex, label } of additional) {
|
|
882
|
-
try {
|
|
883
|
-
if (new RegExp(regex).test(output)) {
|
|
884
|
-
return {
|
|
885
|
-
decision: "deny",
|
|
886
|
-
reason: `${label} detected in tool output`,
|
|
887
|
-
message: `[REDACTED: ${label} removed by failproofai]`
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
} catch {
|
|
891
|
-
hookLogWarn(`additionalPatterns: invalid regex "${regex}", skipping`);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
return allow();
|
|
895
|
-
}
|
|
896
|
-
function sanitizeConnectionStrings(ctx) {
|
|
897
|
-
const output = JSON.stringify(ctx.payload);
|
|
898
|
-
if (CONNECTION_STRING_RE.test(output)) {
|
|
899
|
-
return {
|
|
900
|
-
decision: "deny",
|
|
901
|
-
reason: "Database connection string with credentials detected in tool output",
|
|
902
|
-
message: "[REDACTED: connection string removed by failproofai]"
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
return allow();
|
|
906
|
-
}
|
|
907
|
-
function sanitizePrivateKeyContent(ctx) {
|
|
908
|
-
const output = JSON.stringify(ctx.payload);
|
|
909
|
-
if (PRIVATE_KEY_RE.test(output)) {
|
|
910
|
-
return {
|
|
911
|
-
decision: "deny",
|
|
912
|
-
reason: "Private key content detected in tool output",
|
|
913
|
-
message: "[REDACTED: private key content removed by failproofai]"
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
return allow();
|
|
917
|
-
}
|
|
918
|
-
function sanitizeBearerTokens(ctx) {
|
|
919
|
-
const output = JSON.stringify(ctx.payload);
|
|
920
|
-
if (BEARER_TOKEN_RE.test(output)) {
|
|
921
|
-
return {
|
|
922
|
-
decision: "deny",
|
|
923
|
-
reason: "Bearer token detected in tool output",
|
|
924
|
-
message: "[REDACTED: Bearer token removed by failproofai]"
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
return allow();
|
|
928
|
-
}
|
|
929
|
-
function warnDestructiveSql(ctx) {
|
|
930
|
-
if (ctx.toolName !== "Bash")
|
|
931
|
-
return allow();
|
|
932
|
-
const cmd = getCommand(ctx);
|
|
933
|
-
if (!SQL_TOOL_RE.test(cmd))
|
|
934
|
-
return allow();
|
|
935
|
-
if (DESTRUCTIVE_SQL_RE.test(cmd)) {
|
|
936
|
-
return instruct("STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.");
|
|
937
|
-
}
|
|
938
|
-
if (DELETE_NO_WHERE_RE.test(cmd) && !SQL_WHERE_RE.test(cmd)) {
|
|
939
|
-
return instruct("STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.");
|
|
940
|
-
}
|
|
941
|
-
return allow();
|
|
942
|
-
}
|
|
943
|
-
function warnLargeFileWrite(ctx) {
|
|
944
|
-
if (ctx.toolName !== "Write")
|
|
945
|
-
return allow();
|
|
946
|
-
const content = ctx.toolInput?.content;
|
|
947
|
-
if (typeof content !== "string")
|
|
948
|
-
return allow();
|
|
949
|
-
const thresholdKb = ctx.params?.thresholdKb ?? 1024;
|
|
950
|
-
const thresholdBytes = thresholdKb * 1024;
|
|
951
|
-
if (content.length > thresholdBytes) {
|
|
952
|
-
return instruct(`STOP: You are writing a file larger than ${thresholdKb}KB (${Math.round(content.length / 1024)}KB). This is unusually large. Confirm this is intentional before proceeding.`);
|
|
953
|
-
}
|
|
954
|
-
return allow();
|
|
955
|
-
}
|
|
956
|
-
function warnPackagePublish(ctx) {
|
|
957
|
-
if (ctx.toolName !== "Bash")
|
|
958
|
-
return allow();
|
|
959
|
-
const cmd = getCommand(ctx);
|
|
960
|
-
if (PUBLISH_CMD_RE.test(cmd)) {
|
|
961
|
-
return instruct("STOP: This command publishes a package to a public registry. Confirm with the user that this is intentional.");
|
|
962
|
-
}
|
|
963
|
-
return allow();
|
|
964
|
-
}
|
|
965
|
-
function protectEnvVars(ctx) {
|
|
966
|
-
if (ctx.toolName !== "Bash")
|
|
967
|
-
return allow();
|
|
968
|
-
const cmd = getCommand(ctx);
|
|
969
|
-
if (ENV_PRINTENV_RE.test(cmd)) {
|
|
970
|
-
return deny("Command reads environment variables");
|
|
971
|
-
}
|
|
972
|
-
if (ECHO_ENV_RE.test(cmd)) {
|
|
973
|
-
return deny("Command echoes environment variable");
|
|
974
|
-
}
|
|
975
|
-
if (EXPORT_RE.test(cmd)) {
|
|
976
|
-
return deny("Command exports environment variable");
|
|
977
|
-
}
|
|
978
|
-
if (PS_ENV_VAR_RE.test(cmd)) {
|
|
979
|
-
return deny("Command reads environment variable via PowerShell");
|
|
980
|
-
}
|
|
981
|
-
if (PS_CHILDITEM_ENV_RE.test(cmd)) {
|
|
982
|
-
return deny("Command reads environment variables via PowerShell");
|
|
983
|
-
}
|
|
984
|
-
if (DOTNET_GETENV_RE.test(cmd)) {
|
|
985
|
-
return deny("Command reads environment variable via .NET");
|
|
986
|
-
}
|
|
987
|
-
if (CMD_ECHO_ENV_RE.test(cmd)) {
|
|
988
|
-
return deny("Command echoes environment variable via cmd");
|
|
989
|
-
}
|
|
990
|
-
return allow();
|
|
991
|
-
}
|
|
992
|
-
function blockEnvFiles(ctx) {
|
|
993
|
-
const cmd = getCommand(ctx);
|
|
994
|
-
const filePath = getFilePath(ctx);
|
|
995
|
-
if (filePath && ENV_FILE_PATH_RE.test(filePath)) {
|
|
996
|
-
return deny("Access to .env file blocked");
|
|
997
|
-
}
|
|
998
|
-
if (ctx.toolName === "Bash" && ENV_CMD_RE.test(cmd)) {
|
|
999
|
-
return deny("Command references .env file");
|
|
1000
|
-
}
|
|
1001
|
-
return allow();
|
|
1002
|
-
}
|
|
1003
|
-
function quoteAwareSegments(command) {
|
|
1004
|
-
const segments = [];
|
|
1005
|
-
let tokens = [];
|
|
1006
|
-
let token = "";
|
|
1007
|
-
let quote = null;
|
|
1008
|
-
const endToken = () => {
|
|
1009
|
-
if (token)
|
|
1010
|
-
tokens.push(token);
|
|
1011
|
-
token = "";
|
|
1012
|
-
};
|
|
1013
|
-
const endSegment = () => {
|
|
1014
|
-
endToken();
|
|
1015
|
-
if (tokens.length)
|
|
1016
|
-
segments.push(tokens);
|
|
1017
|
-
tokens = [];
|
|
1018
|
-
};
|
|
1019
|
-
for (let i = 0;i < command.length; i++) {
|
|
1020
|
-
const c = command[i];
|
|
1021
|
-
if (quote) {
|
|
1022
|
-
if (c === "\\" && quote === '"' && i + 1 < command.length) {
|
|
1023
|
-
token += command[++i];
|
|
1024
|
-
continue;
|
|
1025
|
-
}
|
|
1026
|
-
if (c === quote) {
|
|
1027
|
-
quote = null;
|
|
1028
|
-
continue;
|
|
1029
|
-
}
|
|
1030
|
-
token += c;
|
|
1031
|
-
continue;
|
|
1032
|
-
}
|
|
1033
|
-
if (c === "\\" && i + 1 < command.length) {
|
|
1034
|
-
token += command[++i];
|
|
1035
|
-
continue;
|
|
1036
|
-
}
|
|
1037
|
-
if (c === '"' || c === "'") {
|
|
1038
|
-
quote = c;
|
|
1039
|
-
continue;
|
|
1040
|
-
}
|
|
1041
|
-
if (/\s/.test(c)) {
|
|
1042
|
-
endToken();
|
|
1043
|
-
continue;
|
|
1044
|
-
}
|
|
1045
|
-
if (/[;&|\n\r(){}`]/.test(c)) {
|
|
1046
|
-
endSegment();
|
|
1047
|
-
continue;
|
|
1048
|
-
}
|
|
1049
|
-
token += c;
|
|
1050
|
-
}
|
|
1051
|
-
endSegment();
|
|
1052
|
-
return segments;
|
|
1053
|
-
}
|
|
1054
|
-
function namesElevation(command, depth = 0) {
|
|
1055
|
-
for (const tokens of quoteAwareSegments(command)) {
|
|
1056
|
-
let i = 0;
|
|
1057
|
-
while (i < tokens.length) {
|
|
1058
|
-
const token = tokens[i];
|
|
1059
|
-
const base = token.slice(token.lastIndexOf("/") + 1);
|
|
1060
|
-
if (base === "sudo" || base === "doas")
|
|
1061
|
-
return true;
|
|
1062
|
-
const isSkippable = ENV_ASSIGNMENT_RE.test(token) || token.startsWith("-") || token.startsWith(">") || token.startsWith("<") || RUNNER_OPERAND_RE.test(token) || COMMAND_PREFIX_TOKENS.has(base);
|
|
1063
|
-
if (!isSkippable)
|
|
1064
|
-
break;
|
|
1065
|
-
if (EVAL_FLAG_RE.test(token) && depth < 3 && i + 1 < tokens.length) {
|
|
1066
|
-
if (namesElevation(tokens[i + 1], depth + 1))
|
|
1067
|
-
return true;
|
|
1068
|
-
}
|
|
1069
|
-
i++;
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
return false;
|
|
1073
|
-
}
|
|
1074
|
-
function blockSudo(ctx) {
|
|
1075
|
-
if (ctx.toolName !== "Bash")
|
|
1076
|
-
return allow();
|
|
1077
|
-
const cmd = getCommand(ctx).trimStart();
|
|
1078
|
-
if (namesElevation(cmd)) {
|
|
1079
|
-
const allowPatterns = ctx.params?.allowPatterns ?? [];
|
|
1080
|
-
if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
|
|
1081
|
-
return allow();
|
|
1082
|
-
return deny("sudo commands are blocked");
|
|
1083
|
-
}
|
|
1084
|
-
if (PS_ELEVATION_RE.test(cmd)) {
|
|
1085
|
-
return deny("Elevated process launch is blocked");
|
|
1086
|
-
}
|
|
1087
|
-
if (RUNAS_RE.test(cmd)) {
|
|
1088
|
-
return deny("runas elevation is blocked");
|
|
1089
|
-
}
|
|
1090
|
-
return allow();
|
|
1091
|
-
}
|
|
1092
|
-
function blockSelfPause(ctx) {
|
|
1093
|
-
if (ctx.toolName !== "Bash")
|
|
1094
|
-
return allow();
|
|
1095
|
-
const cmd = getCommand(ctx);
|
|
1096
|
-
if (namesSelfPause(cmd) || namesSelfPause(stripShellQuoting(cmd))) {
|
|
1097
|
-
return deny("Pausing failproofai enforcement is a human action, not an agent one. " + "If a policy is blocking legitimate work, say so and let the operator decide.");
|
|
1098
|
-
}
|
|
1099
|
-
return allow();
|
|
1100
|
-
}
|
|
1101
|
-
function blockCurlPipeSh(ctx) {
|
|
1102
|
-
if (ctx.toolName !== "Bash")
|
|
1103
|
-
return allow();
|
|
1104
|
-
const cmd = getCommand(ctx);
|
|
1105
|
-
if (CURL_PIPE_SH_RE.test(cmd)) {
|
|
1106
|
-
return deny("Piping downloads to shell is blocked");
|
|
1107
|
-
}
|
|
1108
|
-
if (PS_WEB_PIPE_RE.test(cmd)) {
|
|
1109
|
-
return deny("Piping downloads to Invoke-Expression is blocked");
|
|
1110
|
-
}
|
|
1111
|
-
return allow();
|
|
1112
|
-
}
|
|
1113
|
-
function extractGitPushArgs(cmd) {
|
|
1114
|
-
return cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => /^git\s+push\s/.test(s)).map((s) => s.replace(/^git\s+push\s+/, ""));
|
|
1115
|
-
}
|
|
1116
|
-
function blockPushMaster(ctx) {
|
|
1117
|
-
if (ctx.toolName !== "Bash")
|
|
1118
|
-
return allow();
|
|
1119
|
-
const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
|
|
1120
|
-
if (protectedBranches.length === 0)
|
|
1121
|
-
return allow();
|
|
1122
|
-
const args = extractGitPushArgs(getCommand(ctx));
|
|
1123
|
-
const branchPattern = new RegExp(`\\b(?:${protectedBranches.map((b) => b.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`);
|
|
1124
|
-
if (args.some((a) => branchPattern.test(a))) {
|
|
1125
|
-
return deny(`Pushing to ${protectedBranches.join("/")} is blocked`);
|
|
1126
|
-
}
|
|
1127
|
-
return allow();
|
|
1128
|
-
}
|
|
1129
|
-
function expandHomePrefix(path) {
|
|
1130
|
-
const m = path.match(/^(?:~|\$HOME|\$\{HOME\})(?=$|\/)/);
|
|
1131
|
-
return m ? homedir3() + path.slice(m[0].length) : path;
|
|
1132
|
-
}
|
|
1133
|
-
function stripTrailingGlob(path) {
|
|
1134
|
-
return path.replace(/\/\*$/, "").replace(/\/+$/, "");
|
|
1135
|
-
}
|
|
1136
|
-
function isCatastrophicTarget(token) {
|
|
1137
|
-
const raw = token.replace(/^['"]|['"]$/g, "");
|
|
1138
|
-
if (raw === "")
|
|
1139
|
-
return false;
|
|
1140
|
-
const homePrefix = raw.match(HOME_PREFIX_RE);
|
|
1141
|
-
if (!homePrefix && /^[$`]/.test(raw))
|
|
1142
|
-
return true;
|
|
1143
|
-
const belowRoot = homePrefix ? raw.slice(homePrefix[0].length) : raw.startsWith("/") ? raw : null;
|
|
1144
|
-
if (belowRoot === null)
|
|
1145
|
-
return false;
|
|
1146
|
-
const segments = stripTrailingGlob(belowRoot).split("/").filter(Boolean);
|
|
1147
|
-
if (!homePrefix && SCRATCH_ROOTS.some((r) => `/${segments.join("/")}`.startsWith(`${r}/`)))
|
|
1148
|
-
return false;
|
|
1149
|
-
return segments.length <= CATASTROPHIC_DEPTH;
|
|
1150
|
-
}
|
|
1151
|
-
function recursiveDeletionTargets(seg) {
|
|
1152
|
-
const tokens = parseArgvTokens(seg);
|
|
1153
|
-
const findIdx = tokens.findIndex((t) => FIND_CMD_RE.test(t));
|
|
1154
|
-
if (findIdx >= 0) {
|
|
1155
|
-
const expr = tokens.slice(findIdx + 1);
|
|
1156
|
-
const execIdx = expr.findIndex((t) => FIND_EXEC_RE.test(t));
|
|
1157
|
-
const deletes = expr.includes("-delete") || execIdx >= 0 && RM_CMD_RE.test(expr[execIdx + 1] ?? "");
|
|
1158
|
-
if (deletes) {
|
|
1159
|
-
let start = 0;
|
|
1160
|
-
while (start < expr.length && FIND_GLOBAL_OPT_RE.test(expr[start])) {
|
|
1161
|
-
start += expr[start] === "-D" ? 2 : 1;
|
|
1162
|
-
}
|
|
1163
|
-
const rest = expr.slice(start);
|
|
1164
|
-
const end = rest.findIndex((t) => FIND_EXPR_START_RE.test(t));
|
|
1165
|
-
return end < 0 ? rest : rest.slice(0, end);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
const rmIdx = tokens.findIndex((t) => RM_CMD_RE.test(t));
|
|
1169
|
-
if (rmIdx >= 0) {
|
|
1170
|
-
const args = tokens.slice(rmIdx + 1);
|
|
1171
|
-
const shortFlags = args.filter((t) => /^-[^-]/.test(t)).join("");
|
|
1172
|
-
const longFlags = args.filter((t) => /^--/.test(t));
|
|
1173
|
-
const recursive = /r/i.test(shortFlags) || longFlags.some((f) => /^--recursive$/i.test(f));
|
|
1174
|
-
const force = /f/.test(shortFlags) || longFlags.some((f) => /^--force$/i.test(f));
|
|
1175
|
-
if (recursive && force)
|
|
1176
|
-
return args.filter((t) => !t.startsWith("-"));
|
|
1177
|
-
}
|
|
1178
|
-
return null;
|
|
1179
|
-
}
|
|
1180
|
-
function shellSegments(cmd) {
|
|
1181
|
-
return cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => s !== "");
|
|
1182
|
-
}
|
|
1183
|
-
function deletionTargetIsAllowed(cmd, allowPaths) {
|
|
1184
|
-
if (allowPaths.length === 0)
|
|
1185
|
-
return false;
|
|
1186
|
-
const normalizedAllowPaths = allowPaths.map((p) => stripTrailingGlob(expandHomePrefix(p)) || "/");
|
|
1187
|
-
let sawRecursiveDelete = false;
|
|
1188
|
-
for (const seg of shellSegments(cmd)) {
|
|
1189
|
-
const targets = recursiveDeletionTargets(seg);
|
|
1190
|
-
if (targets === null)
|
|
1191
|
-
continue;
|
|
1192
|
-
sawRecursiveDelete = true;
|
|
1193
|
-
for (const target of targets) {
|
|
1194
|
-
const normalized = stripTrailingGlob(expandHomePrefix(target)) || "/";
|
|
1195
|
-
const covered = normalizedAllowPaths.some((np) => normalized === np || normalized.startsWith(np + "/"));
|
|
1196
|
-
if (!covered) {
|
|
1197
|
-
const segCovered = allowPaths.some((p) => {
|
|
1198
|
-
const escaped = p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1199
|
-
return new RegExp(`${escaped}(?:[/"'\\s/*]|$)`).test(seg);
|
|
1200
|
-
});
|
|
1201
|
-
if (!segCovered)
|
|
1202
|
-
return false;
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
return sawRecursiveDelete;
|
|
1207
|
-
}
|
|
1208
|
-
function blockRmRf(ctx) {
|
|
1209
|
-
if (ctx.toolName !== "Bash")
|
|
1210
|
-
return allow();
|
|
1211
|
-
const cmd = getCommand(ctx);
|
|
1212
|
-
const hasCatastrophicTarget = shellSegments(cmd).some((seg) => {
|
|
1213
|
-
const targets = recursiveDeletionTargets(seg);
|
|
1214
|
-
return targets !== null && targets.some(isCatastrophicTarget);
|
|
1215
|
-
});
|
|
1216
|
-
if (hasCatastrophicTarget) {
|
|
1217
|
-
const allowPaths = ctx.params?.allowPaths ?? [];
|
|
1218
|
-
if (deletionTargetIsAllowed(cmd, allowPaths))
|
|
1219
|
-
return allow();
|
|
1220
|
-
return deny("Catastrophic deletion blocked");
|
|
1221
|
-
}
|
|
1222
|
-
if (/Remove-Item\s+.*-Recurse.*-Force.*(?:[A-Z]:\\(?:\s|$)|\\\*)/i.test(cmd)) {
|
|
1223
|
-
return deny("Catastrophic deletion blocked");
|
|
1224
|
-
}
|
|
1225
|
-
if (/(?:rd|rmdir)\s+\/s\s+\/q\s+[A-Z]:\\/i.test(cmd)) {
|
|
1226
|
-
return deny("Catastrophic deletion blocked");
|
|
1227
|
-
}
|
|
1228
|
-
return allow();
|
|
1229
|
-
}
|
|
1230
|
-
function blockForcePush(ctx) {
|
|
1231
|
-
if (ctx.toolName !== "Bash")
|
|
1232
|
-
return allow();
|
|
1233
|
-
for (const segment of extractGitPushArgs(getCommand(ctx))) {
|
|
1234
|
-
let sawEndOfOptions = false;
|
|
1235
|
-
for (const token of segment.split(/\s+/)) {
|
|
1236
|
-
if (token === "--") {
|
|
1237
|
-
sawEndOfOptions = true;
|
|
1238
|
-
continue;
|
|
1239
|
-
}
|
|
1240
|
-
if (sawEndOfOptions)
|
|
1241
|
-
continue;
|
|
1242
|
-
if (isForcePushFlag(token)) {
|
|
1243
|
-
return deny("Force-pushing is blocked");
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
return allow();
|
|
1248
|
-
}
|
|
1249
|
-
function isForcePushFlag(token) {
|
|
1250
|
-
if (token === "--force")
|
|
1251
|
-
return true;
|
|
1252
|
-
if (SAFE_FORCE_PREFIXES.some((prefix) => token.startsWith(prefix)))
|
|
1253
|
-
return false;
|
|
1254
|
-
if (token.startsWith("--force"))
|
|
1255
|
-
return true;
|
|
1256
|
-
return SHORT_FLAG_BUNDLE_RE.test(token);
|
|
1257
|
-
}
|
|
1258
|
-
function blockSecretsWrite(ctx) {
|
|
1259
|
-
if (ctx.toolName !== "Write")
|
|
1260
|
-
return allow();
|
|
1261
|
-
const filePath = getFilePath(ctx);
|
|
1262
|
-
if (SECRET_FILE_RE.test(filePath) || SECRET_FILE_ID_RSA_RE.test(filePath) || SECRET_FILE_CREDENTIALS_RE.test(filePath)) {
|
|
1263
|
-
return deny("Writing secret key files is blocked");
|
|
1264
|
-
}
|
|
1265
|
-
const additionalPatterns = ctx.params?.additionalPatterns ?? [];
|
|
1266
|
-
for (const pattern of additionalPatterns) {
|
|
1267
|
-
if (filePath.includes(pattern)) {
|
|
1268
|
-
return deny(`Writing blocked file pattern: ${pattern}`);
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
return allow();
|
|
1272
|
-
}
|
|
1273
|
-
function extractAbsolutePaths(command) {
|
|
1274
|
-
const paths = [];
|
|
1275
|
-
const pathRe = /(?<![a-zA-Z0-9_.\-~\\*?:=])(?:~\/[^\s;|&"'()\[\]{}]*|~(?=\s|$|[;|&"'()\[\]{}])|\/[^\s;|&"'()\[\]{}]*)/g;
|
|
1276
|
-
function addPaths(s) {
|
|
1277
|
-
pathRe.lastIndex = 0;
|
|
1278
|
-
let m;
|
|
1279
|
-
while ((m = pathRe.exec(s)) !== null) {
|
|
1280
|
-
let p = m[0];
|
|
1281
|
-
if (p === "~")
|
|
1282
|
-
p = homedir3();
|
|
1283
|
-
else if (p.startsWith("~/"))
|
|
1284
|
-
p = join2(homedir3(), p.slice(2));
|
|
1285
|
-
paths.push(p);
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
let firstBarePipe = command.length;
|
|
1289
|
-
let inDouble = false, inSingle = false;
|
|
1290
|
-
for (let i = 0;i < command.length; i++) {
|
|
1291
|
-
const c = command[i];
|
|
1292
|
-
if (c === '"' && !inSingle)
|
|
1293
|
-
inDouble = !inDouble;
|
|
1294
|
-
else if (c === "'" && !inDouble)
|
|
1295
|
-
inSingle = !inSingle;
|
|
1296
|
-
else if (c === "|" && !inDouble && !inSingle) {
|
|
1297
|
-
firstBarePipe = i;
|
|
1298
|
-
break;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
const firstSegment = command.slice(0, firstBarePipe);
|
|
1302
|
-
const quotedRe = /"([^"]*)"|'([^']*)'/g;
|
|
1303
|
-
let qm;
|
|
1304
|
-
while ((qm = quotedRe.exec(firstSegment)) !== null) {
|
|
1305
|
-
const content = qm[1] ?? qm[2] ?? "";
|
|
1306
|
-
if (/[*?\[\]^$+()\\]/.test(content))
|
|
1307
|
-
continue;
|
|
1308
|
-
addPaths(content);
|
|
1309
|
-
}
|
|
1310
|
-
const stripped = command.replace(/"[^"]*"/g, (m) => " ".repeat(m.length)).replace(/'[^']*'/g, (m) => " ".repeat(m.length));
|
|
1311
|
-
addPaths(stripped);
|
|
1312
|
-
return paths;
|
|
1313
|
-
}
|
|
1314
|
-
function blockReadOutsideCwd(ctx) {
|
|
1315
|
-
const cwd = process.env.CLAUDE_PROJECT_DIR || ctx.session?.cwd;
|
|
1316
|
-
if (!cwd)
|
|
1317
|
-
return allow();
|
|
1318
|
-
const allowPaths = ctx.params?.allowPaths ?? [];
|
|
1319
|
-
if (ctx.toolName === "Bash") {
|
|
1320
|
-
const cmd = getCommand(ctx);
|
|
1321
|
-
if (!READ_LIKE_CMDS.test(cmd))
|
|
1322
|
-
return allow();
|
|
1323
|
-
const paths = extractAbsolutePaths(cmd);
|
|
1324
|
-
const cwdWithSep2 = cwd.endsWith("/") ? cwd : cwd + "/";
|
|
1325
|
-
for (const p of paths) {
|
|
1326
|
-
const resolved3 = resolve3(cwd, p);
|
|
1327
|
-
if (isClaudeSettingsFile(resolved3)) {
|
|
1328
|
-
return deny(`Reading agent settings file blocked: ${resolved3}`);
|
|
1329
|
-
}
|
|
1330
|
-
if (isClaudeInternalPath(resolved3))
|
|
1331
|
-
continue;
|
|
1332
|
-
if (resolved3 === "/dev/null")
|
|
1333
|
-
continue;
|
|
1334
|
-
if (resolved3 !== cwd && !resolved3.startsWith(cwdWithSep2)) {
|
|
1335
|
-
if (allowPaths.some((ap) => resolved3 === ap || resolved3.startsWith(ap.endsWith("/") ? ap : ap + "/")))
|
|
1336
|
-
continue;
|
|
1337
|
-
return deny(`Bash read outside project directory blocked: ${resolved3}`);
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
return allow();
|
|
1341
|
-
}
|
|
1342
|
-
const filePath = getFilePath(ctx);
|
|
1343
|
-
const searchPath = ctx.toolInput?.path ?? "";
|
|
1344
|
-
const target = filePath || searchPath;
|
|
1345
|
-
if (!target)
|
|
1346
|
-
return allow();
|
|
1347
|
-
const resolved2 = resolve3(cwd, target);
|
|
1348
|
-
if (isClaudeSettingsFile(resolved2)) {
|
|
1349
|
-
return deny(`Reading agent settings file blocked: ${resolved2}`);
|
|
1350
|
-
}
|
|
1351
|
-
if (isClaudeInternalPath(resolved2))
|
|
1352
|
-
return allow();
|
|
1353
|
-
if (resolved2 === "/dev/null")
|
|
1354
|
-
return allow();
|
|
1355
|
-
const cwdWithSep = cwd.endsWith("/") ? cwd : cwd + "/";
|
|
1356
|
-
if (resolved2 !== cwd && !resolved2.startsWith(cwdWithSep)) {
|
|
1357
|
-
if (allowPaths.some((ap) => resolved2 === ap || resolved2.startsWith(ap.endsWith("/") ? ap : ap + "/")))
|
|
1358
|
-
return allow();
|
|
1359
|
-
return deny(`Access outside project directory blocked: ${resolved2}`);
|
|
1360
|
-
}
|
|
1361
|
-
return allow();
|
|
1362
|
-
}
|
|
1363
|
-
function blockWorkOnMain(ctx) {
|
|
1364
|
-
if (ctx.toolName !== "Bash")
|
|
1365
|
-
return allow();
|
|
1366
|
-
const cmd = getCommand(ctx);
|
|
1367
|
-
const match = cmd.match(GIT_COMMIT_MERGE_RE);
|
|
1368
|
-
if (!match)
|
|
1369
|
-
return allow();
|
|
1370
|
-
const cwd = ctx.session?.cwd;
|
|
1371
|
-
if (!cwd)
|
|
1372
|
-
return allow();
|
|
1373
|
-
const branch = getCurrentBranch(cwd);
|
|
1374
|
-
if (!branch)
|
|
1375
|
-
return allow();
|
|
1376
|
-
const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
|
|
1377
|
-
if (protectedBranches.includes(branch)) {
|
|
1378
|
-
return deny(`Git ${match[1]} on ${branch} is blocked. Create a feature branch first.`);
|
|
1379
|
-
}
|
|
1380
|
-
return allow();
|
|
1381
|
-
}
|
|
1382
|
-
function blockFailproofaiCommands(ctx) {
|
|
1383
|
-
if (ctx.toolName !== "Bash")
|
|
1384
|
-
return allow();
|
|
1385
|
-
const cmd = getCommand(ctx);
|
|
1386
|
-
if (FAILPROOFAI_CLI_RE.test(cmd)) {
|
|
1387
|
-
return deny("Running failproofai CLI commands is blocked");
|
|
1388
|
-
}
|
|
1389
|
-
if (FAILPROOFAI_UNINSTALL_RE.test(cmd)) {
|
|
1390
|
-
return deny("Uninstalling failproofai is blocked");
|
|
1391
|
-
}
|
|
1392
|
-
return allow();
|
|
1393
|
-
}
|
|
1394
|
-
function blockInfraCli(ctx, re, denyMsg) {
|
|
1395
|
-
if (ctx.toolName !== "Bash")
|
|
1396
|
-
return allow();
|
|
1397
|
-
const cmd = getCommand(ctx);
|
|
1398
|
-
if (!re.test(cmd))
|
|
1399
|
-
return allow();
|
|
1400
|
-
const allowPatterns = ctx.params?.allowPatterns ?? [];
|
|
1401
|
-
if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
|
|
1402
|
-
return allow();
|
|
1403
|
-
return deny(denyMsg);
|
|
1404
|
-
}
|
|
1405
|
-
function blockKubectl(ctx) {
|
|
1406
|
-
return blockInfraCli(ctx, KUBECTL_RE, "kubectl commands are blocked");
|
|
1407
|
-
}
|
|
1408
|
-
function blockTerraform(ctx) {
|
|
1409
|
-
return blockInfraCli(ctx, TERRAFORM_RE, "terraform/tofu commands are blocked");
|
|
1410
|
-
}
|
|
1411
|
-
function blockAwsCli(ctx) {
|
|
1412
|
-
return blockInfraCli(ctx, AWS_CLI_RE, "aws CLI commands are blocked");
|
|
1413
|
-
}
|
|
1414
|
-
function blockGcloud(ctx) {
|
|
1415
|
-
return blockInfraCli(ctx, GCLOUD_RE, "gcloud commands are blocked");
|
|
1416
|
-
}
|
|
1417
|
-
function blockAzCli(ctx) {
|
|
1418
|
-
return blockInfraCli(ctx, AZ_CLI_RE, "az (Azure) CLI commands are blocked");
|
|
1419
|
-
}
|
|
1420
|
-
function blockHelm(ctx) {
|
|
1421
|
-
return blockInfraCli(ctx, HELM_RE, "helm commands are blocked");
|
|
1422
|
-
}
|
|
1423
|
-
function blockGhPipeline(ctx) {
|
|
1424
|
-
return blockInfraCli(ctx, GH_PIPELINE_RE, "gh pipeline-trigger commands are blocked");
|
|
1425
|
-
}
|
|
1426
|
-
async function warnRepeatedToolCalls(ctx) {
|
|
1427
|
-
const THRESHOLD = 3;
|
|
1428
|
-
const transcriptPath = ctx.session?.transcriptPath;
|
|
1429
|
-
if (!transcriptPath || !ctx.toolName || !ctx.toolInput)
|
|
1430
|
-
return allow();
|
|
1431
|
-
const trackerPath = `${transcriptPath}.tool-calls.json`;
|
|
1432
|
-
const fingerprint = JSON.stringify({ tool: ctx.toolName, input: ctx.toolInput });
|
|
1433
|
-
let counts = {};
|
|
1434
|
-
try {
|
|
1435
|
-
const raw = await readFile(trackerPath, "utf8");
|
|
1436
|
-
counts = JSON.parse(raw);
|
|
1437
|
-
} catch {}
|
|
1438
|
-
const prevCount = counts[fingerprint] ?? 0;
|
|
1439
|
-
if (prevCount >= THRESHOLD) {
|
|
1440
|
-
return instruct(`STOP: You have already called ${ctx.toolName} ${prevCount} times with identical parameters. This is wasteful and unproductive. Do NOT repeat this call — use a different approach or ask the user for clarification.`);
|
|
1441
|
-
}
|
|
1442
|
-
counts[fingerprint] = prevCount + 1;
|
|
1443
|
-
try {
|
|
1444
|
-
const serialized = JSON.stringify(counts);
|
|
1445
|
-
if (serialized.length <= TOOL_CALL_TRACKER_MAX_BYTES) {
|
|
1446
|
-
await writeFile(trackerPath, serialized, "utf8");
|
|
1447
|
-
}
|
|
1448
|
-
} catch {}
|
|
1449
|
-
return allow();
|
|
1450
|
-
}
|
|
1451
|
-
function warnGitAmend(ctx) {
|
|
1452
|
-
if (ctx.toolName !== "Bash")
|
|
1453
|
-
return allow();
|
|
1454
|
-
const cmd = getCommand(ctx);
|
|
1455
|
-
if (GIT_AMEND_RE.test(cmd)) {
|
|
1456
|
-
return instruct("STOP: This command amends the last commit, which rewrites git history. If this commit has already been pushed to a shared branch, this will cause divergence for other contributors. Confirm with the user before executing.");
|
|
1457
|
-
}
|
|
1458
|
-
return allow();
|
|
1459
|
-
}
|
|
1460
|
-
function warnGitStashDrop(ctx) {
|
|
1461
|
-
if (ctx.toolName !== "Bash")
|
|
1462
|
-
return allow();
|
|
1463
|
-
const cmd = getCommand(ctx);
|
|
1464
|
-
if (GIT_STASH_DROP_RE.test(cmd)) {
|
|
1465
|
-
return instruct("STOP: This command permanently deletes stashed changes (git stash drop/clear). Stash entries cannot be recovered after deletion. Confirm with the user before executing.");
|
|
1466
|
-
}
|
|
1467
|
-
return allow();
|
|
1468
|
-
}
|
|
1469
|
-
function warnAllFilesStaged(ctx) {
|
|
1470
|
-
if (ctx.toolName !== "Bash")
|
|
1471
|
-
return allow();
|
|
1472
|
-
const cmd = getCommand(ctx);
|
|
1473
|
-
if (GIT_ADD_ALL_RE.test(cmd)) {
|
|
1474
|
-
return instruct("STOP: This command stages all files in the working tree (git add -A / --all / .). This may inadvertently include build artifacts, generated files, or sensitive files not covered by .gitignore. Confirm with the user before executing.");
|
|
1475
|
-
}
|
|
1476
|
-
return allow();
|
|
1477
|
-
}
|
|
1478
|
-
function warnSchemaAlteration(ctx) {
|
|
1479
|
-
if (ctx.toolName !== "Bash")
|
|
1480
|
-
return allow();
|
|
1481
|
-
const cmd = getCommand(ctx);
|
|
1482
|
-
if (!SQL_TOOL_RE.test(cmd))
|
|
1483
|
-
return allow();
|
|
1484
|
-
if (SCHEMA_ALTER_RE.test(cmd)) {
|
|
1485
|
-
return instruct("STOP: This command contains a schema-altering SQL statement (ALTER TABLE with column or rename operation). Schema changes on production databases are irreversible or disruptive. Confirm with the user before executing.");
|
|
1486
|
-
}
|
|
1487
|
-
return allow();
|
|
1488
|
-
}
|
|
1489
|
-
function warnGlobalPackageInstall(ctx) {
|
|
1490
|
-
if (ctx.toolName !== "Bash")
|
|
1491
|
-
return allow();
|
|
1492
|
-
const cmd = getCommand(ctx);
|
|
1493
|
-
const isGlobal = NPM_GLOBAL_RE.test(cmd) || YARN_GLOBAL_RE.test(cmd) || PNPM_GLOBAL_RE.test(cmd) || BUN_GLOBAL_RE.test(cmd) || CARGO_INSTALL_RE.test(cmd) || PIP_SYSTEM_RE.test(cmd);
|
|
1494
|
-
if (isGlobal) {
|
|
1495
|
-
return instruct("STOP: This command installs a package globally, which modifies the system-wide environment outside the project. This can conflict with other projects or system tools. Confirm with the user before executing.");
|
|
1496
|
-
}
|
|
1497
|
-
return allow();
|
|
1498
|
-
}
|
|
1499
|
-
function preferPackageManager(ctx) {
|
|
1500
|
-
if (ctx.toolName !== "Bash")
|
|
1501
|
-
return allow();
|
|
1502
|
-
const cmd = getCommand(ctx);
|
|
1503
|
-
if (!cmd)
|
|
1504
|
-
return allow();
|
|
1505
|
-
const allowed = ctx.params?.allowed ?? [];
|
|
1506
|
-
if (allowed.length === 0)
|
|
1507
|
-
return allow();
|
|
1508
|
-
const allowedSet = new Set(allowed.map((a) => a.toLowerCase()));
|
|
1509
|
-
const blocked = ctx.params?.blocked ?? [];
|
|
1510
|
-
const allowedList = allowed.join(", ");
|
|
1511
|
-
const segments = cmd.split(SEGMENT_SPLIT_RE);
|
|
1512
|
-
for (const segment of segments) {
|
|
1513
|
-
const trimmed = segment.trim();
|
|
1514
|
-
if (!trimmed)
|
|
1515
|
-
continue;
|
|
1516
|
-
let segmentAllowed = false;
|
|
1517
|
-
for (const manager of allowedSet) {
|
|
1518
|
-
const patterns = PKG_MANAGER_DETECTORS[manager];
|
|
1519
|
-
if (!patterns)
|
|
1520
|
-
continue;
|
|
1521
|
-
for (const pattern of patterns) {
|
|
1522
|
-
if (pattern.test(trimmed)) {
|
|
1523
|
-
segmentAllowed = true;
|
|
1524
|
-
break;
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
if (segmentAllowed)
|
|
1528
|
-
break;
|
|
1529
|
-
}
|
|
1530
|
-
if (segmentAllowed)
|
|
1531
|
-
continue;
|
|
1532
|
-
for (const [manager, patterns] of Object.entries(PKG_MANAGER_DETECTORS)) {
|
|
1533
|
-
if (allowedSet.has(manager))
|
|
1534
|
-
continue;
|
|
1535
|
-
for (const pattern of patterns) {
|
|
1536
|
-
if (pattern.test(trimmed)) {
|
|
1537
|
-
return deny(`"${manager}" is not an allowed package manager. ` + `Allowed package managers for this project: ${allowedList}. ` + `Rewrite this command using an allowed package manager.`);
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
for (const name of blocked) {
|
|
1542
|
-
const lower = name.toLowerCase();
|
|
1543
|
-
if (allowedSet.has(lower))
|
|
1544
|
-
continue;
|
|
1545
|
-
const re = new RegExp(`\\b${lower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
1546
|
-
if (re.test(trimmed)) {
|
|
1547
|
-
return deny(`"${lower}" is not an allowed package manager. ` + `Allowed package managers for this project: ${allowedList}. ` + `Rewrite this command using an allowed package manager.`);
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
return allow();
|
|
1552
|
-
}
|
|
1553
|
-
function warnBackgroundProcess(ctx) {
|
|
1554
|
-
if (ctx.toolName !== "Bash")
|
|
1555
|
-
return allow();
|
|
1556
|
-
const cmd = getCommand(ctx);
|
|
1557
|
-
const isBackground = NOHUP_RE.test(cmd) || SCREEN_DETACH_RE.test(cmd) || TMUX_DETACH_RE.test(cmd) || DISOWN_RE.test(cmd) || BACKGROUND_AMPERSAND_RE.test(cmd);
|
|
1558
|
-
if (isBackground) {
|
|
1559
|
-
return instruct("STOP: This command starts a background or detached process (nohup, screen -d, tmux -d, or trailing &). Background processes persist after Claude's session and may be difficult to track or stop. Confirm with the user before executing.");
|
|
1560
|
-
}
|
|
1561
|
-
return allow();
|
|
1562
|
-
}
|
|
1563
|
-
function isPlanMode(ctx) {
|
|
1564
|
-
return ctx.session?.permissionMode === "plan";
|
|
1565
|
-
}
|
|
1566
|
-
function requireCommitBeforeStop(ctx) {
|
|
1567
|
-
if (isPlanMode(ctx))
|
|
1568
|
-
return allow("Plan mode — no changes made, skipping commit check.");
|
|
1569
|
-
const cwd = ctx.session?.cwd;
|
|
1570
|
-
if (!cwd)
|
|
1571
|
-
return allow("No working directory available, skipping commit check.");
|
|
1572
|
-
try {
|
|
1573
|
-
const status = execSync("git status --porcelain", {
|
|
1574
|
-
cwd,
|
|
1575
|
-
encoding: "utf8",
|
|
1576
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1577
|
-
timeout: 5000
|
|
1578
|
-
}).trim();
|
|
1579
|
-
if (status.length > 0) {
|
|
1580
|
-
return deny("You have uncommitted changes in the working directory. Commit all changes now.");
|
|
1581
|
-
}
|
|
1582
|
-
return allow("All changes are committed.");
|
|
1583
|
-
} catch {
|
|
1584
|
-
return allow("Not a git repository, skipping commit check.");
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
function requirePushBeforeStop(ctx) {
|
|
1588
|
-
if (isPlanMode(ctx))
|
|
1589
|
-
return allow("Plan mode — no changes made, skipping push check.");
|
|
1590
|
-
const cwd = ctx.session?.cwd;
|
|
1591
|
-
if (!cwd)
|
|
1592
|
-
return allow("No working directory available, skipping push check.");
|
|
1593
|
-
try {
|
|
1594
|
-
const remotes = execSync("git remote", {
|
|
1595
|
-
cwd,
|
|
1596
|
-
encoding: "utf8",
|
|
1597
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1598
|
-
timeout: 3000
|
|
1599
|
-
}).trim();
|
|
1600
|
-
if (!remotes)
|
|
1601
|
-
return allow("No git remote configured, skipping push check.");
|
|
1602
|
-
const remote = ctx.params?.remote ?? "origin";
|
|
1603
|
-
const branch = getCurrentBranch(cwd);
|
|
1604
|
-
if (!branch || branch === "HEAD")
|
|
1605
|
-
return allow("Detached HEAD, skipping push check.");
|
|
1606
|
-
const baseBranch = ctx.params?.baseBranch ?? "main";
|
|
1607
|
-
if (branch === baseBranch) {
|
|
1608
|
-
return allow(`On base branch "${baseBranch}", skipping push check.`);
|
|
1609
|
-
}
|
|
1610
|
-
try {
|
|
1611
|
-
const ahead = execFileSync("git", ["log", `${remote}/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
1612
|
-
if (!ahead) {
|
|
1613
|
-
return allow(`No commits ahead of ${remote}/${baseBranch}, skipping push check.`);
|
|
1614
|
-
}
|
|
1615
|
-
const diff = execFileSync("git", ["diff", "--stat", `${remote}/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
1616
|
-
if (!diff) {
|
|
1617
|
-
return allow(`No file changes compared to ${remote}/${baseBranch}, skipping push check.`);
|
|
1618
|
-
}
|
|
1619
|
-
} catch {}
|
|
1620
|
-
let hasTracking = false;
|
|
1621
|
-
try {
|
|
1622
|
-
execFileSync("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
|
|
1623
|
-
cwd,
|
|
1624
|
-
encoding: "utf8",
|
|
1625
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1626
|
-
timeout: 3000
|
|
1627
|
-
});
|
|
1628
|
-
hasTracking = true;
|
|
1629
|
-
} catch {}
|
|
1630
|
-
if (!hasTracking) {
|
|
1631
|
-
return deny(`Branch "${branch}" has not been pushed to remote "${remote}". ` + `Run now: git push -u ${remote} ${branch}`);
|
|
1632
|
-
}
|
|
1633
|
-
const unpushed = execFileSync("git", ["log", `${remote}/${branch}..HEAD`, "--oneline"], {
|
|
1634
|
-
cwd,
|
|
1635
|
-
encoding: "utf8",
|
|
1636
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1637
|
-
timeout: 5000
|
|
1638
|
-
}).trim();
|
|
1639
|
-
if (unpushed.length > 0) {
|
|
1640
|
-
const commitCount = unpushed.split(`
|
|
1641
|
-
`).length;
|
|
1642
|
-
return deny(`You have ${commitCount} unpushed commit${commitCount > 1 ? "s" : ""} on branch "${branch}". ` + `Run now: git push`);
|
|
1643
|
-
}
|
|
1644
|
-
return allow(`All commits pushed to "${remote}".`);
|
|
1645
|
-
} catch {
|
|
1646
|
-
return allow("Could not check push status, skipping.");
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
function requirePrBeforeStop(ctx) {
|
|
1650
|
-
if (isPlanMode(ctx))
|
|
1651
|
-
return allow("Plan mode — no changes made, skipping PR check.");
|
|
1652
|
-
const cwd = ctx.session?.cwd;
|
|
1653
|
-
if (!cwd)
|
|
1654
|
-
return allow("No working directory available, skipping PR check.");
|
|
1655
|
-
try {
|
|
1656
|
-
try {
|
|
1657
|
-
execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
|
|
1658
|
-
} catch {
|
|
1659
|
-
return allow("GitHub CLI (gh) not installed, skipping PR check.");
|
|
1660
|
-
}
|
|
1661
|
-
const branch = getCurrentBranch(cwd);
|
|
1662
|
-
if (!branch || branch === "HEAD")
|
|
1663
|
-
return allow("Detached HEAD, skipping PR check.");
|
|
1664
|
-
const baseBranch = ctx.params?.baseBranch ?? "main";
|
|
1665
|
-
if (branch === baseBranch) {
|
|
1666
|
-
return allow(`On base branch "${baseBranch}", skipping PR check.`);
|
|
1667
|
-
}
|
|
1668
|
-
try {
|
|
1669
|
-
const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
1670
|
-
if (!ahead) {
|
|
1671
|
-
return allow(`No commits ahead of origin/${baseBranch}, skipping PR check.`);
|
|
1672
|
-
}
|
|
1673
|
-
const diff = execFileSync("git", ["diff", "--stat", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
1674
|
-
if (!diff) {
|
|
1675
|
-
return allow(`No file changes compared to origin/${baseBranch}, skipping PR check.`);
|
|
569
|
+
// src/hooks/policy-catalog.ts
|
|
570
|
+
var POLICY_CATALOG;
|
|
571
|
+
var init_policy_catalog = __esm(() => {
|
|
572
|
+
POLICY_CATALOG = [
|
|
573
|
+
{
|
|
574
|
+
name: "sanitize-jwt",
|
|
575
|
+
description: "Stop Claude from reading JWTs in tool responses",
|
|
576
|
+
displayTitle: "Redacted JWT tokens from tool output",
|
|
577
|
+
impact: "Stops the agent from echoing auth tokens it saw in command output.",
|
|
578
|
+
match: { events: ["PostToolUse"] },
|
|
579
|
+
defaultEnabled: true,
|
|
580
|
+
category: "Sanitize"
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
name: "sanitize-api-keys",
|
|
584
|
+
description: "Stop Claude from reading API keys (OpenAI, Anthropic, GitHub, AWS, Stripe, Google) in tool responses",
|
|
585
|
+
displayTitle: "Redacted API keys from tool output",
|
|
586
|
+
impact: "Catches OpenAI / Anthropic / GitHub / AWS / Stripe / Google keys before the model sees them.",
|
|
587
|
+
match: { events: ["PostToolUse"] },
|
|
588
|
+
defaultEnabled: true,
|
|
589
|
+
category: "Sanitize",
|
|
590
|
+
params: {
|
|
591
|
+
additionalPatterns: {
|
|
592
|
+
type: "pattern[]",
|
|
593
|
+
description: "Additional API key patterns to scrub, each with { regex, label }",
|
|
594
|
+
default: []
|
|
595
|
+
}
|
|
1676
596
|
}
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
return allow(`PR #${pr.number} is ${pr.state.toLowerCase()}; skipping conflict check.`);
|
|
1738
|
-
}
|
|
1739
|
-
try {
|
|
1740
|
-
execFileSync("git", ["rev-parse", "--verify", `origin/${baseBranch}`], {
|
|
1741
|
-
cwd,
|
|
1742
|
-
encoding: "utf8",
|
|
1743
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1744
|
-
timeout: 3000
|
|
1745
|
-
});
|
|
1746
|
-
const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
1747
|
-
if (ahead) {
|
|
1748
|
-
execFileSync("git", ["merge-tree", "--write-tree", "--name-only", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 1e4 });
|
|
1749
|
-
}
|
|
1750
|
-
} catch (err) {
|
|
1751
|
-
const e = err;
|
|
1752
|
-
if (e.status === 1) {
|
|
1753
|
-
const out = (typeof e.stdout === "string" ? e.stdout : e.stdout?.toString("utf8") ?? "").trim();
|
|
1754
|
-
const lines = out.split(`
|
|
1755
|
-
`);
|
|
1756
|
-
const files = [];
|
|
1757
|
-
for (let i = 1;i < lines.length; i++) {
|
|
1758
|
-
const line = lines[i];
|
|
1759
|
-
if (line === "")
|
|
1760
|
-
break;
|
|
1761
|
-
files.push(line);
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: "sanitize-connection-strings",
|
|
600
|
+
description: "Stop Claude from reading database connection strings with embedded credentials in tool responses",
|
|
601
|
+
displayTitle: "Redacted database connection strings from tool output",
|
|
602
|
+
impact: "Strips embedded DB credentials before they reach the model context.",
|
|
603
|
+
match: { events: ["PostToolUse"] },
|
|
604
|
+
defaultEnabled: true,
|
|
605
|
+
category: "Sanitize"
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
name: "sanitize-private-key-content",
|
|
609
|
+
description: "Stop Claude from reading PEM private key content in tool responses",
|
|
610
|
+
displayTitle: "Redacted PEM private keys from tool output",
|
|
611
|
+
impact: "Prevents private key bodies from being echoed into chat context.",
|
|
612
|
+
match: { events: ["PostToolUse"] },
|
|
613
|
+
defaultEnabled: true,
|
|
614
|
+
category: "Sanitize"
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: "sanitize-bearer-tokens",
|
|
618
|
+
displayTitle: "Redacted bearer tokens from tool output",
|
|
619
|
+
impact: "Strips Authorization: Bearer values before they hit the model.",
|
|
620
|
+
description: "Stop Claude from reading Authorization Bearer tokens in tool responses",
|
|
621
|
+
match: { events: ["PostToolUse"] },
|
|
622
|
+
defaultEnabled: true,
|
|
623
|
+
category: "Sanitize"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: "protect-env-vars",
|
|
627
|
+
displayTitle: "Tried to dump environment variables to chat",
|
|
628
|
+
impact: "Env vars often contain secrets; blocking `env` / `printenv` keeps them out of the model context.",
|
|
629
|
+
description: "Prevent commands that read environment variables",
|
|
630
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
631
|
+
defaultEnabled: true,
|
|
632
|
+
category: "Environment"
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: "block-env-files",
|
|
636
|
+
displayTitle: "Tried to read or write a .env file",
|
|
637
|
+
impact: "`.env` files routinely contain API keys and DB credentials.",
|
|
638
|
+
description: "Block reading/writing .env files",
|
|
639
|
+
match: { events: ["PreToolUse"] },
|
|
640
|
+
defaultEnabled: true,
|
|
641
|
+
category: "Environment"
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
name: "block-read-outside-cwd",
|
|
645
|
+
displayTitle: "Tried to read files outside your project directory",
|
|
646
|
+
impact: "Stops the agent from peeking at neighboring repos or your home directory.",
|
|
647
|
+
description: "Block file reads outside the session working directory",
|
|
648
|
+
match: { events: ["PreToolUse"], toolNames: ["Read", "Glob", "Grep", "Bash"] },
|
|
649
|
+
defaultEnabled: false,
|
|
650
|
+
category: "Environment",
|
|
651
|
+
params: {
|
|
652
|
+
allowPaths: {
|
|
653
|
+
type: "string[]",
|
|
654
|
+
description: "Absolute paths outside cwd that are allowed to be read",
|
|
655
|
+
default: []
|
|
656
|
+
}
|
|
1762
657
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
const cwd = ctx.session?.cwd;
|
|
1779
|
-
if (!cwd)
|
|
1780
|
-
return allow("No working directory available, skipping CI check.");
|
|
1781
|
-
try {
|
|
1782
|
-
try {
|
|
1783
|
-
execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
|
|
1784
|
-
} catch {
|
|
1785
|
-
return allow("GitHub CLI (gh) not installed, skipping CI check.");
|
|
1786
|
-
}
|
|
1787
|
-
const branch = getCurrentBranch(cwd);
|
|
1788
|
-
if (!branch || branch === "HEAD")
|
|
1789
|
-
return allow("Detached HEAD, skipping CI check.");
|
|
1790
|
-
const sha = getHeadSha(cwd);
|
|
1791
|
-
let workflowRuns = [];
|
|
1792
|
-
try {
|
|
1793
|
-
const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "20", "--json", "status,conclusion,name,headSha"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 }).trim();
|
|
1794
|
-
if (runsJson && runsJson !== "[]") {
|
|
1795
|
-
const allWorkflowRuns = JSON.parse(runsJson);
|
|
1796
|
-
const headRuns = sha ? allWorkflowRuns.filter((r) => r.headSha === sha) : allWorkflowRuns;
|
|
1797
|
-
const seen = new Set;
|
|
1798
|
-
workflowRuns = headRuns.filter((r) => {
|
|
1799
|
-
if (seen.has(r.name))
|
|
1800
|
-
return false;
|
|
1801
|
-
seen.add(r.name);
|
|
1802
|
-
return true;
|
|
1803
|
-
});
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
name: "block-sudo",
|
|
661
|
+
displayTitle: "Tried to run a command with sudo",
|
|
662
|
+
impact: "Sudo gives the agent root — blocked unless explicitly allow-listed.",
|
|
663
|
+
description: "Block sudo commands",
|
|
664
|
+
match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
|
|
665
|
+
defaultEnabled: true,
|
|
666
|
+
category: "Dangerous Commands",
|
|
667
|
+
params: {
|
|
668
|
+
allowPatterns: {
|
|
669
|
+
type: "string[]",
|
|
670
|
+
description: "Sudo command patterns to allow, matched token-by-token (e.g. 'sudo systemctl status')",
|
|
671
|
+
default: []
|
|
672
|
+
}
|
|
1804
673
|
}
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
const enabledSet = new Set(enabledNames.map(normalizePolicyName));
|
|
1832
|
-
for (const policy of BUILTIN_POLICIES) {
|
|
1833
|
-
if (enabledSet.has(normalizePolicyName(policy.name))) {
|
|
1834
|
-
registerPolicy(policy.name, policy.description, policy.fn, policy.match);
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
var isClaudeInternalPath, isClaudeSettingsFile, SHELL_OPERATORS, SHELL_METACHAR_RE, JWT_RE, API_KEY_PATTERNS, CONNECTION_STRING_RE, PRIVATE_KEY_RE, BEARER_TOKEN_RE, SECRET_PATTERNS, SQL_TOOL_RE, DESTRUCTIVE_SQL_RE, DELETE_NO_WHERE_RE, SQL_WHERE_RE, SCHEMA_ALTER_RE, PUBLISH_CMD_RE, ENV_PRINTENV_RE, ECHO_ENV_RE, EXPORT_RE, PS_ENV_VAR_RE, PS_CHILDITEM_ENV_RE, DOTNET_GETENV_RE, CMD_ECHO_ENV_RE, ENV_FILE_PATH_RE, ENV_CMD_RE, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, SEGMENT_SEPARATORS, COMMAND_PREFIX_TOKENS, SELF_BINARY_TOKEN_RE, CONFIG_SUBCOMMAND_RE, PAUSE_FLAG_RE, ENV_ASSIGNMENT_RE, RUNNER_OPERAND_RE, PS_WEB_PIPE_RE, SHORT_FLAG_BUNDLE_RE, SAFE_FORCE_PREFIXES, SECRET_FILE_RE, SECRET_FILE_ID_RSA_RE, SECRET_FILE_CREDENTIALS_RE, GIT_COMMIT_MERGE_RE, FAILPROOFAI_CLI_RE, FAILPROOFAI_UNINSTALL_RE, GIT_AMEND_RE, GIT_STASH_DROP_RE, GIT_ADD_ALL_RE, NPM_GLOBAL_RE, YARN_GLOBAL_RE, PNPM_GLOBAL_RE, BUN_GLOBAL_RE, CARGO_INSTALL_RE, PIP_SYSTEM_RE, PKG_MANAGER_DETECTORS, NOHUP_RE, SCREEN_DETACH_RE, TMUX_DETACH_RE, DISOWN_RE, BACKGROUND_AMPERSAND_RE, KUBECTL_RE, TERRAFORM_RE, AWS_CLI_RE, GCLOUD_RE, AZ_CLI_RE, HELM_RE, GH_PIPELINE_RE, gitBranchCache, GIT_BRANCH_CACHE_MAX_ENTRIES = 500, EVAL_FLAG_RE, HOME_PREFIX_RE, RM_CMD_RE, FIND_CMD_RE, FIND_EXEC_RE, FIND_GLOBAL_OPT_RE, FIND_EXPR_START_RE, SCRATCH_ROOTS, CATASTROPHIC_DEPTH = 2, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, BUILTIN_POLICIES;
|
|
1839
|
-
var init_builtin_policies = __esm(() => {
|
|
1840
|
-
init_hook_logger();
|
|
1841
|
-
isClaudeInternalPath = isAgentInternalPath;
|
|
1842
|
-
isClaudeSettingsFile = isAgentSettingsFile;
|
|
1843
|
-
SHELL_OPERATORS = new Set(["&&", "||", "|", ";"]);
|
|
1844
|
-
SHELL_METACHAR_RE = /[;&<>`$()\\]/;
|
|
1845
|
-
JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
|
|
1846
|
-
API_KEY_PATTERNS = [
|
|
1847
|
-
[/sk-ant-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
|
|
1848
|
-
[/sk-proj-[A-Za-z0-9\-_]{20,}/, "OpenAI project API key"],
|
|
1849
|
-
[/sk-[A-Za-z0-9]{20,}/, "OpenAI API key"],
|
|
1850
|
-
[/ghp_[A-Za-z0-9]{36}/, "GitHub personal access token"],
|
|
1851
|
-
[/github_pat_[A-Za-z0-9_]{82}/, "GitHub fine-grained token"],
|
|
1852
|
-
[/AKIA[A-Z0-9]{16}/, "AWS access key ID"],
|
|
1853
|
-
[/sk_live_[A-Za-z0-9]{24,}/, "Stripe live secret key"],
|
|
1854
|
-
[/sk_test_[A-Za-z0-9]{24,}/, "Stripe test secret key"],
|
|
1855
|
-
[/AIza[0-9A-Za-z\-_]{35}/, "Google API key"]
|
|
1856
|
-
];
|
|
1857
|
-
CONNECTION_STRING_RE = /(?:postgresql|postgres|mysql|mongodb(?:\+srv)?|redis|amqps?|smtps?):\/\/[^@\s]+@/;
|
|
1858
|
-
PRIVATE_KEY_RE = /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/;
|
|
1859
|
-
BEARER_TOKEN_RE = /Authorization:\s*Bearer\s+[A-Za-z0-9\-._~+/]{20,}/i;
|
|
1860
|
-
SECRET_PATTERNS = [
|
|
1861
|
-
[PRIVATE_KEY_RE, "private key"],
|
|
1862
|
-
[JWT_RE, "JWT"],
|
|
1863
|
-
[BEARER_TOKEN_RE, "bearer token"],
|
|
1864
|
-
[CONNECTION_STRING_RE, "database credentials"],
|
|
1865
|
-
...API_KEY_PATTERNS
|
|
1866
|
-
];
|
|
1867
|
-
SQL_TOOL_RE = /\b(?:psql|mysql|sqlite3|pgcli|clickhouse-client)\b/;
|
|
1868
|
-
DESTRUCTIVE_SQL_RE = /\b(?:DROP\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE\b)/i;
|
|
1869
|
-
DELETE_NO_WHERE_RE = /\bDELETE\s+FROM\b/i;
|
|
1870
|
-
SQL_WHERE_RE = /\bWHERE\b/i;
|
|
1871
|
-
SCHEMA_ALTER_RE = /\bALTER\s+TABLE\b[\s\S]*\b(?:DROP\s+COLUMN|ADD\s+COLUMN|RENAME\s+(?:COLUMN|TO)|MODIFY\s+COLUMN)\b/i;
|
|
1872
|
-
PUBLISH_CMD_RE = /(?:npm\s+publish|bun\s+publish|pnpm\s+publish|yarn\s+npm\s+publish|twine\s+upload|poetry\s+publish|cargo\s+publish|gem\s+push)\b/;
|
|
1873
|
-
ENV_PRINTENV_RE = /(?:^|\s|;|&&|\|\|)(?:env|printenv)(?:\s|$|;|&&|\|)/;
|
|
1874
|
-
ECHO_ENV_RE = /echo\s+.*\$\{?[A-Za-z_]/;
|
|
1875
|
-
EXPORT_RE = /(?:^|\s|;|&&|\|\|)export\s+\w+/;
|
|
1876
|
-
PS_ENV_VAR_RE = /\$env:[A-Za-z_]/i;
|
|
1877
|
-
PS_CHILDITEM_ENV_RE = /(?:Get-ChildItem|dir|gci|ls)\s+Env:/i;
|
|
1878
|
-
DOTNET_GETENV_RE = /\[Environment\]::GetEnvironment/i;
|
|
1879
|
-
CMD_ECHO_ENV_RE = /echo\s+%[A-Za-z_]/i;
|
|
1880
|
-
ENV_FILE_PATH_RE = /(?:^|[\\/])\.env(?:\.|$)/;
|
|
1881
|
-
ENV_CMD_RE = /\.env(?:\b|\s|$|\.)/;
|
|
1882
|
-
PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
|
|
1883
|
-
RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
|
|
1884
|
-
CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
|
|
1885
|
-
SEGMENT_SEPARATORS = /[;&|\n\r(){}`]+/;
|
|
1886
|
-
COMMAND_PREFIX_TOKENS = new Set([
|
|
1887
|
-
"npx",
|
|
1888
|
-
"bunx",
|
|
1889
|
-
"pnpx",
|
|
1890
|
-
"npm",
|
|
1891
|
-
"pnpm",
|
|
1892
|
-
"yarn",
|
|
1893
|
-
"dlx",
|
|
1894
|
-
"exec",
|
|
1895
|
-
"run",
|
|
1896
|
-
"node",
|
|
1897
|
-
"bun",
|
|
1898
|
-
"deno",
|
|
1899
|
-
"env",
|
|
1900
|
-
"command",
|
|
1901
|
-
"builtin",
|
|
1902
|
-
"nohup",
|
|
1903
|
-
"setsid",
|
|
1904
|
-
"time",
|
|
1905
|
-
"timeout",
|
|
1906
|
-
"nice",
|
|
1907
|
-
"stdbuf",
|
|
1908
|
-
"xargs",
|
|
1909
|
-
"sudo",
|
|
1910
|
-
"doas",
|
|
1911
|
-
"sh",
|
|
1912
|
-
"bash",
|
|
1913
|
-
"zsh",
|
|
1914
|
-
"dash",
|
|
1915
|
-
"ksh",
|
|
1916
|
-
"fish",
|
|
1917
|
-
"ash"
|
|
1918
|
-
]);
|
|
1919
|
-
SELF_BINARY_TOKEN_RE = /(?:^|\/)failproofai[^/]*$/;
|
|
1920
|
-
CONFIG_SUBCOMMAND_RE = /^(?:config|configure|setup)$/;
|
|
1921
|
-
PAUSE_FLAG_RE = /^--pause(?:=|$)/;
|
|
1922
|
-
ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
1923
|
-
RUNNER_OPERAND_RE = /^\d+[a-z]*$/i;
|
|
1924
|
-
PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
|
|
1925
|
-
SHORT_FLAG_BUNDLE_RE = /^-[a-zA-Z]*f[a-zA-Z]*$/;
|
|
1926
|
-
SAFE_FORCE_PREFIXES = ["--force-with-lease", "--force-if-includes"];
|
|
1927
|
-
SECRET_FILE_RE = /\.(?:pem|key)$/;
|
|
1928
|
-
SECRET_FILE_ID_RSA_RE = /id_rsa/;
|
|
1929
|
-
SECRET_FILE_CREDENTIALS_RE = /credentials/;
|
|
1930
|
-
GIT_COMMIT_MERGE_RE = /git\s+(commit|merge|rebase|cherry-pick)\b/;
|
|
1931
|
-
FAILPROOFAI_CLI_RE = /(?:^|;|&&|\|\||\|)\s*failproofai(?:\s|$)/;
|
|
1932
|
-
FAILPROOFAI_UNINSTALL_RE = /(?:npm\s+(?:uninstall|remove|un|r)\s.*failproofai|bun\s+remove\s.*failproofai|yarn\s+global\s+remove\s+failproofai|pnpm\s+(?:remove|uninstall|un)\s.*failproofai)/;
|
|
1933
|
-
GIT_AMEND_RE = /\bgit\s+commit\b.*--amend\b/;
|
|
1934
|
-
GIT_STASH_DROP_RE = /\bgit\s+stash\s+(?:drop|clear)\b/;
|
|
1935
|
-
GIT_ADD_ALL_RE = /\bgit\s+add\s+(?:-A\b|--all\b|\.(?:\s|$|;|&&|\|\|))/;
|
|
1936
|
-
NPM_GLOBAL_RE = /\bnpm\s+(?:install|i)\b(?=.*(?:\s-g\b|--global\b))/;
|
|
1937
|
-
YARN_GLOBAL_RE = /\byarn\s+global\s+add\b/;
|
|
1938
|
-
PNPM_GLOBAL_RE = /\bpnpm\s+(?:add|install|i)\b(?=.*(?:\s-g\b|--global\b))/;
|
|
1939
|
-
BUN_GLOBAL_RE = /\bbun\s+(?:install|add)\b(?=.*(?:\s-g\b|--global\b))/;
|
|
1940
|
-
CARGO_INSTALL_RE = /\bcargo\s+install\b/;
|
|
1941
|
-
PIP_SYSTEM_RE = /\bpip(?:3)?\s+install\b(?=.*(?:--user\b|--break-system-packages\b))/;
|
|
1942
|
-
PKG_MANAGER_DETECTORS = {
|
|
1943
|
-
pip: [/\bpip\b/, /\bpip3\b/, /\bpython3?\s+-m\s+pip\b/],
|
|
1944
|
-
npm: [/\bnpm\b/, /\bnpx\b/],
|
|
1945
|
-
yarn: [/\byarn\b/],
|
|
1946
|
-
pnpm: [/\bpnpm\b/, /\bpnpx\b/],
|
|
1947
|
-
bun: [/\bbun\b/, /\bbunx\b/],
|
|
1948
|
-
uv: [/\buv\b/],
|
|
1949
|
-
poetry: [/\bpoetry\b/],
|
|
1950
|
-
pipenv: [/\bpipenv\b/],
|
|
1951
|
-
conda: [/\bconda\b/],
|
|
1952
|
-
cargo: [/\bcargo\b/]
|
|
1953
|
-
};
|
|
1954
|
-
NOHUP_RE = /\bnohup\s+\S/;
|
|
1955
|
-
SCREEN_DETACH_RE = /\bscreen\s+-[A-Za-z]*d[A-Za-z]*\b/;
|
|
1956
|
-
TMUX_DETACH_RE = /\btmux\s+(?:new-session|new)\b[^|&;]*-d\b/;
|
|
1957
|
-
DISOWN_RE = /\bdisown\b/;
|
|
1958
|
-
BACKGROUND_AMPERSAND_RE = /(?<![&|])\s?&\s*(?:$|#|;)/;
|
|
1959
|
-
KUBECTL_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*kubectl(?:\s|$)/;
|
|
1960
|
-
TERRAFORM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*(?:terraform|tofu)(?:\s|$)/;
|
|
1961
|
-
AWS_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*aws(?:\s|$)/;
|
|
1962
|
-
GCLOUD_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gcloud(?:\s|$)/;
|
|
1963
|
-
AZ_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*az(?:\s|$)/;
|
|
1964
|
-
HELM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*helm(?:\s|$)/;
|
|
1965
|
-
GH_PIPELINE_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gh\s+(?:workflow\s+(?:run|enable|disable)|run\s+(?:rerun|cancel)|pr\s+merge|release\s+(?:create|delete)|cache\s+delete|secret\s+(?:set|delete))\b/;
|
|
1966
|
-
gitBranchCache = new Map;
|
|
1967
|
-
EVAL_FLAG_RE = /^-{1,2}c$/;
|
|
1968
|
-
HOME_PREFIX_RE = /^(?:~[A-Za-z0-9_.-]*|\$HOME|\$\{HOME\})(?=$|\/)/;
|
|
1969
|
-
RM_CMD_RE = /^(?:\/\S*\/)?rm$/;
|
|
1970
|
-
FIND_CMD_RE = /^(?:\/\S*\/)?find$/;
|
|
1971
|
-
FIND_EXEC_RE = /^-(?:exec|execdir|ok|okdir)$/;
|
|
1972
|
-
FIND_GLOBAL_OPT_RE = /^-(?:[HLP]|D|O\d*)$/;
|
|
1973
|
-
FIND_EXPR_START_RE = /^(?:-|\\?[(!])/;
|
|
1974
|
-
SCRATCH_ROOTS = ["/tmp", "/var/tmp"];
|
|
1975
|
-
READ_LIKE_CMDS = /(?:^|;|&&|\|\||\|)\s*(?:ls|find|cat|head|tail|less|more|wc|file|stat|tree|du)\s/;
|
|
1976
|
-
SEGMENT_SPLIT_RE = /\s*(?:&&|\|\||\||;)\s*/;
|
|
1977
|
-
BUILTIN_POLICIES = [
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
name: "block-curl-pipe-sh",
|
|
677
|
+
displayTitle: "Tried to pipe a downloaded script straight to a shell",
|
|
678
|
+
impact: "`curl ... | sh` runs unverified remote code on your machine.",
|
|
679
|
+
description: "Block piping downloads to shell",
|
|
680
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
681
|
+
defaultEnabled: true,
|
|
682
|
+
category: "Dangerous Commands"
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
name: "block-rm-rf",
|
|
686
|
+
displayTitle: "Tried to recursively delete a system path",
|
|
687
|
+
impact: "Catches catastrophic `rm -rf /` and Windows equivalents.",
|
|
688
|
+
description: "Prevent catastrophic deletions",
|
|
689
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
690
|
+
defaultEnabled: false,
|
|
691
|
+
category: "Dangerous Commands",
|
|
692
|
+
params: {
|
|
693
|
+
allowPaths: {
|
|
694
|
+
type: "string[]",
|
|
695
|
+
description: "Paths that are allowed to be recursively deleted",
|
|
696
|
+
default: []
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
},
|
|
1978
700
|
{
|
|
1979
|
-
name: "
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
match: { events: ["PostToolUse"] },
|
|
701
|
+
name: "block-failproofai-commands",
|
|
702
|
+
displayTitle: "Tried to disable, pause or modify failproofai itself",
|
|
703
|
+
impact: "An agent that can pause or remove enforcement can switch off every other policy.",
|
|
704
|
+
description: "Block failproofai CLI commands, self-pause and uninstallation",
|
|
705
|
+
match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
|
|
1985
706
|
defaultEnabled: true,
|
|
1986
|
-
|
|
707
|
+
alwaysOn: true,
|
|
708
|
+
category: "Dangerous Commands"
|
|
1987
709
|
},
|
|
1988
710
|
{
|
|
1989
|
-
name: "
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
711
|
+
name: "block-kubectl",
|
|
712
|
+
displayTitle: "Tried to run a Kubernetes command",
|
|
713
|
+
impact: "kubectl can change live cluster state — gated unless allow-listed.",
|
|
714
|
+
description: "Block kubectl commands (Kubernetes cluster mutations)",
|
|
715
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
716
|
+
defaultEnabled: false,
|
|
717
|
+
category: "Infra Commands",
|
|
718
|
+
params: {
|
|
719
|
+
allowPatterns: {
|
|
720
|
+
type: "string[]",
|
|
721
|
+
description: "kubectl command patterns to allow, matched token-by-token (e.g. 'kubectl get *', 'kubectl describe *')",
|
|
722
|
+
default: []
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
name: "block-terraform",
|
|
728
|
+
displayTitle: "Tried to run a Terraform/OpenTofu command",
|
|
729
|
+
impact: "Terraform mutates real infrastructure — gated unless allow-listed.",
|
|
730
|
+
description: "Block terraform and tofu (OpenTofu) commands",
|
|
731
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
732
|
+
defaultEnabled: false,
|
|
733
|
+
category: "Infra Commands",
|
|
734
|
+
params: {
|
|
735
|
+
allowPatterns: {
|
|
736
|
+
type: "string[]",
|
|
737
|
+
description: "terraform/tofu command patterns to allow (e.g. 'terraform plan', 'terraform validate')",
|
|
738
|
+
default: []
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
name: "block-aws-cli",
|
|
744
|
+
displayTitle: "Tried to run an AWS CLI command",
|
|
745
|
+
impact: "AWS CLI can spend money or break prod — gated.",
|
|
746
|
+
description: "Block aws CLI commands",
|
|
747
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
748
|
+
defaultEnabled: false,
|
|
749
|
+
category: "Infra Commands",
|
|
750
|
+
params: {
|
|
751
|
+
allowPatterns: {
|
|
752
|
+
type: "string[]",
|
|
753
|
+
description: "aws CLI command patterns to allow (e.g. 'aws s3 ls *', 'aws sts get-caller-identity')",
|
|
754
|
+
default: []
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
name: "block-gcloud",
|
|
760
|
+
displayTitle: "Tried to run a Google Cloud command",
|
|
761
|
+
impact: "gcloud can spend money or break prod — gated.",
|
|
762
|
+
description: "Block gcloud (Google Cloud) CLI commands",
|
|
763
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
764
|
+
defaultEnabled: false,
|
|
765
|
+
category: "Infra Commands",
|
|
766
|
+
params: {
|
|
767
|
+
allowPatterns: {
|
|
768
|
+
type: "string[]",
|
|
769
|
+
description: "gcloud command patterns to allow (e.g. 'gcloud auth list', 'gcloud config list')",
|
|
770
|
+
default: []
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
name: "block-az-cli",
|
|
776
|
+
displayTitle: "Tried to run an Azure CLI command",
|
|
777
|
+
impact: "az can spend money or break prod — gated.",
|
|
778
|
+
description: "Block az (Azure) CLI commands",
|
|
779
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
780
|
+
defaultEnabled: false,
|
|
781
|
+
category: "Infra Commands",
|
|
782
|
+
params: {
|
|
783
|
+
allowPatterns: {
|
|
784
|
+
type: "string[]",
|
|
785
|
+
description: "az CLI command patterns to allow (e.g. 'az account show', 'az group list')",
|
|
786
|
+
default: []
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
name: "block-helm",
|
|
792
|
+
displayTitle: "Tried to run a Helm command",
|
|
793
|
+
impact: "Helm releases mutate cluster state — gated.",
|
|
794
|
+
description: "Block helm commands",
|
|
795
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
796
|
+
defaultEnabled: false,
|
|
797
|
+
category: "Infra Commands",
|
|
798
|
+
params: {
|
|
799
|
+
allowPatterns: {
|
|
800
|
+
type: "string[]",
|
|
801
|
+
description: "helm command patterns to allow (e.g. 'helm list', 'helm status *')",
|
|
802
|
+
default: []
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
name: "block-gh-pipeline",
|
|
808
|
+
displayTitle: "Tried to run a privileged GitHub CLI pipeline command",
|
|
809
|
+
impact: "Catches `gh workflow run`, `gh pr merge`, `gh secret set`, etc.",
|
|
810
|
+
description: "Block gh CLI pipeline-trigger subcommands (workflow run, run rerun/cancel, pr merge, release create/delete, cache delete, secret set/delete)",
|
|
811
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
812
|
+
defaultEnabled: false,
|
|
813
|
+
category: "Infra Commands",
|
|
814
|
+
params: {
|
|
815
|
+
allowPatterns: {
|
|
816
|
+
type: "string[]",
|
|
817
|
+
description: "gh pipeline command patterns to allow (e.g. specific scripted invocations); read-only gh subcommands like 'gh pr view' and 'gh run list' are not matched by this policy",
|
|
818
|
+
default: []
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
name: "block-secrets-write",
|
|
824
|
+
displayTitle: "Tried to write a secret-key file",
|
|
825
|
+
impact: "Stops the agent from creating `.pem`, `id_rsa`, `credentials.json`, etc.",
|
|
826
|
+
description: "Block writing secret key files",
|
|
827
|
+
match: { events: ["PreToolUse"], toolNames: ["Write"] },
|
|
828
|
+
defaultEnabled: false,
|
|
829
|
+
category: "Dangerous Commands",
|
|
1997
830
|
params: {
|
|
1998
831
|
additionalPatterns: {
|
|
1999
|
-
type: "
|
|
2000
|
-
description: "Additional
|
|
832
|
+
type: "string[]",
|
|
833
|
+
description: "Additional filename patterns (substrings) to block",
|
|
2001
834
|
default: []
|
|
2002
835
|
}
|
|
2003
836
|
}
|
|
2004
837
|
},
|
|
2005
838
|
{
|
|
2006
|
-
name: "
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
match: { events: ["PostToolUse"] },
|
|
839
|
+
name: "block-push-master",
|
|
840
|
+
displayTitle: "Tried to push directly to main/master",
|
|
841
|
+
impact: "Direct pushes to a protected branch bypass review.",
|
|
842
|
+
description: "Block pushing to main/master",
|
|
843
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
2012
844
|
defaultEnabled: true,
|
|
2013
|
-
category: "
|
|
845
|
+
category: "Git",
|
|
846
|
+
params: {
|
|
847
|
+
protectedBranches: {
|
|
848
|
+
type: "string[]",
|
|
849
|
+
description: "Branch names to protect from direct pushes",
|
|
850
|
+
default: ["main", "master"]
|
|
851
|
+
}
|
|
852
|
+
}
|
|
2014
853
|
},
|
|
2015
854
|
{
|
|
2016
|
-
name: "
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
category: "Sanitize"
|
|
855
|
+
name: "block-force-push",
|
|
856
|
+
displayTitle: "Tried to force-push",
|
|
857
|
+
impact: "Force-pushes rewrite history and can clobber teammates' work.",
|
|
858
|
+
description: "Prevent force-pushing to any branch",
|
|
859
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
860
|
+
defaultEnabled: false,
|
|
861
|
+
category: "Git"
|
|
2024
862
|
},
|
|
2025
863
|
{
|
|
2026
|
-
name: "
|
|
2027
|
-
displayTitle: "
|
|
2028
|
-
impact: "
|
|
2029
|
-
description: "
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
864
|
+
name: "block-work-on-main",
|
|
865
|
+
displayTitle: "Tried to commit or merge on main/master",
|
|
866
|
+
impact: "Work should land via PR — direct commits skip review.",
|
|
867
|
+
description: "Block git commits and merges on main/master branch",
|
|
868
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
869
|
+
defaultEnabled: false,
|
|
870
|
+
category: "Git",
|
|
871
|
+
params: {
|
|
872
|
+
protectedBranches: {
|
|
873
|
+
type: "string[]",
|
|
874
|
+
description: "Branch names where commits/merges are blocked",
|
|
875
|
+
default: ["main", "master"]
|
|
876
|
+
}
|
|
877
|
+
}
|
|
2034
878
|
},
|
|
2035
879
|
{
|
|
2036
|
-
name: "
|
|
2037
|
-
displayTitle: "
|
|
2038
|
-
impact: "
|
|
2039
|
-
description: "
|
|
2040
|
-
fn: protectEnvVars,
|
|
880
|
+
name: "warn-git-amend",
|
|
881
|
+
displayTitle: "Used git commit --amend",
|
|
882
|
+
impact: "Amending after a push rewrites history that others may have pulled.",
|
|
883
|
+
description: "Warns before amending git commits, which rewrites history",
|
|
2041
884
|
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
2042
|
-
defaultEnabled:
|
|
2043
|
-
category: "
|
|
885
|
+
defaultEnabled: false,
|
|
886
|
+
category: "Git"
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
name: "warn-git-stash-drop",
|
|
890
|
+
displayTitle: "Tried to drop or clear git stash",
|
|
891
|
+
impact: "Stash deletions are permanent and silent.",
|
|
892
|
+
description: "Warns before permanently deleting stashed changes",
|
|
893
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
894
|
+
defaultEnabled: false,
|
|
895
|
+
category: "Git"
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
name: "warn-all-files-staged",
|
|
899
|
+
displayTitle: "Staged all files with git add -A / .",
|
|
900
|
+
impact: "Wide stages routinely catch generated files or secrets you didn't intend to commit.",
|
|
901
|
+
description: "Warns before staging all working tree files with git add -A / . / --all",
|
|
902
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
903
|
+
defaultEnabled: false,
|
|
904
|
+
category: "Git"
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
name: "warn-destructive-sql",
|
|
908
|
+
displayTitle: "Ran destructive SQL (DROP / TRUNCATE / DELETE without WHERE)",
|
|
909
|
+
impact: "Easy way to wipe a table by accident.",
|
|
910
|
+
description: "Warn before executing destructive SQL (DROP/TRUNCATE/DELETE without WHERE) via database clients",
|
|
911
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
912
|
+
defaultEnabled: false,
|
|
913
|
+
category: "Database"
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
name: "warn-schema-alteration",
|
|
917
|
+
displayTitle: "Altered a database schema column",
|
|
918
|
+
impact: "ALTER TABLE operations can lock tables and break readers.",
|
|
919
|
+
description: "Warns before SQL schema changes (ALTER TABLE with column or rename operations)",
|
|
920
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
921
|
+
defaultEnabled: false,
|
|
922
|
+
category: "Database"
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
name: "warn-package-publish",
|
|
926
|
+
displayTitle: "Tried to publish a package",
|
|
927
|
+
impact: "Publishes are irreversible — `npm publish` / `cargo publish` shouldn't happen without intent.",
|
|
928
|
+
description: "Warn before publishing packages to public registries (npm, PyPI, crates.io, RubyGems, etc.)",
|
|
929
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
930
|
+
defaultEnabled: false,
|
|
931
|
+
category: "Packages & System"
|
|
2044
932
|
},
|
|
2045
933
|
{
|
|
2046
|
-
name: "
|
|
2047
|
-
displayTitle: "
|
|
2048
|
-
impact: "
|
|
2049
|
-
description: "
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
category: "Environment"
|
|
934
|
+
name: "warn-global-package-install",
|
|
935
|
+
displayTitle: "Installed a package globally",
|
|
936
|
+
impact: "`npm i -g`, `cargo install`, `pip --user` pollute your machine outside the project.",
|
|
937
|
+
description: "Warns before installing packages globally (npm -g, cargo install, etc.)",
|
|
938
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
939
|
+
defaultEnabled: false,
|
|
940
|
+
category: "Packages & System"
|
|
2054
941
|
},
|
|
2055
942
|
{
|
|
2056
|
-
name: "
|
|
2057
|
-
displayTitle: "
|
|
2058
|
-
impact: "
|
|
2059
|
-
description: "
|
|
2060
|
-
|
|
2061
|
-
match: { events: ["PreToolUse"], toolNames: ["Read", "Glob", "Grep", "Bash"] },
|
|
943
|
+
name: "prefer-package-manager",
|
|
944
|
+
displayTitle: "Used a non-preferred package manager",
|
|
945
|
+
impact: "Mixing package managers creates lockfile churn for your team.",
|
|
946
|
+
description: "Blocks non-preferred package managers and tells Claude to use an allowed one (e.g., uv instead of pip)",
|
|
947
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
2062
948
|
defaultEnabled: false,
|
|
2063
|
-
category: "
|
|
949
|
+
category: "Packages & System",
|
|
2064
950
|
params: {
|
|
2065
|
-
|
|
951
|
+
allowed: {
|
|
2066
952
|
type: "string[]",
|
|
2067
|
-
description: "
|
|
953
|
+
description: "Allowed package manager names (e.g. ['uv', 'bun']). Any detected manager not in this list is blocked.",
|
|
954
|
+
default: []
|
|
955
|
+
},
|
|
956
|
+
blocked: {
|
|
957
|
+
type: "string[]",
|
|
958
|
+
description: "Additional manager names to block beyond the built-in list (e.g. ['pdm', 'pipx']).",
|
|
2068
959
|
default: []
|
|
2069
960
|
}
|
|
2070
961
|
}
|
|
2071
962
|
},
|
|
2072
963
|
{
|
|
2073
|
-
name: "
|
|
2074
|
-
displayTitle: "
|
|
2075
|
-
impact: "
|
|
2076
|
-
description: "
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
category: "Dangerous Commands"
|
|
2081
|
-
},
|
|
2082
|
-
{
|
|
2083
|
-
name: "block-sudo",
|
|
2084
|
-
displayTitle: "Tried to run a command with sudo",
|
|
2085
|
-
impact: "Sudo gives the agent root — blocked unless explicitly allow-listed.",
|
|
2086
|
-
description: "Block sudo commands",
|
|
2087
|
-
fn: blockSudo,
|
|
2088
|
-
match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
|
|
2089
|
-
defaultEnabled: true,
|
|
2090
|
-
category: "Dangerous Commands",
|
|
964
|
+
name: "warn-large-file-write",
|
|
965
|
+
displayTitle: "Wrote a file larger than the configured threshold",
|
|
966
|
+
impact: "Catches accidentally large file writes (logs, binaries, model dumps).",
|
|
967
|
+
description: "Warn before writing files larger than 1MB (configurable via thresholdKb param)",
|
|
968
|
+
match: { events: ["PreToolUse"], toolNames: ["Write"] },
|
|
969
|
+
defaultEnabled: false,
|
|
970
|
+
category: "Packages & System",
|
|
2091
971
|
params: {
|
|
2092
|
-
|
|
2093
|
-
type: "
|
|
2094
|
-
description: "
|
|
2095
|
-
default:
|
|
972
|
+
thresholdKb: {
|
|
973
|
+
type: "number",
|
|
974
|
+
description: "File size threshold in KB above which a warning is issued",
|
|
975
|
+
default: 1024
|
|
2096
976
|
}
|
|
2097
977
|
}
|
|
2098
978
|
},
|
|
2099
979
|
{
|
|
2100
|
-
name: "
|
|
2101
|
-
displayTitle: "
|
|
2102
|
-
impact: "`
|
|
2103
|
-
description: "
|
|
2104
|
-
fn: blockCurlPipeSh,
|
|
980
|
+
name: "warn-background-process",
|
|
981
|
+
displayTitle: "Started a long-lived background process",
|
|
982
|
+
impact: "Catches `nohup` / `&` / `screen` / `tmux` / `disown` patterns that the agent often forgets to clean up.",
|
|
983
|
+
description: "Warns before starting detached or background processes",
|
|
2105
984
|
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
2106
|
-
defaultEnabled:
|
|
2107
|
-
category: "
|
|
985
|
+
defaultEnabled: false,
|
|
986
|
+
category: "Packages & System"
|
|
2108
987
|
},
|
|
2109
988
|
{
|
|
2110
|
-
name: "
|
|
2111
|
-
displayTitle: "
|
|
2112
|
-
impact: "
|
|
2113
|
-
description: "
|
|
2114
|
-
|
|
2115
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
989
|
+
name: "warn-repeated-tool-calls",
|
|
990
|
+
displayTitle: "Called the same tool 3+ times with identical arguments",
|
|
991
|
+
impact: "Usually a sign of a stuck loop burning tokens.",
|
|
992
|
+
description: "Warn when the same tool is called 3+ times with identical parameters",
|
|
993
|
+
match: { events: ["PreToolUse"] },
|
|
2116
994
|
defaultEnabled: false,
|
|
2117
|
-
category: "
|
|
995
|
+
category: "AI Behavior"
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
name: "require-commit-before-stop",
|
|
999
|
+
displayTitle: "Stopped with uncommitted changes",
|
|
1000
|
+
impact: "Work not in a commit is invisible to teammates and easy to lose.",
|
|
1001
|
+
description: "Require all changes to be committed before Claude stops",
|
|
1002
|
+
match: { events: ["Stop"] },
|
|
1003
|
+
defaultEnabled: false,
|
|
1004
|
+
category: "Workflow"
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
name: "require-push-before-stop",
|
|
1008
|
+
displayTitle: "Stopped with unpushed commits",
|
|
1009
|
+
impact: "Local-only commits won't trigger CI or be reviewable.",
|
|
1010
|
+
description: "Require all commits to be pushed to remote before Claude stops",
|
|
1011
|
+
match: { events: ["Stop"] },
|
|
1012
|
+
defaultEnabled: false,
|
|
1013
|
+
category: "Workflow",
|
|
2118
1014
|
params: {
|
|
2119
|
-
|
|
2120
|
-
type: "string
|
|
2121
|
-
description: "
|
|
2122
|
-
default:
|
|
1015
|
+
remote: {
|
|
1016
|
+
type: "string",
|
|
1017
|
+
description: "Remote name to push to (default: origin)",
|
|
1018
|
+
default: "origin"
|
|
1019
|
+
},
|
|
1020
|
+
baseBranch: {
|
|
1021
|
+
type: "string",
|
|
1022
|
+
description: "Base branch to compare against (default: main)",
|
|
1023
|
+
default: "main"
|
|
2123
1024
|
}
|
|
2124
1025
|
}
|
|
2125
1026
|
},
|
|
2126
1027
|
{
|
|
2127
|
-
name: "
|
|
2128
|
-
displayTitle: "
|
|
2129
|
-
impact: "
|
|
2130
|
-
description: "
|
|
2131
|
-
|
|
2132
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
2133
|
-
defaultEnabled: true,
|
|
2134
|
-
category: "Dangerous Commands"
|
|
2135
|
-
},
|
|
2136
|
-
{
|
|
2137
|
-
name: "block-kubectl",
|
|
2138
|
-
displayTitle: "Tried to run a Kubernetes command",
|
|
2139
|
-
impact: "kubectl can change live cluster state — gated unless allow-listed.",
|
|
2140
|
-
description: "Block kubectl commands (Kubernetes cluster mutations)",
|
|
2141
|
-
fn: blockKubectl,
|
|
2142
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1028
|
+
name: "require-pr-before-stop",
|
|
1029
|
+
displayTitle: "Stopped without a PR for the branch",
|
|
1030
|
+
impact: "Branches without PRs don't get reviewed.",
|
|
1031
|
+
description: "Require a pull request to exist for the current branch before Claude stops",
|
|
1032
|
+
match: { events: ["Stop"] },
|
|
2143
1033
|
defaultEnabled: false,
|
|
2144
|
-
category: "
|
|
1034
|
+
category: "Workflow",
|
|
2145
1035
|
params: {
|
|
2146
|
-
|
|
2147
|
-
type: "string
|
|
2148
|
-
description: "
|
|
2149
|
-
default:
|
|
1036
|
+
baseBranch: {
|
|
1037
|
+
type: "string",
|
|
1038
|
+
description: "Base branch to compare against (default: main)",
|
|
1039
|
+
default: "main"
|
|
2150
1040
|
}
|
|
2151
1041
|
}
|
|
2152
1042
|
},
|
|
2153
1043
|
{
|
|
2154
|
-
name: "
|
|
2155
|
-
displayTitle: "
|
|
2156
|
-
impact: "
|
|
2157
|
-
description: "
|
|
2158
|
-
|
|
2159
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1044
|
+
name: "require-no-conflicts-before-stop",
|
|
1045
|
+
displayTitle: "Stopped with a branch that conflicts with main",
|
|
1046
|
+
impact: "Conflicting branches can't merge — surface them early.",
|
|
1047
|
+
description: "Require the current branch to merge cleanly with the base branch before Claude stops",
|
|
1048
|
+
match: { events: ["Stop"] },
|
|
2160
1049
|
defaultEnabled: false,
|
|
2161
|
-
category: "
|
|
1050
|
+
category: "Workflow",
|
|
2162
1051
|
params: {
|
|
2163
|
-
|
|
2164
|
-
type: "string
|
|
2165
|
-
description: "
|
|
2166
|
-
default:
|
|
1052
|
+
baseBranch: {
|
|
1053
|
+
type: "string",
|
|
1054
|
+
description: "Base branch to check for conflicts against (default: main)",
|
|
1055
|
+
default: "main"
|
|
2167
1056
|
}
|
|
2168
1057
|
}
|
|
2169
|
-
},
|
|
2170
|
-
{
|
|
2171
|
-
name: "
|
|
2172
|
-
displayTitle: "
|
|
2173
|
-
impact: "
|
|
2174
|
-
description: "
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
name: "require-ci-green-before-stop",
|
|
1061
|
+
displayTitle: "Stopped with failing CI",
|
|
1062
|
+
impact: "Failing CI blocks deploy.",
|
|
1063
|
+
description: "Require CI checks to pass on the current HEAD commit before Claude stops (ignores stale runs on prior commits)",
|
|
1064
|
+
match: { events: ["Stop"] },
|
|
1065
|
+
defaultEnabled: false,
|
|
1066
|
+
category: "Workflow"
|
|
1067
|
+
}
|
|
1068
|
+
];
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
// src/hooks/policy-helpers.ts
|
|
1072
|
+
function allow(reason) {
|
|
1073
|
+
return reason ? { decision: "allow", reason } : { decision: "allow" };
|
|
1074
|
+
}
|
|
1075
|
+
function deny(reason) {
|
|
1076
|
+
return { decision: "deny", reason };
|
|
1077
|
+
}
|
|
1078
|
+
function instruct(reason) {
|
|
1079
|
+
return { decision: "instruct", reason };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// src/hooks/policy-registry.ts
|
|
1083
|
+
function normalizePolicyName(name) {
|
|
1084
|
+
return name.includes("/") ? name : `${DEFAULT_POLICY_NAMESPACE}/${name}`;
|
|
1085
|
+
}
|
|
1086
|
+
function getIndexCache() {
|
|
1087
|
+
return globalThis[INDEX_CACHE_KEY];
|
|
1088
|
+
}
|
|
1089
|
+
function setIndexCache(cache) {
|
|
1090
|
+
globalThis[INDEX_CACHE_KEY] = cache;
|
|
1091
|
+
}
|
|
1092
|
+
function getRegistry() {
|
|
1093
|
+
const g = globalThis;
|
|
1094
|
+
if (!g[REGISTRY_KEY]) {
|
|
1095
|
+
g[REGISTRY_KEY] = [];
|
|
1096
|
+
}
|
|
1097
|
+
return g[REGISTRY_KEY];
|
|
1098
|
+
}
|
|
1099
|
+
function registerPolicy(name, description, fn, match, priority = 0, params) {
|
|
1100
|
+
const canonical = normalizePolicyName(name);
|
|
1101
|
+
const registry = getRegistry();
|
|
1102
|
+
const idx = registry.findIndex((p) => p.name === canonical);
|
|
1103
|
+
const entry = {
|
|
1104
|
+
name: canonical,
|
|
1105
|
+
description,
|
|
1106
|
+
fn,
|
|
1107
|
+
match,
|
|
1108
|
+
priority,
|
|
1109
|
+
...params ? { params } : {}
|
|
1110
|
+
};
|
|
1111
|
+
if (idx >= 0) {
|
|
1112
|
+
registry[idx] = entry;
|
|
1113
|
+
} else {
|
|
1114
|
+
registry.push(entry);
|
|
1115
|
+
}
|
|
1116
|
+
setIndexCache(null);
|
|
1117
|
+
}
|
|
1118
|
+
function getPoliciesForEvent(eventType, toolName) {
|
|
1119
|
+
let cache = getIndexCache();
|
|
1120
|
+
if (!cache) {
|
|
1121
|
+
cache = new Map;
|
|
1122
|
+
setIndexCache(cache);
|
|
1123
|
+
}
|
|
1124
|
+
const key = `${eventType}:${toolName ?? ""}`;
|
|
1125
|
+
const cached = cache.get(key);
|
|
1126
|
+
if (cached)
|
|
1127
|
+
return cached;
|
|
1128
|
+
const result = getRegistry().filter((p) => {
|
|
1129
|
+
if (p.match.events && p.match.events.length > 0) {
|
|
1130
|
+
if (!p.match.events.includes(eventType))
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
if (p.match.toolNames && p.match.toolNames.length > 0) {
|
|
1134
|
+
if (!toolName || !p.match.toolNames.includes(toolName))
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
return true;
|
|
1138
|
+
}).sort((a, b) => b.priority - a.priority);
|
|
1139
|
+
cache.set(key, result);
|
|
1140
|
+
return result;
|
|
1141
|
+
}
|
|
1142
|
+
function clearPolicies() {
|
|
1143
|
+
const g = globalThis;
|
|
1144
|
+
g[REGISTRY_KEY] = [];
|
|
1145
|
+
setIndexCache(null);
|
|
1146
|
+
}
|
|
1147
|
+
var REGISTRY_KEY = "__FAILPROOFAI_POLICY_REGISTRY__", INDEX_CACHE_KEY = "__FAILPROOFAI_POLICY_INDEX_CACHE__", DEFAULT_POLICY_NAMESPACE = "failproofai";
|
|
1148
|
+
|
|
1149
|
+
// src/hooks/builtin-policies.ts
|
|
1150
|
+
import { resolve as resolve3, join as join2 } from "node:path";
|
|
1151
|
+
import { statSync as statSync3 } from "node:fs";
|
|
1152
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
1153
|
+
import { execSync, execFileSync } from "node:child_process";
|
|
1154
|
+
import { homedir as homedir3 } from "node:os";
|
|
1155
|
+
function isAgentInternalPath(resolved2) {
|
|
1156
|
+
const normResolved = resolved2.replaceAll("\\", "/");
|
|
1157
|
+
for (const dir of [".claude", ".codex", ".copilot", ".cursor", ".opencode", ".pi", ".gemini"]) {
|
|
1158
|
+
const root = join2(homedir3(), dir).replaceAll("\\", "/");
|
|
1159
|
+
if (normResolved === root || normResolved.startsWith(root + "/"))
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1162
|
+
for (const sub of [join2(".config", "opencode"), join2(".local", "share", "opencode")]) {
|
|
1163
|
+
const root = join2(homedir3(), sub).replaceAll("\\", "/");
|
|
1164
|
+
if (normResolved === root || normResolved.startsWith(root + "/"))
|
|
1165
|
+
return true;
|
|
1166
|
+
}
|
|
1167
|
+
return false;
|
|
1168
|
+
}
|
|
1169
|
+
function isAgentSettingsFile(resolved2) {
|
|
1170
|
+
if (/[\\/]\.claude[\\/]settings(?:\.[^/\\]+)?\.json$/.test(resolved2))
|
|
1171
|
+
return true;
|
|
1172
|
+
if (/[\\/]\.codex[\\/]hooks\.json$/.test(resolved2))
|
|
1173
|
+
return true;
|
|
1174
|
+
if (/[\\/]\.copilot[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved2))
|
|
1175
|
+
return true;
|
|
1176
|
+
if (/[\\/]\.github[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved2))
|
|
1177
|
+
return true;
|
|
1178
|
+
if (/[\\/]\.cursor[\\/]hooks\.json$/.test(resolved2))
|
|
1179
|
+
return true;
|
|
1180
|
+
if (/[\\/]\.opencode[\\/]opencode\.jsonc?$/.test(resolved2))
|
|
1181
|
+
return true;
|
|
1182
|
+
if (/[\\/]\.opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved2))
|
|
1183
|
+
return true;
|
|
1184
|
+
if (/[\\/]\.config[\\/]opencode[\\/]opencode\.jsonc?$/.test(resolved2))
|
|
1185
|
+
return true;
|
|
1186
|
+
if (/[\\/]\.config[\\/]opencode[\\/]config\.json$/.test(resolved2))
|
|
1187
|
+
return true;
|
|
1188
|
+
if (/[\\/]\.config[\\/]opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved2))
|
|
1189
|
+
return true;
|
|
1190
|
+
if (/[\\/]\.pi[\\/](?:agent[\\/])?settings\.json$/.test(resolved2))
|
|
1191
|
+
return true;
|
|
1192
|
+
if (/[\\/]\.pi[\\/](?:agent[\\/])?extensions[\\/]/.test(resolved2))
|
|
1193
|
+
return true;
|
|
1194
|
+
if (/[\\/]\.gemini[\\/]settings\.json$/.test(resolved2))
|
|
1195
|
+
return true;
|
|
1196
|
+
if (/[\\/]\.gemini[\\/]config[\\/]hooks\.json$/.test(resolved2))
|
|
1197
|
+
return true;
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
function getCommand(ctx) {
|
|
1201
|
+
return ctx.toolInput?.command ?? "";
|
|
1202
|
+
}
|
|
1203
|
+
function getFilePath(ctx) {
|
|
1204
|
+
return ctx.toolInput?.file_path ?? "";
|
|
1205
|
+
}
|
|
1206
|
+
function parseArgvTokens(cmd) {
|
|
1207
|
+
return cmd.trim().split(/\s+/).map((t) => t.replace(/^['"]|['"]$/g, ""));
|
|
1208
|
+
}
|
|
1209
|
+
function classifySelfInvocation(raw) {
|
|
1210
|
+
const command = raw.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, "$$$1");
|
|
1211
|
+
let found = null;
|
|
1212
|
+
const selfVars = new Set;
|
|
1213
|
+
for (const [, name, value] of command.matchAll(/(?:^|[\s;&|])([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|'[^']*'|[^\s;&|]+)/g)) {
|
|
1214
|
+
const bare = value.replace(/^["']|["']$/g, "");
|
|
1215
|
+
if (SELF_BINARY_TOKEN_RE.test(bare) || SELF_ENTRY_PATH_RE.test(bare))
|
|
1216
|
+
selfVars.add(name);
|
|
1217
|
+
}
|
|
1218
|
+
const isSelfVarRef = (token) => {
|
|
1219
|
+
const m = /^\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?$/.exec(token);
|
|
1220
|
+
return m ? selfVars.has(m[1]) : false;
|
|
1221
|
+
};
|
|
1222
|
+
for (const segment of command.split(SEGMENT_SEPARATORS)) {
|
|
1223
|
+
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
1224
|
+
let i = 0;
|
|
1225
|
+
while (i < tokens.length) {
|
|
1226
|
+
const token = tokens[i];
|
|
1227
|
+
const isSkippable = ENV_ASSIGNMENT_RE.test(token) || token.startsWith("-") || token.startsWith(">") || token.startsWith("<") || RUNNER_OPERAND_RE.test(token) || COMMAND_PREFIX_TOKENS.has(token.slice(token.lastIndexOf("/") + 1));
|
|
1228
|
+
if (!isSkippable)
|
|
1229
|
+
break;
|
|
1230
|
+
i++;
|
|
1231
|
+
}
|
|
1232
|
+
if (i >= tokens.length)
|
|
1233
|
+
continue;
|
|
1234
|
+
if (!SELF_BINARY_TOKEN_RE.test(tokens[i]) && !SELF_ENTRY_PATH_RE.test(tokens[i]) && !isSelfVarRef(tokens[i])) {
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
const args = tokens.slice(i + 1);
|
|
1238
|
+
const configAt = args.findIndex((a) => CONFIG_SUBCOMMAND_RE.test(a));
|
|
1239
|
+
if (configAt !== -1 && args.slice(configAt + 1).some((a) => PAUSE_FLAG_RE.test(a))) {
|
|
1240
|
+
return "pause";
|
|
1241
|
+
}
|
|
1242
|
+
found = "cli";
|
|
1243
|
+
}
|
|
1244
|
+
return found;
|
|
1245
|
+
}
|
|
1246
|
+
function decodeAnsiC(body) {
|
|
1247
|
+
return body.replace(/\\(x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{1,4}|U[0-9A-Fa-f]{1,8}|[0-7]{1,3}|.)/g, (_, seq) => {
|
|
1248
|
+
try {
|
|
1249
|
+
if (seq[0] === "x")
|
|
1250
|
+
return String.fromCharCode(parseInt(seq.slice(1), 16));
|
|
1251
|
+
if (seq[0] === "u" || seq[0] === "U")
|
|
1252
|
+
return String.fromCodePoint(parseInt(seq.slice(1), 16));
|
|
1253
|
+
if (/^[0-7]+$/.test(seq))
|
|
1254
|
+
return String.fromCharCode(parseInt(seq, 8));
|
|
1255
|
+
} catch {
|
|
1256
|
+
return seq;
|
|
1257
|
+
}
|
|
1258
|
+
const named = {
|
|
1259
|
+
a: "\x07",
|
|
1260
|
+
b: "\b",
|
|
1261
|
+
e: "\x1B",
|
|
1262
|
+
f: "\f",
|
|
1263
|
+
n: `
|
|
1264
|
+
`,
|
|
1265
|
+
r: "\r",
|
|
1266
|
+
t: "\t",
|
|
1267
|
+
v: "\v",
|
|
1268
|
+
"\\": "\\",
|
|
1269
|
+
"'": "'",
|
|
1270
|
+
'"': '"',
|
|
1271
|
+
"?": "?"
|
|
1272
|
+
};
|
|
1273
|
+
return named[seq] ?? seq;
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
function stripShellQuoting(command) {
|
|
1277
|
+
const joined = command.replace(/\\\r?\n/g, "");
|
|
1278
|
+
const ansiDecoded = joined.replace(/\$'((?:[^'\\]|\\.)*)'/g, (_, body) => decodeAnsiC(body));
|
|
1279
|
+
return ansiDecoded.replace(/\\(.)/g, "$1").replace(/['"]/g, "");
|
|
1280
|
+
}
|
|
1281
|
+
function statGitHeadMtimeMs(cwd) {
|
|
1282
|
+
try {
|
|
1283
|
+
return statSync3(join2(cwd, ".git", "HEAD")).mtimeMs;
|
|
1284
|
+
} catch {
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
function getCurrentBranch(cwd) {
|
|
1289
|
+
try {
|
|
1290
|
+
const headMtimeMs = statGitHeadMtimeMs(cwd);
|
|
1291
|
+
const cached = gitBranchCache.get(cwd);
|
|
1292
|
+
if (cached && headMtimeMs !== null && cached.headMtimeMs === headMtimeMs) {
|
|
1293
|
+
return cached.branch || null;
|
|
1294
|
+
}
|
|
1295
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
1296
|
+
cwd,
|
|
1297
|
+
encoding: "utf8",
|
|
1298
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1299
|
+
timeout: 3000
|
|
1300
|
+
}).trim();
|
|
1301
|
+
if (headMtimeMs !== null) {
|
|
1302
|
+
if (gitBranchCache.size >= GIT_BRANCH_CACHE_MAX_ENTRIES)
|
|
1303
|
+
gitBranchCache.clear();
|
|
1304
|
+
gitBranchCache.set(cwd, { branch, headMtimeMs });
|
|
1305
|
+
}
|
|
1306
|
+
return branch || null;
|
|
1307
|
+
} catch {
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
function getHeadSha(cwd) {
|
|
1312
|
+
try {
|
|
1313
|
+
const sha = execSync("git rev-parse HEAD", {
|
|
1314
|
+
cwd,
|
|
1315
|
+
encoding: "utf8",
|
|
1316
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1317
|
+
timeout: 3000
|
|
1318
|
+
}).trim();
|
|
1319
|
+
return sha || null;
|
|
1320
|
+
} catch {
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function getThirdPartyCheckRuns(cwd, sha) {
|
|
1325
|
+
try {
|
|
1326
|
+
const json = execFileSync("gh", [
|
|
1327
|
+
"api",
|
|
1328
|
+
`repos/{owner}/{repo}/commits/${sha}/check-runs`,
|
|
1329
|
+
"--jq",
|
|
1330
|
+
'.check_runs | map(select(.app.slug != "github-actions")) | map({name: .name, status: .status, conclusion: (.conclusion // "")})'
|
|
1331
|
+
], {
|
|
1332
|
+
cwd,
|
|
1333
|
+
encoding: "utf8",
|
|
1334
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1335
|
+
timeout: 15000
|
|
1336
|
+
}).trim();
|
|
1337
|
+
if (!json || json === "[]")
|
|
1338
|
+
return [];
|
|
1339
|
+
return JSON.parse(json);
|
|
1340
|
+
} catch {
|
|
1341
|
+
return [];
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
function getCommitStatuses(cwd, sha) {
|
|
1345
|
+
try {
|
|
1346
|
+
const json = execFileSync("gh", [
|
|
1347
|
+
"api",
|
|
1348
|
+
`repos/{owner}/{repo}/commits/${sha}/statuses`,
|
|
1349
|
+
"--jq",
|
|
1350
|
+
"map({name: .context, state: .state}) | unique_by(.name)"
|
|
1351
|
+
], {
|
|
1352
|
+
cwd,
|
|
1353
|
+
encoding: "utf8",
|
|
1354
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1355
|
+
timeout: 15000
|
|
1356
|
+
}).trim();
|
|
1357
|
+
if (!json || json === "[]")
|
|
1358
|
+
return [];
|
|
1359
|
+
const statuses = JSON.parse(json);
|
|
1360
|
+
return statuses.map((s) => ({
|
|
1361
|
+
name: s.name,
|
|
1362
|
+
status: s.state === "pending" ? "in_progress" : "completed",
|
|
1363
|
+
conclusion: s.state === "pending" ? "" : s.state === "success" ? "success" : "failure"
|
|
1364
|
+
}));
|
|
1365
|
+
} catch {
|
|
1366
|
+
return [];
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
function matchesAllowedPattern(cmd, pattern) {
|
|
1370
|
+
const cmdTokens = parseArgvTokens(cmd);
|
|
1371
|
+
const patTokens = parseArgvTokens(pattern);
|
|
1372
|
+
if (cmdTokens.length < patTokens.length)
|
|
1373
|
+
return false;
|
|
1374
|
+
if (cmdTokens.some((tok) => SHELL_OPERATORS.has(tok)))
|
|
1375
|
+
return false;
|
|
1376
|
+
if (cmdTokens.some((tok) => SHELL_METACHAR_RE.test(tok)))
|
|
1377
|
+
return false;
|
|
1378
|
+
return patTokens.every((tok, i) => tok === "*" || tok === cmdTokens[i]);
|
|
1379
|
+
}
|
|
1380
|
+
function sanitizeJwt(ctx) {
|
|
1381
|
+
const output = JSON.stringify(ctx.payload);
|
|
1382
|
+
if (JWT_RE.test(output)) {
|
|
1383
|
+
return {
|
|
1384
|
+
decision: "deny",
|
|
1385
|
+
reason: "JWT token detected in tool output",
|
|
1386
|
+
message: "[REDACTED: JWT token removed by failproofai]"
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
return allow();
|
|
1390
|
+
}
|
|
1391
|
+
function sanitizeApiKeys(ctx) {
|
|
1392
|
+
const output = JSON.stringify(ctx.payload);
|
|
1393
|
+
for (const [pattern, label] of API_KEY_PATTERNS) {
|
|
1394
|
+
if (pattern.test(output)) {
|
|
1395
|
+
return {
|
|
1396
|
+
decision: "deny",
|
|
1397
|
+
reason: `${label} detected in tool output`,
|
|
1398
|
+
message: `[REDACTED: ${label} removed by failproofai]`
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
const additional = ctx.params?.additionalPatterns ?? [];
|
|
1403
|
+
for (const { regex, label } of additional) {
|
|
1404
|
+
try {
|
|
1405
|
+
if (new RegExp(regex).test(output)) {
|
|
1406
|
+
return {
|
|
1407
|
+
decision: "deny",
|
|
1408
|
+
reason: `${label} detected in tool output`,
|
|
1409
|
+
message: `[REDACTED: ${label} removed by failproofai]`
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
} catch {
|
|
1413
|
+
hookLogWarn(`additionalPatterns: invalid regex "${regex}", skipping`);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return allow();
|
|
1417
|
+
}
|
|
1418
|
+
function sanitizeConnectionStrings(ctx) {
|
|
1419
|
+
const output = JSON.stringify(ctx.payload);
|
|
1420
|
+
if (CONNECTION_STRING_RE.test(output)) {
|
|
1421
|
+
return {
|
|
1422
|
+
decision: "deny",
|
|
1423
|
+
reason: "Database connection string with credentials detected in tool output",
|
|
1424
|
+
message: "[REDACTED: connection string removed by failproofai]"
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
return allow();
|
|
1428
|
+
}
|
|
1429
|
+
function sanitizePrivateKeyContent(ctx) {
|
|
1430
|
+
const output = JSON.stringify(ctx.payload);
|
|
1431
|
+
if (PRIVATE_KEY_RE.test(output)) {
|
|
1432
|
+
return {
|
|
1433
|
+
decision: "deny",
|
|
1434
|
+
reason: "Private key content detected in tool output",
|
|
1435
|
+
message: "[REDACTED: private key content removed by failproofai]"
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
return allow();
|
|
1439
|
+
}
|
|
1440
|
+
function sanitizeBearerTokens(ctx) {
|
|
1441
|
+
const output = JSON.stringify(ctx.payload);
|
|
1442
|
+
if (BEARER_TOKEN_RE.test(output)) {
|
|
1443
|
+
return {
|
|
1444
|
+
decision: "deny",
|
|
1445
|
+
reason: "Bearer token detected in tool output",
|
|
1446
|
+
message: "[REDACTED: Bearer token removed by failproofai]"
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
return allow();
|
|
1450
|
+
}
|
|
1451
|
+
function warnDestructiveSql(ctx) {
|
|
1452
|
+
if (ctx.toolName !== "Bash")
|
|
1453
|
+
return allow();
|
|
1454
|
+
const cmd = getCommand(ctx);
|
|
1455
|
+
if (!SQL_TOOL_RE.test(cmd))
|
|
1456
|
+
return allow();
|
|
1457
|
+
if (DESTRUCTIVE_SQL_RE.test(cmd)) {
|
|
1458
|
+
return instruct("STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.");
|
|
1459
|
+
}
|
|
1460
|
+
if (DELETE_NO_WHERE_RE.test(cmd) && !SQL_WHERE_RE.test(cmd)) {
|
|
1461
|
+
return instruct("STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.");
|
|
1462
|
+
}
|
|
1463
|
+
return allow();
|
|
1464
|
+
}
|
|
1465
|
+
function warnLargeFileWrite(ctx) {
|
|
1466
|
+
if (ctx.toolName !== "Write")
|
|
1467
|
+
return allow();
|
|
1468
|
+
const content = ctx.toolInput?.content;
|
|
1469
|
+
if (typeof content !== "string")
|
|
1470
|
+
return allow();
|
|
1471
|
+
const thresholdKb = ctx.params?.thresholdKb ?? 1024;
|
|
1472
|
+
const thresholdBytes = thresholdKb * 1024;
|
|
1473
|
+
if (content.length > thresholdBytes) {
|
|
1474
|
+
return instruct(`STOP: You are writing a file larger than ${thresholdKb}KB (${Math.round(content.length / 1024)}KB). This is unusually large. Confirm this is intentional before proceeding.`);
|
|
1475
|
+
}
|
|
1476
|
+
return allow();
|
|
1477
|
+
}
|
|
1478
|
+
function warnPackagePublish(ctx) {
|
|
1479
|
+
if (ctx.toolName !== "Bash")
|
|
1480
|
+
return allow();
|
|
1481
|
+
const cmd = getCommand(ctx);
|
|
1482
|
+
if (PUBLISH_CMD_RE.test(cmd)) {
|
|
1483
|
+
return instruct("STOP: This command publishes a package to a public registry. Confirm with the user that this is intentional.");
|
|
1484
|
+
}
|
|
1485
|
+
return allow();
|
|
1486
|
+
}
|
|
1487
|
+
function protectEnvVars(ctx) {
|
|
1488
|
+
if (ctx.toolName !== "Bash")
|
|
1489
|
+
return allow();
|
|
1490
|
+
const cmd = getCommand(ctx);
|
|
1491
|
+
if (ENV_PRINTENV_RE.test(cmd)) {
|
|
1492
|
+
return deny("Command reads environment variables");
|
|
1493
|
+
}
|
|
1494
|
+
if (ECHO_ENV_RE.test(cmd)) {
|
|
1495
|
+
return deny("Command echoes environment variable");
|
|
1496
|
+
}
|
|
1497
|
+
if (EXPORT_RE.test(cmd)) {
|
|
1498
|
+
return deny("Command exports environment variable");
|
|
1499
|
+
}
|
|
1500
|
+
if (PS_ENV_VAR_RE.test(cmd)) {
|
|
1501
|
+
return deny("Command reads environment variable via PowerShell");
|
|
1502
|
+
}
|
|
1503
|
+
if (PS_CHILDITEM_ENV_RE.test(cmd)) {
|
|
1504
|
+
return deny("Command reads environment variables via PowerShell");
|
|
1505
|
+
}
|
|
1506
|
+
if (DOTNET_GETENV_RE.test(cmd)) {
|
|
1507
|
+
return deny("Command reads environment variable via .NET");
|
|
1508
|
+
}
|
|
1509
|
+
if (CMD_ECHO_ENV_RE.test(cmd)) {
|
|
1510
|
+
return deny("Command echoes environment variable via cmd");
|
|
1511
|
+
}
|
|
1512
|
+
return allow();
|
|
1513
|
+
}
|
|
1514
|
+
function blockEnvFiles(ctx) {
|
|
1515
|
+
const cmd = getCommand(ctx);
|
|
1516
|
+
const filePath = getFilePath(ctx);
|
|
1517
|
+
if (filePath && ENV_FILE_PATH_RE.test(filePath)) {
|
|
1518
|
+
return deny("Access to .env file blocked");
|
|
1519
|
+
}
|
|
1520
|
+
if (ctx.toolName === "Bash" && ENV_CMD_RE.test(cmd)) {
|
|
1521
|
+
return deny("Command references .env file");
|
|
1522
|
+
}
|
|
1523
|
+
return allow();
|
|
1524
|
+
}
|
|
1525
|
+
function quoteAwareSegments(command) {
|
|
1526
|
+
const segments = [];
|
|
1527
|
+
let tokens = [];
|
|
1528
|
+
let token = "";
|
|
1529
|
+
let quote = null;
|
|
1530
|
+
const endToken = () => {
|
|
1531
|
+
if (token)
|
|
1532
|
+
tokens.push(token);
|
|
1533
|
+
token = "";
|
|
1534
|
+
};
|
|
1535
|
+
const endSegment = () => {
|
|
1536
|
+
endToken();
|
|
1537
|
+
if (tokens.length)
|
|
1538
|
+
segments.push(tokens);
|
|
1539
|
+
tokens = [];
|
|
1540
|
+
};
|
|
1541
|
+
for (let i = 0;i < command.length; i++) {
|
|
1542
|
+
const c = command[i];
|
|
1543
|
+
if (quote) {
|
|
1544
|
+
if (c === "\\" && quote === '"' && i + 1 < command.length) {
|
|
1545
|
+
token += command[++i];
|
|
1546
|
+
continue;
|
|
2185
1547
|
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
displayTitle: "Tried to run a Google Cloud command",
|
|
2190
|
-
impact: "gcloud can spend money or break prod — gated.",
|
|
2191
|
-
description: "Block gcloud (Google Cloud) CLI commands",
|
|
2192
|
-
fn: blockGcloud,
|
|
2193
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
2194
|
-
defaultEnabled: false,
|
|
2195
|
-
category: "Infra Commands",
|
|
2196
|
-
params: {
|
|
2197
|
-
allowPatterns: {
|
|
2198
|
-
type: "string[]",
|
|
2199
|
-
description: "gcloud command patterns to allow (e.g. 'gcloud auth list', 'gcloud config list')",
|
|
2200
|
-
default: []
|
|
2201
|
-
}
|
|
1548
|
+
if (c === quote) {
|
|
1549
|
+
quote = null;
|
|
1550
|
+
continue;
|
|
2202
1551
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
1552
|
+
token += c;
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
if (c === "\\" && i + 1 < command.length) {
|
|
1556
|
+
token += command[++i];
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
if (c === '"' || c === "'") {
|
|
1560
|
+
quote = c;
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
if (/\s/.test(c)) {
|
|
1564
|
+
endToken();
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
if (/[;&|\n\r(){}`]/.test(c)) {
|
|
1568
|
+
endSegment();
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
token += c;
|
|
1572
|
+
}
|
|
1573
|
+
endSegment();
|
|
1574
|
+
return segments;
|
|
1575
|
+
}
|
|
1576
|
+
function namesElevation(command, depth = 0) {
|
|
1577
|
+
for (const tokens of quoteAwareSegments(command)) {
|
|
1578
|
+
let i = 0;
|
|
1579
|
+
while (i < tokens.length) {
|
|
1580
|
+
const token = tokens[i];
|
|
1581
|
+
const base = token.slice(token.lastIndexOf("/") + 1);
|
|
1582
|
+
if (base === "sudo" || base === "doas")
|
|
1583
|
+
return true;
|
|
1584
|
+
const isSkippable = ENV_ASSIGNMENT_RE.test(token) || token.startsWith("-") || token.startsWith(">") || token.startsWith("<") || RUNNER_OPERAND_RE.test(token) || COMMAND_PREFIX_TOKENS.has(base);
|
|
1585
|
+
if (!isSkippable)
|
|
1586
|
+
break;
|
|
1587
|
+
if (EVAL_FLAG_RE.test(token) && depth < 3 && i + 1 < tokens.length) {
|
|
1588
|
+
if (namesElevation(tokens[i + 1], depth + 1))
|
|
1589
|
+
return true;
|
|
2219
1590
|
}
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
1591
|
+
i++;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
return false;
|
|
1595
|
+
}
|
|
1596
|
+
function blockSudo(ctx) {
|
|
1597
|
+
if (ctx.toolName !== "Bash")
|
|
1598
|
+
return allow();
|
|
1599
|
+
const cmd = getCommand(ctx).trimStart();
|
|
1600
|
+
if (namesElevation(cmd)) {
|
|
1601
|
+
const allowPatterns = ctx.params?.allowPatterns ?? [];
|
|
1602
|
+
if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
|
|
1603
|
+
return allow();
|
|
1604
|
+
return deny("sudo commands are blocked");
|
|
1605
|
+
}
|
|
1606
|
+
if (PS_ELEVATION_RE.test(cmd)) {
|
|
1607
|
+
return deny("Elevated process launch is blocked");
|
|
1608
|
+
}
|
|
1609
|
+
if (RUNAS_RE.test(cmd)) {
|
|
1610
|
+
return deny("runas elevation is blocked");
|
|
1611
|
+
}
|
|
1612
|
+
return allow();
|
|
1613
|
+
}
|
|
1614
|
+
function blockCurlPipeSh(ctx) {
|
|
1615
|
+
if (ctx.toolName !== "Bash")
|
|
1616
|
+
return allow();
|
|
1617
|
+
const cmd = getCommand(ctx);
|
|
1618
|
+
if (CURL_PIPE_SH_RE.test(cmd)) {
|
|
1619
|
+
return deny("Piping downloads to shell is blocked");
|
|
1620
|
+
}
|
|
1621
|
+
if (PS_WEB_PIPE_RE.test(cmd)) {
|
|
1622
|
+
return deny("Piping downloads to Invoke-Expression is blocked");
|
|
1623
|
+
}
|
|
1624
|
+
return allow();
|
|
1625
|
+
}
|
|
1626
|
+
function extractGitPushArgs(cmd) {
|
|
1627
|
+
return cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => /^git\s+push\s/.test(s)).map((s) => s.replace(/^git\s+push\s+/, ""));
|
|
1628
|
+
}
|
|
1629
|
+
function blockPushMaster(ctx) {
|
|
1630
|
+
if (ctx.toolName !== "Bash")
|
|
1631
|
+
return allow();
|
|
1632
|
+
const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
|
|
1633
|
+
if (protectedBranches.length === 0)
|
|
1634
|
+
return allow();
|
|
1635
|
+
const args = extractGitPushArgs(getCommand(ctx));
|
|
1636
|
+
const branchPattern = new RegExp(`\\b(?:${protectedBranches.map((b) => b.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`);
|
|
1637
|
+
if (args.some((a) => branchPattern.test(a))) {
|
|
1638
|
+
return deny(`Pushing to ${protectedBranches.join("/")} is blocked`);
|
|
1639
|
+
}
|
|
1640
|
+
return allow();
|
|
1641
|
+
}
|
|
1642
|
+
function expandHomePrefix(path) {
|
|
1643
|
+
const m = path.match(/^(?:~|\$HOME|\$\{HOME\})(?=$|\/)/);
|
|
1644
|
+
return m ? homedir3() + path.slice(m[0].length) : path;
|
|
1645
|
+
}
|
|
1646
|
+
function stripTrailingGlob(path) {
|
|
1647
|
+
return path.replace(/\/\*$/, "").replace(/\/+$/, "");
|
|
1648
|
+
}
|
|
1649
|
+
function isCatastrophicTarget(token) {
|
|
1650
|
+
const raw = token.replace(/^['"]|['"]$/g, "");
|
|
1651
|
+
if (raw === "")
|
|
1652
|
+
return false;
|
|
1653
|
+
const homePrefix = raw.match(HOME_PREFIX_RE);
|
|
1654
|
+
if (!homePrefix && /^[$`]/.test(raw))
|
|
1655
|
+
return true;
|
|
1656
|
+
const belowRoot = homePrefix ? raw.slice(homePrefix[0].length) : raw.startsWith("/") ? raw : null;
|
|
1657
|
+
if (belowRoot === null)
|
|
1658
|
+
return false;
|
|
1659
|
+
const segments = stripTrailingGlob(belowRoot).split("/").filter(Boolean);
|
|
1660
|
+
if (!homePrefix && SCRATCH_ROOTS.some((r) => `/${segments.join("/")}`.startsWith(`${r}/`)))
|
|
1661
|
+
return false;
|
|
1662
|
+
return segments.length <= CATASTROPHIC_DEPTH;
|
|
1663
|
+
}
|
|
1664
|
+
function recursiveDeletionTargets(seg) {
|
|
1665
|
+
const tokens = parseArgvTokens(seg);
|
|
1666
|
+
const findIdx = tokens.findIndex((t) => FIND_CMD_RE.test(t));
|
|
1667
|
+
if (findIdx >= 0) {
|
|
1668
|
+
const expr = tokens.slice(findIdx + 1);
|
|
1669
|
+
const execIdx = expr.findIndex((t) => FIND_EXEC_RE.test(t));
|
|
1670
|
+
const deletes = expr.includes("-delete") || execIdx >= 0 && RM_CMD_RE.test(expr[execIdx + 1] ?? "");
|
|
1671
|
+
if (deletes) {
|
|
1672
|
+
let start = 0;
|
|
1673
|
+
while (start < expr.length && FIND_GLOBAL_OPT_RE.test(expr[start])) {
|
|
1674
|
+
start += expr[start] === "-D" ? 2 : 1;
|
|
2236
1675
|
}
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
1676
|
+
const rest = expr.slice(start);
|
|
1677
|
+
const end = rest.findIndex((t) => FIND_EXPR_START_RE.test(t));
|
|
1678
|
+
return end < 0 ? rest : rest.slice(0, end);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
const rmIdx = tokens.findIndex((t) => RM_CMD_RE.test(t));
|
|
1682
|
+
if (rmIdx >= 0) {
|
|
1683
|
+
const args = tokens.slice(rmIdx + 1);
|
|
1684
|
+
const shortFlags = args.filter((t) => /^-[^-]/.test(t)).join("");
|
|
1685
|
+
const longFlags = args.filter((t) => /^--/.test(t));
|
|
1686
|
+
const recursive = /r/i.test(shortFlags) || longFlags.some((f) => /^--recursive$/i.test(f));
|
|
1687
|
+
const force = /f/.test(shortFlags) || longFlags.some((f) => /^--force$/i.test(f));
|
|
1688
|
+
if (recursive && force)
|
|
1689
|
+
return args.filter((t) => !t.startsWith("-"));
|
|
1690
|
+
}
|
|
1691
|
+
return null;
|
|
1692
|
+
}
|
|
1693
|
+
function shellSegments(cmd) {
|
|
1694
|
+
return cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => s !== "");
|
|
1695
|
+
}
|
|
1696
|
+
function deletionTargetIsAllowed(cmd, allowPaths) {
|
|
1697
|
+
if (allowPaths.length === 0)
|
|
1698
|
+
return false;
|
|
1699
|
+
const normalizedAllowPaths = allowPaths.map((p) => stripTrailingGlob(expandHomePrefix(p)) || "/");
|
|
1700
|
+
let sawRecursiveDelete = false;
|
|
1701
|
+
for (const seg of shellSegments(cmd)) {
|
|
1702
|
+
const targets = recursiveDeletionTargets(seg);
|
|
1703
|
+
if (targets === null)
|
|
1704
|
+
continue;
|
|
1705
|
+
sawRecursiveDelete = true;
|
|
1706
|
+
for (const target of targets) {
|
|
1707
|
+
const normalized = stripTrailingGlob(expandHomePrefix(target)) || "/";
|
|
1708
|
+
const covered = normalizedAllowPaths.some((np) => normalized === np || normalized.startsWith(np + "/"));
|
|
1709
|
+
if (!covered) {
|
|
1710
|
+
const segCovered = allowPaths.some((p) => {
|
|
1711
|
+
const escaped = p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1712
|
+
return new RegExp(`${escaped}(?:[/"'\\s/*]|$)`).test(seg);
|
|
1713
|
+
});
|
|
1714
|
+
if (!segCovered)
|
|
1715
|
+
return false;
|
|
2253
1716
|
}
|
|
2254
|
-
}
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return sawRecursiveDelete;
|
|
1720
|
+
}
|
|
1721
|
+
function blockRmRf(ctx) {
|
|
1722
|
+
if (ctx.toolName !== "Bash")
|
|
1723
|
+
return allow();
|
|
1724
|
+
const cmd = getCommand(ctx);
|
|
1725
|
+
const hasCatastrophicTarget = shellSegments(cmd).some((seg) => {
|
|
1726
|
+
const targets = recursiveDeletionTargets(seg);
|
|
1727
|
+
return targets !== null && targets.some(isCatastrophicTarget);
|
|
1728
|
+
});
|
|
1729
|
+
if (hasCatastrophicTarget) {
|
|
1730
|
+
const allowPaths = ctx.params?.allowPaths ?? [];
|
|
1731
|
+
if (deletionTargetIsAllowed(cmd, allowPaths))
|
|
1732
|
+
return allow();
|
|
1733
|
+
return deny("Catastrophic deletion blocked");
|
|
1734
|
+
}
|
|
1735
|
+
if (/Remove-Item\s+.*-Recurse.*-Force.*(?:[A-Z]:\\(?:\s|$)|\\\*)/i.test(cmd)) {
|
|
1736
|
+
return deny("Catastrophic deletion blocked");
|
|
1737
|
+
}
|
|
1738
|
+
if (/(?:rd|rmdir)\s+\/s\s+\/q\s+[A-Z]:\\/i.test(cmd)) {
|
|
1739
|
+
return deny("Catastrophic deletion blocked");
|
|
1740
|
+
}
|
|
1741
|
+
return allow();
|
|
1742
|
+
}
|
|
1743
|
+
function blockForcePush(ctx) {
|
|
1744
|
+
if (ctx.toolName !== "Bash")
|
|
1745
|
+
return allow();
|
|
1746
|
+
for (const segment of extractGitPushArgs(getCommand(ctx))) {
|
|
1747
|
+
let sawEndOfOptions = false;
|
|
1748
|
+
for (const token of segment.split(/\s+/)) {
|
|
1749
|
+
if (token === "--") {
|
|
1750
|
+
sawEndOfOptions = true;
|
|
1751
|
+
continue;
|
|
2270
1752
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
impact: "Direct pushes to a protected branch bypass review.",
|
|
2276
|
-
description: "Block pushing to main/master",
|
|
2277
|
-
fn: blockPushMaster,
|
|
2278
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
2279
|
-
defaultEnabled: true,
|
|
2280
|
-
category: "Git",
|
|
2281
|
-
params: {
|
|
2282
|
-
protectedBranches: {
|
|
2283
|
-
type: "string[]",
|
|
2284
|
-
description: "Branch names to protect from direct pushes",
|
|
2285
|
-
default: ["main", "master"]
|
|
2286
|
-
}
|
|
1753
|
+
if (sawEndOfOptions)
|
|
1754
|
+
continue;
|
|
1755
|
+
if (isForcePushFlag(token)) {
|
|
1756
|
+
return deny("Force-pushing is blocked");
|
|
2287
1757
|
}
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return allow();
|
|
1761
|
+
}
|
|
1762
|
+
function isForcePushFlag(token) {
|
|
1763
|
+
if (token === "--force")
|
|
1764
|
+
return true;
|
|
1765
|
+
if (SAFE_FORCE_PREFIXES.some((prefix) => token.startsWith(prefix)))
|
|
1766
|
+
return false;
|
|
1767
|
+
if (token.startsWith("--force"))
|
|
1768
|
+
return true;
|
|
1769
|
+
return SHORT_FLAG_BUNDLE_RE.test(token);
|
|
1770
|
+
}
|
|
1771
|
+
function blockSecretsWrite(ctx) {
|
|
1772
|
+
if (ctx.toolName !== "Write")
|
|
1773
|
+
return allow();
|
|
1774
|
+
const filePath = getFilePath(ctx);
|
|
1775
|
+
if (SECRET_FILE_RE.test(filePath) || SECRET_FILE_ID_RSA_RE.test(filePath) || SECRET_FILE_CREDENTIALS_RE.test(filePath)) {
|
|
1776
|
+
return deny("Writing secret key files is blocked");
|
|
1777
|
+
}
|
|
1778
|
+
const additionalPatterns = ctx.params?.additionalPatterns ?? [];
|
|
1779
|
+
for (const pattern of additionalPatterns) {
|
|
1780
|
+
if (filePath.includes(pattern)) {
|
|
1781
|
+
return deny(`Writing blocked file pattern: ${pattern}`);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
return allow();
|
|
1785
|
+
}
|
|
1786
|
+
function extractAbsolutePaths(command) {
|
|
1787
|
+
const paths = [];
|
|
1788
|
+
const pathRe = /(?<![a-zA-Z0-9_.\-~\\*?:=])(?:~\/[^\s;|&"'()\[\]{}]*|~(?=\s|$|[;|&"'()\[\]{}])|\/[^\s;|&"'()\[\]{}]*)/g;
|
|
1789
|
+
function addPaths(s) {
|
|
1790
|
+
pathRe.lastIndex = 0;
|
|
1791
|
+
let m;
|
|
1792
|
+
while ((m = pathRe.exec(s)) !== null) {
|
|
1793
|
+
let p = m[0];
|
|
1794
|
+
if (p === "~")
|
|
1795
|
+
p = homedir3();
|
|
1796
|
+
else if (p.startsWith("~/"))
|
|
1797
|
+
p = join2(homedir3(), p.slice(2));
|
|
1798
|
+
paths.push(p);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
let firstBarePipe = command.length;
|
|
1802
|
+
let inDouble = false, inSingle = false;
|
|
1803
|
+
for (let i = 0;i < command.length; i++) {
|
|
1804
|
+
const c = command[i];
|
|
1805
|
+
if (c === '"' && !inSingle)
|
|
1806
|
+
inDouble = !inDouble;
|
|
1807
|
+
else if (c === "'" && !inDouble)
|
|
1808
|
+
inSingle = !inSingle;
|
|
1809
|
+
else if (c === "|" && !inDouble && !inSingle) {
|
|
1810
|
+
firstBarePipe = i;
|
|
1811
|
+
break;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
const firstSegment = command.slice(0, firstBarePipe);
|
|
1815
|
+
const quotedRe = /"([^"]*)"|'([^']*)'/g;
|
|
1816
|
+
let qm;
|
|
1817
|
+
while ((qm = quotedRe.exec(firstSegment)) !== null) {
|
|
1818
|
+
const content = qm[1] ?? qm[2] ?? "";
|
|
1819
|
+
if (/[*?\[\]^$+()\\]/.test(content))
|
|
1820
|
+
continue;
|
|
1821
|
+
addPaths(content);
|
|
1822
|
+
}
|
|
1823
|
+
const stripped = command.replace(/"[^"]*"/g, (m) => " ".repeat(m.length)).replace(/'[^']*'/g, (m) => " ".repeat(m.length));
|
|
1824
|
+
addPaths(stripped);
|
|
1825
|
+
return paths;
|
|
1826
|
+
}
|
|
1827
|
+
function blockReadOutsideCwd(ctx) {
|
|
1828
|
+
const cwd = process.env.CLAUDE_PROJECT_DIR || ctx.session?.cwd;
|
|
1829
|
+
if (!cwd)
|
|
1830
|
+
return allow();
|
|
1831
|
+
const allowPaths = ctx.params?.allowPaths ?? [];
|
|
1832
|
+
if (ctx.toolName === "Bash") {
|
|
1833
|
+
const cmd = getCommand(ctx);
|
|
1834
|
+
if (!READ_LIKE_CMDS.test(cmd))
|
|
1835
|
+
return allow();
|
|
1836
|
+
const paths = extractAbsolutePaths(cmd);
|
|
1837
|
+
const cwdWithSep2 = cwd.endsWith("/") ? cwd : cwd + "/";
|
|
1838
|
+
for (const p of paths) {
|
|
1839
|
+
const resolved3 = resolve3(cwd, p);
|
|
1840
|
+
if (isClaudeSettingsFile(resolved3)) {
|
|
1841
|
+
return deny(`Reading agent settings file blocked: ${resolved3}`);
|
|
2314
1842
|
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
1843
|
+
if (isClaudeInternalPath(resolved3))
|
|
1844
|
+
continue;
|
|
1845
|
+
if (resolved3 === "/dev/null")
|
|
1846
|
+
continue;
|
|
1847
|
+
if (resolved3 !== cwd && !resolved3.startsWith(cwdWithSep2)) {
|
|
1848
|
+
if (allowPaths.some((ap) => resolved3 === ap || resolved3.startsWith(ap.endsWith("/") ? ap : ap + "/")))
|
|
1849
|
+
continue;
|
|
1850
|
+
return deny(`Bash read outside project directory blocked: ${resolved3}`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
return allow();
|
|
1854
|
+
}
|
|
1855
|
+
const filePath = getFilePath(ctx);
|
|
1856
|
+
const searchPath = ctx.toolInput?.path ?? "";
|
|
1857
|
+
const target = filePath || searchPath;
|
|
1858
|
+
if (!target)
|
|
1859
|
+
return allow();
|
|
1860
|
+
const resolved2 = resolve3(cwd, target);
|
|
1861
|
+
if (isClaudeSettingsFile(resolved2)) {
|
|
1862
|
+
return deny(`Reading agent settings file blocked: ${resolved2}`);
|
|
1863
|
+
}
|
|
1864
|
+
if (isClaudeInternalPath(resolved2))
|
|
1865
|
+
return allow();
|
|
1866
|
+
if (resolved2 === "/dev/null")
|
|
1867
|
+
return allow();
|
|
1868
|
+
const cwdWithSep = cwd.endsWith("/") ? cwd : cwd + "/";
|
|
1869
|
+
if (resolved2 !== cwd && !resolved2.startsWith(cwdWithSep)) {
|
|
1870
|
+
if (allowPaths.some((ap) => resolved2 === ap || resolved2.startsWith(ap.endsWith("/") ? ap : ap + "/")))
|
|
1871
|
+
return allow();
|
|
1872
|
+
return deny(`Access outside project directory blocked: ${resolved2}`);
|
|
1873
|
+
}
|
|
1874
|
+
return allow();
|
|
1875
|
+
}
|
|
1876
|
+
function blockWorkOnMain(ctx) {
|
|
1877
|
+
if (ctx.toolName !== "Bash")
|
|
1878
|
+
return allow();
|
|
1879
|
+
const cmd = getCommand(ctx);
|
|
1880
|
+
const match = cmd.match(GIT_COMMIT_MERGE_RE);
|
|
1881
|
+
if (!match)
|
|
1882
|
+
return allow();
|
|
1883
|
+
const cwd = ctx.session?.cwd;
|
|
1884
|
+
if (!cwd)
|
|
1885
|
+
return allow();
|
|
1886
|
+
const branch = getCurrentBranch(cwd);
|
|
1887
|
+
if (!branch)
|
|
1888
|
+
return allow();
|
|
1889
|
+
const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
|
|
1890
|
+
if (protectedBranches.includes(branch)) {
|
|
1891
|
+
return deny(`Git ${match[1]} on ${branch} is blocked. Create a feature branch first.`);
|
|
1892
|
+
}
|
|
1893
|
+
return allow();
|
|
1894
|
+
}
|
|
1895
|
+
function blockFailproofaiCommands(ctx) {
|
|
1896
|
+
if (ctx.toolName !== "Bash")
|
|
1897
|
+
return allow();
|
|
1898
|
+
const cmd = getCommand(ctx);
|
|
1899
|
+
const unescaped = stripShellQuoting(cmd);
|
|
1900
|
+
const kind = classifySelfInvocation(cmd) ?? classifySelfInvocation(unescaped);
|
|
1901
|
+
if (kind === "pause") {
|
|
1902
|
+
return deny("Pausing failproofai enforcement is a human action, not an agent one. " + "If a policy is blocking legitimate work, say so and let the operator decide.");
|
|
1903
|
+
}
|
|
1904
|
+
if (kind === "cli") {
|
|
1905
|
+
return deny("Running failproofai CLI commands is blocked");
|
|
1906
|
+
}
|
|
1907
|
+
if (FAILPROOFAI_UNINSTALL_RE.test(cmd) || FAILPROOFAI_UNINSTALL_RE.test(unescaped)) {
|
|
1908
|
+
return deny("Uninstalling failproofai is blocked");
|
|
1909
|
+
}
|
|
1910
|
+
if (FAILPROOFAI_STATE_WRITE_RE.test(cmd) || FAILPROOFAI_STATE_WRITE_RE.test(unescaped)) {
|
|
1911
|
+
return deny("Deleting or moving failproofai's own state would switch enforcement off. " + "If a policy is blocking legitimate work, say so and let the operator decide.");
|
|
1912
|
+
}
|
|
1913
|
+
return allow();
|
|
1914
|
+
}
|
|
1915
|
+
function blockInfraCli(ctx, re, denyMsg) {
|
|
1916
|
+
if (ctx.toolName !== "Bash")
|
|
1917
|
+
return allow();
|
|
1918
|
+
const cmd = getCommand(ctx);
|
|
1919
|
+
if (!re.test(cmd))
|
|
1920
|
+
return allow();
|
|
1921
|
+
const allowPatterns = ctx.params?.allowPatterns ?? [];
|
|
1922
|
+
if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
|
|
1923
|
+
return allow();
|
|
1924
|
+
return deny(denyMsg);
|
|
1925
|
+
}
|
|
1926
|
+
function blockKubectl(ctx) {
|
|
1927
|
+
return blockInfraCli(ctx, KUBECTL_RE, "kubectl commands are blocked");
|
|
1928
|
+
}
|
|
1929
|
+
function blockTerraform(ctx) {
|
|
1930
|
+
return blockInfraCli(ctx, TERRAFORM_RE, "terraform/tofu commands are blocked");
|
|
1931
|
+
}
|
|
1932
|
+
function blockAwsCli(ctx) {
|
|
1933
|
+
return blockInfraCli(ctx, AWS_CLI_RE, "aws CLI commands are blocked");
|
|
1934
|
+
}
|
|
1935
|
+
function blockGcloud(ctx) {
|
|
1936
|
+
return blockInfraCli(ctx, GCLOUD_RE, "gcloud commands are blocked");
|
|
1937
|
+
}
|
|
1938
|
+
function blockAzCli(ctx) {
|
|
1939
|
+
return blockInfraCli(ctx, AZ_CLI_RE, "az (Azure) CLI commands are blocked");
|
|
1940
|
+
}
|
|
1941
|
+
function blockHelm(ctx) {
|
|
1942
|
+
return blockInfraCli(ctx, HELM_RE, "helm commands are blocked");
|
|
1943
|
+
}
|
|
1944
|
+
function blockGhPipeline(ctx) {
|
|
1945
|
+
return blockInfraCli(ctx, GH_PIPELINE_RE, "gh pipeline-trigger commands are blocked");
|
|
1946
|
+
}
|
|
1947
|
+
async function warnRepeatedToolCalls(ctx) {
|
|
1948
|
+
const THRESHOLD = 3;
|
|
1949
|
+
const transcriptPath = ctx.session?.transcriptPath;
|
|
1950
|
+
if (!transcriptPath || !ctx.toolName || !ctx.toolInput)
|
|
1951
|
+
return allow();
|
|
1952
|
+
const trackerPath = `${transcriptPath}.tool-calls.json`;
|
|
1953
|
+
const fingerprint = JSON.stringify({ tool: ctx.toolName, input: ctx.toolInput });
|
|
1954
|
+
let counts = {};
|
|
1955
|
+
try {
|
|
1956
|
+
const raw = await readFile(trackerPath, "utf8");
|
|
1957
|
+
counts = JSON.parse(raw);
|
|
1958
|
+
} catch {}
|
|
1959
|
+
const prevCount = counts[fingerprint] ?? 0;
|
|
1960
|
+
if (prevCount >= THRESHOLD) {
|
|
1961
|
+
return instruct(`STOP: You have already called ${ctx.toolName} ${prevCount} times with identical parameters. This is wasteful and unproductive. Do NOT repeat this call — use a different approach or ask the user for clarification.`);
|
|
1962
|
+
}
|
|
1963
|
+
counts[fingerprint] = prevCount + 1;
|
|
1964
|
+
try {
|
|
1965
|
+
const serialized = JSON.stringify(counts);
|
|
1966
|
+
if (serialized.length <= TOOL_CALL_TRACKER_MAX_BYTES) {
|
|
1967
|
+
await writeFile(trackerPath, serialized, "utf8");
|
|
1968
|
+
}
|
|
1969
|
+
} catch {}
|
|
1970
|
+
return allow();
|
|
1971
|
+
}
|
|
1972
|
+
function warnGitAmend(ctx) {
|
|
1973
|
+
if (ctx.toolName !== "Bash")
|
|
1974
|
+
return allow();
|
|
1975
|
+
const cmd = getCommand(ctx);
|
|
1976
|
+
if (GIT_AMEND_RE.test(cmd)) {
|
|
1977
|
+
return instruct("STOP: This command amends the last commit, which rewrites git history. If this commit has already been pushed to a shared branch, this will cause divergence for other contributors. Confirm with the user before executing.");
|
|
1978
|
+
}
|
|
1979
|
+
return allow();
|
|
1980
|
+
}
|
|
1981
|
+
function warnGitStashDrop(ctx) {
|
|
1982
|
+
if (ctx.toolName !== "Bash")
|
|
1983
|
+
return allow();
|
|
1984
|
+
const cmd = getCommand(ctx);
|
|
1985
|
+
if (GIT_STASH_DROP_RE.test(cmd)) {
|
|
1986
|
+
return instruct("STOP: This command permanently deletes stashed changes (git stash drop/clear). Stash entries cannot be recovered after deletion. Confirm with the user before executing.");
|
|
1987
|
+
}
|
|
1988
|
+
return allow();
|
|
1989
|
+
}
|
|
1990
|
+
function warnAllFilesStaged(ctx) {
|
|
1991
|
+
if (ctx.toolName !== "Bash")
|
|
1992
|
+
return allow();
|
|
1993
|
+
const cmd = getCommand(ctx);
|
|
1994
|
+
if (GIT_ADD_ALL_RE.test(cmd)) {
|
|
1995
|
+
return instruct("STOP: This command stages all files in the working tree (git add -A / --all / .). This may inadvertently include build artifacts, generated files, or sensitive files not covered by .gitignore. Confirm with the user before executing.");
|
|
1996
|
+
}
|
|
1997
|
+
return allow();
|
|
1998
|
+
}
|
|
1999
|
+
function warnSchemaAlteration(ctx) {
|
|
2000
|
+
if (ctx.toolName !== "Bash")
|
|
2001
|
+
return allow();
|
|
2002
|
+
const cmd = getCommand(ctx);
|
|
2003
|
+
if (!SQL_TOOL_RE.test(cmd))
|
|
2004
|
+
return allow();
|
|
2005
|
+
if (SCHEMA_ALTER_RE.test(cmd)) {
|
|
2006
|
+
return instruct("STOP: This command contains a schema-altering SQL statement (ALTER TABLE with column or rename operation). Schema changes on production databases are irreversible or disruptive. Confirm with the user before executing.");
|
|
2007
|
+
}
|
|
2008
|
+
return allow();
|
|
2009
|
+
}
|
|
2010
|
+
function warnGlobalPackageInstall(ctx) {
|
|
2011
|
+
if (ctx.toolName !== "Bash")
|
|
2012
|
+
return allow();
|
|
2013
|
+
const cmd = getCommand(ctx);
|
|
2014
|
+
const isGlobal = NPM_GLOBAL_RE.test(cmd) || YARN_GLOBAL_RE.test(cmd) || PNPM_GLOBAL_RE.test(cmd) || BUN_GLOBAL_RE.test(cmd) || CARGO_INSTALL_RE.test(cmd) || PIP_SYSTEM_RE.test(cmd);
|
|
2015
|
+
if (isGlobal) {
|
|
2016
|
+
return instruct("STOP: This command installs a package globally, which modifies the system-wide environment outside the project. This can conflict with other projects or system tools. Confirm with the user before executing.");
|
|
2017
|
+
}
|
|
2018
|
+
return allow();
|
|
2019
|
+
}
|
|
2020
|
+
function preferPackageManager(ctx) {
|
|
2021
|
+
if (ctx.toolName !== "Bash")
|
|
2022
|
+
return allow();
|
|
2023
|
+
const cmd = getCommand(ctx);
|
|
2024
|
+
if (!cmd)
|
|
2025
|
+
return allow();
|
|
2026
|
+
const allowed = ctx.params?.allowed ?? [];
|
|
2027
|
+
if (allowed.length === 0)
|
|
2028
|
+
return allow();
|
|
2029
|
+
const allowedSet = new Set(allowed.map((a) => a.toLowerCase()));
|
|
2030
|
+
const blocked = ctx.params?.blocked ?? [];
|
|
2031
|
+
const allowedList = allowed.join(", ");
|
|
2032
|
+
const segments = cmd.split(SEGMENT_SPLIT_RE);
|
|
2033
|
+
for (const segment of segments) {
|
|
2034
|
+
const trimmed = segment.trim();
|
|
2035
|
+
if (!trimmed)
|
|
2036
|
+
continue;
|
|
2037
|
+
let segmentAllowed = false;
|
|
2038
|
+
for (const manager of allowedSet) {
|
|
2039
|
+
const patterns = PKG_MANAGER_DETECTORS[manager];
|
|
2040
|
+
if (!patterns)
|
|
2041
|
+
continue;
|
|
2042
|
+
for (const pattern of patterns) {
|
|
2043
|
+
if (pattern.test(trimmed)) {
|
|
2044
|
+
segmentAllowed = true;
|
|
2045
|
+
break;
|
|
2405
2046
|
}
|
|
2406
2047
|
}
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
thresholdKb: {
|
|
2419
|
-
type: "number",
|
|
2420
|
-
description: "File size threshold in KB above which a warning is issued",
|
|
2421
|
-
default: 1024
|
|
2048
|
+
if (segmentAllowed)
|
|
2049
|
+
break;
|
|
2050
|
+
}
|
|
2051
|
+
if (segmentAllowed)
|
|
2052
|
+
continue;
|
|
2053
|
+
for (const [manager, patterns] of Object.entries(PKG_MANAGER_DETECTORS)) {
|
|
2054
|
+
if (allowedSet.has(manager))
|
|
2055
|
+
continue;
|
|
2056
|
+
for (const pattern of patterns) {
|
|
2057
|
+
if (pattern.test(trimmed)) {
|
|
2058
|
+
return deny(`"${manager}" is not an allowed package manager. ` + `Allowed package managers for this project: ${allowedList}. ` + `Rewrite this command using an allowed package manager.`);
|
|
2422
2059
|
}
|
|
2423
2060
|
}
|
|
2424
|
-
}
|
|
2425
|
-
{
|
|
2426
|
-
name
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
defaultEnabled: false,
|
|
2433
|
-
category: "Packages & System"
|
|
2434
|
-
},
|
|
2435
|
-
{
|
|
2436
|
-
name: "warn-repeated-tool-calls",
|
|
2437
|
-
displayTitle: "Called the same tool 3+ times with identical arguments",
|
|
2438
|
-
impact: "Usually a sign of a stuck loop burning tokens.",
|
|
2439
|
-
description: "Warn when the same tool is called 3+ times with identical parameters",
|
|
2440
|
-
fn: warnRepeatedToolCalls,
|
|
2441
|
-
match: { events: ["PreToolUse"] },
|
|
2442
|
-
defaultEnabled: false,
|
|
2443
|
-
category: "AI Behavior"
|
|
2444
|
-
},
|
|
2445
|
-
{
|
|
2446
|
-
name: "require-commit-before-stop",
|
|
2447
|
-
displayTitle: "Stopped with uncommitted changes",
|
|
2448
|
-
impact: "Work not in a commit is invisible to teammates and easy to lose.",
|
|
2449
|
-
description: "Require all changes to be committed before Claude stops",
|
|
2450
|
-
fn: requireCommitBeforeStop,
|
|
2451
|
-
match: { events: ["Stop"] },
|
|
2452
|
-
defaultEnabled: false,
|
|
2453
|
-
category: "Workflow"
|
|
2454
|
-
},
|
|
2455
|
-
{
|
|
2456
|
-
name: "require-push-before-stop",
|
|
2457
|
-
displayTitle: "Stopped with unpushed commits",
|
|
2458
|
-
impact: "Local-only commits won't trigger CI or be reviewable.",
|
|
2459
|
-
description: "Require all commits to be pushed to remote before Claude stops",
|
|
2460
|
-
fn: requirePushBeforeStop,
|
|
2461
|
-
match: { events: ["Stop"] },
|
|
2462
|
-
defaultEnabled: false,
|
|
2463
|
-
category: "Workflow",
|
|
2464
|
-
params: {
|
|
2465
|
-
remote: {
|
|
2466
|
-
type: "string",
|
|
2467
|
-
description: "Remote name to push to (default: origin)",
|
|
2468
|
-
default: "origin"
|
|
2469
|
-
},
|
|
2470
|
-
baseBranch: {
|
|
2471
|
-
type: "string",
|
|
2472
|
-
description: "Base branch to compare against (default: main)",
|
|
2473
|
-
default: "main"
|
|
2474
|
-
}
|
|
2061
|
+
}
|
|
2062
|
+
for (const name of blocked) {
|
|
2063
|
+
const lower = name.toLowerCase();
|
|
2064
|
+
if (allowedSet.has(lower))
|
|
2065
|
+
continue;
|
|
2066
|
+
const re = new RegExp(`\\b${lower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
2067
|
+
if (re.test(trimmed)) {
|
|
2068
|
+
return deny(`"${lower}" is not an allowed package manager. ` + `Allowed package managers for this project: ${allowedList}. ` + `Rewrite this command using an allowed package manager.`);
|
|
2475
2069
|
}
|
|
2476
|
-
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
return allow();
|
|
2073
|
+
}
|
|
2074
|
+
function warnBackgroundProcess(ctx) {
|
|
2075
|
+
if (ctx.toolName !== "Bash")
|
|
2076
|
+
return allow();
|
|
2077
|
+
const cmd = getCommand(ctx);
|
|
2078
|
+
const isBackground = NOHUP_RE.test(cmd) || SCREEN_DETACH_RE.test(cmd) || TMUX_DETACH_RE.test(cmd) || DISOWN_RE.test(cmd) || BACKGROUND_AMPERSAND_RE.test(cmd);
|
|
2079
|
+
if (isBackground) {
|
|
2080
|
+
return instruct("STOP: This command starts a background or detached process (nohup, screen -d, tmux -d, or trailing &). Background processes persist after Claude's session and may be difficult to track or stop. Confirm with the user before executing.");
|
|
2081
|
+
}
|
|
2082
|
+
return allow();
|
|
2083
|
+
}
|
|
2084
|
+
function isPlanMode(ctx) {
|
|
2085
|
+
return ctx.session?.permissionMode === "plan";
|
|
2086
|
+
}
|
|
2087
|
+
function requireCommitBeforeStop(ctx) {
|
|
2088
|
+
if (isPlanMode(ctx))
|
|
2089
|
+
return allow("Plan mode — no changes made, skipping commit check.");
|
|
2090
|
+
const cwd = ctx.session?.cwd;
|
|
2091
|
+
if (!cwd)
|
|
2092
|
+
return allow("No working directory available, skipping commit check.");
|
|
2093
|
+
try {
|
|
2094
|
+
const status = execSync("git status --porcelain", {
|
|
2095
|
+
cwd,
|
|
2096
|
+
encoding: "utf8",
|
|
2097
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2098
|
+
timeout: 5000
|
|
2099
|
+
}).trim();
|
|
2100
|
+
if (status.length > 0) {
|
|
2101
|
+
return deny("You have uncommitted changes in the working directory. Commit all changes now.");
|
|
2102
|
+
}
|
|
2103
|
+
return allow("All changes are committed.");
|
|
2104
|
+
} catch {
|
|
2105
|
+
return allow("Not a git repository, skipping commit check.");
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
function requirePushBeforeStop(ctx) {
|
|
2109
|
+
if (isPlanMode(ctx))
|
|
2110
|
+
return allow("Plan mode — no changes made, skipping push check.");
|
|
2111
|
+
const cwd = ctx.session?.cwd;
|
|
2112
|
+
if (!cwd)
|
|
2113
|
+
return allow("No working directory available, skipping push check.");
|
|
2114
|
+
try {
|
|
2115
|
+
const remotes = execSync("git remote", {
|
|
2116
|
+
cwd,
|
|
2117
|
+
encoding: "utf8",
|
|
2118
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2119
|
+
timeout: 3000
|
|
2120
|
+
}).trim();
|
|
2121
|
+
if (!remotes)
|
|
2122
|
+
return allow("No git remote configured, skipping push check.");
|
|
2123
|
+
const remote = ctx.params?.remote ?? "origin";
|
|
2124
|
+
const branch = getCurrentBranch(cwd);
|
|
2125
|
+
if (!branch || branch === "HEAD")
|
|
2126
|
+
return allow("Detached HEAD, skipping push check.");
|
|
2127
|
+
const baseBranch = ctx.params?.baseBranch ?? "main";
|
|
2128
|
+
if (branch === baseBranch) {
|
|
2129
|
+
return allow(`On base branch "${baseBranch}", skipping push check.`);
|
|
2130
|
+
}
|
|
2131
|
+
try {
|
|
2132
|
+
const ahead = execFileSync("git", ["log", `${remote}/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
2133
|
+
if (!ahead) {
|
|
2134
|
+
return allow(`No commits ahead of ${remote}/${baseBranch}, skipping push check.`);
|
|
2135
|
+
}
|
|
2136
|
+
const diff = execFileSync("git", ["diff", "--stat", `${remote}/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
2137
|
+
if (!diff) {
|
|
2138
|
+
return allow(`No file changes compared to ${remote}/${baseBranch}, skipping push check.`);
|
|
2139
|
+
}
|
|
2140
|
+
} catch {}
|
|
2141
|
+
let hasTracking = false;
|
|
2142
|
+
try {
|
|
2143
|
+
execFileSync("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
|
|
2144
|
+
cwd,
|
|
2145
|
+
encoding: "utf8",
|
|
2146
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2147
|
+
timeout: 3000
|
|
2148
|
+
});
|
|
2149
|
+
hasTracking = true;
|
|
2150
|
+
} catch {}
|
|
2151
|
+
if (!hasTracking) {
|
|
2152
|
+
return deny(`Branch "${branch}" has not been pushed to remote "${remote}". ` + `Run now: git push -u ${remote} ${branch}`);
|
|
2153
|
+
}
|
|
2154
|
+
const unpushed = execFileSync("git", ["log", `${remote}/${branch}..HEAD`, "--oneline"], {
|
|
2155
|
+
cwd,
|
|
2156
|
+
encoding: "utf8",
|
|
2157
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2158
|
+
timeout: 5000
|
|
2159
|
+
}).trim();
|
|
2160
|
+
if (unpushed.length > 0) {
|
|
2161
|
+
const commitCount = unpushed.split(`
|
|
2162
|
+
`).length;
|
|
2163
|
+
return deny(`You have ${commitCount} unpushed commit${commitCount > 1 ? "s" : ""} on branch "${branch}". ` + `Run now: git push`);
|
|
2164
|
+
}
|
|
2165
|
+
return allow(`All commits pushed to "${remote}".`);
|
|
2166
|
+
} catch {
|
|
2167
|
+
return allow("Could not check push status, skipping.");
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
function requirePrBeforeStop(ctx) {
|
|
2171
|
+
if (isPlanMode(ctx))
|
|
2172
|
+
return allow("Plan mode — no changes made, skipping PR check.");
|
|
2173
|
+
const cwd = ctx.session?.cwd;
|
|
2174
|
+
if (!cwd)
|
|
2175
|
+
return allow("No working directory available, skipping PR check.");
|
|
2176
|
+
try {
|
|
2177
|
+
try {
|
|
2178
|
+
execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
|
|
2179
|
+
} catch {
|
|
2180
|
+
return allow("GitHub CLI (gh) not installed, skipping PR check.");
|
|
2181
|
+
}
|
|
2182
|
+
const branch = getCurrentBranch(cwd);
|
|
2183
|
+
if (!branch || branch === "HEAD")
|
|
2184
|
+
return allow("Detached HEAD, skipping PR check.");
|
|
2185
|
+
const baseBranch = ctx.params?.baseBranch ?? "main";
|
|
2186
|
+
if (branch === baseBranch) {
|
|
2187
|
+
return allow(`On base branch "${baseBranch}", skipping PR check.`);
|
|
2188
|
+
}
|
|
2189
|
+
try {
|
|
2190
|
+
const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
2191
|
+
if (!ahead) {
|
|
2192
|
+
return allow(`No commits ahead of origin/${baseBranch}, skipping PR check.`);
|
|
2492
2193
|
}
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
displayTitle: "Stopped with a branch that conflicts with main",
|
|
2497
|
-
impact: "Conflicting branches can't merge — surface them early.",
|
|
2498
|
-
description: "Require the current branch to merge cleanly with the base branch before Claude stops",
|
|
2499
|
-
fn: requireNoConflictsBeforeStop,
|
|
2500
|
-
match: { events: ["Stop"] },
|
|
2501
|
-
defaultEnabled: false,
|
|
2502
|
-
category: "Workflow",
|
|
2503
|
-
params: {
|
|
2504
|
-
baseBranch: {
|
|
2505
|
-
type: "string",
|
|
2506
|
-
description: "Base branch to check for conflicts against (default: main)",
|
|
2507
|
-
default: "main"
|
|
2508
|
-
}
|
|
2194
|
+
const diff = execFileSync("git", ["diff", "--stat", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
2195
|
+
if (!diff) {
|
|
2196
|
+
return allow(`No file changes compared to origin/${baseBranch}, skipping PR check.`);
|
|
2509
2197
|
}
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2198
|
+
} catch {}
|
|
2199
|
+
let prJson;
|
|
2200
|
+
try {
|
|
2201
|
+
prJson = execSync("gh pr view --json number,url,state", {
|
|
2202
|
+
cwd,
|
|
2203
|
+
encoding: "utf8",
|
|
2204
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2205
|
+
timeout: 15000
|
|
2206
|
+
}).trim();
|
|
2207
|
+
} catch {
|
|
2208
|
+
return deny(`No pull request found for branch "${branch}". ` + `Run now: gh pr create`);
|
|
2209
|
+
}
|
|
2210
|
+
const pr = JSON.parse(prJson);
|
|
2211
|
+
if (pr.state === "OPEN") {
|
|
2212
|
+
return allow(`PR #${pr.number} exists: ${pr.url}`);
|
|
2213
|
+
}
|
|
2214
|
+
if (pr.state === "MERGED") {
|
|
2215
|
+
return allow(`PR #${pr.number} was merged: ${pr.url}. ` + `Switch off this branch (e.g. 'git checkout ${baseBranch} && git pull') before stopping again.`);
|
|
2216
|
+
}
|
|
2217
|
+
return deny(`Pull request for branch "${branch}" is ${pr.state.toLowerCase()}. Run now: gh pr create`);
|
|
2218
|
+
} catch {
|
|
2219
|
+
return allow("Could not check PR status, skipping.");
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
function requireNoConflictsBeforeStop(ctx) {
|
|
2223
|
+
if (isPlanMode(ctx))
|
|
2224
|
+
return allow("Plan mode — no changes made, skipping conflict check.");
|
|
2225
|
+
const cwd = ctx.session?.cwd;
|
|
2226
|
+
if (!cwd)
|
|
2227
|
+
return allow("No working directory available, skipping conflict check.");
|
|
2228
|
+
const branch = getCurrentBranch(cwd);
|
|
2229
|
+
if (!branch || branch === "HEAD")
|
|
2230
|
+
return allow("Detached HEAD, skipping conflict check.");
|
|
2231
|
+
const baseBranch = ctx.params?.baseBranch ?? "main";
|
|
2232
|
+
if (branch === baseBranch) {
|
|
2233
|
+
return allow(`On base branch "${baseBranch}", skipping conflict check.`);
|
|
2234
|
+
}
|
|
2235
|
+
try {
|
|
2236
|
+
execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
|
|
2237
|
+
} catch {
|
|
2238
|
+
return allow("gh CLI not installed, skipping conflict check.");
|
|
2239
|
+
}
|
|
2240
|
+
let prJson;
|
|
2241
|
+
try {
|
|
2242
|
+
prJson = execSync("gh pr view --json mergeable,number,url,state", {
|
|
2243
|
+
cwd,
|
|
2244
|
+
encoding: "utf8",
|
|
2245
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2246
|
+
timeout: 15000
|
|
2247
|
+
}).trim();
|
|
2248
|
+
} catch {
|
|
2249
|
+
return allow("No pull request found for branch, skipping conflict check.");
|
|
2250
|
+
}
|
|
2251
|
+
let pr;
|
|
2252
|
+
try {
|
|
2253
|
+
pr = JSON.parse(prJson);
|
|
2254
|
+
} catch {
|
|
2255
|
+
return allow("Could not parse gh pr view output, skipping conflict check.");
|
|
2256
|
+
}
|
|
2257
|
+
if (pr.state !== "OPEN") {
|
|
2258
|
+
return allow(`PR #${pr.number} is ${pr.state.toLowerCase()}; skipping conflict check.`);
|
|
2259
|
+
}
|
|
2260
|
+
try {
|
|
2261
|
+
execFileSync("git", ["rev-parse", "--verify", `origin/${baseBranch}`], {
|
|
2262
|
+
cwd,
|
|
2263
|
+
encoding: "utf8",
|
|
2264
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2265
|
+
timeout: 3000
|
|
2266
|
+
});
|
|
2267
|
+
const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
2268
|
+
if (ahead) {
|
|
2269
|
+
execFileSync("git", ["merge-tree", "--write-tree", "--name-only", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 1e4 });
|
|
2270
|
+
}
|
|
2271
|
+
} catch (err) {
|
|
2272
|
+
const e = err;
|
|
2273
|
+
if (e.status === 1) {
|
|
2274
|
+
const out = (typeof e.stdout === "string" ? e.stdout : e.stdout?.toString("utf8") ?? "").trim();
|
|
2275
|
+
const lines = out.split(`
|
|
2276
|
+
`);
|
|
2277
|
+
const files = [];
|
|
2278
|
+
for (let i = 1;i < lines.length; i++) {
|
|
2279
|
+
const line = lines[i];
|
|
2280
|
+
if (line === "")
|
|
2281
|
+
break;
|
|
2282
|
+
files.push(line);
|
|
2283
|
+
}
|
|
2284
|
+
const fileList = files.length ? files.join(", ") : "one or more files";
|
|
2285
|
+
return deny(`Branch "${branch}" has merge conflicts with ${baseBranch} in: ${fileList}. ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
if (pr.mergeable === "CONFLICTING") {
|
|
2289
|
+
return deny(`PR #${pr.number} has merge conflicts per GitHub (${pr.url}). ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
|
|
2290
|
+
}
|
|
2291
|
+
if (pr.mergeable === "UNKNOWN") {
|
|
2292
|
+
return deny(`GitHub is still computing mergeability for PR #${pr.number} (${pr.url}). ` + `Wait ~10 seconds, then re-check with \`gh pr view --json mergeable\` before attempting to stop again.`);
|
|
2293
|
+
}
|
|
2294
|
+
return allow(`PR #${pr.number} merges cleanly per GitHub.`);
|
|
2295
|
+
}
|
|
2296
|
+
function requireCiGreenBeforeStop(ctx) {
|
|
2297
|
+
if (isPlanMode(ctx))
|
|
2298
|
+
return allow("Plan mode — no changes made, skipping CI check.");
|
|
2299
|
+
const cwd = ctx.session?.cwd;
|
|
2300
|
+
if (!cwd)
|
|
2301
|
+
return allow("No working directory available, skipping CI check.");
|
|
2302
|
+
try {
|
|
2303
|
+
try {
|
|
2304
|
+
execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
|
|
2305
|
+
} catch {
|
|
2306
|
+
return allow("GitHub CLI (gh) not installed, skipping CI check.");
|
|
2307
|
+
}
|
|
2308
|
+
const branch = getCurrentBranch(cwd);
|
|
2309
|
+
if (!branch || branch === "HEAD")
|
|
2310
|
+
return allow("Detached HEAD, skipping CI check.");
|
|
2311
|
+
const sha = getHeadSha(cwd);
|
|
2312
|
+
let workflowRuns = [];
|
|
2313
|
+
try {
|
|
2314
|
+
const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "20", "--json", "status,conclusion,name,headSha"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 }).trim();
|
|
2315
|
+
if (runsJson && runsJson !== "[]") {
|
|
2316
|
+
const allWorkflowRuns = JSON.parse(runsJson);
|
|
2317
|
+
const headRuns = sha ? allWorkflowRuns.filter((r) => r.headSha === sha) : allWorkflowRuns;
|
|
2318
|
+
const seen = new Set;
|
|
2319
|
+
workflowRuns = headRuns.filter((r) => {
|
|
2320
|
+
if (seen.has(r.name))
|
|
2321
|
+
return false;
|
|
2322
|
+
seen.add(r.name);
|
|
2323
|
+
return true;
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
} catch {}
|
|
2327
|
+
let thirdPartyChecks = [];
|
|
2328
|
+
let commitStatuses = [];
|
|
2329
|
+
if (sha) {
|
|
2330
|
+
thirdPartyChecks = getThirdPartyCheckRuns(cwd, sha);
|
|
2331
|
+
commitStatuses = getCommitStatuses(cwd, sha);
|
|
2332
|
+
}
|
|
2333
|
+
const allChecks = [...workflowRuns, ...thirdPartyChecks, ...commitStatuses];
|
|
2334
|
+
if (allChecks.length === 0)
|
|
2335
|
+
return allow(`No CI runs found for branch "${branch}".`);
|
|
2336
|
+
const failing = allChecks.filter((r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped" && r.conclusion !== "cancelled" && r.conclusion !== "neutral");
|
|
2337
|
+
if (failing.length > 0) {
|
|
2338
|
+
const names = failing.map((r) => `"${r.name}"`).join(", ");
|
|
2339
|
+
return deny(`CI checks are failing on branch "${branch}": ${names}. Fix the failing checks now.`);
|
|
2340
|
+
}
|
|
2341
|
+
const pending = allChecks.filter((r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting");
|
|
2342
|
+
if (pending.length > 0) {
|
|
2343
|
+
const names = pending.map((r) => `"${r.name}"`).join(", ");
|
|
2344
|
+
return deny(`CI checks are still running on branch "${branch}": ${names}. Wait for all checks to complete, then verify they pass.`);
|
|
2345
|
+
}
|
|
2346
|
+
return allow(`All CI checks passed on branch "${branch}".`);
|
|
2347
|
+
} catch {
|
|
2348
|
+
return allow("Could not check CI status, skipping.");
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
function assertCatalogBijection() {
|
|
2352
|
+
const implNames = new Set(Object.keys(POLICY_IMPLEMENTATIONS));
|
|
2353
|
+
const missing = POLICY_CATALOG.filter((e) => !implNames.has(e.name)).map((e) => e.name);
|
|
2354
|
+
if (missing.length > 0) {
|
|
2355
|
+
throw new Error(`failproofai: builtin policies missing an implementation: ${missing.join(", ")}`);
|
|
2356
|
+
}
|
|
2357
|
+
const catalogNames = new Set(POLICY_CATALOG.map((e) => e.name));
|
|
2358
|
+
const orphaned = [...implNames].filter((n) => !catalogNames.has(n));
|
|
2359
|
+
if (orphaned.length > 0) {
|
|
2360
|
+
throw new Error(`failproofai: policy implementations with no catalog entry: ${orphaned.join(", ")}`);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
function registerBuiltinPolicies(enabledNames) {
|
|
2364
|
+
const enabledSet = new Set(enabledNames.map(normalizePolicyName));
|
|
2365
|
+
for (const policy of BUILTIN_POLICIES) {
|
|
2366
|
+
if (policy.alwaysOn || enabledSet.has(normalizePolicyName(policy.name))) {
|
|
2367
|
+
registerPolicy(policy.name, policy.description, policy.fn, policy.match, 0, policy.params);
|
|
2520
2368
|
}
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
var isClaudeInternalPath, isClaudeSettingsFile, SHELL_OPERATORS, SHELL_METACHAR_RE, JWT_RE, API_KEY_PATTERNS, CONNECTION_STRING_RE, PRIVATE_KEY_RE, BEARER_TOKEN_RE, SECRET_PATTERNS, SQL_TOOL_RE, DESTRUCTIVE_SQL_RE, DELETE_NO_WHERE_RE, SQL_WHERE_RE, SCHEMA_ALTER_RE, PUBLISH_CMD_RE, ENV_PRINTENV_RE, ECHO_ENV_RE, EXPORT_RE, PS_ENV_VAR_RE, PS_CHILDITEM_ENV_RE, DOTNET_GETENV_RE, CMD_ECHO_ENV_RE, ENV_FILE_PATH_RE, ENV_CMD_RE, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, SEGMENT_SEPARATORS, COMMAND_PREFIX_TOKENS, SELF_BINARY_TOKEN_RE, SELF_ENTRY_PATH_RE, CONFIG_SUBCOMMAND_RE, PAUSE_FLAG_RE, ENV_ASSIGNMENT_RE, RUNNER_OPERAND_RE, PS_WEB_PIPE_RE, SHORT_FLAG_BUNDLE_RE, SAFE_FORCE_PREFIXES, SECRET_FILE_RE, SECRET_FILE_ID_RSA_RE, SECRET_FILE_CREDENTIALS_RE, GIT_COMMIT_MERGE_RE, FAILPROOFAI_STATE_WRITE_RE, FAILPROOFAI_UNINSTALL_RE, GIT_AMEND_RE, GIT_STASH_DROP_RE, GIT_ADD_ALL_RE, NPM_GLOBAL_RE, YARN_GLOBAL_RE, PNPM_GLOBAL_RE, BUN_GLOBAL_RE, CARGO_INSTALL_RE, PIP_SYSTEM_RE, PKG_MANAGER_DETECTORS, NOHUP_RE, SCREEN_DETACH_RE, TMUX_DETACH_RE, DISOWN_RE, BACKGROUND_AMPERSAND_RE, KUBECTL_RE, TERRAFORM_RE, AWS_CLI_RE, GCLOUD_RE, AZ_CLI_RE, HELM_RE, GH_PIPELINE_RE, gitBranchCache, GIT_BRANCH_CACHE_MAX_ENTRIES = 500, EVAL_FLAG_RE, HOME_PREFIX_RE, RM_CMD_RE, FIND_CMD_RE, FIND_EXEC_RE, FIND_GLOBAL_OPT_RE, FIND_EXPR_START_RE, SCRATCH_ROOTS, CATASTROPHIC_DEPTH = 2, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, POLICY_IMPLEMENTATIONS, BUILTIN_POLICIES;
|
|
2372
|
+
var init_builtin_policies = __esm(() => {
|
|
2373
|
+
init_policy_catalog();
|
|
2374
|
+
init_hook_logger();
|
|
2375
|
+
isClaudeInternalPath = isAgentInternalPath;
|
|
2376
|
+
isClaudeSettingsFile = isAgentSettingsFile;
|
|
2377
|
+
SHELL_OPERATORS = new Set(["&&", "||", "|", ";"]);
|
|
2378
|
+
SHELL_METACHAR_RE = /[;&<>`$()\\]/;
|
|
2379
|
+
JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
|
|
2380
|
+
API_KEY_PATTERNS = [
|
|
2381
|
+
[/sk-ant-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
|
|
2382
|
+
[/sk-proj-[A-Za-z0-9\-_]{20,}/, "OpenAI project API key"],
|
|
2383
|
+
[/sk-[A-Za-z0-9]{20,}/, "OpenAI API key"],
|
|
2384
|
+
[/ghp_[A-Za-z0-9]{36}/, "GitHub personal access token"],
|
|
2385
|
+
[/github_pat_[A-Za-z0-9_]{82}/, "GitHub fine-grained token"],
|
|
2386
|
+
[/AKIA[A-Z0-9]{16}/, "AWS access key ID"],
|
|
2387
|
+
[/sk_live_[A-Za-z0-9]{24,}/, "Stripe live secret key"],
|
|
2388
|
+
[/sk_test_[A-Za-z0-9]{24,}/, "Stripe test secret key"],
|
|
2389
|
+
[/AIza[0-9A-Za-z\-_]{35}/, "Google API key"]
|
|
2390
|
+
];
|
|
2391
|
+
CONNECTION_STRING_RE = /(?:postgresql|postgres|mysql|mongodb(?:\+srv)?|redis|amqps?|smtps?):\/\/[^@\s]+@/;
|
|
2392
|
+
PRIVATE_KEY_RE = /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/;
|
|
2393
|
+
BEARER_TOKEN_RE = /Authorization:\s*Bearer\s+[A-Za-z0-9\-._~+/]{20,}/i;
|
|
2394
|
+
SECRET_PATTERNS = [
|
|
2395
|
+
[PRIVATE_KEY_RE, "private key"],
|
|
2396
|
+
[JWT_RE, "JWT"],
|
|
2397
|
+
[BEARER_TOKEN_RE, "bearer token"],
|
|
2398
|
+
[CONNECTION_STRING_RE, "database credentials"],
|
|
2399
|
+
...API_KEY_PATTERNS
|
|
2521
2400
|
];
|
|
2401
|
+
SQL_TOOL_RE = /\b(?:psql|mysql|sqlite3|pgcli|clickhouse-client)\b/;
|
|
2402
|
+
DESTRUCTIVE_SQL_RE = /\b(?:DROP\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE\b)/i;
|
|
2403
|
+
DELETE_NO_WHERE_RE = /\bDELETE\s+FROM\b/i;
|
|
2404
|
+
SQL_WHERE_RE = /\bWHERE\b/i;
|
|
2405
|
+
SCHEMA_ALTER_RE = /\bALTER\s+TABLE\b[\s\S]*\b(?:DROP\s+COLUMN|ADD\s+COLUMN|RENAME\s+(?:COLUMN|TO)|MODIFY\s+COLUMN)\b/i;
|
|
2406
|
+
PUBLISH_CMD_RE = /(?:npm\s+publish|bun\s+publish|pnpm\s+publish|yarn\s+npm\s+publish|twine\s+upload|poetry\s+publish|cargo\s+publish|gem\s+push)\b/;
|
|
2407
|
+
ENV_PRINTENV_RE = /(?:^|\s|;|&&|\|\|)(?:env|printenv)(?:\s|$|;|&&|\|)/;
|
|
2408
|
+
ECHO_ENV_RE = /echo\s+.*\$\{?[A-Za-z_]/;
|
|
2409
|
+
EXPORT_RE = /(?:^|\s|;|&&|\|\|)export\s+\w+/;
|
|
2410
|
+
PS_ENV_VAR_RE = /\$env:[A-Za-z_]/i;
|
|
2411
|
+
PS_CHILDITEM_ENV_RE = /(?:Get-ChildItem|dir|gci|ls)\s+Env:/i;
|
|
2412
|
+
DOTNET_GETENV_RE = /\[Environment\]::GetEnvironment/i;
|
|
2413
|
+
CMD_ECHO_ENV_RE = /echo\s+%[A-Za-z_]/i;
|
|
2414
|
+
ENV_FILE_PATH_RE = /(?:^|[\\/])\.env(?:\.|$)/;
|
|
2415
|
+
ENV_CMD_RE = /\.env(?:\b|\s|$|\.)/;
|
|
2416
|
+
PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
|
|
2417
|
+
RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
|
|
2418
|
+
CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
|
|
2419
|
+
SEGMENT_SEPARATORS = /[;&|\n\r(){}`]+/;
|
|
2420
|
+
COMMAND_PREFIX_TOKENS = new Set([
|
|
2421
|
+
"npx",
|
|
2422
|
+
"bunx",
|
|
2423
|
+
"pnpx",
|
|
2424
|
+
"npm",
|
|
2425
|
+
"pnpm",
|
|
2426
|
+
"yarn",
|
|
2427
|
+
"dlx",
|
|
2428
|
+
"exec",
|
|
2429
|
+
"run",
|
|
2430
|
+
"eval",
|
|
2431
|
+
"node",
|
|
2432
|
+
"bun",
|
|
2433
|
+
"deno",
|
|
2434
|
+
"env",
|
|
2435
|
+
"command",
|
|
2436
|
+
"builtin",
|
|
2437
|
+
"nohup",
|
|
2438
|
+
"setsid",
|
|
2439
|
+
"time",
|
|
2440
|
+
"timeout",
|
|
2441
|
+
"nice",
|
|
2442
|
+
"stdbuf",
|
|
2443
|
+
"xargs",
|
|
2444
|
+
"sudo",
|
|
2445
|
+
"doas",
|
|
2446
|
+
"sh",
|
|
2447
|
+
"bash",
|
|
2448
|
+
"zsh",
|
|
2449
|
+
"dash",
|
|
2450
|
+
"ksh",
|
|
2451
|
+
"fish",
|
|
2452
|
+
"ash"
|
|
2453
|
+
]);
|
|
2454
|
+
SELF_BINARY_TOKEN_RE = /(?:^|\/)failproofai[^/]*$/;
|
|
2455
|
+
SELF_ENTRY_PATH_RE = /failproofai\/(?:dist\/(?:cli|index)\.mjs|bin\/failproofai\.mjs)$/;
|
|
2456
|
+
CONFIG_SUBCOMMAND_RE = /^(?:config|configure|setup)$/;
|
|
2457
|
+
PAUSE_FLAG_RE = /^--pause(?:=|$)/;
|
|
2458
|
+
ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
2459
|
+
RUNNER_OPERAND_RE = /^\d+[a-z]*$/i;
|
|
2460
|
+
PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
|
|
2461
|
+
SHORT_FLAG_BUNDLE_RE = /^-[a-zA-Z]*f[a-zA-Z]*$/;
|
|
2462
|
+
SAFE_FORCE_PREFIXES = ["--force-with-lease", "--force-if-includes"];
|
|
2463
|
+
SECRET_FILE_RE = /\.(?:pem|key)$/;
|
|
2464
|
+
SECRET_FILE_ID_RSA_RE = /id_rsa/;
|
|
2465
|
+
SECRET_FILE_CREDENTIALS_RE = /credentials/;
|
|
2466
|
+
GIT_COMMIT_MERGE_RE = /git\s+(commit|merge|rebase|cherry-pick)\b/;
|
|
2467
|
+
FAILPROOFAI_STATE_WRITE_RE = /\b(?:rm|unlink|shred|mv|truncate)\b[^;&|]*\.failproofai(?:\/|\b)/;
|
|
2468
|
+
FAILPROOFAI_UNINSTALL_RE = /(?:npm\s+(?:uninstall|remove|un|r)\s.*failproofai|bun\s+remove\s.*failproofai|yarn\s+global\s+remove\s+failproofai|pnpm\s+(?:remove|uninstall|un)\s.*failproofai)/;
|
|
2469
|
+
GIT_AMEND_RE = /\bgit\s+commit\b.*--amend\b/;
|
|
2470
|
+
GIT_STASH_DROP_RE = /\bgit\s+stash\s+(?:drop|clear)\b/;
|
|
2471
|
+
GIT_ADD_ALL_RE = /\bgit\s+add\s+(?:-A\b|--all\b|\.(?:\s|$|;|&&|\|\|))/;
|
|
2472
|
+
NPM_GLOBAL_RE = /\bnpm\s+(?:install|i)\b(?=.*(?:\s-g\b|--global\b))/;
|
|
2473
|
+
YARN_GLOBAL_RE = /\byarn\s+global\s+add\b/;
|
|
2474
|
+
PNPM_GLOBAL_RE = /\bpnpm\s+(?:add|install|i)\b(?=.*(?:\s-g\b|--global\b))/;
|
|
2475
|
+
BUN_GLOBAL_RE = /\bbun\s+(?:install|add)\b(?=.*(?:\s-g\b|--global\b))/;
|
|
2476
|
+
CARGO_INSTALL_RE = /\bcargo\s+install\b/;
|
|
2477
|
+
PIP_SYSTEM_RE = /\bpip(?:3)?\s+install\b(?=.*(?:--user\b|--break-system-packages\b))/;
|
|
2478
|
+
PKG_MANAGER_DETECTORS = {
|
|
2479
|
+
pip: [/\bpip\b/, /\bpip3\b/, /\bpython3?\s+-m\s+pip\b/],
|
|
2480
|
+
npm: [/\bnpm\b/, /\bnpx\b/],
|
|
2481
|
+
yarn: [/\byarn\b/],
|
|
2482
|
+
pnpm: [/\bpnpm\b/, /\bpnpx\b/],
|
|
2483
|
+
bun: [/\bbun\b/, /\bbunx\b/],
|
|
2484
|
+
uv: [/\buv\b/],
|
|
2485
|
+
poetry: [/\bpoetry\b/],
|
|
2486
|
+
pipenv: [/\bpipenv\b/],
|
|
2487
|
+
conda: [/\bconda\b/],
|
|
2488
|
+
cargo: [/\bcargo\b/]
|
|
2489
|
+
};
|
|
2490
|
+
NOHUP_RE = /\bnohup\s+\S/;
|
|
2491
|
+
SCREEN_DETACH_RE = /\bscreen\s+-[A-Za-z]*d[A-Za-z]*\b/;
|
|
2492
|
+
TMUX_DETACH_RE = /\btmux\s+(?:new-session|new)\b[^|&;]*-d\b/;
|
|
2493
|
+
DISOWN_RE = /\bdisown\b/;
|
|
2494
|
+
BACKGROUND_AMPERSAND_RE = /(?<![&|])\s?&\s*(?:$|#|;)/;
|
|
2495
|
+
KUBECTL_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*kubectl(?:\s|$)/;
|
|
2496
|
+
TERRAFORM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*(?:terraform|tofu)(?:\s|$)/;
|
|
2497
|
+
AWS_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*aws(?:\s|$)/;
|
|
2498
|
+
GCLOUD_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gcloud(?:\s|$)/;
|
|
2499
|
+
AZ_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*az(?:\s|$)/;
|
|
2500
|
+
HELM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*helm(?:\s|$)/;
|
|
2501
|
+
GH_PIPELINE_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gh\s+(?:workflow\s+(?:run|enable|disable)|run\s+(?:rerun|cancel)|pr\s+merge|release\s+(?:create|delete)|cache\s+delete|secret\s+(?:set|delete))\b/;
|
|
2502
|
+
gitBranchCache = new Map;
|
|
2503
|
+
EVAL_FLAG_RE = /^-{1,2}c$/;
|
|
2504
|
+
HOME_PREFIX_RE = /^(?:~[A-Za-z0-9_.-]*|\$HOME|\$\{HOME\})(?=$|\/)/;
|
|
2505
|
+
RM_CMD_RE = /^(?:\/\S*\/)?rm$/;
|
|
2506
|
+
FIND_CMD_RE = /^(?:\/\S*\/)?find$/;
|
|
2507
|
+
FIND_EXEC_RE = /^-(?:exec|execdir|ok|okdir)$/;
|
|
2508
|
+
FIND_GLOBAL_OPT_RE = /^-(?:[HLP]|D|O\d*)$/;
|
|
2509
|
+
FIND_EXPR_START_RE = /^(?:-|\\?[(!])/;
|
|
2510
|
+
SCRATCH_ROOTS = ["/tmp", "/var/tmp"];
|
|
2511
|
+
READ_LIKE_CMDS = /(?:^|;|&&|\|\||\|)\s*(?:ls|find|cat|head|tail|less|more|wc|file|stat|tree|du)\s/;
|
|
2512
|
+
SEGMENT_SPLIT_RE = /\s*(?:&&|\|\||\||;)\s*/;
|
|
2513
|
+
POLICY_IMPLEMENTATIONS = {
|
|
2514
|
+
"sanitize-jwt": sanitizeJwt,
|
|
2515
|
+
"sanitize-api-keys": sanitizeApiKeys,
|
|
2516
|
+
"sanitize-connection-strings": sanitizeConnectionStrings,
|
|
2517
|
+
"sanitize-private-key-content": sanitizePrivateKeyContent,
|
|
2518
|
+
"sanitize-bearer-tokens": sanitizeBearerTokens,
|
|
2519
|
+
"protect-env-vars": protectEnvVars,
|
|
2520
|
+
"block-env-files": blockEnvFiles,
|
|
2521
|
+
"block-read-outside-cwd": blockReadOutsideCwd,
|
|
2522
|
+
"block-sudo": blockSudo,
|
|
2523
|
+
"block-curl-pipe-sh": blockCurlPipeSh,
|
|
2524
|
+
"block-rm-rf": blockRmRf,
|
|
2525
|
+
"block-failproofai-commands": blockFailproofaiCommands,
|
|
2526
|
+
"block-kubectl": blockKubectl,
|
|
2527
|
+
"block-terraform": blockTerraform,
|
|
2528
|
+
"block-aws-cli": blockAwsCli,
|
|
2529
|
+
"block-gcloud": blockGcloud,
|
|
2530
|
+
"block-az-cli": blockAzCli,
|
|
2531
|
+
"block-helm": blockHelm,
|
|
2532
|
+
"block-gh-pipeline": blockGhPipeline,
|
|
2533
|
+
"block-secrets-write": blockSecretsWrite,
|
|
2534
|
+
"block-push-master": blockPushMaster,
|
|
2535
|
+
"block-force-push": blockForcePush,
|
|
2536
|
+
"block-work-on-main": blockWorkOnMain,
|
|
2537
|
+
"warn-git-amend": warnGitAmend,
|
|
2538
|
+
"warn-git-stash-drop": warnGitStashDrop,
|
|
2539
|
+
"warn-all-files-staged": warnAllFilesStaged,
|
|
2540
|
+
"warn-destructive-sql": warnDestructiveSql,
|
|
2541
|
+
"warn-schema-alteration": warnSchemaAlteration,
|
|
2542
|
+
"warn-package-publish": warnPackagePublish,
|
|
2543
|
+
"warn-global-package-install": warnGlobalPackageInstall,
|
|
2544
|
+
"prefer-package-manager": preferPackageManager,
|
|
2545
|
+
"warn-large-file-write": warnLargeFileWrite,
|
|
2546
|
+
"warn-background-process": warnBackgroundProcess,
|
|
2547
|
+
"warn-repeated-tool-calls": warnRepeatedToolCalls,
|
|
2548
|
+
"require-commit-before-stop": requireCommitBeforeStop,
|
|
2549
|
+
"require-push-before-stop": requirePushBeforeStop,
|
|
2550
|
+
"require-pr-before-stop": requirePrBeforeStop,
|
|
2551
|
+
"require-no-conflicts-before-stop": requireNoConflictsBeforeStop,
|
|
2552
|
+
"require-ci-green-before-stop": requireCiGreenBeforeStop
|
|
2553
|
+
};
|
|
2554
|
+
assertCatalogBijection();
|
|
2555
|
+
BUILTIN_POLICIES = POLICY_CATALOG.map((entry) => ({
|
|
2556
|
+
...entry,
|
|
2557
|
+
fn: POLICY_IMPLEMENTATIONS[entry.name]
|
|
2558
|
+
}));
|
|
2522
2559
|
});
|
|
2523
2560
|
|
|
2524
2561
|
// package.json
|
|
2525
|
-
var version = "1.0.1";
|
|
2562
|
+
var version = "1.0.2-beta.1";
|
|
2526
2563
|
var init_package = () => {};
|
|
2527
2564
|
|
|
2528
2565
|
// src/posthog-key.ts
|
|
@@ -2864,17 +2901,17 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
2864
2901
|
const instructEntries = [];
|
|
2865
2902
|
const allowEntries = [];
|
|
2866
2903
|
for (const policy of policies) {
|
|
2867
|
-
const schema =
|
|
2904
|
+
const schema = policy.params;
|
|
2905
|
+
const userParams = getConfigParamsFor(config, policy.name) ?? {};
|
|
2868
2906
|
let ctx;
|
|
2869
2907
|
if (schema) {
|
|
2870
|
-
const userParams = getConfigParamsFor(config, policy.name) ?? {};
|
|
2871
2908
|
const resolvedParams = {};
|
|
2872
2909
|
for (const [key, spec] of Object.entries(schema)) {
|
|
2873
2910
|
resolvedParams[key] = key in userParams ? userParams[key] : spec.default;
|
|
2874
2911
|
}
|
|
2875
2912
|
ctx = { ...baseCtx, params: resolvedParams };
|
|
2876
2913
|
} else {
|
|
2877
|
-
ctx = { ...baseCtx, params:
|
|
2914
|
+
ctx = { ...baseCtx, params: userParams };
|
|
2878
2915
|
}
|
|
2879
2916
|
let result;
|
|
2880
2917
|
try {
|
|
@@ -2882,8 +2919,8 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
2882
2919
|
} catch (err) {
|
|
2883
2920
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2884
2921
|
hookLogWarn(`policy "${policy.name}" threw: ${msg}`);
|
|
2885
|
-
const
|
|
2886
|
-
if (
|
|
2922
|
+
const isBuiltin = policy.name.startsWith(`${DEFAULT_POLICY_NAMESPACE}/`);
|
|
2923
|
+
if (isBuiltin) {
|
|
2887
2924
|
trackHookEvent(getInstanceId(), "policy_evaluation_error", {
|
|
2888
2925
|
policy_name: policy.name,
|
|
2889
2926
|
event_type: eventType,
|
|
@@ -3560,13 +3597,10 @@ You MUST complete the above action(s) NOW. Do NOT ask the user for confirmation
|
|
|
3560
3597
|
}
|
|
3561
3598
|
return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
|
|
3562
3599
|
}
|
|
3563
|
-
var POLICY_PARAMS_MAP;
|
|
3564
3600
|
var init_policy_evaluator = __esm(() => {
|
|
3565
|
-
init_builtin_policies();
|
|
3566
3601
|
init_hook_logger();
|
|
3567
3602
|
init_hook_telemetry();
|
|
3568
3603
|
init_telemetry_id();
|
|
3569
|
-
POLICY_PARAMS_MAP = new Map(BUILTIN_POLICIES.filter((p) => p.params).map((p) => [normalizePolicyName(p.name), p.params]));
|
|
3570
3604
|
});
|
|
3571
3605
|
|
|
3572
3606
|
// src/hooks/custom-hooks-registry.ts
|
|
@@ -3836,7 +3870,7 @@ async function loadSingleFile(absPath, opts) {
|
|
|
3836
3870
|
if (cached?.fingerprint === fingerprint) {
|
|
3837
3871
|
for (const hook of cached.hooks)
|
|
3838
3872
|
customPolicies.add({ ...hook });
|
|
3839
|
-
return;
|
|
3873
|
+
return null;
|
|
3840
3874
|
}
|
|
3841
3875
|
const entryTmp = absPath + tmpSuffix;
|
|
3842
3876
|
const fileUrl = pathToFileURL2(entryTmp).href;
|
|
@@ -3848,6 +3882,7 @@ async function loadSingleFile(absPath, opts) {
|
|
|
3848
3882
|
fingerprint,
|
|
3849
3883
|
hooks: getCustomHooks().slice(hooksBefore).map((hook) => ({ ...hook }))
|
|
3850
3884
|
});
|
|
3885
|
+
return null;
|
|
3851
3886
|
} catch (err) {
|
|
3852
3887
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3853
3888
|
const errorType = /Cannot find module|MODULE_NOT_FOUND|ENOENT/i.test(msg) ? "module_not_found" : /SyntaxError|Unexpected token/i.test(msg) ? "syntax_error" : /timed out loading module/i.test(msg) ? "load_timeout" : "runtime_error";
|
|
@@ -3860,6 +3895,7 @@ async function loadSingleFile(absPath, opts) {
|
|
|
3860
3895
|
if (opts?.strict)
|
|
3861
3896
|
throw new Error(`Failed to load custom hooks from ${absPath}: ${msg}`);
|
|
3862
3897
|
hookLogError(`failed to load custom hooks from ${absPath}: ${msg}`);
|
|
3898
|
+
return { type: errorType, reason: msg };
|
|
3863
3899
|
} finally {
|
|
3864
3900
|
g[LOADING_KEY] = false;
|
|
3865
3901
|
await cleanupTmpFiles(tmpFiles);
|
|
@@ -3887,9 +3923,24 @@ function warnSkippedPolicyFiles(dir, scope) {
|
|
|
3887
3923
|
const suggest = (n) => n.replace(/\.(js|mjs|ts)$/, "-policies.$1");
|
|
3888
3924
|
hookLogWarn(`${scope} policies: ${skipped.length} file(s) in ${dir} are NOT loaded — ` + `the convention only loads names ending in "policies.js|mjs|ts". ` + skipped.map((n) => `${n} → rename to ${suggest(n)}`).join("; "));
|
|
3889
3925
|
}
|
|
3926
|
+
function reconcilePackManifest(pack, loaded) {
|
|
3927
|
+
if (!pack || pack.policies.length === 0)
|
|
3928
|
+
return;
|
|
3929
|
+
const declared = new Set(pack.policies.map((p) => p.name));
|
|
3930
|
+
const registered = new Set(loaded.map((h) => h.name));
|
|
3931
|
+
const missing = [...declared].filter((n) => !registered.has(n));
|
|
3932
|
+
const extra = [...registered].filter((n) => !declared.has(n));
|
|
3933
|
+
if (missing.length > 0) {
|
|
3934
|
+
hookLogWarn(`pack ${pack.id}@${pack.version} declares ${missing.join(", ")} but its artifact does not ` + `register ${missing.length === 1 ? "it" : "them"} — listed as protection that never runs`);
|
|
3935
|
+
}
|
|
3936
|
+
if (extra.length > 0) {
|
|
3937
|
+
hookLogWarn(`pack ${pack.id}@${pack.version} registers undeclared ${extra.join(", ")} — ` + `${extra.length === 1 ? "it enforces" : "they enforce"} but will not appear in listings`);
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3890
3940
|
async function loadAllCustomHooks(customPoliciesPaths, opts) {
|
|
3891
3941
|
clearCustomHooks();
|
|
3892
3942
|
const conventionSources = [];
|
|
3943
|
+
const packFailures = new Map;
|
|
3893
3944
|
const projectRoot = findProjectConfigDir(opts?.sessionCwd ?? process.cwd());
|
|
3894
3945
|
const conventionEnabled = opts?.customPoliciesEnabled !== false;
|
|
3895
3946
|
const loadedPaths = new Set;
|
|
@@ -3906,28 +3957,50 @@ async function loadAllCustomHooks(customPoliciesPaths, opts) {
|
|
|
3906
3957
|
cloudManagedByPath.set(key, policy);
|
|
3907
3958
|
}
|
|
3908
3959
|
}
|
|
3960
|
+
const packByPath = new Map;
|
|
3961
|
+
for (const pack of opts?.packs ?? []) {
|
|
3962
|
+
const key = resolve5(pack.path);
|
|
3963
|
+
const existing = packByPath.get(key);
|
|
3964
|
+
if (!existing) {
|
|
3965
|
+
packByPath.set(key, pack);
|
|
3966
|
+
continue;
|
|
3967
|
+
}
|
|
3968
|
+
hookLogWarn(`packs ${existing.id} and ${pack.id} have identical source, so they share one artifact ` + `and load as one pack; enforcing it if either asks to enforce`);
|
|
3969
|
+
if (existing.effect !== "enforce" && pack.effect === "enforce") {
|
|
3970
|
+
packByPath.set(key, pack);
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3909
3973
|
for (const customPoliciesPath of typeof customPoliciesPaths === "string" ? [customPoliciesPaths] : customPoliciesPaths ?? []) {
|
|
3910
3974
|
const absPath = resolve5(projectRoot, customPoliciesPath);
|
|
3975
|
+
const cloudManaged = cloudManagedByPath.get(absPath);
|
|
3976
|
+
const pack = packByPath.get(absPath);
|
|
3911
3977
|
if (existsSync4(absPath)) {
|
|
3912
3978
|
if (!loadedPaths.has(absPath)) {
|
|
3913
3979
|
loadedPaths.add(absPath);
|
|
3914
3980
|
const hooksBefore = getCustomHooks().length;
|
|
3915
|
-
await loadSingleFile(absPath, {
|
|
3916
|
-
verifyEntrySha:
|
|
3981
|
+
const failure = await loadSingleFile(absPath, {
|
|
3982
|
+
verifyEntrySha: cloudManaged?.sha256 ?? pack?.sha256
|
|
3917
3983
|
});
|
|
3984
|
+
if (failure && pack)
|
|
3985
|
+
packFailures.set(pack.id, failure);
|
|
3918
3986
|
for (const hook of getCustomHooks().slice(hooksBefore)) {
|
|
3919
|
-
const cloudManaged = cloudManagedByPath.get(absPath);
|
|
3920
3987
|
const tagged = hook;
|
|
3921
3988
|
if (cloudManaged) {
|
|
3922
3989
|
tagged.__cloudManaged = cloudManaged;
|
|
3923
3990
|
tagged.__policyId = `cloud:${cloudManaged.id}@${cloudManaged.version}:${hook.name}`;
|
|
3991
|
+
} else if (pack) {
|
|
3992
|
+
tagged.__pack = pack;
|
|
3993
|
+
tagged.__policyId = `pack:${pack.id}@${pack.version}:${hook.name}`;
|
|
3924
3994
|
} else {
|
|
3925
3995
|
tagged.__policyId = customPolicyId(absPath, hook.name);
|
|
3926
3996
|
}
|
|
3927
3997
|
}
|
|
3998
|
+
reconcilePackManifest(pack, getCustomHooks().slice(hooksBefore));
|
|
3928
3999
|
}
|
|
3929
4000
|
} else {
|
|
3930
4001
|
hookLogWarn(`custom policy path not found: ${absPath}`);
|
|
4002
|
+
if (pack)
|
|
4003
|
+
packFailures.set(pack.id, { type: "path_missing", reason: `path missing: ${absPath}` });
|
|
3931
4004
|
}
|
|
3932
4005
|
}
|
|
3933
4006
|
const hooksBeforeConvention = getCustomHooks().length;
|
|
@@ -3999,7 +4072,7 @@ async function loadAllCustomHooks(customPoliciesPaths, opts) {
|
|
|
3999
4072
|
hook.__conventionScope = hookNameToScope.get(hook.name) ?? "project";
|
|
4000
4073
|
}
|
|
4001
4074
|
}
|
|
4002
|
-
return { hooks: allHooks, conventionSources };
|
|
4075
|
+
return { hooks: allHooks, conventionSources, packFailures };
|
|
4003
4076
|
}
|
|
4004
4077
|
var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__", CONVENTION_FILE_RE, loadSequence = 0, policyModuleCache, POLICY_MODULE_CACHE_MAX_ENTRIES = 500, LOADABLE_EXT_RE;
|
|
4005
4078
|
var init_custom_hooks_loader = __esm(() => {
|
|
@@ -4285,12 +4358,12 @@ function formatDate(date) {
|
|
|
4285
4358
|
// lib/codex-projects.ts
|
|
4286
4359
|
var exports_codex_projects = {};
|
|
4287
4360
|
__export(exports_codex_projects, {
|
|
4288
|
-
|
|
4289
|
-
getCodexSessionsByEncodedName: () => getCodexSessionsByEncodedName,
|
|
4290
|
-
getCodexProjects: () => getCodexProjects,
|
|
4291
|
-
getCachedCodexSessionsForCwd: () => getCachedCodexSessionsForCwd,
|
|
4361
|
+
getCachedCodexProjects: () => getCachedCodexProjects,
|
|
4292
4362
|
getCachedCodexSessionsByEncodedName: () => getCachedCodexSessionsByEncodedName,
|
|
4293
|
-
|
|
4363
|
+
getCachedCodexSessionsForCwd: () => getCachedCodexSessionsForCwd,
|
|
4364
|
+
getCodexProjects: () => getCodexProjects,
|
|
4365
|
+
getCodexSessionsByEncodedName: () => getCodexSessionsByEncodedName,
|
|
4366
|
+
getCodexSessionsForCwd: () => getCodexSessionsForCwd
|
|
4294
4367
|
});
|
|
4295
4368
|
import { open, readdir } from "node:fs/promises";
|
|
4296
4369
|
import { homedir as homedir5 } from "node:os";
|
|
@@ -4484,12 +4557,12 @@ var init_codex_projects = __esm(() => {
|
|
|
4484
4557
|
// lib/copilot-projects.ts
|
|
4485
4558
|
var exports_copilot_projects = {};
|
|
4486
4559
|
__export(exports_copilot_projects, {
|
|
4487
|
-
|
|
4488
|
-
getCopilotSessionsByEncodedName: () => getCopilotSessionsByEncodedName,
|
|
4489
|
-
getCopilotProjects: () => getCopilotProjects,
|
|
4490
|
-
getCachedCopilotSessionsForCwd: () => getCachedCopilotSessionsForCwd,
|
|
4560
|
+
getCachedCopilotProjects: () => getCachedCopilotProjects,
|
|
4491
4561
|
getCachedCopilotSessionsByEncodedName: () => getCachedCopilotSessionsByEncodedName,
|
|
4492
|
-
|
|
4562
|
+
getCachedCopilotSessionsForCwd: () => getCachedCopilotSessionsForCwd,
|
|
4563
|
+
getCopilotProjects: () => getCopilotProjects,
|
|
4564
|
+
getCopilotSessionsByEncodedName: () => getCopilotSessionsByEncodedName,
|
|
4565
|
+
getCopilotSessionsForCwd: () => getCopilotSessionsForCwd
|
|
4493
4566
|
});
|
|
4494
4567
|
import { readdir as readdir2, readFile as readFile4, stat } from "node:fs/promises";
|
|
4495
4568
|
import { homedir as homedir6 } from "node:os";
|
|
@@ -4632,12 +4705,12 @@ var init_copilot_projects = __esm(() => {
|
|
|
4632
4705
|
// lib/cursor-projects.ts
|
|
4633
4706
|
var exports_cursor_projects = {};
|
|
4634
4707
|
__export(exports_cursor_projects, {
|
|
4635
|
-
|
|
4636
|
-
getCursorSessionsByEncodedName: () => getCursorSessionsByEncodedName,
|
|
4637
|
-
getCursorProjects: () => getCursorProjects,
|
|
4638
|
-
getCachedCursorSessionsForCwd: () => getCachedCursorSessionsForCwd,
|
|
4708
|
+
getCachedCursorProjects: () => getCachedCursorProjects,
|
|
4639
4709
|
getCachedCursorSessionsByEncodedName: () => getCachedCursorSessionsByEncodedName,
|
|
4640
|
-
|
|
4710
|
+
getCachedCursorSessionsForCwd: () => getCachedCursorSessionsForCwd,
|
|
4711
|
+
getCursorProjects: () => getCursorProjects,
|
|
4712
|
+
getCursorSessionsByEncodedName: () => getCursorSessionsByEncodedName,
|
|
4713
|
+
getCursorSessionsForCwd: () => getCursorSessionsForCwd
|
|
4641
4714
|
});
|
|
4642
4715
|
import { readdir as readdir3, readFile as readFile5, stat as stat2 } from "node:fs/promises";
|
|
4643
4716
|
import { homedir as homedir7 } from "node:os";
|
|
@@ -4856,12 +4929,12 @@ var init_cursor_projects = __esm(() => {
|
|
|
4856
4929
|
// lib/opencode-projects.ts
|
|
4857
4930
|
var exports_opencode_projects = {};
|
|
4858
4931
|
__export(exports_opencode_projects, {
|
|
4859
|
-
|
|
4860
|
-
getOpenCodeSessionsByEncodedName: () => getOpenCodeSessionsByEncodedName,
|
|
4861
|
-
getOpenCodeProjects: () => getOpenCodeProjects,
|
|
4862
|
-
getCachedOpenCodeSessionsForCwd: () => getCachedOpenCodeSessionsForCwd,
|
|
4932
|
+
getCachedOpenCodeProjects: () => getCachedOpenCodeProjects,
|
|
4863
4933
|
getCachedOpenCodeSessionsByEncodedName: () => getCachedOpenCodeSessionsByEncodedName,
|
|
4864
|
-
|
|
4934
|
+
getCachedOpenCodeSessionsForCwd: () => getCachedOpenCodeSessionsForCwd,
|
|
4935
|
+
getOpenCodeProjects: () => getOpenCodeProjects,
|
|
4936
|
+
getOpenCodeSessionsByEncodedName: () => getOpenCodeSessionsByEncodedName,
|
|
4937
|
+
getOpenCodeSessionsForCwd: () => getOpenCodeSessionsForCwd
|
|
4865
4938
|
});
|
|
4866
4939
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
4867
4940
|
function runOpenCodeDb(sql) {
|
|
@@ -5005,12 +5078,12 @@ var init_opencode_projects = __esm(() => {
|
|
|
5005
5078
|
// lib/pi-projects.ts
|
|
5006
5079
|
var exports_pi_projects = {};
|
|
5007
5080
|
__export(exports_pi_projects, {
|
|
5008
|
-
|
|
5009
|
-
getPiSessionsByEncodedName: () => getPiSessionsByEncodedName,
|
|
5010
|
-
getPiProjects: () => getPiProjects,
|
|
5011
|
-
getCachedPiSessionsForCwd: () => getCachedPiSessionsForCwd,
|
|
5081
|
+
getCachedPiProjects: () => getCachedPiProjects,
|
|
5012
5082
|
getCachedPiSessionsByEncodedName: () => getCachedPiSessionsByEncodedName,
|
|
5013
|
-
|
|
5083
|
+
getCachedPiSessionsForCwd: () => getCachedPiSessionsForCwd,
|
|
5084
|
+
getPiProjects: () => getPiProjects,
|
|
5085
|
+
getPiSessionsByEncodedName: () => getPiSessionsByEncodedName,
|
|
5086
|
+
getPiSessionsForCwd: () => getPiSessionsForCwd
|
|
5014
5087
|
});
|
|
5015
5088
|
import { readdir as readdir4, readFile as readFile6, stat as stat3 } from "node:fs/promises";
|
|
5016
5089
|
import { homedir as homedir8 } from "node:os";
|
|
@@ -5496,12 +5569,12 @@ var init_hermes_sessions = __esm(() => {
|
|
|
5496
5569
|
// lib/hermes-projects.ts
|
|
5497
5570
|
var exports_hermes_projects = {};
|
|
5498
5571
|
__export(exports_hermes_projects, {
|
|
5499
|
-
|
|
5500
|
-
getHermesSessions: () => getHermesSessions,
|
|
5501
|
-
getHermesProjects: () => getHermesProjects,
|
|
5502
|
-
getCachedHermesSessionsByEncodedName: () => getCachedHermesSessionsByEncodedName,
|
|
5572
|
+
getCachedHermesProjects: () => getCachedHermesProjects,
|
|
5503
5573
|
getCachedHermesSessions: () => getCachedHermesSessions,
|
|
5504
|
-
|
|
5574
|
+
getCachedHermesSessionsByEncodedName: () => getCachedHermesSessionsByEncodedName,
|
|
5575
|
+
getHermesProjects: () => getHermesProjects,
|
|
5576
|
+
getHermesSessions: () => getHermesSessions,
|
|
5577
|
+
getHermesSessionsByEncodedName: () => getHermesSessionsByEncodedName
|
|
5505
5578
|
});
|
|
5506
5579
|
async function getHermesSessions() {
|
|
5507
5580
|
const out = [];
|
|
@@ -5814,16 +5887,16 @@ var init_openclaw_sessions = __esm(() => {
|
|
|
5814
5887
|
// lib/openclaw-projects.ts
|
|
5815
5888
|
var exports_openclaw_projects = {};
|
|
5816
5889
|
__export(exports_openclaw_projects, {
|
|
5817
|
-
|
|
5818
|
-
openClawProjectPath: () => openClawProjectPath,
|
|
5819
|
-
openClawProjectNameCandidates: () => openClawProjectNameCandidates,
|
|
5820
|
-
openClawProjectName: () => openClawProjectName,
|
|
5821
|
-
getOpenClawSessionsByEncodedName: () => getOpenClawSessionsByEncodedName,
|
|
5822
|
-
getOpenClawSessions: () => getOpenClawSessions,
|
|
5823
|
-
getOpenClawProjects: () => getOpenClawProjects,
|
|
5824
|
-
getCachedOpenClawSessionsByEncodedName: () => getCachedOpenClawSessionsByEncodedName,
|
|
5890
|
+
getCachedOpenClawProjects: () => getCachedOpenClawProjects,
|
|
5825
5891
|
getCachedOpenClawSessions: () => getCachedOpenClawSessions,
|
|
5826
|
-
|
|
5892
|
+
getCachedOpenClawSessionsByEncodedName: () => getCachedOpenClawSessionsByEncodedName,
|
|
5893
|
+
getOpenClawProjects: () => getOpenClawProjects,
|
|
5894
|
+
getOpenClawSessions: () => getOpenClawSessions,
|
|
5895
|
+
getOpenClawSessionsByEncodedName: () => getOpenClawSessionsByEncodedName,
|
|
5896
|
+
openClawProjectName: () => openClawProjectName,
|
|
5897
|
+
openClawProjectNameCandidates: () => openClawProjectNameCandidates,
|
|
5898
|
+
openClawProjectPath: () => openClawProjectPath,
|
|
5899
|
+
parseOpenClawProjectName: () => parseOpenClawProjectName
|
|
5827
5900
|
});
|
|
5828
5901
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
5829
5902
|
import { join as join13 } from "node:path";
|
|
@@ -6203,10 +6276,10 @@ var init_factory_sessions = __esm(() => {
|
|
|
6203
6276
|
// lib/factory-projects.ts
|
|
6204
6277
|
var exports_factory_projects = {};
|
|
6205
6278
|
__export(exports_factory_projects, {
|
|
6206
|
-
|
|
6207
|
-
getFactoryProjects: () => getFactoryProjects,
|
|
6279
|
+
getCachedFactoryProjects: () => getCachedFactoryProjects,
|
|
6208
6280
|
getCachedFactorySessionsByEncodedName: () => getCachedFactorySessionsByEncodedName,
|
|
6209
|
-
|
|
6281
|
+
getFactoryProjects: () => getFactoryProjects,
|
|
6282
|
+
getFactorySessionsByEncodedName: () => getFactorySessionsByEncodedName
|
|
6210
6283
|
});
|
|
6211
6284
|
async function getFactoryProjects() {
|
|
6212
6285
|
let transcripts;
|
|
@@ -6493,12 +6566,12 @@ var init_devin_sessions = __esm(() => {
|
|
|
6493
6566
|
// lib/devin-projects.ts
|
|
6494
6567
|
var exports_devin_projects = {};
|
|
6495
6568
|
__export(exports_devin_projects, {
|
|
6496
|
-
|
|
6497
|
-
getDevinSessions: () => getDevinSessions,
|
|
6498
|
-
getDevinProjects: () => getDevinProjects,
|
|
6499
|
-
getCachedDevinSessionsByEncodedName: () => getCachedDevinSessionsByEncodedName,
|
|
6569
|
+
getCachedDevinProjects: () => getCachedDevinProjects,
|
|
6500
6570
|
getCachedDevinSessions: () => getCachedDevinSessions,
|
|
6501
|
-
|
|
6571
|
+
getCachedDevinSessionsByEncodedName: () => getCachedDevinSessionsByEncodedName,
|
|
6572
|
+
getDevinProjects: () => getDevinProjects,
|
|
6573
|
+
getDevinSessions: () => getDevinSessions,
|
|
6574
|
+
getDevinSessionsByEncodedName: () => getDevinSessionsByEncodedName
|
|
6502
6575
|
});
|
|
6503
6576
|
async function getDevinSessions() {
|
|
6504
6577
|
const db = await openSqliteReadonly(devinDbPath());
|
|
@@ -6761,12 +6834,12 @@ var init_antigravity_sessions = __esm(() => {
|
|
|
6761
6834
|
// lib/antigravity-projects.ts
|
|
6762
6835
|
var exports_antigravity_projects = {};
|
|
6763
6836
|
__export(exports_antigravity_projects, {
|
|
6764
|
-
|
|
6765
|
-
getCachedAntigravityProjects: () => getCachedAntigravityProjects,
|
|
6766
|
-
getAntigravitySessionsByEncodedName: () => getAntigravitySessionsByEncodedName,
|
|
6767
|
-
getAntigravityProjects: () => getAntigravityProjects,
|
|
6837
|
+
antigravitySummariesDbPath: () => antigravitySummariesDbPath,
|
|
6768
6838
|
getAntigravityIndex: () => getAntigravityIndex,
|
|
6769
|
-
|
|
6839
|
+
getAntigravityProjects: () => getAntigravityProjects,
|
|
6840
|
+
getAntigravitySessionsByEncodedName: () => getAntigravitySessionsByEncodedName,
|
|
6841
|
+
getCachedAntigravityProjects: () => getCachedAntigravityProjects,
|
|
6842
|
+
getCachedAntigravitySessionsByEncodedName: () => getCachedAntigravitySessionsByEncodedName
|
|
6770
6843
|
});
|
|
6771
6844
|
import { join as join17 } from "node:path";
|
|
6772
6845
|
function antigravitySummariesDbPath() {
|
|
@@ -7100,12 +7173,12 @@ var init_goose_sessions = __esm(() => {
|
|
|
7100
7173
|
// lib/goose-projects.ts
|
|
7101
7174
|
var exports_goose_projects = {};
|
|
7102
7175
|
__export(exports_goose_projects, {
|
|
7103
|
-
|
|
7104
|
-
getGooseSessions: () => getGooseSessions,
|
|
7105
|
-
getGooseProjects: () => getGooseProjects,
|
|
7106
|
-
getCachedGooseSessionsByEncodedName: () => getCachedGooseSessionsByEncodedName,
|
|
7176
|
+
getCachedGooseProjects: () => getCachedGooseProjects,
|
|
7107
7177
|
getCachedGooseSessions: () => getCachedGooseSessions,
|
|
7108
|
-
|
|
7178
|
+
getCachedGooseSessionsByEncodedName: () => getCachedGooseSessionsByEncodedName,
|
|
7179
|
+
getGooseProjects: () => getGooseProjects,
|
|
7180
|
+
getGooseSessions: () => getGooseSessions,
|
|
7181
|
+
getGooseSessionsByEncodedName: () => getGooseSessionsByEncodedName
|
|
7109
7182
|
});
|
|
7110
7183
|
async function getGooseSessions() {
|
|
7111
7184
|
const db = await openSqliteReadonly(gooseDbPath());
|
|
@@ -8806,21 +8879,299 @@ var init_cloud_managed_policies = __esm(() => {
|
|
|
8806
8879
|
POLICY_ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
8807
8880
|
});
|
|
8808
8881
|
|
|
8882
|
+
// src/hooks/pack-manifest.ts
|
|
8883
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
8884
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
|
|
8885
|
+
import { resolve as resolve12 } from "node:path";
|
|
8886
|
+
function packsRoot() {
|
|
8887
|
+
return process.env.FAILPROOFAI_PACK_DIR ?? packsDir();
|
|
8888
|
+
}
|
|
8889
|
+
function parsePackIdentity(value) {
|
|
8890
|
+
if (typeof value.id !== "string" || !PACK_ID_RE.test(value.id)) {
|
|
8891
|
+
throw new Error(`unsafe pack id ${JSON.stringify(value.id)}`);
|
|
8892
|
+
}
|
|
8893
|
+
if (typeof value.version !== "string" || !PACK_VERSION_RE.test(value.version)) {
|
|
8894
|
+
throw new Error(`invalid version for pack ${value.id}`);
|
|
8895
|
+
}
|
|
8896
|
+
if (value.effect !== undefined && value.effect !== "enforce" && value.effect !== "observe") {
|
|
8897
|
+
throw new Error(`unknown effect ${JSON.stringify(value.effect)} for pack ${value.id}`);
|
|
8898
|
+
}
|
|
8899
|
+
return {
|
|
8900
|
+
id: value.id,
|
|
8901
|
+
version: value.version,
|
|
8902
|
+
effect: value.effect ?? "enforce"
|
|
8903
|
+
};
|
|
8904
|
+
}
|
|
8905
|
+
function hasInstalledPacks() {
|
|
8906
|
+
try {
|
|
8907
|
+
const raw = JSON.parse(readFileSync12(installedFilePath(), "utf8"));
|
|
8908
|
+
return Array.isArray(raw.packs) && raw.packs.length > 0;
|
|
8909
|
+
} catch {
|
|
8910
|
+
return false;
|
|
8911
|
+
}
|
|
8912
|
+
}
|
|
8913
|
+
function installedFilePath() {
|
|
8914
|
+
return process.env.FAILPROOFAI_PACK_DIR ? resolve12(process.env.FAILPROOFAI_PACK_DIR, "installed.json") : packsInstalledFile();
|
|
8915
|
+
}
|
|
8916
|
+
function parsePackPolicy(packId, value, index) {
|
|
8917
|
+
const where = `${packId} policy #${index}`;
|
|
8918
|
+
if (!value || typeof value !== "object")
|
|
8919
|
+
throw new Error(`${where} is not an object`);
|
|
8920
|
+
const raw = value;
|
|
8921
|
+
const name = raw.name;
|
|
8922
|
+
if (typeof name !== "string" || !PACK_POLICY_NAME_RE.test(name)) {
|
|
8923
|
+
throw new Error(`${where} has an unsafe name ${JSON.stringify(name)}`);
|
|
8924
|
+
}
|
|
8925
|
+
if ("alwaysOn" in raw) {
|
|
8926
|
+
throw new Error(`${where} declares alwaysOn, which packs may not set`);
|
|
8927
|
+
}
|
|
8928
|
+
if (typeof raw.description !== "string" || raw.description.length === 0) {
|
|
8929
|
+
throw new Error(`${where} is missing a description`);
|
|
8930
|
+
}
|
|
8931
|
+
if (typeof raw.category !== "string" || raw.category.length === 0) {
|
|
8932
|
+
throw new Error(`${where} is missing a category`);
|
|
8933
|
+
}
|
|
8934
|
+
if (typeof raw.defaultEnabled !== "boolean") {
|
|
8935
|
+
throw new Error(`${where} is missing defaultEnabled`);
|
|
8936
|
+
}
|
|
8937
|
+
if (!raw.match || typeof raw.match !== "object") {
|
|
8938
|
+
throw new Error(`${where} is missing a match`);
|
|
8939
|
+
}
|
|
8940
|
+
return raw;
|
|
8941
|
+
}
|
|
8942
|
+
function parsePack(root, value) {
|
|
8943
|
+
if (!value || typeof value !== "object")
|
|
8944
|
+
throw new Error("pack entry is not an object");
|
|
8945
|
+
const raw = value;
|
|
8946
|
+
const identity = parsePackIdentity(raw);
|
|
8947
|
+
if (typeof raw.source !== "string" || raw.source.length === 0) {
|
|
8948
|
+
throw new Error(`pack ${raw.id} has no source`);
|
|
8949
|
+
}
|
|
8950
|
+
if (typeof raw.sha256 !== "string" || !SHA256_RE2.test(raw.sha256)) {
|
|
8951
|
+
throw new Error(`invalid SHA-256 for pack ${raw.id}`);
|
|
8952
|
+
}
|
|
8953
|
+
const effect = identity.effect;
|
|
8954
|
+
const path2 = resolveManagedPath(root, raw.entry);
|
|
8955
|
+
const actual = createHash4("sha256").update(readFileSync12(path2)).digest("hex");
|
|
8956
|
+
if (actual !== raw.sha256) {
|
|
8957
|
+
throw new Error(`pack ${raw.id} failed integrity verification: expected ${raw.sha256}, got ${actual}`);
|
|
8958
|
+
}
|
|
8959
|
+
if (!Array.isArray(raw.policies))
|
|
8960
|
+
throw new Error(`pack ${raw.id} policies is not an array`);
|
|
8961
|
+
const policies = raw.policies.map((p, i) => parsePackPolicy(raw.id, p, i));
|
|
8962
|
+
const names = new Set;
|
|
8963
|
+
for (const p of policies) {
|
|
8964
|
+
if (names.has(p.name))
|
|
8965
|
+
throw new Error(`pack ${raw.id} declares ${p.name} twice`);
|
|
8966
|
+
names.add(p.name);
|
|
8967
|
+
}
|
|
8968
|
+
let enabled = null;
|
|
8969
|
+
if (raw.enabled !== undefined) {
|
|
8970
|
+
if (!Array.isArray(raw.enabled) || raw.enabled.some((n) => typeof n !== "string")) {
|
|
8971
|
+
throw new Error(`pack ${raw.id} enabled is not an array of names`);
|
|
8972
|
+
}
|
|
8973
|
+
enabled = raw.enabled.filter((n) => names.has(n));
|
|
8974
|
+
}
|
|
8975
|
+
return {
|
|
8976
|
+
id: identity.id,
|
|
8977
|
+
version: identity.version,
|
|
8978
|
+
source: raw.source,
|
|
8979
|
+
path: path2,
|
|
8980
|
+
sha256: raw.sha256,
|
|
8981
|
+
effect,
|
|
8982
|
+
policies,
|
|
8983
|
+
enabled
|
|
8984
|
+
};
|
|
8985
|
+
}
|
|
8986
|
+
function readInstalledPacks() {
|
|
8987
|
+
const manifestPath = installedFilePath();
|
|
8988
|
+
if (!existsSync12(manifestPath))
|
|
8989
|
+
return { packs: [], errors: [] };
|
|
8990
|
+
let raw;
|
|
8991
|
+
try {
|
|
8992
|
+
raw = JSON.parse(readFileSync12(manifestPath, "utf8"));
|
|
8993
|
+
} catch (err) {
|
|
8994
|
+
return {
|
|
8995
|
+
packs: [],
|
|
8996
|
+
errors: [{ id: null, reason: `unreadable pack manifest: ${errText(err)}` }]
|
|
8997
|
+
};
|
|
8998
|
+
}
|
|
8999
|
+
if (!raw || typeof raw !== "object") {
|
|
9000
|
+
return { packs: [], errors: [{ id: null, reason: "pack manifest is not an object" }] };
|
|
9001
|
+
}
|
|
9002
|
+
const manifest = raw;
|
|
9003
|
+
if (!ACCEPTED_PACK_SCHEMA_VERSIONS.includes(manifest.schemaVersion)) {
|
|
9004
|
+
return {
|
|
9005
|
+
packs: [],
|
|
9006
|
+
errors: [
|
|
9007
|
+
{
|
|
9008
|
+
id: null,
|
|
9009
|
+
reason: `unsupported pack manifest schema ${String(manifest.schemaVersion)} ` + `(supported: ${ACCEPTED_PACK_SCHEMA_VERSIONS.join(", ")})`
|
|
9010
|
+
}
|
|
9011
|
+
]
|
|
9012
|
+
};
|
|
9013
|
+
}
|
|
9014
|
+
if (!Array.isArray(manifest.packs)) {
|
|
9015
|
+
return { packs: [], errors: [{ id: null, reason: "pack manifest packs is not an array" }] };
|
|
9016
|
+
}
|
|
9017
|
+
const root = packsRoot();
|
|
9018
|
+
const packs = [];
|
|
9019
|
+
const errors = [];
|
|
9020
|
+
const seen = new Set;
|
|
9021
|
+
for (const entry of manifest.packs) {
|
|
9022
|
+
const declaredId = entry && typeof entry === "object" && typeof entry.id === "string" ? entry.id : null;
|
|
9023
|
+
try {
|
|
9024
|
+
const pack = parsePack(root, entry);
|
|
9025
|
+
if (seen.has(pack.id))
|
|
9026
|
+
throw new Error(`duplicate pack id ${pack.id}`);
|
|
9027
|
+
seen.add(pack.id);
|
|
9028
|
+
packs.push(pack);
|
|
9029
|
+
} catch (err) {
|
|
9030
|
+
const rec = entry;
|
|
9031
|
+
const declaredEffect = rec && (rec.effect === "observe" || rec.effect === "enforce") ? rec.effect : "enforce";
|
|
9032
|
+
errors.push({
|
|
9033
|
+
id: declaredId,
|
|
9034
|
+
reason: errText(err),
|
|
9035
|
+
effect: declaredEffect,
|
|
9036
|
+
...Array.isArray(rec?.policies) ? { declared: safeDeclared(rec.policies) } : {}
|
|
9037
|
+
});
|
|
9038
|
+
}
|
|
9039
|
+
}
|
|
9040
|
+
return { packs, errors };
|
|
9041
|
+
}
|
|
9042
|
+
function safeDeclared(raw) {
|
|
9043
|
+
const out = [];
|
|
9044
|
+
for (const value of raw) {
|
|
9045
|
+
if (!value || typeof value !== "object")
|
|
9046
|
+
continue;
|
|
9047
|
+
const p = value;
|
|
9048
|
+
if (typeof p.name !== "string" || !p.match || typeof p.match !== "object")
|
|
9049
|
+
continue;
|
|
9050
|
+
out.push(p);
|
|
9051
|
+
}
|
|
9052
|
+
return out;
|
|
9053
|
+
}
|
|
9054
|
+
function errText(err) {
|
|
9055
|
+
return err instanceof Error ? err.message : String(err);
|
|
9056
|
+
}
|
|
9057
|
+
var ACCEPTED_PACK_SCHEMA_VERSIONS, SHA256_RE2, PACK_ID_RE, PACK_VERSION_RE, PACK_POLICY_NAME_RE;
|
|
9058
|
+
var init_pack_manifest = __esm(() => {
|
|
9059
|
+
init_fp_home();
|
|
9060
|
+
init_cloud_managed_policies();
|
|
9061
|
+
ACCEPTED_PACK_SCHEMA_VERSIONS = [1];
|
|
9062
|
+
SHA256_RE2 = /^[a-f0-9]{64}$/;
|
|
9063
|
+
PACK_ID_RE = /^[A-Za-z0-9._-]{1,64}\/[A-Za-z0-9._-]{1,64}$/;
|
|
9064
|
+
PACK_VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
|
|
9065
|
+
PACK_POLICY_NAME_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
9066
|
+
});
|
|
9067
|
+
|
|
9068
|
+
// src/hooks/pack-failclosed.ts
|
|
9069
|
+
function unionMatch(matchers) {
|
|
9070
|
+
const events = new Set;
|
|
9071
|
+
const toolNames = new Set;
|
|
9072
|
+
let anyEvent = false;
|
|
9073
|
+
let anyTool = false;
|
|
9074
|
+
for (const m of matchers) {
|
|
9075
|
+
if (!m.events || m.events.length === 0)
|
|
9076
|
+
anyEvent = true;
|
|
9077
|
+
else
|
|
9078
|
+
for (const e of m.events)
|
|
9079
|
+
events.add(e);
|
|
9080
|
+
if (!m.toolNames || m.toolNames.length === 0)
|
|
9081
|
+
anyTool = true;
|
|
9082
|
+
else
|
|
9083
|
+
for (const t of m.toolNames)
|
|
9084
|
+
toolNames.add(t);
|
|
9085
|
+
}
|
|
9086
|
+
const scoped = {};
|
|
9087
|
+
if (!anyEvent)
|
|
9088
|
+
scoped.events = [...events];
|
|
9089
|
+
if (!anyTool)
|
|
9090
|
+
scoped.toolNames = [...toolNames];
|
|
9091
|
+
return scoped;
|
|
9092
|
+
}
|
|
9093
|
+
function missingGuards(input) {
|
|
9094
|
+
const out = [];
|
|
9095
|
+
for (const err of input.errors) {
|
|
9096
|
+
if (err.effect === "observe")
|
|
9097
|
+
continue;
|
|
9098
|
+
const declared = err.declared ?? [];
|
|
9099
|
+
out.push({
|
|
9100
|
+
packId: err.id ?? "(unnamed pack)",
|
|
9101
|
+
packVersion: null,
|
|
9102
|
+
policies: declared.map((p) => p.name),
|
|
9103
|
+
match: declared.length > 0 ? unionMatch(declared.map((p) => p.match)) : unionMatch([{}]),
|
|
9104
|
+
reason: err.reason
|
|
9105
|
+
});
|
|
9106
|
+
}
|
|
9107
|
+
for (const pack of input.packs) {
|
|
9108
|
+
if (pack.effect === "observe")
|
|
9109
|
+
continue;
|
|
9110
|
+
const loadFailure = input.failed.get(pack.id);
|
|
9111
|
+
if (loadFailure && PERMANENT_LOAD_FAILURES.has(loadFailure.type)) {
|
|
9112
|
+
const taken2 = pack.enabled ?? pack.policies.map((p) => p.name);
|
|
9113
|
+
const unavailable = pack.policies.filter((p) => taken2.includes(p.name) && !input.disabled.has(`pack:${pack.id}@${pack.version}:${p.name}`));
|
|
9114
|
+
if (unavailable.length > 0) {
|
|
9115
|
+
out.push({
|
|
9116
|
+
packId: pack.id,
|
|
9117
|
+
packVersion: pack.version,
|
|
9118
|
+
policies: unavailable.map((p) => p.name),
|
|
9119
|
+
match: unionMatch(unavailable.map((p) => p.match)),
|
|
9120
|
+
reason: `artifact failed to load: ${loadFailure.reason}`
|
|
9121
|
+
});
|
|
9122
|
+
}
|
|
9123
|
+
continue;
|
|
9124
|
+
}
|
|
9125
|
+
const registered = input.registered.get(pack.id);
|
|
9126
|
+
if (!registered)
|
|
9127
|
+
continue;
|
|
9128
|
+
const taken = pack.enabled ?? pack.policies.map((p) => p.name);
|
|
9129
|
+
const missing = pack.policies.filter((p) => taken.includes(p.name) && !registered.has(p.name) && !input.disabled.has(`pack:${pack.id}@${pack.version}:${p.name}`));
|
|
9130
|
+
if (missing.length === 0)
|
|
9131
|
+
continue;
|
|
9132
|
+
out.push({
|
|
9133
|
+
packId: pack.id,
|
|
9134
|
+
packVersion: pack.version,
|
|
9135
|
+
policies: missing.map((p) => p.name),
|
|
9136
|
+
match: unionMatch(missing.map((p) => p.match)),
|
|
9137
|
+
reason: `declared ${missing.length} polic${missing.length === 1 ? "y" : "ies"} its artifact did not register`
|
|
9138
|
+
});
|
|
9139
|
+
}
|
|
9140
|
+
return out.filter((g) => !(g.match.events && g.match.events.length === 0));
|
|
9141
|
+
}
|
|
9142
|
+
function packFailureReason(guards) {
|
|
9143
|
+
const named = guards.map((g) => {
|
|
9144
|
+
const where = g.packVersion ? `${g.packId}@${g.packVersion}` : g.packId;
|
|
9145
|
+
const what = g.policies.length > 0 ? `: ${g.policies.slice(0, 4).join(", ")}` : "";
|
|
9146
|
+
return `${where} (${g.reason})${what}`;
|
|
9147
|
+
}).join("; ");
|
|
9148
|
+
return `Blocked because a policy pack this machine is configured to enforce is not running — ${named}. ` + `Enforcement you were told you had is missing, so this is refused rather than allowed silently. ` + `A human needs to run: failproofai policies, then failproofai policies add <source> (or failproofai config). ` + `The agent cannot run those itself.`;
|
|
9149
|
+
}
|
|
9150
|
+
var PERMANENT_LOAD_FAILURES;
|
|
9151
|
+
var init_pack_failclosed = __esm(() => {
|
|
9152
|
+
PERMANENT_LOAD_FAILURES = new Set([
|
|
9153
|
+
"module_not_found",
|
|
9154
|
+
"syntax_error",
|
|
9155
|
+
"runtime_error",
|
|
9156
|
+
"path_missing"
|
|
9157
|
+
]);
|
|
9158
|
+
});
|
|
9159
|
+
|
|
8809
9160
|
// lib/atomic-write.ts
|
|
8810
9161
|
var init_atomic_write = () => {};
|
|
8811
9162
|
|
|
8812
9163
|
// src/hooks/session-pause.ts
|
|
8813
|
-
import { existsSync as
|
|
8814
|
-
import { createHash as
|
|
8815
|
-
import { join as join26, resolve as
|
|
9164
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readdirSync as readdirSync11, readFileSync as readFileSync13, rmSync as rmSync2 } from "node:fs";
|
|
9165
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
9166
|
+
import { join as join26, resolve as resolve13 } from "node:path";
|
|
8816
9167
|
function pauseStateDir() {
|
|
8817
9168
|
const override = process.env.FAILPROOFAI_STATE_DIR;
|
|
8818
9169
|
if (override)
|
|
8819
|
-
return
|
|
9170
|
+
return resolve13(override, "sessions");
|
|
8820
9171
|
return sessionPauseDir();
|
|
8821
9172
|
}
|
|
8822
9173
|
function pauseFilePath(sessionId) {
|
|
8823
|
-
const digest =
|
|
9174
|
+
const digest = createHash5("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
8824
9175
|
return join26(pauseStateDir(), `${digest}.json`);
|
|
8825
9176
|
}
|
|
8826
9177
|
function parseStored(raw) {
|
|
@@ -8850,9 +9201,9 @@ function readActivePause(sessionId, now = Date.now()) {
|
|
|
8850
9201
|
return null;
|
|
8851
9202
|
try {
|
|
8852
9203
|
const path2 = pauseFilePath(sessionId);
|
|
8853
|
-
if (!
|
|
9204
|
+
if (!existsSync13(path2))
|
|
8854
9205
|
return null;
|
|
8855
|
-
const stored = parseStored(JSON.parse(
|
|
9206
|
+
const stored = parseStored(JSON.parse(readFileSync13(path2, "utf8")));
|
|
8856
9207
|
if (!stored)
|
|
8857
9208
|
return null;
|
|
8858
9209
|
if (stored.expiresAt <= now)
|
|
@@ -8891,9 +9242,9 @@ var init_daemon_service = __esm(() => {
|
|
|
8891
9242
|
// src/hooks/fp-reset.ts
|
|
8892
9243
|
import {
|
|
8893
9244
|
copyFileSync,
|
|
8894
|
-
existsSync as
|
|
9245
|
+
existsSync as existsSync14,
|
|
8895
9246
|
mkdirSync as mkdirSync7,
|
|
8896
|
-
readFileSync as
|
|
9247
|
+
readFileSync as readFileSync14,
|
|
8897
9248
|
readdirSync as readdirSync12,
|
|
8898
9249
|
renameSync as renameSync3,
|
|
8899
9250
|
rmSync as rmSync3,
|
|
@@ -8908,7 +9259,7 @@ function layoutWarningForHook() {
|
|
|
8908
9259
|
if (state.kind === "future") {
|
|
8909
9260
|
return `[failproofai] this directory was written by a newer version (layout ${state.found} vs ${LAYOUT_VERSION}) — policies are NOT being enforced. ` + `Upgrade failproofai.`;
|
|
8910
9261
|
}
|
|
8911
|
-
if (
|
|
9262
|
+
if (existsSync14(globalPolicyConfigFile()))
|
|
8912
9263
|
return null;
|
|
8913
9264
|
return `[failproofai] setup predates this version — global policies are NOT being enforced. ` + `Run \`failproofai config\` to re-create it.`;
|
|
8914
9265
|
}
|
|
@@ -8917,6 +9268,7 @@ var init_fp_reset = __esm(() => {
|
|
|
8917
9268
|
init_fp_home();
|
|
8918
9269
|
init_fp_config();
|
|
8919
9270
|
init_daemon_service();
|
|
9271
|
+
init_hooks_config();
|
|
8920
9272
|
});
|
|
8921
9273
|
|
|
8922
9274
|
// src/hooks/handler.ts
|
|
@@ -9018,6 +9370,8 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
|
|
|
9018
9370
|
let activePause = null;
|
|
9019
9371
|
const policyAttribution = new Map;
|
|
9020
9372
|
let cloudDeployment;
|
|
9373
|
+
const registeredByPack = new Map;
|
|
9374
|
+
let packErrors = [];
|
|
9021
9375
|
const observedResults = [];
|
|
9022
9376
|
if (opts?.forceDecision) {
|
|
9023
9377
|
config = { enabledPolicies: [] };
|
|
@@ -9028,7 +9382,13 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
|
|
|
9028
9382
|
config = readMergedHooksConfig(session.cwd);
|
|
9029
9383
|
clearPolicies();
|
|
9030
9384
|
activePause = readActivePause(session.sessionId);
|
|
9031
|
-
|
|
9385
|
+
const packsInstalledHere = hasInstalledPacks();
|
|
9386
|
+
const legacyNames = activePause || packsInstalledHere ? [] : config.enabledPolicies;
|
|
9387
|
+
registerBuiltinPolicies(activePause ? [] : legacyNames);
|
|
9388
|
+
if (legacyNames.length > 0) {
|
|
9389
|
+
hookLogWarn(`enforcing ${legacyNames.length} policies from this build because no pack is installed — ` + `run \`failproofai policies add core\` to move them into a real pack`);
|
|
9390
|
+
}
|
|
9391
|
+
const enabledBuiltinNames = new Set(legacyNames);
|
|
9032
9392
|
let cloudManagedPolicies = [];
|
|
9033
9393
|
try {
|
|
9034
9394
|
cloudManagedPolicies = readActiveCloudManagedPolicies();
|
|
@@ -9041,15 +9401,28 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
|
|
|
9041
9401
|
});
|
|
9042
9402
|
}
|
|
9043
9403
|
cloudDeployment = cloudManagedPolicies[0]?.deployment;
|
|
9404
|
+
let installedPacks = [];
|
|
9405
|
+
try {
|
|
9406
|
+
const packResult = readInstalledPacks();
|
|
9407
|
+
installedPacks = packResult.packs;
|
|
9408
|
+
packErrors = packResult.errors;
|
|
9409
|
+
for (const err of packResult.errors) {
|
|
9410
|
+
hookLogWarn(`pack ${err.id ?? "(unnamed)"} not loaded: ${err.reason}`);
|
|
9411
|
+
}
|
|
9412
|
+
} catch (err) {
|
|
9413
|
+
hookLogWarn(`pack manifest unreadable: ${err instanceof Error ? err.message : String(err)}`);
|
|
9414
|
+
}
|
|
9044
9415
|
const configuredCustomPaths = config.customPoliciesPaths ?? config.customPoliciesPath;
|
|
9045
|
-
const allExplicitPaths = cloudManagedPolicies.length === 0 ? configuredCustomPaths : [
|
|
9416
|
+
const allExplicitPaths = cloudManagedPolicies.length === 0 && installedPacks.length === 0 ? configuredCustomPaths : [
|
|
9046
9417
|
...typeof configuredCustomPaths === "string" ? [configuredCustomPaths] : configuredCustomPaths ?? [],
|
|
9047
|
-
...cloudManagedPolicies.map((policy) => policy.path)
|
|
9418
|
+
...cloudManagedPolicies.map((policy) => policy.path),
|
|
9419
|
+
...installedPacks.map((pack) => pack.path)
|
|
9048
9420
|
];
|
|
9049
9421
|
const loadResult = await loadAllCustomHooks(allExplicitPaths, {
|
|
9050
9422
|
sessionCwd: session.cwd,
|
|
9051
9423
|
customPoliciesEnabled: config.customPoliciesEnabled,
|
|
9052
|
-
...cloudManagedPolicies.length > 0 ? { cloudManagedPolicies } : {}
|
|
9424
|
+
...cloudManagedPolicies.length > 0 ? { cloudManagedPolicies } : {},
|
|
9425
|
+
...installedPacks.length > 0 ? { packs: installedPacks } : {}
|
|
9053
9426
|
});
|
|
9054
9427
|
customHooksList = loadResult.hooks;
|
|
9055
9428
|
const disabledCustomPolicies = new Set(config.disabledCustomPolicies ?? []);
|
|
@@ -9058,22 +9431,33 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
|
|
|
9058
9431
|
const taggedHook = hook;
|
|
9059
9432
|
const policyId = taggedHook.__policyId;
|
|
9060
9433
|
const cloudManaged = taggedHook.__cloudManaged;
|
|
9434
|
+
const pack = taggedHook.__pack;
|
|
9061
9435
|
if (!cloudManaged && policyId && disabledCustomPolicies.has(policyId))
|
|
9062
9436
|
continue;
|
|
9063
9437
|
if (!cloudManaged && activePause)
|
|
9064
9438
|
continue;
|
|
9439
|
+
if (pack?.enabled && !pack.enabled.includes(hook.name))
|
|
9440
|
+
continue;
|
|
9441
|
+
if (pack && enabledBuiltinNames.has(hook.name))
|
|
9442
|
+
continue;
|
|
9443
|
+
if (pack) {
|
|
9444
|
+
const seen = registeredByPack.get(pack.id) ?? new Set;
|
|
9445
|
+
seen.add(hook.name);
|
|
9446
|
+
registeredByPack.set(pack.id, seen);
|
|
9447
|
+
}
|
|
9065
9448
|
const hookName = hook.name;
|
|
9066
9449
|
const conventionScope = hook.__conventionScope;
|
|
9067
9450
|
const isConvention = !!conventionScope;
|
|
9068
|
-
const prefix = cloudManaged ? `cloud/${cloudManaged.id}@${cloudManaged.version}` : isConvention ? `.failproofai-${conventionScope}` : "custom";
|
|
9069
|
-
const observeOnly = cloudManaged?.effect === "observe";
|
|
9451
|
+
const prefix = cloudManaged ? `cloud/${cloudManaged.id}@${cloudManaged.version}` : pack ? `pack/${pack.id}@${pack.version}` : isConvention ? `.failproofai-${conventionScope}` : "custom";
|
|
9452
|
+
const observeOnly = cloudManaged?.effect === "observe" || pack?.effect === "observe";
|
|
9070
9453
|
const fn = async (ctx) => {
|
|
9071
9454
|
if (observeOnly) {
|
|
9072
9455
|
const shadow = await runObserved(hook, ctx, hookName, eventType, cli);
|
|
9073
9456
|
if (shadow.decision !== "allow") {
|
|
9457
|
+
const observer = cloudManaged ?? pack;
|
|
9074
9458
|
observedResults.push({
|
|
9075
|
-
policyId:
|
|
9076
|
-
version:
|
|
9459
|
+
policyId: observer.id,
|
|
9460
|
+
version: observer.version,
|
|
9077
9461
|
decision: shadow.decision,
|
|
9078
9462
|
reason: shadow.reason ?? null
|
|
9079
9463
|
});
|
|
@@ -9103,10 +9487,34 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
|
|
|
9103
9487
|
};
|
|
9104
9488
|
const registeredName = `${prefix}/${hookName}`;
|
|
9105
9489
|
policyAttribution.set(registeredName, {
|
|
9106
|
-
source: cloudManaged ? "cloud" : isConvention ? "convention" : "custom",
|
|
9107
|
-
...cloudManaged ? { cloudPolicyId: cloudManaged.id, cloudVersion: cloudManaged.version } : {}
|
|
9490
|
+
source: cloudManaged ? "cloud" : pack ? "pack" : isConvention ? "convention" : "custom",
|
|
9491
|
+
...cloudManaged ? { cloudPolicyId: cloudManaged.id, cloudVersion: cloudManaged.version } : {},
|
|
9492
|
+
...pack ? { packId: pack.id, packVersion: pack.version } : {}
|
|
9493
|
+
});
|
|
9494
|
+
registerPolicy(registeredName, hook.description ?? "", fn, hook.match ?? {}, -1, pack?.policies.find((p) => p.name === hook.name)?.params);
|
|
9495
|
+
}
|
|
9496
|
+
if (!activePause) {
|
|
9497
|
+
const guards = missingGuards({
|
|
9498
|
+
errors: packErrors,
|
|
9499
|
+
packs: installedPacks,
|
|
9500
|
+
registered: registeredByPack,
|
|
9501
|
+
failed: loadResult.packFailures,
|
|
9502
|
+
disabled: disabledCustomPolicies
|
|
9108
9503
|
});
|
|
9109
|
-
|
|
9504
|
+
if (guards.length > 0) {
|
|
9505
|
+
const reason = packFailureReason(guards);
|
|
9506
|
+
const match = guards.length === 1 ? guards[0].match : {
|
|
9507
|
+
events: guards.every((g) => g.match.events) ? [...new Set(guards.flatMap((g) => g.match.events ?? []))] : undefined
|
|
9508
|
+
};
|
|
9509
|
+
const name = "pack/failproofai-pack-unavailable";
|
|
9510
|
+
policyAttribution.set(name, {
|
|
9511
|
+
source: "pack",
|
|
9512
|
+
packId: guards[0].packId,
|
|
9513
|
+
...guards[0].packVersion ? { packVersion: guards[0].packVersion } : {}
|
|
9514
|
+
});
|
|
9515
|
+
registerPolicy(name, "A policy pack this machine enforces could not be loaded", async () => canonicalEventType === "UserPromptSubmit" ? { decision: "instruct", reason } : { decision: "deny", reason }, match, 1);
|
|
9516
|
+
hookLogWarn(reason);
|
|
9517
|
+
}
|
|
9110
9518
|
}
|
|
9111
9519
|
if (customHooksList.length > 0) {
|
|
9112
9520
|
trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
|
|
@@ -9153,7 +9561,9 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
|
|
|
9153
9561
|
return {
|
|
9154
9562
|
policySource: attribution?.source ?? "builtin",
|
|
9155
9563
|
...attribution?.cloudPolicyId ? { cloudPolicyId: attribution.cloudPolicyId } : {},
|
|
9156
|
-
...attribution?.cloudVersion !== undefined ? { cloudVersion: attribution.cloudVersion } : {}
|
|
9564
|
+
...attribution?.cloudVersion !== undefined ? { cloudVersion: attribution.cloudVersion } : {},
|
|
9565
|
+
...attribution?.packId ? { packId: attribution.packId } : {},
|
|
9566
|
+
...attribution?.packVersion ? { packVersion: attribution.packVersion } : {}
|
|
9157
9567
|
};
|
|
9158
9568
|
})() : {},
|
|
9159
9569
|
...cloudDeployment !== undefined ? { cloudDeployment } : {},
|
|
@@ -9212,6 +9622,8 @@ var init_handler = __esm(() => {
|
|
|
9212
9622
|
init_telemetry_id();
|
|
9213
9623
|
init_hook_logger();
|
|
9214
9624
|
init_cloud_managed_policies();
|
|
9625
|
+
init_pack_manifest();
|
|
9626
|
+
init_pack_failclosed();
|
|
9215
9627
|
init_session_pause();
|
|
9216
9628
|
init_fp_reset();
|
|
9217
9629
|
});
|
|
@@ -9222,7 +9634,7 @@ __export(exports_worker_server, {
|
|
|
9222
9634
|
startWorkerServer: () => startWorkerServer
|
|
9223
9635
|
});
|
|
9224
9636
|
import { createConnection, createServer } from "node:net";
|
|
9225
|
-
import { existsSync as
|
|
9637
|
+
import { existsSync as existsSync15, unlinkSync as unlinkSync2 } from "node:fs";
|
|
9226
9638
|
function isWorkerHookRequest(msg) {
|
|
9227
9639
|
if (!msg || typeof msg !== "object")
|
|
9228
9640
|
return false;
|
|
@@ -9355,14 +9767,14 @@ function requestExistingWorkerShutdown(socketPath) {
|
|
|
9355
9767
|
function startWorkerServer(socketPath, shutdown) {
|
|
9356
9768
|
const server = createServer((socket) => handleConnection(socket, shutdown ?? (() => server.close())));
|
|
9357
9769
|
const listen = () => {
|
|
9358
|
-
if (
|
|
9770
|
+
if (existsSync15(socketPath)) {
|
|
9359
9771
|
try {
|
|
9360
9772
|
unlinkSync2(socketPath);
|
|
9361
9773
|
} catch {}
|
|
9362
9774
|
}
|
|
9363
9775
|
server.listen(socketPath);
|
|
9364
9776
|
};
|
|
9365
|
-
if (!
|
|
9777
|
+
if (!existsSync15(socketPath))
|
|
9366
9778
|
listen();
|
|
9367
9779
|
else
|
|
9368
9780
|
requestExistingWorkerShutdown(socketPath).then(listen, (err) => server.emit("error", err));
|
|
@@ -9378,13 +9790,13 @@ var init_worker_server = __esm(() => {
|
|
|
9378
9790
|
|
|
9379
9791
|
// bin/failproofai-worker.mjs
|
|
9380
9792
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
9381
|
-
import { dirname as dirname5, resolve as
|
|
9793
|
+
import { dirname as dirname5, resolve as resolve14 } from "node:path";
|
|
9382
9794
|
import { fileURLToPath } from "node:url";
|
|
9383
9795
|
if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
|
|
9384
|
-
process.env.FAILPROOFAI_PACKAGE_ROOT =
|
|
9796
|
+
process.env.FAILPROOFAI_PACKAGE_ROOT = resolve14(dirname5(realpathSync2(fileURLToPath(import.meta.url))), "..");
|
|
9385
9797
|
}
|
|
9386
9798
|
if (!process.env.FAILPROOFAI_DIST_PATH) {
|
|
9387
|
-
process.env.FAILPROOFAI_DIST_PATH =
|
|
9799
|
+
process.env.FAILPROOFAI_DIST_PATH = resolve14(dirname5(realpathSync2(fileURLToPath(import.meta.url))), "..", "dist");
|
|
9388
9800
|
}
|
|
9389
9801
|
var socketPath = process.env.FAILPROOFAI_WORKER_SOCKET;
|
|
9390
9802
|
if (!socketPath) {
|