failproofai 0.0.14-beta.0 → 0.0.14-beta.2
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/.codex/hooks.json +6 -6
- package/.next/standalone/.cursor/hooks.json +6 -6
- package/.next/standalone/.factory/hooks.json +9 -9
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08w4wmd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g6bbuw._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xuaoik._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ffkmds._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qb590j._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1shcqgr._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xuedea._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cz9as._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__05r_17v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07qfsfa._.js → [root-of-the-server]__09v9cwb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e2l3c1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h12me5._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hci7t3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p84xee._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0twi4fj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ynf7tx._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11rtg6s._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dky4g0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__212nf49._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_087bt9w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_1fvisnp._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_209u41o._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0g5r64e.js → node_modules_html-to-image_es_index_06xsmte.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0-uvagv.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_03c7gi5.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_09p-8om.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0v-kfiu.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0xrgzyz.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1806lsc.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1j6dd-e.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1sa65r-.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/static/chunks/{13f1kmjea-0md.js → 02jcotghcxrbu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1374x9bv_s_vg.js → 0h-6ymo9v86b0.js} +2 -2
- package/.next/standalone/.next/static/chunks/0pndjhcxfu-0t.js +1 -0
- package/.next/standalone/.next/static/chunks/1-33f82_l32or.js +1 -0
- package/.next/standalone/.next/static/chunks/{003uz088wbmz7.js → 1bg_g3g359c_d.js} +1 -1
- package/.next/standalone/.next/static/chunks/{454dyizqik6t9.js → 1gg-8e9q_m__9.js} +2 -2
- package/.next/standalone/.next/static/chunks/{2ufuwhv03yvtb.js → 29zkom4s9de2d.js} +1 -1
- package/.next/standalone/.next/static/chunks/{17sdztgbcc0fi.js → 2llkdafnaejw_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0iinhn4417nk1.js → 3-ogr79xqbw0h.js} +1 -1
- package/.next/standalone/.next/static/chunks/36j05kb2x323i.css +2 -0
- package/.next/standalone/.next/static/chunks/{43weu8f59tlix.js → 388hgfqye67xa.js} +1 -1
- package/.next/standalone/.next/static/chunks/396764jgd7-9j.js +1 -0
- package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-0vavqo2d-tlt3.js → turbopack-10hj8t5pf33tu.js} +1 -1
- package/.next/standalone/.opencode/plugins/failproofai.mjs +30 -11
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +11 -2
- package/.next/standalone/app/components/copy-button.tsx +22 -6
- package/.next/standalone/app/components/global-error-listeners.tsx +27 -0
- package/.next/standalone/components/reach-developers.tsx +1 -1
- package/.next/standalone/docker-hook-sync/Dockerfile +8 -4
- package/.next/standalone/integration-suite/Dockerfile +47 -0
- package/.next/standalone/integration-suite/README.md +87 -0
- package/.next/standalone/integration-suite/canary-policies.mjs +45 -0
- package/.next/standalone/integration-suite/capture-tokens.sh +48 -0
- package/.next/standalone/integration-suite/ci-entrypoint.sh +164 -0
- package/.next/standalone/integration-suite/inject-tokens.sh +44 -0
- package/.next/standalone/integration-suite/install-clis.sh +91 -0
- package/.next/standalone/integration-suite/probe-cli.sh +208 -0
- package/.next/standalone/integration-suite/report.js +74 -0
- package/.next/standalone/integration-suite/run.sh +107 -0
- package/.next/standalone/lib/error-origin.ts +67 -0
- package/.next/standalone/lib/format-duration.ts +23 -9
- package/.next/standalone/lib/install-check.ts +231 -0
- package/.next/standalone/package.json +1 -3
- package/.next/standalone/server.js +1 -1
- package/README.md +1 -17
- package/bin/failproofai.mjs +11 -1
- package/dist/cli.mjs +818 -320
- package/lib/error-origin.ts +67 -0
- package/lib/format-duration.ts +23 -9
- package/lib/install-check.ts +231 -0
- package/package.json +1 -3
- package/scripts/dev-hook.mjs +292 -0
- package/scripts/install-diagnosis.mjs +5 -8
- package/scripts/install-telemetry.mjs +12 -4
- package/scripts/translate-docs/cli.ts +53 -1
- package/scripts/translate-docs/mdx-translator.ts +158 -18
- package/scripts/translate-docs/readme-translator.ts +36 -18
- package/scripts/translate-docs/translator.ts +180 -9
- package/scripts/translate-docs/types.ts +6 -0
- package/scripts/translate-docs/validate-translation.ts +132 -0
- package/scripts/validate-mdx.ts +92 -15
- package/src/audit/cli.ts +53 -15
- package/src/auth/cli.ts +14 -5
- package/src/hooks/builtin-policies.ts +168 -50
- package/src/hooks/configure-wizard.ts +238 -22
- package/src/hooks/custom-hooks-loader.ts +70 -4
- package/src/hooks/handler.ts +4 -1
- package/src/hooks/policy-types.ts +8 -0
- package/src/hooks/tui.ts +96 -29
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ge8e7w._.js +0 -3
- package/.next/standalone/.next/static/chunks/0405htbxay9vd.js +0 -1
- package/.next/standalone/.next/static/chunks/2m_tbl39fcedn.js +0 -1
- package/.next/standalone/.next/static/chunks/2o3ql0n5owqsw.js +0 -1
- package/.next/standalone/.next/static/chunks/35-90m9py64d4.js +0 -1
- package/.next/standalone/.next/static/chunks/3r--yyjkp9_7b.css +0 -2
- package/scripts/postinstall.mjs +0 -242
- package/scripts/preuninstall.mjs +0 -131
- /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → YKxI22hC-F_nHp7MEKfb1}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → YKxI22hC-F_nHp7MEKfb1}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → YKxI22hC-F_nHp7MEKfb1}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
# failproofai integration suite — orchestrator (runs on the GitHub Actions runner).
|
|
4
|
+
# ./run.sh [cli ...] default: all 12
|
|
5
|
+
#
|
|
6
|
+
# Mirrors the box's run-canary.sh, but every path is env-parameterised (the
|
|
7
|
+
# runner is ephemeral) and the report is POSTed to a Slack Incoming Webhook
|
|
8
|
+
# instead of delivered via Hermes. Assumes ci-entrypoint.sh has already:
|
|
9
|
+
# • checked out + built failproofai (dist/index.js + dist/cli.mjs) at $CANARY_REPO
|
|
10
|
+
# • built the sandbox image ($CANARY_IMAGE)
|
|
11
|
+
# • created the per-run volume ($CANARY_VOL) and INSTALLED the CLIs into it
|
|
12
|
+
# • injected OAuth token files into the volume (inject-tokens.sh)
|
|
13
|
+
# • assembled the gateway/PAT env-file ($CANARY_ENVFILE)
|
|
14
|
+
#
|
|
15
|
+
# VERSION-GATING (identical to the box): a CLI is re-probed only when its own
|
|
16
|
+
# binary version OR failproofai's HEAD changed since its last GREEN run — so
|
|
17
|
+
# daily runs don't burn LLM credits re-testing an unchanged (CLI, failproofai)
|
|
18
|
+
# pair. FAIL/INCONCLUSIVE/ERROR always re-probe until they recover. Override the
|
|
19
|
+
# gated set with CANARY_VERSION_GATED (comma-sep) or "" to force-probe all.
|
|
20
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
set -u
|
|
22
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
23
|
+
|
|
24
|
+
REPO="${CANARY_REPO:?CANARY_REPO (built failproofai checkout) required}"
|
|
25
|
+
SANDBOX="${CANARY_SANDBOX:-$HERE}"
|
|
26
|
+
VOL="${CANARY_VOL:-integration-suite}"
|
|
27
|
+
IMAGE="${CANARY_IMAGE:-failproofai-integration-suite:base}"
|
|
28
|
+
STATE="${CANARY_STATE:-$HERE/state.json}"
|
|
29
|
+
ENVFILE="${CANARY_ENVFILE:?CANARY_ENVFILE (docker --env-file with gateway creds) required}"
|
|
30
|
+
MODEL="${CANARY_LLM_MODEL:-deepseek-v4-pro}"
|
|
31
|
+
GATED="${CANARY_VERSION_GATED-all}"
|
|
32
|
+
|
|
33
|
+
CLIS=("$@"); [ ${#CLIS[@]} -eq 0 ] && CLIS=(claude codex copilot cursor factory devin antigravity goose opencode pi hermes openclaw)
|
|
34
|
+
|
|
35
|
+
# failproofai main HEAD — the second gate dimension. Prefer the value the workflow
|
|
36
|
+
# computed; fall back to git in the checkout.
|
|
37
|
+
FP_SHA="${CANARY_FP_SHA:-$(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo unknown)}"
|
|
38
|
+
|
|
39
|
+
# Installed versions from the install step, keyed by cli.
|
|
40
|
+
VERSIONS_JSON="$(docker run --rm -v "$VOL:/home/canary" "$IMAGE" cat /home/canary/canary-tier0.json 2>/dev/null || echo '[]')"
|
|
41
|
+
|
|
42
|
+
run_probe() {
|
|
43
|
+
docker run --rm --env-file "$ENVFILE" \
|
|
44
|
+
-v "$REPO:/repo:ro" -v "$SANDBOX:/opt/canary:ro" -v "$VOL:/home/canary" \
|
|
45
|
+
"$IMAGE" bash /opt/canary/probe-cli.sh "$1" 2>&1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
results="[]"
|
|
49
|
+
for cli in "${CLIS[@]}"; do
|
|
50
|
+
cur_ver="$(node -e 'const a=JSON.parse(process.argv[1]);const x=a.find(y=>y.cli===process.argv[2]);process.stdout.write(x&&x.version?x.version:"")' "$VERSIONS_JSON" "$cli")"
|
|
51
|
+
|
|
52
|
+
gate="$(node -e '
|
|
53
|
+
const [statePath, cli, curVer, curSha, gated] = process.argv.slice(1);
|
|
54
|
+
let st={clis:{}}; try{ st=JSON.parse(require("fs").readFileSync(statePath,"utf8")); }catch{}
|
|
55
|
+
const isGated = gated === "all" || gated.split(",").filter(Boolean).includes(cli);
|
|
56
|
+
const prev = st.clis && st.clis[cli];
|
|
57
|
+
let skip = false;
|
|
58
|
+
if (isGated && prev && prev.version && curVer && prev.version===curVer
|
|
59
|
+
&& prev.fpSha && curSha && prev.fpSha===curSha) {
|
|
60
|
+
const vals=Object.values((prev.probes||prev)||{});
|
|
61
|
+
// Only carry forward a genuinely GREEN result — FAIL/INCONCLUSIVE/ERROR
|
|
62
|
+
// must keep re-probing so they recover (e.g. an expired token clears once
|
|
63
|
+
// the secret is refreshed) instead of being frozen.
|
|
64
|
+
const green = vals.length>0 && !vals.includes("FAIL") && !vals.includes("INCONCLUSIVE") && !vals.includes("ERROR");
|
|
65
|
+
if (green) skip = true;
|
|
66
|
+
}
|
|
67
|
+
process.stdout.write(skip ? "skip" : "probe");
|
|
68
|
+
' "$STATE" "$cli" "$cur_ver" "$FP_SHA" "$GATED")"
|
|
69
|
+
|
|
70
|
+
if [ "$gate" = skip ]; then
|
|
71
|
+
echo ">> $cli: CLI $cur_ver + failproofai $FP_SHA both unchanged & last green → gated-skip (protecting quota)" >&2
|
|
72
|
+
vj="$(node -e 'const st=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));const p=st.clis[process.argv[2]];process.stdout.write(JSON.stringify({cli:process.argv[2],probes:p.probes||p,version:p.version,fpSha:p.fpSha,gated:true}))' "$STATE" "$cli")"
|
|
73
|
+
else
|
|
74
|
+
echo ">> probing $cli ..." >&2
|
|
75
|
+
vj="$(run_probe "$cli" | sed -n 's/^VERDICT_JSON //p' | tail -1)"
|
|
76
|
+
[ -z "$vj" ] && vj="{\"cli\":\"$cli\",\"probes\":{}}"
|
|
77
|
+
vj="$(node -e 'const v=JSON.parse(process.argv[1]);v.version=process.argv[2]||null;v.fpSha=process.argv[3]||null;process.stdout.write(JSON.stringify(v))' "$vj" "$cur_ver" "$FP_SHA")"
|
|
78
|
+
fi
|
|
79
|
+
results="$(node -e 'const a=JSON.parse(process.argv[1]);a.push(JSON.parse(process.argv[2]));process.stdout.write(JSON.stringify(a))' "$results" "$vj")"
|
|
80
|
+
done
|
|
81
|
+
|
|
82
|
+
report="$(node "$HERE/report.js" "$results" "$STATE" "$MODEL")"
|
|
83
|
+
echo "════ report ════" >&2; printf '%s\n' "$report" >&2; echo "════════════════" >&2
|
|
84
|
+
printf '%s\n' "$report" # also to stdout for the workflow log / artifact
|
|
85
|
+
|
|
86
|
+
if [ -n "${CANARY_SLACK_WEBHOOK:-}" ]; then
|
|
87
|
+
payload="$(node -e 'const t=require("fs").readFileSync(0,"utf8");process.stdout.write(JSON.stringify({text:t}))' <<<"$report")"
|
|
88
|
+
code="$(curl -sS --connect-timeout 10 --max-time 30 \
|
|
89
|
+
-o /dev/null -w '%{http_code}' -X POST -H 'Content-type: application/json' \
|
|
90
|
+
--data "$payload" "$CANARY_SLACK_WEBHOOK" 2>/dev/null || echo 000)"
|
|
91
|
+
if [ "$code" = 200 ]; then echo "✓ posted to Slack webhook" >&2
|
|
92
|
+
else echo "⚠️ Slack webhook POST returned HTTP $code" >&2; fi
|
|
93
|
+
else
|
|
94
|
+
echo "(no CANARY_SLACK_WEBHOOK set — report not posted)" >&2
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Fail the job when any probe reported a hard FAIL (broken enforcement) so this
|
|
98
|
+
# scheduled check actually blocks regressions. ERROR (vendor quota/auth) and
|
|
99
|
+
# INCONCLUSIVE (model didn't attempt the tool) are NOT failures — they mean
|
|
100
|
+
# "couldn't confirm", not "enforcement broke". The report is already emitted +
|
|
101
|
+
# posted above regardless, so the signal is never lost.
|
|
102
|
+
if node -e 'const r=JSON.parse(process.argv[1]);process.exit(r.some(x=>Object.values(x.probes||{}).includes("FAIL"))?1:0)' "$results"; then
|
|
103
|
+
exit 0
|
|
104
|
+
else
|
|
105
|
+
echo "✗ FAIL verdict(s) present — failing the job" >&2
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Works out whether a browser error actually came from the dashboard.
|
|
3
|
+
*
|
|
4
|
+
* `window`'s "error" and "unhandledrejection" events are page-global: browser
|
|
5
|
+
* extensions inject content scripts into the same page and share the same
|
|
6
|
+
* window, so their failures reach our listeners and get reported as ours. The
|
|
7
|
+
* observed case was MetaMask's "Failed to connect to MetaMask" arriving as a
|
|
8
|
+
* failproofai `unhandled_rejection`. Extensions are user-installed and
|
|
9
|
+
* open-ended, so the noise is unbounded and unrelated to our code.
|
|
10
|
+
*
|
|
11
|
+
* We therefore attribute positively — report an error only when it can be
|
|
12
|
+
* traced to our own origin — rather than maintaining a denylist of extensions
|
|
13
|
+
* we happen to know about. React render errors don't depend on this: the error
|
|
14
|
+
* boundaries (`app/global-error.tsx`, `app/components/error-fallback.tsx`)
|
|
15
|
+
* report `client_error` directly, and React only invokes those for errors thrown
|
|
16
|
+
* inside our own tree.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Any `<vendor>-extension://` URL — `chrome-extension://`, `moz-extension://`,
|
|
21
|
+
* `safari-web-extension://`, `ms-browser-extension://`. Matching the shared
|
|
22
|
+
* suffix rather than an explicit vendor list means a new browser's scheme is
|
|
23
|
+
* filtered the day it ships.
|
|
24
|
+
*/
|
|
25
|
+
const EXTENSION_URL = /\b[\w-]+-extension:\/\//i;
|
|
26
|
+
|
|
27
|
+
export type ErrorOrigin =
|
|
28
|
+
/** Traceable to our own origin — a real dashboard failure. */
|
|
29
|
+
| "app"
|
|
30
|
+
/** Came from a browser extension sharing the page. */
|
|
31
|
+
| "extension"
|
|
32
|
+
/** Not attributable: no stack, or a cross-origin "Script error.". */
|
|
33
|
+
| "unknown";
|
|
34
|
+
|
|
35
|
+
export interface ErrorOriginInput {
|
|
36
|
+
/** `error.stack` / `reason.stack`, when the thrown value was an Error. */
|
|
37
|
+
stack?: string | null;
|
|
38
|
+
/** `ErrorEvent.filename` — the script URL. Absent on rejections. */
|
|
39
|
+
filename?: string | null;
|
|
40
|
+
/** Typically `window.location.origin`. */
|
|
41
|
+
appOrigin: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Classify where an error came from, using whatever location information the
|
|
46
|
+
* browser gave us.
|
|
47
|
+
*
|
|
48
|
+
* Extension frames are checked first: an error whose stack passes through an
|
|
49
|
+
* extension is that extension's problem even if one of our frames appears
|
|
50
|
+
* further down.
|
|
51
|
+
*/
|
|
52
|
+
export function classifyErrorOrigin({ stack, filename, appOrigin }: ErrorOriginInput): ErrorOrigin {
|
|
53
|
+
const text = [filename, stack].filter(Boolean).join("\n");
|
|
54
|
+
if (!text.trim()) return "unknown";
|
|
55
|
+
if (EXTENSION_URL.test(text)) return "extension";
|
|
56
|
+
if (appOrigin && text.includes(appOrigin)) return "app";
|
|
57
|
+
return "unknown";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether an error is ours to report. Only positively-attributed errors qualify:
|
|
62
|
+
* `unknown` covers cross-origin "Script error." and stack-less rejections, which
|
|
63
|
+
* are unactionable anyway — there's nothing in them to debug.
|
|
64
|
+
*/
|
|
65
|
+
export function isAppError(input: ErrorOriginInput): boolean {
|
|
66
|
+
return classifyErrorOrigin(input) === "app";
|
|
67
|
+
}
|
|
@@ -16,14 +16,28 @@ export function formatRelativeTime(ts: number): string {
|
|
|
16
16
|
|
|
17
17
|
export function formatDuration(ms: number): string {
|
|
18
18
|
if (ms < 1000) return `${ms}ms`;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return `${
|
|
19
|
+
|
|
20
|
+
// Round to the precision the output will actually use, then bucket.
|
|
21
|
+
// Seconds are shown with one decimal place, so round to the nearest 0.1 s
|
|
22
|
+
// before deciding whether we have crossed into the minute range.
|
|
23
|
+
const deciseconds = Math.round(ms / 100);
|
|
24
|
+
if (deciseconds < 600) {
|
|
25
|
+
return `${(deciseconds / 10).toFixed(1)}s`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Minutes are shown with whole seconds, so round to the nearest second
|
|
29
|
+
// before splitting into minutes and seconds.
|
|
30
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
31
|
+
if (totalSeconds < 3600) {
|
|
32
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
33
|
+
const seconds = totalSeconds % 60;
|
|
34
|
+
return `${minutes}m ${seconds}s`;
|
|
26
35
|
}
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
|
|
37
|
+
// Hours are shown with whole minutes, so round to the nearest minute
|
|
38
|
+
// before splitting into hours and minutes.
|
|
39
|
+
const totalMinutes = Math.round(ms / 60000);
|
|
40
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
41
|
+
const minutes = totalMinutes % 60;
|
|
42
|
+
return `${hours}h ${minutes}m`;
|
|
29
43
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install reporting, driven from the CLI instead of an npm lifecycle script.
|
|
3
|
+
*
|
|
4
|
+
* This used to live in `scripts/postinstall.mjs`. Every package manager now
|
|
5
|
+
* blocks dependency install scripts by default — npm 12 (`allowScripts`), pnpm,
|
|
6
|
+
* bun, and Yarn ≥4.14 — and a publisher cannot opt its own package in, so a
|
|
7
|
+
* postinstall hook is no longer a channel that reaches users. The first non-hook
|
|
8
|
+
* CLI invocation is.
|
|
9
|
+
*
|
|
10
|
+
* Two constraints shape this module:
|
|
11
|
+
*
|
|
12
|
+
* 1. It must never run on the `--hook` fast path in bin/failproofai.mjs. That
|
|
13
|
+
* path fires on every tool call of every agent session; the check belongs
|
|
14
|
+
* in runCli() only.
|
|
15
|
+
* 2. It must be free on the steady-state path. When the recorded version
|
|
16
|
+
* matches, this returns before touching anything else.
|
|
17
|
+
*
|
|
18
|
+
* Everything here is best-effort: telemetry must never break, delay, or fail a
|
|
19
|
+
* real command.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
|
+
import { platform, arch, release, homedir, hostname } from "node:os";
|
|
24
|
+
import { createHmac } from "node:crypto";
|
|
25
|
+
import { trackInstallEvent } from "../scripts/install-telemetry.mjs";
|
|
26
|
+
|
|
27
|
+
const FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
|
|
28
|
+
const NAMESPACE = "failproofai-telemetry-v1";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cap on the once-per-version report so a hanging network never stalls a command.
|
|
32
|
+
*
|
|
33
|
+
* A ceiling, not a cost: delivery resolves as soon as PostHog answers, measured
|
|
34
|
+
* at ~1.1s from a cold process (these events are the first thing a fresh process
|
|
35
|
+
* sends, so they always pay the DNS + TLS handshake). Raising this only matters
|
|
36
|
+
* when the network is slow — precisely when a tight budget would silently drop
|
|
37
|
+
* the report instead.
|
|
38
|
+
*
|
|
39
|
+
* 5s matches hook-telemetry and the postinstall script this replaces. An earlier
|
|
40
|
+
* 2s left barely any headroom over a 1.85s worst case seen in testing, for a
|
|
41
|
+
* saving nobody experiences on a healthy network.
|
|
42
|
+
*/
|
|
43
|
+
const REPORT_TIMEOUT_MS = 5000;
|
|
44
|
+
|
|
45
|
+
function hashToId(raw: string): string {
|
|
46
|
+
return createHmac("sha256", NAMESPACE).update(raw).digest("hex");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function lastVersionPath(): string {
|
|
50
|
+
return resolve(homedir(), ".failproofai", "last-version");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Semver ordering. A release outranks the same version carrying a prerelease
|
|
55
|
+
* tag (semver §11); within a prerelease, numeric identifiers rank below
|
|
56
|
+
* non-numeric ones.
|
|
57
|
+
*
|
|
58
|
+
* Prerelease identifiers are separated by dots only (semver §9). A hyphen is a
|
|
59
|
+
* legal character *inside* an identifier, so `beta-2` is one non-numeric
|
|
60
|
+
* identifier compared lexically — not `beta` and `2`.
|
|
61
|
+
*/
|
|
62
|
+
export function compareSemver(a: string, b: string): number {
|
|
63
|
+
const parse = (v: string) => {
|
|
64
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(v);
|
|
65
|
+
if (!m) return null;
|
|
66
|
+
return { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ?? null };
|
|
67
|
+
};
|
|
68
|
+
const pa = parse(a);
|
|
69
|
+
const pb = parse(b);
|
|
70
|
+
if (!pa || !pb) return a < b ? -1 : a > b ? 1 : 0;
|
|
71
|
+
for (let i = 0; i < 3; i++) {
|
|
72
|
+
if (pa.nums[i] !== pb.nums[i]) return pa.nums[i] < pb.nums[i] ? -1 : 1;
|
|
73
|
+
}
|
|
74
|
+
if (pa.pre === null && pb.pre === null) return 0;
|
|
75
|
+
if (pa.pre === null) return 1;
|
|
76
|
+
if (pb.pre === null) return -1;
|
|
77
|
+
const ax = pa.pre.split(".");
|
|
78
|
+
const bx = pb.pre.split(".");
|
|
79
|
+
for (let i = 0; i < Math.max(ax.length, bx.length); i++) {
|
|
80
|
+
const ai = ax[i], bi = bx[i];
|
|
81
|
+
if (ai === undefined) return -1;
|
|
82
|
+
if (bi === undefined) return 1;
|
|
83
|
+
const aNum = /^\d+$/.test(ai), bNum = /^\d+$/.test(bi);
|
|
84
|
+
if (aNum && bNum) {
|
|
85
|
+
const d = Number(ai) - Number(bi);
|
|
86
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
87
|
+
} else if (aNum) {
|
|
88
|
+
return -1;
|
|
89
|
+
} else if (bNum) {
|
|
90
|
+
return 1;
|
|
91
|
+
} else if (ai !== bi) {
|
|
92
|
+
return ai < bi ? -1 : 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface HooksState {
|
|
99
|
+
configured: boolean;
|
|
100
|
+
registered: boolean;
|
|
101
|
+
policyCount: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Snapshot of the user's hook configuration, carried as properties on
|
|
106
|
+
* package_installed.
|
|
107
|
+
*
|
|
108
|
+
* Only inspects Claude's settings, which understates `registered` for someone
|
|
109
|
+
* running failproofai against another CLI. Preserved as-is from the postinstall
|
|
110
|
+
* script so the event keeps its historical meaning.
|
|
111
|
+
*/
|
|
112
|
+
export function checkHooks(): HooksState {
|
|
113
|
+
const hooksConfigPath = resolve(homedir(), ".failproofai", "policies-config.json");
|
|
114
|
+
if (!existsSync(hooksConfigPath)) {
|
|
115
|
+
return { configured: false, registered: false, policyCount: 0 };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let config;
|
|
119
|
+
try {
|
|
120
|
+
config = JSON.parse(readFileSync(hooksConfigPath, "utf8"));
|
|
121
|
+
} catch {
|
|
122
|
+
return { configured: false, registered: false, policyCount: 0 };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!Array.isArray(config.enabledPolicies) || config.enabledPolicies.length === 0) {
|
|
126
|
+
return { configured: false, registered: false, policyCount: 0 };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const policyCount = config.enabledPolicies.length;
|
|
130
|
+
const settingsPath = resolve(homedir(), ".claude", "settings.json");
|
|
131
|
+
if (!existsSync(settingsPath)) return { configured: true, registered: false, policyCount };
|
|
132
|
+
|
|
133
|
+
let settings;
|
|
134
|
+
try {
|
|
135
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
136
|
+
} catch {
|
|
137
|
+
return { configured: true, registered: false, policyCount };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!settings?.hooks) return { configured: true, registered: false, policyCount };
|
|
141
|
+
|
|
142
|
+
for (const matchers of Object.values(settings.hooks)) {
|
|
143
|
+
if (!Array.isArray(matchers)) continue;
|
|
144
|
+
for (const matcher of matchers) {
|
|
145
|
+
if (!matcher?.hooks) continue;
|
|
146
|
+
if (matcher.hooks.some((h: Record<string, unknown>) => h[FAILPROOFAI_HOOK_MARKER] === true)) {
|
|
147
|
+
return { configured: true, registered: true, policyCount };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { configured: true, registered: false, policyCount };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Report an install or upgrade, at most once per version.
|
|
157
|
+
*
|
|
158
|
+
* Fires first_install (nothing recorded yet) or version_changed (recorded
|
|
159
|
+
* version differs), then package_installed, then records `version`. A same-version
|
|
160
|
+
* run is a no-op — unlike the postinstall script this replaces, the CLI has no
|
|
161
|
+
* signal that would distinguish a reinstall from an ordinary invocation, so
|
|
162
|
+
* `direction: "reinstall"` is no longer reported.
|
|
163
|
+
*/
|
|
164
|
+
export async function maybeReportInstall(version: string): Promise<void> {
|
|
165
|
+
try {
|
|
166
|
+
const versionFile = lastVersionPath();
|
|
167
|
+
|
|
168
|
+
let previousVersion: string | null = null;
|
|
169
|
+
try {
|
|
170
|
+
if (existsSync(versionFile)) {
|
|
171
|
+
previousVersion = readFileSync(versionFile, "utf8").trim() || null;
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// Unreadable marker — treat as first run rather than give up.
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Steady state: same version already reported. Costs one stat() per command.
|
|
178
|
+
if (previousVersion === version) return;
|
|
179
|
+
|
|
180
|
+
const hooks = checkHooks();
|
|
181
|
+
|
|
182
|
+
const events: Promise<void>[] = [];
|
|
183
|
+
if (previousVersion === null) {
|
|
184
|
+
events.push(
|
|
185
|
+
trackInstallEvent("first_install", {
|
|
186
|
+
platform: platform(),
|
|
187
|
+
arch: arch(),
|
|
188
|
+
os_release: release(),
|
|
189
|
+
node_version: process.versions.node,
|
|
190
|
+
}, { version, timeoutMs: REPORT_TIMEOUT_MS }),
|
|
191
|
+
);
|
|
192
|
+
} else {
|
|
193
|
+
const cmp = compareSemver(previousVersion, version);
|
|
194
|
+
events.push(
|
|
195
|
+
trackInstallEvent("version_changed", {
|
|
196
|
+
from_version: previousVersion,
|
|
197
|
+
to_version: version,
|
|
198
|
+
direction: cmp < 0 ? "upgrade" : "downgrade",
|
|
199
|
+
platform: platform(),
|
|
200
|
+
arch: arch(),
|
|
201
|
+
}, { version, timeoutMs: REPORT_TIMEOUT_MS }),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
events.push(
|
|
206
|
+
trackInstallEvent("package_installed", {
|
|
207
|
+
platform: platform(),
|
|
208
|
+
arch: arch(),
|
|
209
|
+
os_release: release(),
|
|
210
|
+
node_version: process.versions.node,
|
|
211
|
+
hostname_hash: hashToId(hostname()),
|
|
212
|
+
hooks_configured: hooks.configured,
|
|
213
|
+
hooks_registered: hooks.registered,
|
|
214
|
+
enabled_policy_count: hooks.policyCount,
|
|
215
|
+
}, { version, timeoutMs: REPORT_TIMEOUT_MS }),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Record before awaiting: a dropped event is cheaper than re-reporting the
|
|
219
|
+
// same version on every command because delivery kept failing.
|
|
220
|
+
try {
|
|
221
|
+
mkdirSync(resolve(homedir(), ".failproofai"), { recursive: true });
|
|
222
|
+
writeFileSync(versionFile, version, "utf8");
|
|
223
|
+
} catch {
|
|
224
|
+
// best-effort
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await Promise.all(events.map((p) => p.catch(() => {})));
|
|
228
|
+
} catch {
|
|
229
|
+
// Reporting must never break a command.
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "0.0.14-beta.
|
|
3
|
+
"version": "0.0.14-beta.2",
|
|
4
4
|
"description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs"
|
|
@@ -30,8 +30,6 @@
|
|
|
30
30
|
"test": "vitest",
|
|
31
31
|
"test:run": "vitest run",
|
|
32
32
|
"lint": "eslint . --config eslint.config.mjs",
|
|
33
|
-
"postinstall": "node scripts/postinstall.mjs",
|
|
34
|
-
"preuninstall": "node scripts/preuninstall.mjs",
|
|
35
33
|
"prepare": "bun run build",
|
|
36
34
|
"test:e2e": "vitest run --config vitest.config.e2e.mts",
|
|
37
35
|
"test:e2e:watch": "vitest --config vitest.config.e2e.mts",
|
|
@@ -9,7 +9,7 @@ const currentPort = parseInt(process.env.PORT, 10) || 3000
|
|
|
9
9
|
const hostname = process.env.HOSTNAME || '0.0.0.0'
|
|
10
10
|
|
|
11
11
|
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
|
|
12
|
-
const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"0.0.14-beta.
|
|
12
|
+
const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"0.0.14-beta.2"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":false,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"partialFallbacks":false,"dynamicOnHover":false,"varyParams":false,"prefetchInlining":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"imgOptSkipMetadata":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["node_modules/@img/**","node_modules/sharp/**"]},"turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"distDirRoot":".next"}
|
|
13
13
|
|
|
14
14
|
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
|
|
15
15
|
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/failproofai)
|
|
6
6
|
[](https://github.com/failproofai/failproofai/actions)
|
|
7
7
|
[](https://github.com/failproofai/failproofai/actions/workflows/osv-scanner.yml)
|
|
8
|
-
[](https://discord.
|
|
8
|
+
[](https://discord.befailproof.ai/)
|
|
9
9
|
[](https://docs.befailproof.ai/introduction)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
|
|
@@ -119,22 +119,6 @@ before they become incidents. Zero latency. Runs locally.
|
|
|
119
119
|
</tr>
|
|
120
120
|
</table>
|
|
121
121
|
|
|
122
|
-
> Install hooks for one or any combination: `failproofai policies --install --cli opencode pi` (or `--cli claude codex copilot cursor opencode pi hermes openclaw factory devin antigravity goose`). Omit `--cli` to auto-detect installed CLIs and prompt.
|
|
123
|
-
>
|
|
124
|
-
> **Hermes** (hermes-agent, a Slack/Telegram gateway) is supported for both **live-hook enforcement** (`--cli hermes` — one install intercepts tool calls from every platform and subagent) and offline **audit** replay of its gateway sessions from the single `~/.hermes/state.db`.
|
|
125
|
-
>
|
|
126
|
-
> **OpenClaw** (openclaw gateway, a self-hosted multi-channel assistant) is supported for both **live-hook enforcement** (`--cli openclaw`, user-scope) and offline **audit** replay of its JSONL sessions (`~/.openclaw/agents/<id>/sessions/*.jsonl`). Enforcement uses OpenClaw's **in-process plugin hooks** (a shipped `openclaw-plugin/` that async-spawns failproofai — its file-based internal hooks are observation-only and can't block): `before_tool_call` blocks a tool, and `before_agent_finalize` is a real turn-end gate, so the `require-*-before-stop` builtins enforce.
|
|
127
|
-
>
|
|
128
|
-
> **Factory Droid** (`droid`) is supported for both **live-hook enforcement** (`--cli factory`, user + project scope) and offline **audit** replay of its on-disk JSONL sessions. droid blocks tool calls off hook **exit code 2** (not a JSON decision) and honors `{decision:"block"}` only on the turn-end `Stop` event — failproofai emits the right shape per event automatically.
|
|
129
|
-
>
|
|
130
|
-
> **Devin CLI** (`devin`, Cognition) is supported for both **live-hook enforcement** (`--cli devin`, user + project scope) and offline **audit** replay of its SQLite sessions (`~/.local/share/devin/cli/sessions.db`). Devin is a **pure Claude-clone** — same event names, same snake_case payload, same `"hooks"`-wrapper config (`~/.config/devin/config.json` / `<cwd>/.devin/config.json`) — blocking via `{decision:"block"}` JSON on every event.
|
|
131
|
-
>
|
|
132
|
-
> **Antigravity CLI** (`agy`) is supported for both **live-hook enforcement** (`--cli antigravity`, user + project scope) and offline **audit** replay of its plain-JSONL sessions (`~/.gemini/antigravity-cli/brain/<id>/…/transcript_full.jsonl`). Antigravity has its **own** contract (not a Claude-clone): a **named-hook** `hooks.json` schema (`~/.gemini/config/hooks.json` / `<cwd>/.agents/hooks.json`), a camelCase stdin payload that failproofai normalizes, and its own response shapes — `{decision:"deny"}` to block a tool, `{decision:"continue"}` to force another turn at `Stop`, `{injectSteps}` to inject a reminder before the model runs.
|
|
133
|
-
>
|
|
134
|
-
> **Goose** (codename goose, Block) is supported for both **live-hook enforcement** (`--cli goose`, user + project scope) and offline **audit** replay of its SQLite sessions (`~/.local/share/goose/sessions/sessions.db`). Enforcement uses Goose's **hooks** system (the cross-agent **Open Plugins** spec) — the installer just drops a plugin dir at `~/.agents/plugins/failproofai/` and Goose auto-discovers it. Blocking is `{"decision":"block"}` JSON on the `PreToolUse` event (which fires for the shell tool and inside delegated subagents), verified live against goose v1.43.0; Goose has no turn-end `Stop` event, so the `require-*-before-stop` builtins don't apply (as with Hermes).
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
122
|
## Install
|
|
139
123
|
|
|
140
124
|
```sh
|
package/bin/failproofai.mjs
CHANGED
|
@@ -124,6 +124,16 @@ if (hookIdx >= 0) {
|
|
|
124
124
|
* Error → unexpected; shows message only, exits 2
|
|
125
125
|
*/
|
|
126
126
|
async function runCli() {
|
|
127
|
+
// Report a fresh install / upgrade. Deliberately here rather than at module
|
|
128
|
+
// scope: everything above this point is the --hook fast path, which runs on
|
|
129
|
+
// every tool call. No-ops after the first run on a given version.
|
|
130
|
+
try {
|
|
131
|
+
const { maybeReportInstall } = await import("../lib/install-check");
|
|
132
|
+
await maybeReportInstall(version);
|
|
133
|
+
} catch {
|
|
134
|
+
// never block a command on reporting
|
|
135
|
+
}
|
|
136
|
+
|
|
127
137
|
// --help / -h (only when not inside a subcommand that handles its own --help)
|
|
128
138
|
const SUBCOMMANDS = ["policies", "policy", "auth", "audit", "config"];
|
|
129
139
|
if ((args.includes("--help") || args.includes("-h")) && !SUBCOMMANDS.includes(args[0])) {
|
|
@@ -209,7 +219,7 @@ EXAMPLES
|
|
|
209
219
|
LINKS
|
|
210
220
|
⭐ Star us: https://github.com/failproofai/failproofai
|
|
211
221
|
📖 Docs: https://docs.befailproof.ai/introduction
|
|
212
|
-
💬 Discord: https://discord.
|
|
222
|
+
💬 Discord: https://discord.befailproof.ai/
|
|
213
223
|
`.trimStart());
|
|
214
224
|
process.exit(0);
|
|
215
225
|
}
|