failproofai 1.0.7-beta.1 → 1.0.7
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 +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +7 -8
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +5 -7
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +6 -6
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +11 -13
- 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 +7 -8
- 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 +7 -7
- 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 +6 -7
- 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 +8 -41
- package/.next/standalone/.next/server/app/settings/page.js +9 -12
- 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]__1j-zsg5._.js → [externals]__1_bftcl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__19_pzeq._.js → [externals]__1msfs-h._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1bf34x4._.js → [root-of-the-server]__1_r2rbg._.js} +7 -5
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1q5i8mb._.js → _1c3k-8x._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
- 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/src_hooks_0iu54mz._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +4 -0
- 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]__0yxwl6j._.js → [root-of-the-server]__0bd3mje._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__0cxe_2_._.js} +3 -3
- 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]__0p-5p8u._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +22 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1w_5l7t._.js → _0h_douw._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_214wgrp._.js → _1-i_gzc._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0bn2oo8._.js → _166t73i._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1q46vxx._.js → _1_qswah._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifp._.js → _1es2j7i._.js} +5 -5
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_18_d8l1._.js → node_modules_next_dist_0drixxt._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.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/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -56
- package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +1 -0
- package/.next/standalone/.next/static/chunks/0__8a7m868fvf.js +1 -0
- package/.next/standalone/.next/static/chunks/{12tvm75t5ffui.js → 0o6qlkgubtoex.js} +1 -1
- package/.next/standalone/.next/static/chunks/13i7-9is-vhys.js +1 -0
- package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +6 -0
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
- package/.next/standalone/.next/static/chunks/{0vmd180qfntfb.js → 2klitrtzpaoe0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{258668t68du6b.js → 2mdh397ghgnvv.js} +1 -1
- package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +2 -0
- package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +1 -0
- package/.next/standalone/.next/static/chunks/{0qrbdkv9qmvli.js → 3rh5o7e16irrm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{29fql3nbnfc9q.js → 43ufqrz8qo3h-.js} +1 -1
- package/.next/standalone/SECURITY.md +53 -0
- package/.next/standalone/app/actions/pack-actions.ts +0 -12
- package/.next/standalone/app/policies/hooks-client.tsx +0 -9
- package/.next/standalone/app/settings/page.tsx +1 -20
- package/.next/standalone/app/settings/settings-client.tsx +1 -27
- package/.next/standalone/app/settings/settings.css +0 -79
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/sdk/python/skill/SKILL.md +60 -14
- package/.next/standalone/sdk/python/skill/agents/openai.yaml +2 -1
- package/.next/standalone/sdk/python/skill/references/evaluator.md +255 -0
- package/.next/standalone/sdk/python/skill/references/events.md +17 -8
- package/.next/standalone/sdk/python/skill/references/frameworks.md +3 -0
- package/.next/standalone/sdk/python/skill/references/install.md +3 -0
- package/.next/standalone/sdk/python/skill/references/integration.md +6 -2
- package/.next/standalone/sdk/python/skill/references/typescript.md +568 -0
- package/.next/standalone/sdk/typescript/CHANGELOG.md +133 -0
- package/.next/standalone/sdk/typescript/LICENSE +42 -0
- package/.next/standalone/sdk/typescript/README.md +552 -0
- package/.next/standalone/sdk/typescript/eslint.config.mjs +59 -0
- package/.next/standalone/sdk/typescript/examples/research-agent.ts +197 -0
- package/.next/standalone/sdk/typescript/integration/ai.test.ts +920 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/agent.ts +337 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +261 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +16 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/surfaces.ts +605 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/agent.ts +342 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +156 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +16 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/surfaces.ts +628 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/agent.ts +346 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +156 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +13 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/surfaces.ts +651 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/agent.ts +350 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +153 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +13 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/surfaces.ts +651 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/agent.ts +623 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +344 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +19 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent-v1.ts +99 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent.ts +623 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +336 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/agent.ts +96 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +441 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +19 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.cjs +53 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.mjs +55 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/package.json +18 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/agent.ts +659 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +635 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/agent.ts +659 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +553 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/agent.ts +877 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/mcp-server.mjs +66 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +6797 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +24 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/agent.ts +872 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/mcp-server.mjs +66 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +2540 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/actions.ts +18 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/ai/route.ts +42 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/edge/route.ts +29 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/langgraph/route.ts +21 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/llamaindex/route.ts +11 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/mastra/route.ts +19 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/status/route.ts +7 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/layout.tsx +9 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/page.tsx +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/instrumentation.ts +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/ai.ts +61 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/langgraph.ts +68 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/llamaindex.ts +98 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/mastra.ts +90 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/next.config.ts +52 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +4343 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +28 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/agent.ts +51 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/deno-npm.ts +76 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +484 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/types/agent.ts +79 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/types/package-lock.json +740 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/types/package.json +11 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/agent.ts +197 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package-lock.json +70 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/global-setup.ts +29 -0
- package/.next/standalone/sdk/typescript/integration/harness.ts +451 -0
- package/.next/standalone/sdk/typescript/integration/langchain.test.ts +682 -0
- package/.next/standalone/sdk/typescript/integration/llamaindex.test.ts +709 -0
- package/.next/standalone/sdk/typescript/integration/mastra-coverage.test.ts +386 -0
- package/.next/standalone/sdk/typescript/integration/mastra.test.ts +311 -0
- package/.next/standalone/sdk/typescript/integration/nextjs.test.ts +341 -0
- package/.next/standalone/sdk/typescript/integration/runtime-parity.ts +180 -0
- package/.next/standalone/sdk/typescript/integration/runtimes.bun.test.ts +15 -0
- package/.next/standalone/sdk/typescript/integration/runtimes.core.test.ts +113 -0
- package/.next/standalone/sdk/typescript/integration/runtimes.deno.test.ts +19 -0
- package/.next/standalone/sdk/typescript/integration/types.test.ts +141 -0
- package/.next/standalone/sdk/typescript/integration/vanilla.test.ts +255 -0
- package/.next/standalone/sdk/typescript/package-lock.json +2640 -0
- package/.next/standalone/sdk/typescript/package.json +401 -0
- package/.next/standalone/sdk/typescript/scripts/finalize-build.mjs +123 -0
- package/.next/standalone/sdk/typescript/scripts/release.mjs +177 -0
- package/.next/standalone/sdk/typescript/src/clock.ts +58 -0
- package/.next/standalone/sdk/typescript/src/context.ts +214 -0
- package/.next/standalone/sdk/typescript/src/edge/adapter.ts +18 -0
- package/.next/standalone/sdk/typescript/src/edge/ai.ts +116 -0
- package/.next/standalone/sdk/typescript/src/edge/index.ts +238 -0
- package/.next/standalone/sdk/typescript/src/edge/langchain.ts +18 -0
- package/.next/standalone/sdk/typescript/src/edge/llamaindex.ts +12 -0
- package/.next/standalone/sdk/typescript/src/edge/mastra.ts +17 -0
- package/.next/standalone/sdk/typescript/src/edge/notice.ts +33 -0
- package/.next/standalone/sdk/typescript/src/environment.ts +75 -0
- package/.next/standalone/sdk/typescript/src/evaluator/authoring.ts +480 -0
- package/.next/standalone/sdk/typescript/src/evaluator/cli.ts +96 -0
- package/.next/standalone/sdk/typescript/src/evaluator/client.ts +421 -0
- package/.next/standalone/sdk/typescript/src/evaluator/expression.ts +1292 -0
- package/.next/standalone/sdk/typescript/src/evaluator/index.ts +144 -0
- package/.next/standalone/sdk/typescript/src/evaluator/protocol.ts +747 -0
- package/.next/standalone/sdk/typescript/src/evaluator/runtime.ts +930 -0
- package/.next/standalone/sdk/typescript/src/evaluator/sandbox-worker.ts +171 -0
- package/.next/standalone/sdk/typescript/src/evaluator/source-limits.ts +27 -0
- package/.next/standalone/sdk/typescript/src/evaluator/source.ts +509 -0
- package/.next/standalone/sdk/typescript/src/events.ts +879 -0
- package/.next/standalone/sdk/typescript/src/exit.ts +117 -0
- package/.next/standalone/sdk/typescript/src/index.ts +186 -0
- package/.next/standalone/sdk/typescript/src/integrations/ai.ts +1566 -0
- package/.next/standalone/sdk/typescript/src/integrations/compat.ts +322 -0
- package/.next/standalone/sdk/typescript/src/integrations/core.ts +1321 -0
- package/.next/standalone/sdk/typescript/src/integrations/index.ts +355 -0
- package/.next/standalone/sdk/typescript/src/integrations/langchain.ts +2340 -0
- package/.next/standalone/sdk/typescript/src/integrations/llamaindex.ts +2111 -0
- package/.next/standalone/sdk/typescript/src/integrations/mastra.ts +1802 -0
- package/.next/standalone/sdk/typescript/src/logger.ts +98 -0
- package/.next/standalone/sdk/typescript/src/next.ts +115 -0
- package/.next/standalone/sdk/typescript/src/node-require.ts +446 -0
- package/.next/standalone/sdk/typescript/src/redact.ts +305 -0
- package/.next/standalone/sdk/typescript/src/resolver.ts +120 -0
- package/.next/standalone/sdk/typescript/src/runtime.ts +29 -0
- package/.next/standalone/sdk/typescript/src/schema.ts +410 -0
- package/.next/standalone/sdk/typescript/src/scopes.ts +701 -0
- package/.next/standalone/sdk/typescript/src/shared.ts +33 -0
- package/.next/standalone/sdk/typescript/src/version.ts +5 -0
- package/.next/standalone/sdk/typescript/src/writer.ts +934 -0
- package/.next/standalone/sdk/typescript/test/adapters.test.ts +397 -0
- package/.next/standalone/sdk/typescript/test/ai.test.ts +1076 -0
- package/.next/standalone/sdk/typescript/test/copies.test.ts +204 -0
- package/.next/standalone/sdk/typescript/test/edge.test.ts +183 -0
- package/.next/standalone/sdk/typescript/test/evaluator-client.test.ts +234 -0
- package/.next/standalone/sdk/typescript/test/evaluator-protocol.test.ts +225 -0
- package/.next/standalone/sdk/typescript/test/events.test.ts +193 -0
- package/.next/standalone/sdk/typescript/test/expression.test.ts +181 -0
- package/.next/standalone/sdk/typescript/test/global-setup.ts +26 -0
- package/.next/standalone/sdk/typescript/test/helpers.ts +130 -0
- package/.next/standalone/sdk/typescript/test/integrations.test.ts +369 -0
- package/.next/standalone/sdk/typescript/test/langchain-copies.test.ts +204 -0
- package/.next/standalone/sdk/typescript/test/langchain.test.ts +999 -0
- package/.next/standalone/sdk/typescript/test/llamaindex.test.ts +1760 -0
- package/.next/standalone/sdk/typescript/test/mastra-coverage.test.ts +501 -0
- package/.next/standalone/sdk/typescript/test/mastra-lifecycle.test.ts +479 -0
- package/.next/standalone/sdk/typescript/test/mastra.test.ts +285 -0
- package/.next/standalone/sdk/typescript/test/next.test.ts +109 -0
- package/.next/standalone/sdk/typescript/test/packaging.test.ts +312 -0
- package/.next/standalone/sdk/typescript/test/redaction.test.ts +171 -0
- package/.next/standalone/sdk/typescript/test/runtimes.test.ts +101 -0
- package/.next/standalone/sdk/typescript/test/sandbox.test.ts +189 -0
- package/.next/standalone/sdk/typescript/test/scopes.test.ts +271 -0
- package/.next/standalone/sdk/typescript/test/setup.ts +19 -0
- package/.next/standalone/sdk/typescript/test/skill-snippets.test.ts +73 -0
- package/.next/standalone/sdk/typescript/test/spool-contract.test.ts +124 -0
- package/.next/standalone/sdk/typescript/test/tracker-bounds.test.ts +191 -0
- package/.next/standalone/sdk/typescript/test/wire-format.test.ts +214 -0
- package/.next/standalone/sdk/typescript/test/writer.test.ts +407 -0
- package/.next/standalone/sdk/typescript/tsconfig.build.json +15 -0
- package/.next/standalone/sdk/typescript/tsconfig.cjs.json +19 -0
- package/.next/standalone/sdk/typescript/tsconfig.json +28 -0
- package/.next/standalone/sdk/typescript/vitest.config.ts +33 -0
- package/.next/standalone/sdk/typescript/vitest.integration.config.ts +23 -0
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +2 -115
- package/dist/cli.mjs +6538 -13686
- package/dist/index.js +1 -19
- package/dist/worker.mjs +2085 -8012
- package/package.json +9 -9
- package/pi-extension/index.ts +0 -11
- package/scripts/build-policy-pack.mjs +2 -39
- package/src/hooks/builtin-policies.ts +3 -21
- package/src/hooks/cloud-enrollment-cli.ts +1 -1
- package/src/hooks/cloud-managed-policies.ts +0 -22
- package/src/hooks/custom-hooks-loader.ts +7 -45
- package/src/hooks/custom-hooks-registry.ts +1 -45
- package/src/hooks/first-run-gate.ts +0 -5
- package/src/hooks/fp-home.ts +0 -23
- package/src/hooks/handler.ts +6 -265
- package/src/hooks/hook-activity-store.ts +1 -105
- package/src/hooks/hook-telemetry.ts +0 -41
- package/src/hooks/loader-utils.ts +0 -6
- package/src/hooks/pack-cli.ts +17 -278
- package/src/hooks/pack-manifest.ts +7 -479
- package/src/hooks/pack-store.ts +10 -156
- package/src/hooks/policy-catalog.ts +0 -65
- package/src/hooks/policy-evaluator.ts +796 -940
- package/src/hooks/policy-registry.ts +0 -25
- package/src/hooks/policy-types.ts +0 -126
- package/src/hooks/worker-server.ts +26 -119
- package/src/index.ts +0 -6
- package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
- package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +0 -22
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qxnccm._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0soxz2z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0w6mzq5._.js +0 -151
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +0 -3
- package/.next/standalone/.next/static/chunks/0cd-_8-c-m1ea.js +0 -6
- package/.next/standalone/.next/static/chunks/0uldbut9y2-e8.js +0 -1
- package/.next/standalone/.next/static/chunks/1u5zsejmgrir_.js +0 -1
- package/.next/standalone/.next/static/chunks/285spx855h_3r.css +0 -2
- package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +0 -1
- package/.next/standalone/.next/static/chunks/2vkvu9-opa_1z.js +0 -1
- package/.next/standalone/.next/static/chunks/37lhv7wa3ywt6.js +0 -1
- package/.next/standalone/app/actions/get-jev-config.ts +0 -409
- package/.next/standalone/app/actions/update-jev-config.ts +0 -420
- package/.next/standalone/app/components/jev-notices.tsx +0 -96
- package/.next/standalone/app/settings/jev-panel.tsx +0 -469
- package/src/hooks/effective-reviewers.ts +0 -79
- package/src/hooks/jev-activity.ts +0 -385
- package/src/hooks/jev-cli.ts +0 -1193
- package/src/hooks/policy-authority.ts +0 -333
- package/src/hooks/policy-reviewability.ts +0 -229
- package/src/hooks/semantic/combine.ts +0 -541
- package/src/hooks/semantic/compile.ts +0 -176
- package/src/hooks/semantic/decide.ts +0 -392
- package/src/hooks/semantic/envelope.ts +0 -1296
- package/src/hooks/semantic/evaluator.ts +0 -547
- package/src/hooks/semantic/facts.ts +0 -292
- package/src/hooks/semantic/intent.ts +0 -1190
- package/src/hooks/semantic/jev-client.ts +0 -643
- package/src/hooks/semantic/jev-config.ts +0 -594
- package/src/hooks/semantic/jev-review.ts +0 -374
- package/src/hooks/semantic/jev-stats.ts +0 -289
- package/src/hooks/semantic/jev-throttle.ts +0 -421
- package/src/hooks/semantic/pack-policies.ts +0 -251
- package/src/hooks/semantic/policies.ts +0 -596
- package/src/hooks/semantic/precondition-names.ts +0 -60
- package/src/hooks/semantic/preconditions.ts +0 -58
- package/src/hooks/semantic/redact.ts +0 -2910
- package/src/hooks/semantic/types.ts +0 -145
- package/src/hooks/semver-precedence.ts +0 -128
- /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_ssgManifest.js +0 -0
|
@@ -1,1190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the human actually asked for.
|
|
3
|
-
*
|
|
4
|
-
* The semantic evaluator may clear a REVIEWABLE policy when the user explicitly
|
|
5
|
-
* asked for the action — `git push --force` is right when you just said "force
|
|
6
|
-
* push it". Cutting those false blocks is the whole point of the evaluator, so
|
|
7
|
-
* where "the user asked" comes from is a product decision, not a puzzle. It was
|
|
8
|
-
* taken on 2026-09-23 and it is written out here in full.
|
|
9
|
-
*
|
|
10
|
-
* WHAT IS RECORDED. The prompt the harness itself hands the hook at its
|
|
11
|
-
* prompt-submit event (canonical `UserPromptSubmit`), on every harness whose
|
|
12
|
-
* event carries the human's text — `PROMPT_CHANNELS` below audits all twelve,
|
|
13
|
-
* one row each, and docs/reference/jev-intent.mdx prints the same table. The
|
|
14
|
-
* text is cleaned of harness wrapping (`cleanHumanTurn`), redacted, capped and
|
|
15
|
-
* stored 0600; the last five prompts of a session are kept for six hours.
|
|
16
|
-
* Nothing on the file system is consulted: the session transcript is a file the
|
|
17
|
-
* agent can rewrite in one command, so it is never asked who wrote a prompt.
|
|
18
|
-
*
|
|
19
|
-
* WHOSE FILE IT IS. 0600 on the file proves nothing if another local user can
|
|
20
|
-
* write to a directory above it — they rename it away and leave their own, and
|
|
21
|
-
* every check on the file then passes. So the read path holds the store's whole
|
|
22
|
-
* path to the same rule `jev-config.ts` holds `jev.json`'s directory to
|
|
23
|
-
* (`looseIntentStoreDir` below): loose write bits are taken off where we can,
|
|
24
|
-
* and a directory that is still writable by someone else stops the read. Then
|
|
25
|
-
* nothing is recorded as the human's, and nothing is cleared.
|
|
26
|
-
*
|
|
27
|
-
* THE ACCEPTED RISK, PLAINLY. An agent that can already run commands can make a
|
|
28
|
-
* harness submit a prompt: `claude -p "…"`, `codex exec`, `copilot -p`,
|
|
29
|
-
* `cursor-agent -p`, `devin -p`, `droid -p`, `goose run -t` and `pi -p` all
|
|
30
|
-
* fire the same event, with the same payload, as a person typing, in a child
|
|
31
|
-
* session that agent controls. It can also run failproofai's own hook binary
|
|
32
|
-
* with a payload it wrote. Nothing inside this module can tell either from the
|
|
33
|
-
* real thing — both are the same program reading the same stdin — so an agent
|
|
34
|
-
* with a shell can forge its own consent, and this module does not pretend
|
|
35
|
-
* otherwise.
|
|
36
|
-
*
|
|
37
|
-
* WHY THAT IS ACCEPTED. The alternative was built and measured (round 9): every
|
|
38
|
-
* field that would prove a human typed the prompt is a field no shipping
|
|
39
|
-
* harness sends, so requiring one recorded NOTHING, anywhere, and Jev judged
|
|
40
|
-
* every call with no stated intent — unable to clear a single regex deny, which
|
|
41
|
-
* is the entire feature. A capture that works is worth more than a forgery an
|
|
42
|
-
* agent can only perform once it is already executing commands.
|
|
43
|
-
*
|
|
44
|
-
* WHAT CONSENT CAN AND CANNOT DO. A recorded prompt can only ever clear a
|
|
45
|
-
* policy that is already marked reviewable (`effectiveAuthority` in
|
|
46
|
-
* policy-types.ts) and whose semantic policy sets `userCanOverride`. A hard
|
|
47
|
-
* policy is never cleared by anything Jev says, so a forged prompt can never
|
|
48
|
-
* turn a HARD deny into an allow, and skipping the hook entirely gains an
|
|
49
|
-
* agent nothing, since the harness invokes failproofai for the tool call
|
|
50
|
-
* independently.
|
|
51
|
-
*
|
|
52
|
-
* What it CAN do is worth stating exactly, because "it only stops a nudge" is
|
|
53
|
-
* not true: of the seven reviewable builtins (policy-catalog.ts), FOUR are
|
|
54
|
-
* blocking denies — `block-read-outside-cwd`, `protect-env-vars`,
|
|
55
|
-
* `block-env-files` and `block-work-on-main` — and only `warn-git-amend`,
|
|
56
|
-
* `warn-destructive-sql` and `warn-global-package-install` are nudges. So a
|
|
57
|
-
* forged consent can turn a real deny into an allow on reading `.env` files,
|
|
58
|
-
* reading outside the project, printing environment secrets, or committing on
|
|
59
|
-
* a protected branch. That is the size of the trade, and it is the reason the
|
|
60
|
-
* hard floor is where the catastrophic cases live.
|
|
61
|
-
*
|
|
62
|
-
* WHAT IS STILL REFUSED, because it is cheap and an agent cannot forge it by
|
|
63
|
-
* asking: a turn the harness's own payload marks as machine-submitted (a cron
|
|
64
|
-
* or loop wakeup, an injected system turn, another extension's message, an
|
|
65
|
-
* OpenClaw heartbeat), a payload naming a sub-agent (`agent_id`), a session id
|
|
66
|
-
* that is not a plain name, an event that is not the prompt-submit one, and
|
|
67
|
-
* text that is nothing but harness wrapping — failproofai's own stop-gate
|
|
68
|
-
* words among it, which some harnesses feed back as the next user turn.
|
|
69
|
-
*
|
|
70
|
-
* - Replay: the eval harness reads historical transcripts, where human
|
|
71
|
-
* messages are the non-meta `user` entries whose content is text rather than
|
|
72
|
-
* a tool result.
|
|
73
|
-
*
|
|
74
|
-
* Alongside each prompt, `captureIntent` snapshots the agent's last visible
|
|
75
|
-
* message from the transcript at that moment, so a reply like "yes, do it" can
|
|
76
|
-
* be understood. The agent wrote that message: it is sent to Jev labelled as
|
|
77
|
-
* agent-written and is never consent on its own. (One caveat worth knowing, in
|
|
78
|
-
* a consumer rather than here: decide v1 lets that message satisfy the
|
|
79
|
-
* deterministic "did the user name this target" check, so an agent that writes
|
|
80
|
-
* its own transcript can supply the target name — see decide.ts and the docs'
|
|
81
|
-
* Known limits.)
|
|
82
|
-
*
|
|
83
|
-
* Text inside a tool call claiming "the user approved this" is never consulted.
|
|
84
|
-
*/
|
|
85
|
-
import {
|
|
86
|
-
chmodSync,
|
|
87
|
-
closeSync,
|
|
88
|
-
mkdirSync,
|
|
89
|
-
openSync,
|
|
90
|
-
readdirSync,
|
|
91
|
-
readFileSync,
|
|
92
|
-
readSync,
|
|
93
|
-
renameSync,
|
|
94
|
-
statSync,
|
|
95
|
-
unlinkSync,
|
|
96
|
-
writeFileSync,
|
|
97
|
-
} from "node:fs";
|
|
98
|
-
import { dirname, resolve } from "node:path";
|
|
99
|
-
import { failproofaiHome, semanticDir } from "../fp-home";
|
|
100
|
-
import type { IntegrationType } from "../types";
|
|
101
|
-
import { MAX_USER_MESSAGE_CHARS, redactSecrets } from "./envelope";
|
|
102
|
-
|
|
103
|
-
export const MAX_RECORDED_PROMPTS = 5;
|
|
104
|
-
/** Older than this and a prompt no longer describes what the agent is doing. */
|
|
105
|
-
export const INTENT_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
|
106
|
-
/** A prompt stamped further in the future than this was not written by us. */
|
|
107
|
-
const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
|
|
108
|
-
|
|
109
|
-
const SESSION_ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
110
|
-
|
|
111
|
-
const sessionsDir = (): string => resolve(semanticDir(), "sessions");
|
|
112
|
-
const intentFile = (sessionId: string): string => resolve(sessionsDir(), `${sessionId}.json`);
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Group- or world-WRITE bits on a directory the store sits under. Anyone who
|
|
116
|
-
* can write to one can rename it away and leave their own in its place, and
|
|
117
|
-
* every check on the file inside then passes — so the file's own 0600 proves
|
|
118
|
-
* nothing about who wrote it. Read bits are deliberately not included: a 0755
|
|
119
|
-
* directory is ordinary and gives nobody that power.
|
|
120
|
-
*
|
|
121
|
-
* Exactly `jev-config.ts`'s `DIR_WRITABLE_BY_OTHERS`, for exactly its reason.
|
|
122
|
-
* The config refuses a `jev.json` whose DIRECTORY is loose; this is the same
|
|
123
|
-
* check on the store that answers "did the human ask for this", which is the
|
|
124
|
-
* one input that can clear a reviewable deny.
|
|
125
|
-
*/
|
|
126
|
-
const DIR_WRITABLE_BY_OTHERS = 0o022;
|
|
127
|
-
|
|
128
|
-
/** Whether permission bits can be trusted to mean anything here. */
|
|
129
|
-
function modesAreMeaningful(): boolean {
|
|
130
|
-
// On Windows `stat().mode` is synthesized, so the check would refuse every
|
|
131
|
-
// read without protecting anything (`jev-config.ts` says the same).
|
|
132
|
-
return process.platform !== "win32";
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* The directories the session files sit under, innermost first, up to and
|
|
137
|
-
* including the failproofai home. Every one of them is ours by construction —
|
|
138
|
-
* `state/`, `state/semantic/` and its `sessions/` — but only the two this
|
|
139
|
-
* module creates are created 0700; `state/` is the daemon's
|
|
140
|
-
* (`fpai-collect`'s `create_dir_all`) and arrives at the umask, which on a
|
|
141
|
-
* umask-002 box is 0775.
|
|
142
|
-
*/
|
|
143
|
-
function storeDirChain(): string[] {
|
|
144
|
-
const home = resolve(failproofaiHome());
|
|
145
|
-
const chain: string[] = [];
|
|
146
|
-
let dir = sessionsDir();
|
|
147
|
-
// Bounded: the chain is four deep, and a home that is not an ancestor (a
|
|
148
|
-
// layout change, a symlink) must not walk to the filesystem root.
|
|
149
|
-
for (let i = 0; i < 8; i++) {
|
|
150
|
-
chain.push(dir);
|
|
151
|
-
if (dir === home) break;
|
|
152
|
-
const parent = dirname(dir);
|
|
153
|
-
if (parent === dir) break;
|
|
154
|
-
dir = parent;
|
|
155
|
-
}
|
|
156
|
-
return chain;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* The first directory under the failproofai home that someone else can write
|
|
161
|
-
* to and we cannot fix, or null when the store's whole path is ours alone.
|
|
162
|
-
*
|
|
163
|
-
* Loose bits are TIGHTENED before they are refused, because on an ordinary
|
|
164
|
-
* umask-002 machine `~/.failproofai/state` is 0775 the moment the daemon
|
|
165
|
-
* creates it, and refusing that would switch the clearing half of the
|
|
166
|
-
* evaluator off for most Linux users with nothing said. Taking the write bits
|
|
167
|
-
* off is the same remedy `failproofai jev setup` applies to the home, it only
|
|
168
|
-
* ever removes access, and it only touches directories inside failproofai's
|
|
169
|
-
* own home. What survives it — a directory we do not own — is the case the
|
|
170
|
-
* check exists for: another local user who can rename `semantic/` away and
|
|
171
|
-
* leave their own `sessions/<id>.json`, whose prompts would be read as the
|
|
172
|
-
* human's and could clear the four reviewable BLOCKING policies.
|
|
173
|
-
*
|
|
174
|
-
* Exported so `failproofai jev status` can report it the way it reports a
|
|
175
|
-
* refused config.
|
|
176
|
-
*/
|
|
177
|
-
export function looseIntentStoreDir(): string | null {
|
|
178
|
-
if (!modesAreMeaningful()) return null;
|
|
179
|
-
for (const dir of storeDirChain()) {
|
|
180
|
-
let mode: number;
|
|
181
|
-
try {
|
|
182
|
-
mode = statSync(dir).mode & 0o777;
|
|
183
|
-
} catch {
|
|
184
|
-
// Not there — nothing can be read out of it either — or not ours to
|
|
185
|
-
// stat, which the read itself then reports.
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
if ((mode & DIR_WRITABLE_BY_OTHERS) === 0) continue;
|
|
189
|
-
try {
|
|
190
|
-
chmodSync(dir, mode & ~DIR_WRITABLE_BY_OTHERS);
|
|
191
|
-
if ((statSync(dir).mode & DIR_WRITABLE_BY_OTHERS) !== 0) return dir;
|
|
192
|
-
} catch {
|
|
193
|
-
return dir;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
interface RecordedPrompt {
|
|
200
|
-
at: number;
|
|
201
|
-
text: string;
|
|
202
|
-
/** The agent's last visible message when this prompt was submitted. */
|
|
203
|
-
agent?: string | null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
interface IntentFile {
|
|
207
|
-
prompts: RecordedPrompt[];
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function readIntentFile(sessionId: string): IntentFile {
|
|
211
|
-
try {
|
|
212
|
-
const parsed = JSON.parse(readFileSync(intentFile(sessionId), "utf8")) as IntentFile;
|
|
213
|
-
return Array.isArray(parsed?.prompts) ? parsed : { prompts: [] };
|
|
214
|
-
} catch {
|
|
215
|
-
return { prompts: [] };
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Append one prompt to the session's file, the newest `MAX_RECORDED_PROMPTS`
|
|
221
|
-
* kept. Atomic, 0600 file in a 0700 directory. Returns false instead of
|
|
222
|
-
* throwing: losing intent only means no override.
|
|
223
|
-
*
|
|
224
|
-
* A prompt identical to the one just recorded REPLACES it instead of being
|
|
225
|
-
* appended, so a harness that fires its prompt event more than once for the
|
|
226
|
-
* same message — OpenCode's `message.updated` fires on every update of it —
|
|
227
|
-
* cannot push the rest of the session's task out of a five-slot window with
|
|
228
|
-
* copies of one line. The replacement carries the new timestamp and the new
|
|
229
|
-
* agent snapshot, so the entry stays the latest thing the human said.
|
|
230
|
-
*/
|
|
231
|
-
function appendPrompt(sessionId: string, entry: RecordedPrompt): boolean {
|
|
232
|
-
try {
|
|
233
|
-
const file = readIntentFile(sessionId);
|
|
234
|
-
const last = file.prompts[file.prompts.length - 1];
|
|
235
|
-
const kept = last && last.text === entry.text ? file.prompts.slice(0, -1) : file.prompts;
|
|
236
|
-
file.prompts = [...kept, entry].slice(-MAX_RECORDED_PROMPTS);
|
|
237
|
-
const dir = sessionsDir();
|
|
238
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
239
|
-
const target = intentFile(sessionId);
|
|
240
|
-
const isNew = !fileExists(target);
|
|
241
|
-
const tmp = `${target}.${process.pid}.tmp`;
|
|
242
|
-
writeFileSync(tmp, JSON.stringify(file), { mode: 0o600 });
|
|
243
|
-
renameSync(tmp, target);
|
|
244
|
-
// Once per session, not per prompt: sweep the files no read can use.
|
|
245
|
-
if (isNew) pruneExpiredSessions(entry.at);
|
|
246
|
-
return true;
|
|
247
|
-
} catch {
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function fileExists(path: string): boolean {
|
|
253
|
-
try {
|
|
254
|
-
statSync(path);
|
|
255
|
-
return true;
|
|
256
|
-
} catch {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const SESSION_FILE_RE = /^[A-Za-z0-9._-]{1,128}\.json(?:\.\d+\.tmp)?$/;
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Delete session files last written longer ago than `INTENT_MAX_AGE_MS`. A
|
|
265
|
-
* file's mtime is its newest recorded prompt's, so every prompt in such a
|
|
266
|
-
* file is already outside the window every read filters by and can never be
|
|
267
|
-
* read again. Removing it loses nothing and keeps one-file-per-session from
|
|
268
|
-
* growing without bound.
|
|
269
|
-
*/
|
|
270
|
-
export function pruneExpiredSessions(now: number = Date.now()): number {
|
|
271
|
-
let removed = 0;
|
|
272
|
-
try {
|
|
273
|
-
const dir = sessionsDir();
|
|
274
|
-
for (const name of readdirSync(dir)) {
|
|
275
|
-
if (!SESSION_FILE_RE.test(name)) continue;
|
|
276
|
-
const path = resolve(dir, name);
|
|
277
|
-
try {
|
|
278
|
-
const st = statSync(path);
|
|
279
|
-
if (st.isFile() && now - st.mtimeMs > INTENT_MAX_AGE_MS) {
|
|
280
|
-
unlinkSync(path);
|
|
281
|
-
removed++;
|
|
282
|
-
}
|
|
283
|
-
} catch {
|
|
284
|
-
// Raced with another writer or already gone: nothing to do.
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
} catch {
|
|
288
|
-
// No directory yet.
|
|
289
|
-
}
|
|
290
|
-
return removed;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Store one cleaned prompt, with the agent message it replies to. Module-
|
|
295
|
-
* private on purpose: `captureIntent` is the only door into `user_said`, and
|
|
296
|
-
* every origin check lives on the other side of it. An exported writer that
|
|
297
|
-
* took a bare string would be a second door with no check on it at all, which
|
|
298
|
-
* is what this used to be (`recordUserPrompt`, deleted 2026-09-23).
|
|
299
|
-
*
|
|
300
|
-
* Redacted before it is capped, like everything stored here, so a cut can never
|
|
301
|
-
* leave half a secret the patterns no longer match. Never throws: losing intent
|
|
302
|
-
* only means no override.
|
|
303
|
-
*/
|
|
304
|
-
function recordPrompt(sessionId: string, prompt: string, agent: string | null, now: number): boolean {
|
|
305
|
-
if (!SESSION_ID_RE.test(sessionId)) return false;
|
|
306
|
-
if (prompt.trim().length === 0) return false;
|
|
307
|
-
try {
|
|
308
|
-
return appendPrompt(sessionId, { at: now, text: storable(prompt.trim()), agent: agent === null ? null : storable(agent) });
|
|
309
|
-
} catch {
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* The session's unexpired prompts, oldest first. Tolerates a hand-edited file.
|
|
316
|
-
*
|
|
317
|
-
* Nothing is read out of a store whose path someone else can write to
|
|
318
|
-
* (`looseIntentStoreDir`). Returning none is the fail-closed answer: Jev is
|
|
319
|
-
* still asked, and judges with no stated intent, so no reviewable deny is
|
|
320
|
-
* cleared on a prompt we cannot say is the owner's.
|
|
321
|
-
*/
|
|
322
|
-
function livePrompts(sessionId: string | undefined, now: number): RecordedPrompt[] {
|
|
323
|
-
if (!sessionId || !SESSION_ID_RE.test(sessionId)) return [];
|
|
324
|
-
if (looseIntentStoreDir() !== null) return [];
|
|
325
|
-
return readIntentFile(sessionId).prompts.filter(
|
|
326
|
-
(p) =>
|
|
327
|
-
typeof p?.text === "string" &&
|
|
328
|
-
typeof p.at === "number" &&
|
|
329
|
-
now - p.at <= INTENT_MAX_AGE_MS &&
|
|
330
|
-
p.at - now <= MAX_CLOCK_SKEW_MS,
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ── Transcript replay ────────────────────────────────────────────────────────
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Harness-generated user entries: slash-command echoes, caveats,
|
|
338
|
-
* notifications — and failproofai's own words. Cursor submits a Stop gate's
|
|
339
|
-
* `followup_message` as the next user message, and Copilot, Devin and
|
|
340
|
-
* OpenClaw feed a Stop block's reason back into the next turn, so what
|
|
341
|
-
* policy-evaluator.ts wrote can arrive looking like a prompt.
|
|
342
|
-
*
|
|
343
|
-
* Also the wrappers Claude Code puts around a user-role turn that another
|
|
344
|
-
* agent or session wrote (a peer session, a teammate, a coordinator, a
|
|
345
|
-
* channel): its own classifier calls those "never user intent".
|
|
346
|
-
*/
|
|
347
|
-
const NON_HUMAN_PREFIXES = [
|
|
348
|
-
"<local-command-caveat>",
|
|
349
|
-
"<local-command-stdout>",
|
|
350
|
-
"<local-command-stderr>",
|
|
351
|
-
"<task-notification>",
|
|
352
|
-
"<system-reminder>",
|
|
353
|
-
"[Request interrupted",
|
|
354
|
-
"MANDATORY ACTION REQUIRED from failproofai",
|
|
355
|
-
"Instruction from failproofai:",
|
|
356
|
-
"<cross-session-message",
|
|
357
|
-
"<teammate-message",
|
|
358
|
-
"<agent-message",
|
|
359
|
-
"<coordinator-relay",
|
|
360
|
-
"<channel source=",
|
|
361
|
-
];
|
|
362
|
-
|
|
363
|
-
// ── Tag blocks, in linear time ───────────────────────────────────────────────
|
|
364
|
-
//
|
|
365
|
-
// A prompt can be megabytes of pasted text, and it is cleaned on the hook path
|
|
366
|
-
// before anything caps it. A lazy regex such as /<x>[\s\S]*?<\/x>/g rescans to
|
|
367
|
-
// the end of the text from every opener that has no closer, so its cost grows
|
|
368
|
-
// with the square of the number of unclosed openers: 1 MB of them took over
|
|
369
|
-
// 20 s, long enough for the daemon client to give up and deny every hook on
|
|
370
|
-
// the machine. These helpers find exactly the regex's matches with indexOf:
|
|
371
|
-
// once an opener has no closer after it, no later opener can have one either,
|
|
372
|
-
// so the scan stops there.
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Replace every block the lazy regex `/<tag>([\s\S]*?)<\/tag>/g` matches —
|
|
376
|
-
* with `attributes`, `/<tag[^>]*>([\s\S]*?)<\/tag[^>]*>/g` — by
|
|
377
|
-
* `replace(inner)`. Same matches, same order, linear time.
|
|
378
|
-
*/
|
|
379
|
-
function replaceTagBlocks(text: string, tag: string, replace: (inner: string) => string, attributes = false): string {
|
|
380
|
-
const open = attributes ? `<${tag}` : `<${tag}>`;
|
|
381
|
-
const close = attributes ? `</${tag}` : `</${tag}>`;
|
|
382
|
-
let out = "";
|
|
383
|
-
let from = 0;
|
|
384
|
-
for (;;) {
|
|
385
|
-
const at = text.indexOf(open, from);
|
|
386
|
-
if (at < 0) break;
|
|
387
|
-
let inner = at + open.length;
|
|
388
|
-
if (attributes) {
|
|
389
|
-
const gt = text.indexOf(">", inner);
|
|
390
|
-
if (gt < 0) break;
|
|
391
|
-
inner = gt + 1;
|
|
392
|
-
}
|
|
393
|
-
const closeAt = text.indexOf(close, inner);
|
|
394
|
-
if (closeAt < 0) break;
|
|
395
|
-
let end = closeAt + close.length;
|
|
396
|
-
if (attributes) {
|
|
397
|
-
const gt = text.indexOf(">", end);
|
|
398
|
-
if (gt < 0) break;
|
|
399
|
-
end = gt + 1;
|
|
400
|
-
}
|
|
401
|
-
out += text.slice(from, at) + replace(text.slice(inner, closeAt));
|
|
402
|
-
from = end;
|
|
403
|
-
}
|
|
404
|
-
return from === 0 ? text : out + text.slice(from);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/** The inner text of the first block `/<tag>([\s\S]*?)<\/tag>/` matches, in linear time. */
|
|
408
|
-
function firstTagContent(text: string, tag: string): string | undefined {
|
|
409
|
-
const open = `<${tag}>`;
|
|
410
|
-
const at = text.indexOf(open);
|
|
411
|
-
if (at < 0) return undefined;
|
|
412
|
-
const closeAt = text.indexOf(`</${tag}>`, at + open.length);
|
|
413
|
-
return closeAt < 0 ? undefined : text.slice(at + open.length, closeAt);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function stripHarnessMarkup(text: string): string {
|
|
417
|
-
const noReminders = replaceTagBlocks(text, "system-reminder", () => "");
|
|
418
|
-
return replaceTagBlocks(noReminders, "pasted_content", () => "[pasted content]", true).trim();
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* The human-typed text of a transcript entry, or null if the entry is not a
|
|
423
|
-
* human message. Slash commands count only through their typed arguments.
|
|
424
|
-
*/
|
|
425
|
-
export function humanMessageText(entry: unknown): string | null {
|
|
426
|
-
const e = entry as {
|
|
427
|
-
type?: string;
|
|
428
|
-
isMeta?: boolean;
|
|
429
|
-
isSidechain?: boolean;
|
|
430
|
-
message?: { role?: string; content?: unknown };
|
|
431
|
-
};
|
|
432
|
-
if (e?.type !== "user" || e.isMeta || e.isSidechain) return null;
|
|
433
|
-
const content = e.message?.content;
|
|
434
|
-
let text: string;
|
|
435
|
-
if (typeof content === "string") {
|
|
436
|
-
text = content;
|
|
437
|
-
} else if (Array.isArray(content)) {
|
|
438
|
-
if (content.some((b) => (b as { type?: string })?.type === "tool_result")) return null;
|
|
439
|
-
text = content
|
|
440
|
-
.filter((b) => (b as { type?: string })?.type === "text")
|
|
441
|
-
.map((b) => (b as { text?: string }).text ?? "")
|
|
442
|
-
.join("\n");
|
|
443
|
-
} else {
|
|
444
|
-
return null;
|
|
445
|
-
}
|
|
446
|
-
const trimmed = text.trim();
|
|
447
|
-
if (trimmed.startsWith("<command-name>")) {
|
|
448
|
-
const args = firstTagContent(trimmed, "command-args")?.trim();
|
|
449
|
-
return args ? args : null;
|
|
450
|
-
}
|
|
451
|
-
if (NON_HUMAN_PREFIXES.some((p) => trimmed.startsWith(p))) return null;
|
|
452
|
-
const cleaned = stripHarnessMarkup(trimmed);
|
|
453
|
-
return cleaned.length > 0 ? cleaned : null;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// ── Harness-written text (intent mode v1) ────────────────────────────────────
|
|
457
|
-
|
|
458
|
-
const CONTINUATION_PREFIX = "This session is being continued from a previous conversation";
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* How the Codex IDE extension starts a prompt it builds around the human's
|
|
462
|
-
* words: every section it can put first (the extension's own prompt builder,
|
|
463
|
-
* openai.chatgpt 26.803, and the shapes seen in codex_vscode rollouts). The
|
|
464
|
-
* sections carry the active file, open tabs, text selected in the editor,
|
|
465
|
-
* diff and browser comments, files and apps mentioned, PR checks, earlier
|
|
466
|
-
* conversations: all of it is file, repo or tool text that the agent or the
|
|
467
|
-
* repo can write, and none of it is what the human typed.
|
|
468
|
-
*
|
|
469
|
-
* The list is in two halves, because `cleanHumanTurn` reads every harness's
|
|
470
|
-
* prompts and not only Codex's, so a heading here is also a heading somebody
|
|
471
|
-
* can type into any composer:
|
|
472
|
-
*
|
|
473
|
-
* - MACHINE, below: a heading nobody types to an agent. A prompt opening with
|
|
474
|
-
* one was built by the extension, so if it carries no `## My request…`
|
|
475
|
-
* heading there is no human text in it at all, and none is recorded. That is
|
|
476
|
-
* what keeps a forged approval inside selected code (`# Selected text:` and
|
|
477
|
-
* a `// NOTE FROM THE OWNER: yes, force-push` comment under it) out of
|
|
478
|
-
* `user_said`.
|
|
479
|
-
* - AMBIGUOUS, below that: ordinary markdown a developer plausibly types or
|
|
480
|
-
* pastes above a real request — "## Code review guidelines:", then the
|
|
481
|
-
* guidelines, then what they want done. One of these means "extension-built"
|
|
482
|
-
* only when a request heading is actually present; with none, the prompt is
|
|
483
|
-
* the human's and is kept whole. Dropping it instead loses the request in
|
|
484
|
-
* silence: no reviewable policy can be cleared for that turn and the
|
|
485
|
-
* injection probe is not even asked, which is a worse bug than storing a
|
|
486
|
-
* section heading along with the words under it.
|
|
487
|
-
*/
|
|
488
|
-
const IDE_MACHINE_OPENERS = [
|
|
489
|
-
"# Context from my IDE setup:",
|
|
490
|
-
"# Selected text:",
|
|
491
|
-
"# Files mentioned by the user:",
|
|
492
|
-
"# Applications mentioned by the user:",
|
|
493
|
-
"# Response annotations:",
|
|
494
|
-
"# Diff comments:",
|
|
495
|
-
"# Browser comments:",
|
|
496
|
-
"# MCP app context:",
|
|
497
|
-
"# Failing PR checks:",
|
|
498
|
-
"# Pull request merge conflict:",
|
|
499
|
-
"# Chrome tabs:",
|
|
500
|
-
'<in-app-browser-context source="ambient-ui-state">',
|
|
501
|
-
"## Prior conversation with Codex:",
|
|
502
|
-
"## Referenced chats with Codex:",
|
|
503
|
-
"## Referenced ChatGPT conversation:",
|
|
504
|
-
"The attached pasted text file(s) contain the user's request.",
|
|
505
|
-
];
|
|
506
|
-
const IDE_AMBIGUOUS_OPENERS = [
|
|
507
|
-
"# In app browser:",
|
|
508
|
-
"## Code review guidelines:",
|
|
509
|
-
"## Pull request fix:",
|
|
510
|
-
"## Pull request merge task:",
|
|
511
|
-
"## Auto resolve merge:",
|
|
512
|
-
];
|
|
513
|
-
/**
|
|
514
|
-
* Which half of the opener list a turn starts with, or null when it starts
|
|
515
|
-
* with neither: `"machine"` means the extension built this prompt, and
|
|
516
|
-
* `"ambiguous"` means it did only if a request heading follows.
|
|
517
|
-
*/
|
|
518
|
-
function ideOpenerKind(text: string): "machine" | "ambiguous" | null {
|
|
519
|
-
if (IDE_MACHINE_OPENERS.some((p) => text.startsWith(p))) return "machine";
|
|
520
|
-
if (IDE_AMBIGUOUS_OPENERS.some((p) => text.startsWith(p))) return "ambiguous";
|
|
521
|
-
return null;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* The heading the extension puts right before the human's words, in both
|
|
526
|
-
* spellings: older builds wrote "for Codex", 26.803 writes `## My request:`.
|
|
527
|
-
* The extension itself shows the text after the last one as the message.
|
|
528
|
-
*/
|
|
529
|
-
const IDE_REQUEST_HEADINGS = ["## My request for Codex:", "## My request:"];
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* The human's request from a prompt the Codex IDE extension built, or null
|
|
533
|
-
* when there is none: the text after the LAST request heading, since the
|
|
534
|
-
* human's words come last and a selection above them can contain the heading.
|
|
535
|
-
*/
|
|
536
|
-
function ideRequest(text: string): string | null {
|
|
537
|
-
let at = -1;
|
|
538
|
-
let heading = "";
|
|
539
|
-
for (const h of IDE_REQUEST_HEADINGS) {
|
|
540
|
-
const i = text.lastIndexOf(h);
|
|
541
|
-
if (i > at) {
|
|
542
|
-
at = i;
|
|
543
|
-
heading = h;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
return at < 0 ? null : text.slice(at + heading.length).trim();
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Whether a turn is text a harness, a tool or another agent wrote: the
|
|
551
|
-
* session-continuation summary, or anything opening with a
|
|
552
|
-
* `NON_HUMAN_PREFIXES` marker (failproofai's own words among them).
|
|
553
|
-
*
|
|
554
|
-
* Asked of a whole turn, and again of every span pulled out of one — the text
|
|
555
|
-
* after a Codex IDE prompt's request heading is a span the repo, an extension
|
|
556
|
-
* or a Stop gate's follow-up can land in, so it has to pass the same test the
|
|
557
|
-
* turn did rather than inherit its answer.
|
|
558
|
-
*/
|
|
559
|
-
function harnessAuthored(text: string): boolean {
|
|
560
|
-
return text.startsWith(CONTINUATION_PREFIX) || NON_HUMAN_PREFIXES.some((p) => text.startsWith(p));
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* The part of one "user" turn the human actually typed, or null when none of
|
|
565
|
-
* it is theirs.
|
|
566
|
-
*
|
|
567
|
-
* Harnesses deliver more than the human's words in a user turn, and every
|
|
568
|
-
* extra is written by something other than the human: Codex's IDE extension
|
|
569
|
-
* puts the active file, open tabs, selected text and more (see
|
|
570
|
-
* `IDE_MACHINE_OPENERS`) before the `## My request…:` heading that introduces
|
|
571
|
-
* the human's words, Claude Code files its
|
|
572
|
-
* session-continuation summary as a user turn, reminders arrive in
|
|
573
|
-
* `<system-reminder>` blocks, and a slash command carries the command's own
|
|
574
|
-
* instructions. The task is what the human typed, so only that is kept: a
|
|
575
|
-
* slash command counts as the command and arguments they typed, never the
|
|
576
|
-
* body the harness expanded it into.
|
|
577
|
-
*
|
|
578
|
-
* Everything here runs on every harness's prompts, so a rule that drops a
|
|
579
|
-
* whole turn has to be one no human's turn can match: an extension's section
|
|
580
|
-
* heading that somebody might also type is an `IDE_AMBIGUOUS_OPENERS` entry
|
|
581
|
-
* and never drops a prompt on its own.
|
|
582
|
-
*/
|
|
583
|
-
export function cleanHumanTurn(raw: string): string | null {
|
|
584
|
-
// Reminders first: one can precede the human's words in the same turn, and
|
|
585
|
-
// a turn that merely starts with a reminder is not therefore machine-written.
|
|
586
|
-
// Every step is linear in the prompt's length (see replaceTagBlocks): this
|
|
587
|
-
// runs on the hook path, on the whole prompt, before anything caps it.
|
|
588
|
-
let text = replaceTagBlocks(raw, "system-reminder", () => "").trim();
|
|
589
|
-
if (!text) return null;
|
|
590
|
-
if (harnessAuthored(text)) return null;
|
|
591
|
-
// Unwrap an extension-built prompt, then judge what came out exactly as the
|
|
592
|
-
// turn itself was judged. The request is whatever follows the last heading,
|
|
593
|
-
// and the extension appends its sections around text it did not write: a
|
|
594
|
-
// Stop gate's follow-up, a continuation summary, a peer session's message or
|
|
595
|
-
// another context section can all land there. A prompt that opens with a
|
|
596
|
-
// machine section and has no request heading is all machine text and is
|
|
597
|
-
// dropped; one that opens with an ambiguous heading and has none is somebody
|
|
598
|
-
// typing markdown, and is kept whole (see the opener lists). The loop ends
|
|
599
|
-
// on its second pass at the latest — `ideRequest` takes the LAST heading, so
|
|
600
|
-
// no request heading is left in what it returns, which also means the
|
|
601
|
-
// ambiguous branch cannot run twice — and each pass shortens the text.
|
|
602
|
-
for (let pass = 0; ; pass++) {
|
|
603
|
-
const kind = ideOpenerKind(text);
|
|
604
|
-
if (kind === null) break;
|
|
605
|
-
const request = ideRequest(text);
|
|
606
|
-
if (request === null) {
|
|
607
|
-
// An ambiguous heading is somebody's own markdown only at the top of a
|
|
608
|
-
// turn. Once a pass has established that this prompt WAS built by the
|
|
609
|
-
// extension, a heading of either kind in what came out of it is another
|
|
610
|
-
// of the extension's sections landing where the request goes, and the
|
|
611
|
-
// prompt is dropped exactly as it was before this split.
|
|
612
|
-
if (kind === "ambiguous" && pass === 0) break;
|
|
613
|
-
return null;
|
|
614
|
-
}
|
|
615
|
-
if (harnessAuthored(request)) return null;
|
|
616
|
-
text = request;
|
|
617
|
-
}
|
|
618
|
-
if (/^<command-(?:name|message)>/.test(text)) {
|
|
619
|
-
const name = firstTagContent(text, "command-name")?.trim() ?? "";
|
|
620
|
-
const args = firstTagContent(text, "command-args")?.trim() ?? "";
|
|
621
|
-
const typed = `${name} ${args}`.trim();
|
|
622
|
-
return typed.length > 0 ? typed : null;
|
|
623
|
-
}
|
|
624
|
-
text = replaceTagBlocks(text, "pasted_content", (inner) => `[pasted by the human]\n${inner}\n[end of pasted text]`, true).trim();
|
|
625
|
-
return text.length > 0 ? text : null;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/** Every human turn cleaned, harness-only turns dropped, order kept. */
|
|
629
|
-
export function cleanUserSaid(userSaid: ReadonlyArray<string>): string[] {
|
|
630
|
-
return userSaid.map(cleanHumanTurn).filter((t): t is string => t !== null);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/** Text blocks of a content array, in every spelling the harnesses use. */
|
|
634
|
-
function textOfContent(content: unknown): string | null {
|
|
635
|
-
if (typeof content === "string") return content.trim() || null;
|
|
636
|
-
if (!Array.isArray(content)) return null;
|
|
637
|
-
const text = content
|
|
638
|
-
.filter((b) => {
|
|
639
|
-
const type = (b as { type?: string })?.type;
|
|
640
|
-
return (type === "text" || type === "Text" || type === "output_text") && typeof (b as { text?: unknown }).text === "string";
|
|
641
|
-
})
|
|
642
|
-
.map((b) => (b as { text: string }).text)
|
|
643
|
-
.join("\n")
|
|
644
|
-
.trim();
|
|
645
|
-
return text.length > 0 ? text : null;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* The visible text of an assistant transcript entry, or null. Recognises the
|
|
650
|
-
* transcript formats of the harnesses whose prompts are recorded:
|
|
651
|
-
*
|
|
652
|
-
* - Claude Code: `{type:"assistant", message:{content:[{type:"text"}…]}}`,
|
|
653
|
-
* skipping sidechains and the harness's own `<synthetic>` / API-error
|
|
654
|
-
* entries, which the model never wrote.
|
|
655
|
-
* - Codex rollouts: `event_msg` `agent_message` (≤ 0.153), `event_msg`
|
|
656
|
-
* `item_completed` with an `AgentMessage` item (0.154+), and the
|
|
657
|
-
* `response_item` assistant message that accompanies either.
|
|
658
|
-
* - Pi, Factory and OpenClaw: `{type:"message", message:{role:"assistant"}}`.
|
|
659
|
-
* - Cursor agent transcripts: `{role:"assistant", message:{content}}`.
|
|
660
|
-
* - Copilot `events.jsonl`: `{type:"assistant.message", data:{content}}`.
|
|
661
|
-
*/
|
|
662
|
-
export function agentMessageText(entry: unknown): string | null {
|
|
663
|
-
const e = entry as {
|
|
664
|
-
type?: string;
|
|
665
|
-
role?: string;
|
|
666
|
-
isSidechain?: boolean;
|
|
667
|
-
isApiErrorMessage?: boolean;
|
|
668
|
-
message?: { role?: string; model?: string; content?: unknown };
|
|
669
|
-
payload?: { type?: string; role?: string; message?: unknown; content?: unknown; item?: { type?: string; content?: unknown } };
|
|
670
|
-
data?: { content?: unknown };
|
|
671
|
-
};
|
|
672
|
-
if (!e || typeof e !== "object") return null;
|
|
673
|
-
// Codex: the clean agent message is an event, like the human's.
|
|
674
|
-
if (e.type === "event_msg" && e.payload?.type === "agent_message" && typeof e.payload.message === "string") {
|
|
675
|
-
return e.payload.message.trim() || null;
|
|
676
|
-
}
|
|
677
|
-
if (e.type === "event_msg" && e.payload?.type === "item_completed" && e.payload.item?.type === "AgentMessage") {
|
|
678
|
-
return textOfContent(e.payload.item.content);
|
|
679
|
-
}
|
|
680
|
-
if (e.type === "response_item" && e.payload?.type === "message" && e.payload.role === "assistant") {
|
|
681
|
-
return textOfContent(e.payload.content);
|
|
682
|
-
}
|
|
683
|
-
if (e.type === "assistant.message") return textOfContent(e.data?.content);
|
|
684
|
-
if (e.type === "message" && e.message?.role === "assistant") return textOfContent(e.message.content);
|
|
685
|
-
if (e.type === undefined && e.role === "assistant") return textOfContent(e.message?.content);
|
|
686
|
-
if (e.type !== "assistant" || e.isSidechain) return null;
|
|
687
|
-
if (e.isApiErrorMessage === true || e.message?.model === "<synthetic>") return null;
|
|
688
|
-
return textOfContent(e.message?.content);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// ── Live capture (T4) ────────────────────────────────────────────────────────
|
|
692
|
-
|
|
693
|
-
const obj = (v: unknown): Record<string, unknown> | undefined =>
|
|
694
|
-
v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : undefined;
|
|
695
|
-
|
|
696
|
-
/**
|
|
697
|
-
* Where each harness delivers what the human typed, and what its own payload
|
|
698
|
-
* says about turns nobody typed. The source of truth for
|
|
699
|
-
* docs/reference/jev-intent.mdx (a test holds the two together).
|
|
700
|
-
*
|
|
701
|
-
* - `nativeEvent`: the harness's own name for its prompt-submit event, which
|
|
702
|
-
* the handler canonicalizes to `UserPromptSubmit`; null when there is none
|
|
703
|
-
* (Hermes).
|
|
704
|
-
* - `field`: the payload field carrying the text after `normalizeCliPayload`.
|
|
705
|
-
* `null` means this event delivers no human text at all — Hermes has no
|
|
706
|
-
* event, Antigravity's `PreInvocation` carries none — so nothing is ever
|
|
707
|
-
* recorded for it.
|
|
708
|
-
* - `machineTurn`: reads the payload and answers whether the harness's OWN
|
|
709
|
-
* marks say this particular turn was submitted by a machine rather than a
|
|
710
|
-
* person. `null` where the harness sends no such marks.
|
|
711
|
-
*
|
|
712
|
-
* `machineTurn` is a veto, never a requirement. Requiring a positive "a human
|
|
713
|
-
* typed this" mark is what round 9 shipped, and it recorded nothing anywhere,
|
|
714
|
-
* because no shipping harness sends such a field: capture that never fires
|
|
715
|
-
* clears no regex deny, which is the whole feature. So the rule is the user's
|
|
716
|
-
* decision of 2026-09-23 — trust the prompt the harness hands the hook — and
|
|
717
|
-
* the marks are used only in the direction they are reliable in. A heartbeat
|
|
718
|
-
* run, a cron wakeup or another extension's injected message is a turn no
|
|
719
|
-
* person submitted whoever is asking; an absent mark says nothing, and a
|
|
720
|
-
* present, positive one is not believed any harder than the event itself.
|
|
721
|
-
*
|
|
722
|
-
* What that costs is in the module header: a harness started headless with the
|
|
723
|
-
* prompt in argv (`claude -p`, `codex exec`, `copilot -p`, `cursor-agent -p`,
|
|
724
|
-
* `devin -p`, `droid -p`, `goose run -t`, `pi -p`) fires this event with the
|
|
725
|
-
* same payload as a person typing, so an agent with a shell can record a
|
|
726
|
-
* prompt for a child session it controls. Accepted: it only ever clears a
|
|
727
|
-
* reviewable policy, never a hard one.
|
|
728
|
-
*/
|
|
729
|
-
export interface PromptChannel {
|
|
730
|
-
nativeEvent: string | null;
|
|
731
|
-
field: string | null;
|
|
732
|
-
machineTurn: ((payload: Record<string, unknown>) => boolean) | null;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* Claude Code's `UserPromptSubmit.source` values for turns nobody submitted:
|
|
737
|
-
* `loop_wakeup` and `schedule_wakeup` (a `/loop`, a CronCreate or a routine
|
|
738
|
-
* firing, whose text was composed in an earlier turn), `poll_event`, and
|
|
739
|
-
* `system` (peer and channel messages, task notifications, auto-continuation).
|
|
740
|
-
* Those are not the human's current request whoever is asking, so they are
|
|
741
|
-
* refused.
|
|
742
|
-
*
|
|
743
|
-
* Every other value records, `user` (the interactive composer) and `sdk`
|
|
744
|
-
* (`claude -p` and the Agent SDK) alike. `sdk` is usually a person's own
|
|
745
|
-
* command line or their pipeline; it is also what an agent's own `claude -p`
|
|
746
|
-
* reports, which is the accepted risk stated in the module header, and exactly
|
|
747
|
-
* the same risk every other harness's `-p` carries with no field at all.
|
|
748
|
-
*
|
|
749
|
-
* The field is optional in Claude Code's own hook-input schema ("Payloads may
|
|
750
|
-
* omit it while the field rolls out") and 2.1.280 does not send it, so an
|
|
751
|
-
* absent `source` must record — requiring it is what emptied the feature.
|
|
752
|
-
*/
|
|
753
|
-
const CLAUDE_MACHINE_SOURCES: ReadonlySet<string> = new Set(["loop_wakeup", "schedule_wakeup", "poll_event", "system"]);
|
|
754
|
-
const claudeMachineTurn = (payload: Record<string, unknown>): boolean =>
|
|
755
|
-
typeof payload.source === "string" && CLAUDE_MACHINE_SOURCES.has(payload.source);
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* Pi's `InputEvent.source`, which pi-extension forwards as `input_source`:
|
|
759
|
-
* `interactive` (its editor, and `pi -p`), `rpc` (the program driving it), and
|
|
760
|
-
* `extension` — another extension calling `sendUserMessage()`, whose text can
|
|
761
|
-
* be model-written or repo-derived. Only `extension` is refused; a missing
|
|
762
|
-
* source records, since older bridges send none.
|
|
763
|
-
*/
|
|
764
|
-
const piMachineTurn = (payload: Record<string, unknown>): boolean =>
|
|
765
|
-
payload.input_source === "extension" || payload.source === "extension";
|
|
766
|
-
|
|
767
|
-
/**
|
|
768
|
-
* OpenClaw's run metadata, read only for what it rules out. `before_agent_run`
|
|
769
|
-
* fires for heartbeat, cron, memory and inter-session runs as well as for a
|
|
770
|
-
* chat message, and the plugin can say so: a `trigger` that is not `user`, an
|
|
771
|
-
* `inputProvenance.kind` that is not `external_user` (`inter_session` is
|
|
772
|
-
* another agent, `internal_system` is the gateway itself), or an explicit
|
|
773
|
-
* `senderIsOwner: false` on a shared channel. A mark that is absent — which is
|
|
774
|
-
* every mark the shipped plugin sends today — rules nothing out.
|
|
775
|
-
*/
|
|
776
|
-
const openclawMachineRun = (payload: Record<string, unknown>): boolean => {
|
|
777
|
-
const meta = obj(payload.openclaw);
|
|
778
|
-
if (!meta) return false;
|
|
779
|
-
if (typeof meta.trigger === "string" && meta.trigger !== "user") return true;
|
|
780
|
-
const kind = obj(meta.inputProvenance)?.kind;
|
|
781
|
-
if (typeof kind === "string" && kind !== "external_user") return true;
|
|
782
|
-
return meta.senderIsOwner === false;
|
|
783
|
-
};
|
|
784
|
-
|
|
785
|
-
export const PROMPT_CHANNELS: Readonly<Record<IntegrationType, PromptChannel>> = {
|
|
786
|
-
// The only harness that names the author at all; its machine values are
|
|
787
|
-
// refused and everything else — including an absent field — is recorded.
|
|
788
|
-
claude: { nativeEvent: "UserPromptSubmit", field: "prompt", machineTurn: claudeMachineTurn },
|
|
789
|
-
// Also fires inside sub-agent threads, which its payload does not mark; the
|
|
790
|
-
// IDE extension's context sections are stripped by `cleanHumanTurn`.
|
|
791
|
-
codex: { nativeEvent: "user_prompt_submit", field: "prompt", machineTurn: null },
|
|
792
|
-
// Marks nothing; it also runs in-process sidekick subagents.
|
|
793
|
-
copilot: { nativeEvent: "UserPromptSubmit", field: "prompt", machineTurn: null },
|
|
794
|
-
// Marks nothing. Its own transcripts wrap a query in `<user_query>`, and a
|
|
795
|
-
// payload carrying that form is unwrapped (`unwrapCursorQuery`).
|
|
796
|
-
cursor: { nativeEvent: "beforeSubmitPrompt", field: "prompt", machineTurn: null },
|
|
797
|
-
// Current OpenCode's Message has no parts, so the forwarded text is empty
|
|
798
|
-
// and nothing is recorded in practice. It also fires again on every update
|
|
799
|
-
// of the same message — `appendPrompt` collapses the repeats — for task-tool
|
|
800
|
-
// child sessions, and for failproofai's own instruct re-prompts, which
|
|
801
|
-
// `cleanHumanTurn` drops by their marker.
|
|
802
|
-
opencode: { nativeEvent: "message.updated", field: "prompt", machineTurn: null },
|
|
803
|
-
pi: { nativeEvent: "input", field: "prompt", machineTurn: piMachineTurn },
|
|
804
|
-
// pre_llm_call is handled inside the native plugin; nothing reaches the
|
|
805
|
-
// handler, so Hermes has no prompt channel to record from.
|
|
806
|
-
hermes: { nativeEvent: null, field: null, machineTurn: null },
|
|
807
|
-
openclaw: { nativeEvent: "before_agent_run", field: "prompt", machineTurn: openclawMachineRun },
|
|
808
|
-
// Claude-shaped payloads and transcripts; sends no `source`.
|
|
809
|
-
factory: { nativeEvent: "UserPromptSubmit", field: "prompt", machineTurn: null },
|
|
810
|
-
devin: { nativeEvent: "UserPromptSubmit", field: "prompt", machineTurn: null },
|
|
811
|
-
// PreInvocation fires before EVERY model call in a turn and carries no
|
|
812
|
-
// prompt text: there is no human text in it to record, on a human turn or
|
|
813
|
-
// any other.
|
|
814
|
-
antigravity: { nativeEvent: "PreInvocation", field: null, machineTurn: null },
|
|
815
|
-
// Its text is in `message`, not `prompt`. Goose also has a `delegate`
|
|
816
|
-
// subagent tool and a scheduler of its own, neither of which it marks.
|
|
817
|
-
goose: { nativeEvent: "UserPromptSubmit", field: "message", machineTurn: null },
|
|
818
|
-
};
|
|
819
|
-
|
|
820
|
-
/**
|
|
821
|
-
* What the handler (T3) passes for every canonical `UserPromptSubmit`:
|
|
822
|
-
* `captureIntent({ eventType, sessionId, transcriptPath, cli, payload: parsed })`.
|
|
823
|
-
*
|
|
824
|
-
* The payload is the whole stdin object, not just its `prompt`, and it is
|
|
825
|
-
* required: the text is in a different field on some harnesses (Goose sends
|
|
826
|
-
* `message`), and the marks that rule a turn out are elsewhere in the payload.
|
|
827
|
-
* The first draft of this contract (JEV-BUILD-PLAN §7) passed a `prompt` and
|
|
828
|
-
* no payload; that shape no longer compiles, on purpose. A payload-less call
|
|
829
|
-
* records nothing on every harness, which is silent and looks exactly like an
|
|
830
|
-
* ordinary turn with nothing to record, so the type is the only place the
|
|
831
|
-
* divergence can be noticed.
|
|
832
|
-
*/
|
|
833
|
-
export interface CaptureEvent {
|
|
834
|
-
/** Canonical event type; anything but `UserPromptSubmit` is ignored. */
|
|
835
|
-
eventType: string;
|
|
836
|
-
sessionId?: string;
|
|
837
|
-
/** Read only for the agent's last message. Never consulted for origin. */
|
|
838
|
-
transcriptPath?: string;
|
|
839
|
-
cli: string;
|
|
840
|
-
/** The stdin payload after `normalizeCliPayload`: the handler's `parsed`. */
|
|
841
|
-
payload: Record<string, unknown>;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function isKnownCli(cli: string): cli is IntegrationType {
|
|
845
|
-
return Object.prototype.hasOwnProperty.call(PROMPT_CHANNELS, cli);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
/**
|
|
849
|
-
* The raw prompt text the harness delivered with this prompt-submit event, or
|
|
850
|
-
* null when there is none to record. Five lines, each one a reason this event
|
|
851
|
-
* carries no human turn; everything else is the prompt, trusted as the header
|
|
852
|
-
* says.
|
|
853
|
-
*/
|
|
854
|
-
function humanPromptText(ev: CaptureEvent): string | null {
|
|
855
|
-
// A harness we do not know.
|
|
856
|
-
if (!isKnownCli(ev.cli)) return null;
|
|
857
|
-
const { field, machineTurn } = PROMPT_CHANNELS[ev.cli];
|
|
858
|
-
// This harness's prompt event carries no human text (Hermes, Antigravity).
|
|
859
|
-
if (field === null) return null;
|
|
860
|
-
// The payload is where the text and the marks both are.
|
|
861
|
-
const payload = obj(ev.payload);
|
|
862
|
-
if (!payload) return null;
|
|
863
|
-
// A payload naming a sub-agent is the agent prompting itself. Claude-shaped
|
|
864
|
-
// harnesses put that at the top level; a harness with its own spelling for
|
|
865
|
-
// it is a gap, not a check this can make (see the docs' Known limits).
|
|
866
|
-
if (payload.agent_id !== undefined) return null;
|
|
867
|
-
// The harness's own marks say no person submitted this turn.
|
|
868
|
-
if (machineTurn?.(payload) === true) return null;
|
|
869
|
-
const raw = payload[field];
|
|
870
|
-
if (typeof raw !== "string") return null;
|
|
871
|
-
// Cursor's own transcripts wrap a query; a hook payload may carry the form.
|
|
872
|
-
return ev.cli === "cursor" ? unwrapCursorQuery(raw) : raw;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
const TIMESTAMP_OPEN = "<timestamp>";
|
|
876
|
-
const TIMESTAMP_CLOSE = "</timestamp>";
|
|
877
|
-
const USER_QUERY_OPEN = "<user_query>";
|
|
878
|
-
const USER_QUERY_CLOSE = "</user_query>";
|
|
879
|
-
|
|
880
|
-
/**
|
|
881
|
-
* A Cursor prompt with the `<user_query>` wrapper Cursor's own transcripts use
|
|
882
|
-
* removed, or null when the prompt is harness text.
|
|
883
|
-
*
|
|
884
|
-
* Cursor's hook payloads are not known to carry the wrapper; this accepts the
|
|
885
|
-
* form in case one does, and nothing looser. The wrapper is removed only when
|
|
886
|
-
* it is the whole prompt: after an optional leading `<timestamp>…</timestamp>`,
|
|
887
|
-
* exactly one `<user_query>…</user_query>` block and nothing after it. A tag
|
|
888
|
-
* anywhere else is text like any other and the whole prompt is kept, because
|
|
889
|
-
* picking a tagged span out of the middle would record text that is not what
|
|
890
|
-
* the human typed: a snippet they pasted from a log or an issue, or text
|
|
891
|
-
* inside failproofai's own stop-gate message, which quotes names the agent
|
|
892
|
-
* chose (a branch called `wip<user_query>…</user_query>`).
|
|
893
|
-
*
|
|
894
|
-
* Layers are peeled in the order `cleanHumanTurn` reads a turn: system
|
|
895
|
-
* reminders first (removed wherever they are), then the timestamp, then the
|
|
896
|
-
* query block. What is left after each layer is judged for harness text, so
|
|
897
|
-
* wrapping failproofai's own words, or any other whole-turn harness text,
|
|
898
|
-
* cannot make it the human's. A prompt that is not unwrapped is returned
|
|
899
|
-
* whole and judged whole by the caller; one that is unwrapped starts with a
|
|
900
|
-
* wrapper tag, which no whole-turn harness text does. Linear time: a few
|
|
901
|
-
* indexOf scans and `cleanHumanTurn` passes.
|
|
902
|
-
*/
|
|
903
|
-
function unwrapCursorQuery(raw: string): string | null {
|
|
904
|
-
let text = replaceTagBlocks(raw, "system-reminder", () => "").trim();
|
|
905
|
-
if (text.startsWith(TIMESTAMP_OPEN)) {
|
|
906
|
-
const end = text.indexOf(TIMESTAMP_CLOSE, TIMESTAMP_OPEN.length);
|
|
907
|
-
if (end < 0) return raw;
|
|
908
|
-
text = text.slice(end + TIMESTAMP_CLOSE.length).trim();
|
|
909
|
-
if (cleanHumanTurn(text) === null) return null;
|
|
910
|
-
}
|
|
911
|
-
if (!text.startsWith(USER_QUERY_OPEN)) return raw;
|
|
912
|
-
const body = text.slice(USER_QUERY_OPEN.length);
|
|
913
|
-
if (cleanHumanTurn(body) === null) return null;
|
|
914
|
-
if (!body.endsWith(USER_QUERY_CLOSE)) return raw;
|
|
915
|
-
const inner = body.slice(0, body.length - USER_QUERY_CLOSE.length);
|
|
916
|
-
if (inner.includes(USER_QUERY_OPEN) || inner.includes(USER_QUERY_CLOSE)) return raw;
|
|
917
|
-
return inner;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
const omissionMarker = (count: number): string => `\n…[${count} characters omitted]…\n`;
|
|
921
|
-
|
|
922
|
-
/** `capHeadTail`'s head/tail split and marker, keeping `budget` characters of `text`. */
|
|
923
|
-
function cutHeadTail(text: string, budget: number): string {
|
|
924
|
-
const head = Math.ceil(budget * 0.6);
|
|
925
|
-
const tail = budget - head;
|
|
926
|
-
return `${text.slice(0, head)}${omissionMarker(text.length - budget)}${text.slice(text.length - tail)}`;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
/**
|
|
930
|
-
* Cap to at most `max` characters INCLUDING the omission marker, so the
|
|
931
|
-
* envelope's own cap (the same `MAX_USER_MESSAGE_CHARS`) never fires again on
|
|
932
|
-
* a stored message and flags the whole request as truncated.
|
|
933
|
-
*/
|
|
934
|
-
function capWithin(text: string, max: number): string {
|
|
935
|
-
if (text.length <= max) return text;
|
|
936
|
-
let budget = max;
|
|
937
|
-
for (let i = 0; i < 4; i++) {
|
|
938
|
-
const capped = cutHeadTail(text, budget);
|
|
939
|
-
if (capped.length <= max) return capped;
|
|
940
|
-
budget -= capped.length - max;
|
|
941
|
-
}
|
|
942
|
-
return cutHeadTail(text, budget).slice(0, max);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
/**
|
|
946
|
-
* A prefix of `head` and a suffix of `tail` around one omission marker, in at
|
|
947
|
-
* most `max` characters, the marker included (see `capWithin`). The head gets
|
|
948
|
-
* 60% of the room unless the tail needs less, and the tail gets the rest. The
|
|
949
|
-
* marker counts `omitted` plus whatever of either piece is left out.
|
|
950
|
-
*/
|
|
951
|
-
function joinWithin(head: string, tail: string, omitted: number, max: number): string {
|
|
952
|
-
// Room for the text around the longest marker this can need.
|
|
953
|
-
const room = Math.max(0, max - omissionMarker(omitted + head.length + tail.length).length);
|
|
954
|
-
const keepTail = Math.min(tail.length, room - Math.min(head.length, Math.ceil(room * 0.6)));
|
|
955
|
-
const keepHead = Math.min(head.length, room - keepTail);
|
|
956
|
-
const dropped = omitted + (head.length - keepHead) + (tail.length - keepTail);
|
|
957
|
-
return head.slice(0, keepHead) + omissionMarker(dropped) + tail.slice(tail.length - keepTail);
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
/**
|
|
961
|
-
* The pre-cap: a bound on what the redaction regexes scan (a pasted log can be
|
|
962
|
-
* megabytes, and some patterns cost the square of the length on the text they
|
|
963
|
-
* scan), and far more than the final cap keeps.
|
|
964
|
-
*/
|
|
965
|
-
const PRE_CAP_CHARS = MAX_USER_MESSAGE_CHARS * 8;
|
|
966
|
-
/**
|
|
967
|
-
* Text this close to a pre-cap cut is never stored. A secret the cut split no
|
|
968
|
-
* longer matches any pattern, so its piece on our side of the cut is left
|
|
969
|
-
* unredacted, and redaction elsewhere in the same piece (a long JWT becomes a
|
|
970
|
-
* 14-character marker) can pull that piece into the head or tail the final
|
|
971
|
-
* cap keeps.
|
|
972
|
-
*/
|
|
973
|
-
const PRE_CAP_GUARD_CHARS = 256;
|
|
974
|
-
/**
|
|
975
|
-
* How much further a token the guard ends inside is followed, so all of a
|
|
976
|
-
* long one is dropped, not just its end. Bounded so the start of a giant
|
|
977
|
-
* paste with no whitespace in it can still be kept.
|
|
978
|
-
*/
|
|
979
|
-
const PRE_CAP_TOKEN_CHARS = 4_096;
|
|
980
|
-
|
|
981
|
-
const isSpace = (c: string | undefined): boolean => c !== undefined && /\s/.test(c);
|
|
982
|
-
|
|
983
|
-
/**
|
|
984
|
-
* Where the kept part of a pre-capped head ends: `PRE_CAP_GUARD_CHARS` short
|
|
985
|
-
* of the cut, moved back to the start of any token that reaches into the
|
|
986
|
-
* guard (up to `PRE_CAP_TOKEN_CHARS` back). A secret is almost always one
|
|
987
|
-
* token, so no piece of one the cut split survives, whatever its length.
|
|
988
|
-
*/
|
|
989
|
-
function headEndClearOfCut(head: string): number {
|
|
990
|
-
let end = Math.max(0, head.length - PRE_CAP_GUARD_CHARS);
|
|
991
|
-
if (end > 0 && !isSpace(head[end])) {
|
|
992
|
-
const floor = Math.max(0, end - PRE_CAP_TOKEN_CHARS);
|
|
993
|
-
while (end > floor && !isSpace(head[end - 1])) end--;
|
|
994
|
-
}
|
|
995
|
-
return end;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
/** `headEndClearOfCut` for the tail: where its kept part starts. */
|
|
999
|
-
function tailStartClearOfCut(tail: string): number {
|
|
1000
|
-
let start = Math.min(tail.length, PRE_CAP_GUARD_CHARS);
|
|
1001
|
-
if (start < tail.length && !isSpace(tail[start - 1])) {
|
|
1002
|
-
const ceiling = Math.min(tail.length, start + PRE_CAP_TOKEN_CHARS);
|
|
1003
|
-
while (start < ceiling && !isSpace(tail[start])) start++;
|
|
1004
|
-
}
|
|
1005
|
-
return start;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
/**
|
|
1009
|
-
* Redact, then cap. Redacting first means the final cut can never leave half
|
|
1010
|
-
* a secret the patterns no longer match.
|
|
1011
|
-
*
|
|
1012
|
-
* A text longer than the pre-cap is redacted as two pieces, its head and its
|
|
1013
|
-
* tail, and the middle is never looked at. The pre-cap's cuts CAN split a
|
|
1014
|
-
* secret, so each piece drops the text next to its cut before the final cap
|
|
1015
|
-
* picks what to keep: a piece that redaction shrank to almost nothing
|
|
1016
|
-
* contributes almost nothing, rather than the fragment by its cut. The
|
|
1017
|
-
* marker counts every character left out, in the redacted text's terms.
|
|
1018
|
-
*
|
|
1019
|
-
* `blunt: false` at every call below — the default, spelled out so a future
|
|
1020
|
-
* change to it cannot reach this path silently. The credential-header and flag
|
|
1021
|
-
* rules give up a whole line or a whole argument on the strength of a NAME,
|
|
1022
|
-
* which is the right trade for the Jev request body and the wrong one here.
|
|
1023
|
-
* This is the evaluator's record of what the HUMAN asked for — it never leaves
|
|
1024
|
-
* the machine, `buildEnvelope` redacts it again (bluntly) before it does, and
|
|
1025
|
-
* storing it cut off after a `cookie:` or an `authorization:` lost the targets
|
|
1026
|
-
* the human named. The narrow rules still run.
|
|
1027
|
-
*/
|
|
1028
|
-
function storable(text: string): string {
|
|
1029
|
-
if (text.length <= PRE_CAP_CHARS) return capWithin(redactSecrets(text, { blunt: false }).text, MAX_USER_MESSAGE_CHARS);
|
|
1030
|
-
const headLen = Math.ceil(PRE_CAP_CHARS * 0.6);
|
|
1031
|
-
const tailLen = PRE_CAP_CHARS - headLen;
|
|
1032
|
-
const head = redactSecrets(text.slice(0, headLen), { blunt: false }).text;
|
|
1033
|
-
const tail = redactSecrets(text.slice(text.length - tailLen), { blunt: false }).text;
|
|
1034
|
-
const keptHead = head.slice(0, headEndClearOfCut(head));
|
|
1035
|
-
const keptTail = tail.slice(tailStartClearOfCut(tail));
|
|
1036
|
-
const omitted = head.length - keptHead.length + (text.length - headLen - tailLen) + (tail.length - keptTail.length);
|
|
1037
|
-
return joinWithin(keptHead, keptTail, omitted, MAX_USER_MESSAGE_CHARS);
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// ── Transcript snapshot ──────────────────────────────────────────────────────
|
|
1041
|
-
|
|
1042
|
-
const TAIL_CHUNK_BYTES = 256 * 1024;
|
|
1043
|
-
/** The furthest back from the end a snapshot will look. */
|
|
1044
|
-
export const TRANSCRIPT_TAIL_MAX_BYTES = 4 * 1024 * 1024;
|
|
1045
|
-
/** Only lines containing one of these can hold an agent message. */
|
|
1046
|
-
const AGENT_LINE_HINTS = ['"assistant"', '"agent_message"', '"AgentMessage"', '"assistant.message"'];
|
|
1047
|
-
|
|
1048
|
-
function readTranscriptPath(path: string | undefined): string | null {
|
|
1049
|
-
// Virtual paths (opencode-db://, devin-db://, goose-db://) are SQLite
|
|
1050
|
-
// sessions, not files.
|
|
1051
|
-
if (!path || path.includes("://")) return null;
|
|
1052
|
-
return path;
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
function agentTextOfLine(line: Buffer): string | null {
|
|
1056
|
-
if (line.length === 0) return null;
|
|
1057
|
-
if (!AGENT_LINE_HINTS.some((h) => line.includes(h))) return null;
|
|
1058
|
-
try {
|
|
1059
|
-
return agentMessageText(JSON.parse(line.toString("utf8")));
|
|
1060
|
-
} catch {
|
|
1061
|
-
return null;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
/**
|
|
1066
|
-
* Visit a transcript's lines from the last to the first, reading backwards
|
|
1067
|
-
* from the end in chunks and at most `maxBytes`, until `visit` returns true.
|
|
1068
|
-
* Only whole lines are visited, empty ones included. Does nothing when there
|
|
1069
|
-
* is no transcript file, and stops quietly if anything goes wrong. Only a
|
|
1070
|
-
* regular file is opened, so a path naming a FIFO or a device cannot stall a
|
|
1071
|
-
* hook.
|
|
1072
|
-
*/
|
|
1073
|
-
function visitLinesBackwards(transcriptPath: string | undefined, maxBytes: number, visit: (line: Buffer) => boolean): void {
|
|
1074
|
-
const path = readTranscriptPath(transcriptPath);
|
|
1075
|
-
if (!path) return;
|
|
1076
|
-
let fd: number | undefined;
|
|
1077
|
-
try {
|
|
1078
|
-
const st = statSync(path);
|
|
1079
|
-
if (!st.isFile() || st.size === 0) return;
|
|
1080
|
-
fd = openSync(path, "r");
|
|
1081
|
-
let end = st.size;
|
|
1082
|
-
let consumed = 0;
|
|
1083
|
-
// Bytes of a line that started before the chunk just read; lines are
|
|
1084
|
-
// split on the 0x0A byte, which never occurs inside a UTF-8 sequence, so
|
|
1085
|
-
// a multi-byte character across a chunk boundary survives intact.
|
|
1086
|
-
let partial: Buffer = Buffer.alloc(0);
|
|
1087
|
-
while (end > 0 && consumed < maxBytes) {
|
|
1088
|
-
const len = Math.min(TAIL_CHUNK_BYTES, end, maxBytes - consumed);
|
|
1089
|
-
const start = end - len;
|
|
1090
|
-
const chunk = Buffer.alloc(len);
|
|
1091
|
-
readSync(fd, chunk, 0, len, start);
|
|
1092
|
-
consumed += len;
|
|
1093
|
-
end = start;
|
|
1094
|
-
const buf = partial.length > 0 ? Buffer.concat([chunk, partial]) : chunk;
|
|
1095
|
-
let lineEnd = buf.length;
|
|
1096
|
-
while (lineEnd > 0) {
|
|
1097
|
-
const nl = buf.lastIndexOf(0x0a, lineEnd - 1);
|
|
1098
|
-
if (nl < 0) break;
|
|
1099
|
-
if (visit(buf.subarray(nl + 1, lineEnd))) return;
|
|
1100
|
-
lineEnd = nl;
|
|
1101
|
-
}
|
|
1102
|
-
if (start === 0) {
|
|
1103
|
-
visit(buf.subarray(0, lineEnd));
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
partial = Buffer.from(buf.subarray(0, lineEnd));
|
|
1107
|
-
}
|
|
1108
|
-
// The budget ran out. What is left in `partial` starts exactly where the
|
|
1109
|
-
// reading stopped; if the byte before it ends a line, it is a whole line,
|
|
1110
|
-
// read in full, and gets looked at like any other.
|
|
1111
|
-
if (end > 0 && partial.length > 0) {
|
|
1112
|
-
const before = Buffer.alloc(1);
|
|
1113
|
-
if (readSync(fd, before, 0, 1, end - 1) === 1 && before[0] === 0x0a) visit(partial);
|
|
1114
|
-
}
|
|
1115
|
-
} catch {
|
|
1116
|
-
// An unreadable transcript is one with nothing in it.
|
|
1117
|
-
} finally {
|
|
1118
|
-
if (fd !== undefined) {
|
|
1119
|
-
try {
|
|
1120
|
-
closeSync(fd);
|
|
1121
|
-
} catch {
|
|
1122
|
-
// Nothing useful to do.
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
/**
|
|
1129
|
-
* The agent's last visible message in a transcript (JSONL of any format
|
|
1130
|
-
* `agentMessageText` knows), reading backwards from the end and at most
|
|
1131
|
-
* `maxBytes`. Null when there is no transcript file, no such message within
|
|
1132
|
-
* reach, or anything goes wrong.
|
|
1133
|
-
*/
|
|
1134
|
-
export function lastAgentMessage(transcriptPath: string | undefined, maxBytes: number = TRANSCRIPT_TAIL_MAX_BYTES): string | null {
|
|
1135
|
-
let found: string | null = null;
|
|
1136
|
-
visitLinesBackwards(transcriptPath, maxBytes, (line) => {
|
|
1137
|
-
found = agentTextOfLine(line);
|
|
1138
|
-
return found !== null;
|
|
1139
|
-
});
|
|
1140
|
-
return found;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
/**
|
|
1144
|
-
* Record what the human just typed, from a prompt-submit hook event, with the
|
|
1145
|
-
* agent message it replies to. Never throws.
|
|
1146
|
-
*
|
|
1147
|
-
* The prompt the harness hands the hook is taken as the human's, per the
|
|
1148
|
-
* decision in this module's header. Five things stop a record, and none of
|
|
1149
|
-
* them asks the file system: the event is not the canonical
|
|
1150
|
-
* `UserPromptSubmit`; the session id is not a plain name; the harness's prompt
|
|
1151
|
-
* event carries no human text (Hermes, Antigravity) or the payload is missing;
|
|
1152
|
-
* the payload marks the turn as a machine's or a sub-agent's; or nothing is
|
|
1153
|
-
* left once the harness's own wrapping is stripped (`cleanHumanTurn` — which
|
|
1154
|
-
* is also what keeps failproofai's own stop-gate words, fed back as a user
|
|
1155
|
-
* turn by several harnesses, from ever being recorded as a request).
|
|
1156
|
-
*
|
|
1157
|
-
* The transcript is read for exactly one thing, and never for origin: the
|
|
1158
|
-
* agent's last visible message at this moment. The agent wrote that message
|
|
1159
|
-
* by definition — it is sent to Jev labelled that way and is never consent.
|
|
1160
|
-
*/
|
|
1161
|
-
export function captureIntent(ev: CaptureEvent, now: number = Date.now()): void {
|
|
1162
|
-
try {
|
|
1163
|
-
if (ev?.eventType !== "UserPromptSubmit") return;
|
|
1164
|
-
const sessionId = ev.sessionId;
|
|
1165
|
-
if (!sessionId || !SESSION_ID_RE.test(sessionId)) return;
|
|
1166
|
-
const raw = humanPromptText(ev);
|
|
1167
|
-
if (raw === null) return;
|
|
1168
|
-
const cleaned = cleanHumanTurn(raw);
|
|
1169
|
-
if (cleaned === null) return;
|
|
1170
|
-
recordPrompt(sessionId, cleaned, lastAgentMessage(ev.transcriptPath), now);
|
|
1171
|
-
} catch {
|
|
1172
|
-
// Losing intent only means Jev judges without it.
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
/**
|
|
1177
|
-
* What the human asked for recently (oldest first, at most
|
|
1178
|
-
* `MAX_RECORDED_PROMPTS`, none older than `INTENT_MAX_AGE_MS`) and the agent
|
|
1179
|
-
* message their latest prompt replied to — null when that prompt had none or
|
|
1180
|
-
* nothing is recorded.
|
|
1181
|
-
*/
|
|
1182
|
-
export function readIntent(
|
|
1183
|
-
sessionId?: string,
|
|
1184
|
-
now: number = Date.now(),
|
|
1185
|
-
): { userSaid: string[]; agentLastMessage: string | null } {
|
|
1186
|
-
const prompts = livePrompts(sessionId, now);
|
|
1187
|
-
const latest = prompts[prompts.length - 1];
|
|
1188
|
-
const agent = latest && typeof latest.agent === "string" && latest.agent.length > 0 ? latest.agent : null;
|
|
1189
|
-
return { userSaid: prompts.map((p) => p.text), agentLastMessage: agent };
|
|
1190
|
-
}
|