failproofai 1.0.12-beta.1 → 1.0.12
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.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 +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
- 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.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 +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/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.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 +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- 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.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.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.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 -8
- 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/_0tovk6q0miud6._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__167mipqmjprq5._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0b78b0j8iyv6l._.js → [root-of-the-server]__1vofkb2oq7p7e._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1a3tubds7s7ow._.js → _0ouu886szh6yj._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1d98fyfrcaqhy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1i8oid1gc7s1f.js → node_modules_html-to-image_es_index_0tqc05n832v7a.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589gp4p60j._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.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/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/{1b32-afqacaen.js → 0el9n3h9_zp5m.js} +1 -1
- package/.next/standalone/.next/static/chunks/0k7g_8x3o6gd-.js +1 -0
- package/.next/standalone/.next/static/chunks/{42e8zab314654.js → 0umzct31duk5g.js} +1 -1
- package/.next/standalone/.next/static/chunks/{24w3x2da9sk49.js → 0wobqgosirs8p.js} +1 -1
- package/.next/standalone/.next/static/chunks/12rug3jilwqzs.js +1 -0
- package/.next/standalone/.next/static/chunks/{2-tf6_7u623kd.js → 1kduj74-715jh.js} +1 -1
- package/.next/standalone/.next/static/chunks/2701amhrbsbot.js +1 -0
- package/.next/standalone/.next/static/chunks/{18433qz-8njv3.js → 27cavoc8paelg.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0_zfad4co-t2z.js → 2ve23a7keupjw.js} +1 -1
- package/.next/standalone/.next/static/chunks/3bv6xaoitjpnh.js +6 -0
- package/.next/standalone/app/actions/update-jev-config.ts +5 -3
- package/.next/standalone/app/settings/jev-panel.tsx +1 -1
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +28 -18
- package/.next/standalone/fp-cloud-cli/README.md +127 -9
- package/.next/standalone/fp-cloud-cli/fp_cli/app.py +15 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +199 -34
- package/.next/standalone/fp-cloud-cli/fp_cli/cloud_output.py +523 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/_cloud.py +45 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +3 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/account_cmds.py +171 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +80 -7
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +23 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +2 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/contracts_cmds.py +67 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/dashboards_cmds.py +159 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/evaluators_cmds.py +380 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +14 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +12 -216
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +82 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +4 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +17 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +143 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +70 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/sentiment_cmds.py +49 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +95 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +2 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +21 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +14 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +7 -53
- package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +3 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/evaluator_spec.py +266 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +189 -152
- package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +21 -24
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +124 -23
- package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +2 -1
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +173 -22
- package/.next/standalone/fp-cloud-cli/tests/fake_cloud_smoke.py +312 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_account.py +125 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_agent.py +253 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_human_views.py +287 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +32 -197
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_observability.py +116 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_transport.py +141 -0
- package/.next/standalone/fp-cloud-cli/tests/test_commands.py +3 -3
- package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +3 -3
- package/.next/standalone/fp-cloud-cli/tests/test_evaluator_authoring.py +449 -0
- package/.next/standalone/fp-cloud-cli/tests/test_facets.py +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +15 -0
- package/.next/standalone/fp-cloud-cli/tests/test_operator.py +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_output.py +37 -37
- package/.next/standalone/fp-cloud-cli/tests/test_round4_behaviour.py +144 -0
- package/.next/standalone/fp-cloud-cli/tests/test_screen_language.py +80 -0
- package/.next/standalone/fp-cloud-cli/tests/test_session_reevaluate.py +33 -0
- package/.next/standalone/fp-cloud-cli/tests/test_staging_parity.py +198 -0
- package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +1 -1
- package/.next/standalone/hermes-plugin/__init__.py +11 -0
- package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
- package/.next/standalone/package.json +11 -10
- package/.next/standalone/sdk/python/uv.lock +31 -8
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +68 -176
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +2 -2
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +98 -115
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +3 -3
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +149 -171
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +2 -2
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +25 -25
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +10 -10
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +1760 -638
- package/dist/worker.mjs +426 -76
- package/hermes-plugin/__init__.py +11 -0
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +11 -10
- package/src/audit/open-browser.ts +24 -7
- package/src/hooks/agent-roster.ts +9 -1
- package/src/hooks/agent-skill.ts +206 -0
- package/src/hooks/cloud-connection.ts +27 -51
- package/src/hooks/cloud-introspect.ts +5 -2
- package/src/hooks/cloud-login.ts +248 -0
- package/src/hooks/cloud-managed-policies.ts +54 -4
- package/src/hooks/codex-system-hooks.ts +10 -4
- package/src/hooks/config-status.ts +9 -3
- package/src/hooks/configure-wizard.ts +356 -32
- package/src/hooks/delivery-health.ts +47 -2
- package/src/hooks/effective-reviewers.ts +14 -0
- package/src/hooks/fp-config.ts +19 -0
- package/src/hooks/fp-home.ts +1 -1
- package/src/hooks/handler.ts +43 -5
- package/src/hooks/hook-activity-store.ts +7 -1
- package/src/hooks/integrations.ts +18 -3
- package/src/hooks/jev-activity.ts +117 -1
- package/src/hooks/jev-cli.ts +7 -9
- package/src/hooks/manager.ts +80 -46
- package/src/hooks/policy-evaluator.ts +17 -0
- package/src/hooks/policy-reviewability.ts +4 -2
- package/src/hooks/semantic/cloud-jev.ts +219 -28
- package/src/hooks/semantic/combine.ts +26 -1
- package/src/hooks/semantic/intent.ts +127 -24
- package/src/hooks/semantic/jev-client.ts +1 -1
- package/src/hooks/semantic/jev-config.ts +6 -27
- package/src/hooks/semantic/jev-review.ts +111 -4
- package/src/hooks/tui.ts +280 -0
- package/src/hooks/uninstall-cli.ts +12 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0588st-tpiuia._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_128ebxr83nk6g._.js +0 -3
- package/.next/standalone/.next/static/chunks/0jutamwqz8pg5.js +0 -1
- package/.next/standalone/.next/static/chunks/114hbolvrv94e.js +0 -1
- package/.next/standalone/.next/static/chunks/1p-hsi58i-6h0.js +0 -6
- package/.next/standalone/.next/static/chunks/3-pku9qwv1yfu.js +0 -1
- /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_ssgManifest.js +0 -0
|
@@ -81,6 +81,26 @@
|
|
|
81
81
|
* Known limits.)
|
|
82
82
|
*
|
|
83
83
|
* Text inside a tool call claiming "the user approved this" is never consulted.
|
|
84
|
+
*
|
|
85
|
+
* WHOSE SESSION IT IS. Prompts are stored per AGENT PROFILE and session id
|
|
86
|
+
* (`sessions/<profile>/<sessionId>.json`), the profile being the runtime agent
|
|
87
|
+
* identity FailproofAI Cloud's agent targeting uses (`AgentIdentity`, read by
|
|
88
|
+
* the handler from the daemon's roster) — `<integration>.<instanceId>`, or the
|
|
89
|
+
* bare integration name when the profile has no rostered identity. Session ids
|
|
90
|
+
* are chosen by the harness, not by us, so two profiles can submit the same
|
|
91
|
+
* one; keyed by session id alone, a prompt typed into a profile a Cloud Jev
|
|
92
|
+
* deployment does NOT target could be read back as the stated intent of one it
|
|
93
|
+
* does, and clear a reviewable deny there. The hook path always names the
|
|
94
|
+
* profile on both sides (`captureIntent`'s `profile`, `readIntent`'s), so a
|
|
95
|
+
* prompt is only ever read by the profile it was typed into.
|
|
96
|
+
*
|
|
97
|
+
* The flat `sessions/<sessionId>.json` layout that came before is the store a
|
|
98
|
+
* caller that names NO profile still reads and writes (library and test use).
|
|
99
|
+
* The hook path never reads it: such a file cannot say which profile wrote it,
|
|
100
|
+
* so a pre-upgrade entry is ignored rather than guessed at, and expires under
|
|
101
|
+
* the six-hour sweep like any other. The cost is bounded and fail-closed: a
|
|
102
|
+
* session in flight across the upgrade judges with no stated intent until its
|
|
103
|
+
* next prompt is recorded.
|
|
84
104
|
*/
|
|
85
105
|
import {
|
|
86
106
|
chmodSync,
|
|
@@ -99,6 +119,7 @@ import { dirname, resolve } from "node:path";
|
|
|
99
119
|
import { failproofaiHome, semanticDir } from "../fp-home";
|
|
100
120
|
import type { IntegrationType } from "../types";
|
|
101
121
|
import { MAX_USER_MESSAGE_CHARS, redactSecrets } from "./envelope";
|
|
122
|
+
import { validAgentIdentity, type AgentIdentity } from "../agent-targets";
|
|
102
123
|
|
|
103
124
|
export const MAX_RECORDED_PROMPTS = 5;
|
|
104
125
|
/** Older than this and a prompt no longer describes what the agent is doing. */
|
|
@@ -109,7 +130,51 @@ const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
|
|
|
109
130
|
const SESSION_ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
110
131
|
|
|
111
132
|
const sessionsDir = (): string => resolve(semanticDir(), "sessions");
|
|
112
|
-
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The agent profile a prompt belongs to: the harness, and the runtime agent
|
|
136
|
+
* identity the handler resolved for it (`readRuntimeAgentIdentity`) — the same
|
|
137
|
+
* identity Cloud Jev's agent targeting matches on. `agent` null or absent is a
|
|
138
|
+
* profile with no rostered identity, which targeting treats as "unknown" and
|
|
139
|
+
* which is kept apart from every rostered one.
|
|
140
|
+
*/
|
|
141
|
+
export interface IntentProfile {
|
|
142
|
+
cli: string;
|
|
143
|
+
agent?: AgentIdentity | null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A profile directory's name: `<integration>` or `<integration>.agt_<hex>`. */
|
|
147
|
+
const PROFILE_DIR_RE = /^[a-z]+(?:\.agt_[0-9a-f]{16,32})?$/;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The directory name a profile's prompts live under, or null when the profile
|
|
151
|
+
* cannot be named (an unknown harness): nothing is then recorded or read.
|
|
152
|
+
* Injective: integration names are lowercase letters and instance ids carry
|
|
153
|
+
* their own `agt_` prefix, so no two profiles share a directory.
|
|
154
|
+
*/
|
|
155
|
+
export function intentProfileKey(profile: IntentProfile): string | null {
|
|
156
|
+
if (validAgentIdentity(profile.agent)) return `${profile.agent.integration}.${profile.agent.instanceId}`;
|
|
157
|
+
return isKnownCli(profile.cli) ? profile.cli : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Where one session's prompts are kept: the directory and the file in it. */
|
|
161
|
+
interface StoreSlot {
|
|
162
|
+
dir: string;
|
|
163
|
+
file: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The slot for `sessionId` under `profile`. No profile at all (`undefined`) is
|
|
168
|
+
* the flat, pre-profile layout — never used by the hook path (see the header).
|
|
169
|
+
* A profile that cannot be named has no slot.
|
|
170
|
+
*/
|
|
171
|
+
function storeSlot(sessionId: string, profile: IntentProfile | undefined): StoreSlot | null {
|
|
172
|
+
if (profile === undefined) return { dir: sessionsDir(), file: resolve(sessionsDir(), `${sessionId}.json`) };
|
|
173
|
+
const key = intentProfileKey(profile);
|
|
174
|
+
if (key === null || !PROFILE_DIR_RE.test(key)) return null;
|
|
175
|
+
const dir = resolve(sessionsDir(), key);
|
|
176
|
+
return { dir, file: resolve(dir, `${sessionId}.json`) };
|
|
177
|
+
}
|
|
113
178
|
|
|
114
179
|
/**
|
|
115
180
|
* Group- or world-WRITE bits on a directory the store sits under. Anyone who
|
|
@@ -140,11 +205,11 @@ function modesAreMeaningful(): boolean {
|
|
|
140
205
|
* (`fpai-collect`'s `create_dir_all`) and arrives at the umask, which on a
|
|
141
206
|
* umask-002 box is 0775.
|
|
142
207
|
*/
|
|
143
|
-
function storeDirChain(): string[] {
|
|
208
|
+
function storeDirChain(innermost: string = sessionsDir()): string[] {
|
|
144
209
|
const home = resolve(failproofaiHome());
|
|
145
210
|
const chain: string[] = [];
|
|
146
|
-
let dir =
|
|
147
|
-
// Bounded: the chain is
|
|
211
|
+
let dir = innermost;
|
|
212
|
+
// Bounded: the chain is five deep, and a home that is not an ancestor (a
|
|
148
213
|
// layout change, a symlink) must not walk to the filesystem root.
|
|
149
214
|
for (let i = 0; i < 8; i++) {
|
|
150
215
|
chain.push(dir);
|
|
@@ -172,11 +237,12 @@ function storeDirChain(): string[] {
|
|
|
172
237
|
* human's and could clear the four reviewable BLOCKING policies.
|
|
173
238
|
*
|
|
174
239
|
* Exported so `failproofai jev status` can report it the way it reports a
|
|
175
|
-
* refused config.
|
|
240
|
+
* refused config. `innermost` is a profile's directory when the read is for
|
|
241
|
+
* one, so that directory is held to the same rule.
|
|
176
242
|
*/
|
|
177
|
-
export function looseIntentStoreDir(): string | null {
|
|
243
|
+
export function looseIntentStoreDir(innermost?: string): string | null {
|
|
178
244
|
if (!modesAreMeaningful()) return null;
|
|
179
|
-
for (const dir of storeDirChain()) {
|
|
245
|
+
for (const dir of storeDirChain(innermost)) {
|
|
180
246
|
let mode: number;
|
|
181
247
|
try {
|
|
182
248
|
mode = statSync(dir).mode & 0o777;
|
|
@@ -207,9 +273,9 @@ interface IntentFile {
|
|
|
207
273
|
prompts: RecordedPrompt[];
|
|
208
274
|
}
|
|
209
275
|
|
|
210
|
-
function readIntentFile(
|
|
276
|
+
function readIntentFile(slot: StoreSlot): IntentFile {
|
|
211
277
|
try {
|
|
212
|
-
const parsed = JSON.parse(readFileSync(
|
|
278
|
+
const parsed = JSON.parse(readFileSync(slot.file, "utf8")) as IntentFile;
|
|
213
279
|
return Array.isArray(parsed?.prompts) ? parsed : { prompts: [] };
|
|
214
280
|
} catch {
|
|
215
281
|
return { prompts: [] };
|
|
@@ -228,15 +294,14 @@ function readIntentFile(sessionId: string): IntentFile {
|
|
|
228
294
|
* copies of one line. The replacement carries the new timestamp and the new
|
|
229
295
|
* agent snapshot, so the entry stays the latest thing the human said.
|
|
230
296
|
*/
|
|
231
|
-
function appendPrompt(
|
|
297
|
+
function appendPrompt(slot: StoreSlot, entry: RecordedPrompt): boolean {
|
|
232
298
|
try {
|
|
233
|
-
const file = readIntentFile(
|
|
299
|
+
const file = readIntentFile(slot);
|
|
234
300
|
const last = file.prompts[file.prompts.length - 1];
|
|
235
301
|
const kept = last && last.text === entry.text ? file.prompts.slice(0, -1) : file.prompts;
|
|
236
302
|
file.prompts = [...kept, entry].slice(-MAX_RECORDED_PROMPTS);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const target = intentFile(sessionId);
|
|
303
|
+
mkdirSync(slot.dir, { recursive: true, mode: 0o700 });
|
|
304
|
+
const target = slot.file;
|
|
240
305
|
const isNew = !fileExists(target);
|
|
241
306
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
242
307
|
writeFileSync(tmp, JSON.stringify(file), { mode: 0o600 });
|
|
@@ -265,12 +330,31 @@ const SESSION_FILE_RE = /^[A-Za-z0-9._-]{1,128}\.json(?:\.\d+\.tmp)?$/;
|
|
|
265
330
|
* file's mtime is its newest recorded prompt's, so every prompt in such a
|
|
266
331
|
* file is already outside the window every read filters by and can never be
|
|
267
332
|
* read again. Removing it loses nothing and keeps one-file-per-session from
|
|
268
|
-
* growing without bound.
|
|
333
|
+
* growing without bound. Swept: the flat layout's files (pre-profile entries
|
|
334
|
+
* among them) and every profile directory's, one level down.
|
|
269
335
|
*/
|
|
270
336
|
export function pruneExpiredSessions(now: number = Date.now()): number {
|
|
337
|
+
const root = sessionsDir();
|
|
338
|
+
let removed = pruneDir(root, now);
|
|
339
|
+
try {
|
|
340
|
+
for (const name of readdirSync(root)) {
|
|
341
|
+
if (!PROFILE_DIR_RE.test(name)) continue;
|
|
342
|
+
const dir = resolve(root, name);
|
|
343
|
+
try {
|
|
344
|
+
if (statSync(dir).isDirectory()) removed += pruneDir(dir, now);
|
|
345
|
+
} catch {
|
|
346
|
+
// Raced with something else: nothing to do.
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} catch {
|
|
350
|
+
// No directory yet.
|
|
351
|
+
}
|
|
352
|
+
return removed;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function pruneDir(dir: string, now: number): number {
|
|
271
356
|
let removed = 0;
|
|
272
357
|
try {
|
|
273
|
-
const dir = sessionsDir();
|
|
274
358
|
for (const name of readdirSync(dir)) {
|
|
275
359
|
if (!SESSION_FILE_RE.test(name)) continue;
|
|
276
360
|
const path = resolve(dir, name);
|
|
@@ -301,11 +385,19 @@ export function pruneExpiredSessions(now: number = Date.now()): number {
|
|
|
301
385
|
* leave half a secret the patterns no longer match. Never throws: losing intent
|
|
302
386
|
* only means no override.
|
|
303
387
|
*/
|
|
304
|
-
function recordPrompt(
|
|
388
|
+
function recordPrompt(
|
|
389
|
+
sessionId: string,
|
|
390
|
+
profile: IntentProfile | undefined,
|
|
391
|
+
prompt: string,
|
|
392
|
+
agent: string | null,
|
|
393
|
+
now: number,
|
|
394
|
+
): boolean {
|
|
305
395
|
if (!SESSION_ID_RE.test(sessionId)) return false;
|
|
306
396
|
if (prompt.trim().length === 0) return false;
|
|
397
|
+
const slot = storeSlot(sessionId, profile);
|
|
398
|
+
if (!slot) return false;
|
|
307
399
|
try {
|
|
308
|
-
return appendPrompt(
|
|
400
|
+
return appendPrompt(slot, { at: now, text: storable(prompt.trim()), agent: agent === null ? null : storable(agent) });
|
|
309
401
|
} catch {
|
|
310
402
|
return false;
|
|
311
403
|
}
|
|
@@ -319,10 +411,12 @@ function recordPrompt(sessionId: string, prompt: string, agent: string | null, n
|
|
|
319
411
|
* still asked, and judges with no stated intent, so no reviewable deny is
|
|
320
412
|
* cleared on a prompt we cannot say is the owner's.
|
|
321
413
|
*/
|
|
322
|
-
function livePrompts(sessionId: string | undefined, now: number): RecordedPrompt[] {
|
|
414
|
+
function livePrompts(sessionId: string | undefined, now: number, profile: IntentProfile | undefined): RecordedPrompt[] {
|
|
323
415
|
if (!sessionId || !SESSION_ID_RE.test(sessionId)) return [];
|
|
324
|
-
|
|
325
|
-
|
|
416
|
+
const slot = storeSlot(sessionId, profile);
|
|
417
|
+
if (!slot) return [];
|
|
418
|
+
if (looseIntentStoreDir(slot.dir) !== null) return [];
|
|
419
|
+
return readIntentFile(slot).prompts.filter(
|
|
326
420
|
(p) =>
|
|
327
421
|
typeof p?.text === "string" &&
|
|
328
422
|
typeof p.at === "number" &&
|
|
@@ -839,6 +933,13 @@ export interface CaptureEvent {
|
|
|
839
933
|
cli: string;
|
|
840
934
|
/** The stdin payload after `normalizeCliPayload`: the handler's `parsed`. */
|
|
841
935
|
payload: Record<string, unknown>;
|
|
936
|
+
/**
|
|
937
|
+
* The agent profile the prompt was typed into. The hook path always sets it
|
|
938
|
+
* (the agent null when the profile has no rostered identity), and the prompt
|
|
939
|
+
* is then readable only under that same profile. Absent is the flat layout
|
|
940
|
+
* (see the header).
|
|
941
|
+
*/
|
|
942
|
+
profile?: IntentProfile;
|
|
842
943
|
}
|
|
843
944
|
|
|
844
945
|
function isKnownCli(cli: string): cli is IntegrationType {
|
|
@@ -1167,7 +1268,7 @@ export function captureIntent(ev: CaptureEvent, now: number = Date.now()): void
|
|
|
1167
1268
|
if (raw === null) return;
|
|
1168
1269
|
const cleaned = cleanHumanTurn(raw);
|
|
1169
1270
|
if (cleaned === null) return;
|
|
1170
|
-
recordPrompt(sessionId, cleaned, lastAgentMessage(ev.transcriptPath), now);
|
|
1271
|
+
recordPrompt(sessionId, ev.profile, cleaned, lastAgentMessage(ev.transcriptPath), now);
|
|
1171
1272
|
} catch {
|
|
1172
1273
|
// Losing intent only means Jev judges without it.
|
|
1173
1274
|
}
|
|
@@ -1177,13 +1278,15 @@ export function captureIntent(ev: CaptureEvent, now: number = Date.now()): void
|
|
|
1177
1278
|
* What the human asked for recently (oldest first, at most
|
|
1178
1279
|
* `MAX_RECORDED_PROMPTS`, none older than `INTENT_MAX_AGE_MS`) and the agent
|
|
1179
1280
|
* message their latest prompt replied to — null when that prompt had none or
|
|
1180
|
-
* nothing is recorded.
|
|
1281
|
+
* nothing is recorded. Read under `profile`, the one the prompts were captured
|
|
1282
|
+
* under; the hook path always passes it (see the header).
|
|
1181
1283
|
*/
|
|
1182
1284
|
export function readIntent(
|
|
1183
1285
|
sessionId?: string,
|
|
1184
1286
|
now: number = Date.now(),
|
|
1287
|
+
profile?: IntentProfile,
|
|
1185
1288
|
): { userSaid: string[]; agentLastMessage: string | null } {
|
|
1186
|
-
const prompts = livePrompts(sessionId, now);
|
|
1289
|
+
const prompts = livePrompts(sessionId, now, profile);
|
|
1187
1290
|
const latest = prompts[prompts.length - 1];
|
|
1188
1291
|
const agent = latest && typeof latest.agent === "string" && latest.agent.length > 0 ? latest.agent : null;
|
|
1189
1292
|
return { userSaid: prompts.map((p) => p.text), agentLastMessage: agent };
|
|
@@ -469,7 +469,7 @@ async function postJson(url: string, bearer: string, body: unknown, signal: Abor
|
|
|
469
469
|
body: JSON.stringify(body),
|
|
470
470
|
signal,
|
|
471
471
|
// Never followed. The configured URL is the one `validateBaseUrl` checked
|
|
472
|
-
// (https, or loopback http
|
|
472
|
+
// (https, or loopback http); a redirect would hand the
|
|
473
473
|
// answer — the thing that can clear a deny — to an origin nobody checked,
|
|
474
474
|
// plain http included. No provider redirects this POST.
|
|
475
475
|
redirect: "manual",
|
|
@@ -327,8 +327,7 @@ function credentialQueryProblem(url: URL): string | null {
|
|
|
327
327
|
* An endpoint base URL: https, or http to a loopback host only (a local proxy).
|
|
328
328
|
* No credentials in the URL — not as userinfo, not as a query parameter — and no
|
|
329
329
|
* fragment, because a key belongs in the key field, where it is sent as a bearer
|
|
330
|
-
* and never printed.
|
|
331
|
-
* only in observe mode.
|
|
330
|
+
* and never printed. Loopback http is accepted in every mode.
|
|
332
331
|
*/
|
|
333
332
|
export function validateBaseUrl(raw: unknown): ValidationResult<string> {
|
|
334
333
|
if (typeof raw !== "string" || raw.trim() === "") return { ok: false, problem: "baseUrl must be a non-empty string" };
|
|
@@ -483,14 +482,6 @@ export function providerHostConflict(provider: string, url: string): string | nu
|
|
|
483
482
|
: null;
|
|
484
483
|
}
|
|
485
484
|
|
|
486
|
-
function isPlainHttp(url: string): boolean {
|
|
487
|
-
try {
|
|
488
|
-
return new URL(url).protocol === "http:";
|
|
489
|
-
} catch {
|
|
490
|
-
return false;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
485
|
/**
|
|
495
486
|
* The origin a bearer token would be sent to: https, or http to a loopback host
|
|
496
487
|
* — the same schemes `validateBaseUrl` admits. Null for anything else, so a
|
|
@@ -697,23 +688,11 @@ export function validateJevConfig(
|
|
|
697
688
|
cfg.mode = mode;
|
|
698
689
|
}
|
|
699
690
|
|
|
700
|
-
// Plain http reaches only a loopback host (`validateBaseUrl`)
|
|
701
|
-
//
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
// nothing at all, so enforce is the one mode it is refused in.
|
|
706
|
-
if (cfg.baseUrl !== undefined && isPlainHttp(cfg.baseUrl) && cfg.mode === "enforce") {
|
|
707
|
-
return {
|
|
708
|
-
ok: false,
|
|
709
|
-
problem:
|
|
710
|
-
"plain http (to localhost) is accepted only with mode observe: in enforce mode Jev's answers can clear a deny, " +
|
|
711
|
-
"and while the local proxy is down any process on this machine could take its port and answer. " +
|
|
712
|
-
(cfg.provider === JEV_CLOUD_PROVIDER
|
|
713
|
-
? "Reconnect to an https FailproofAI Cloud URL (failproofai config --token <key> --url https://…), or keep mode observe"
|
|
714
|
-
: "Use https, or mode observe"),
|
|
715
|
-
};
|
|
716
|
-
}
|
|
691
|
+
// Plain http reaches only a loopback host (`validateBaseUrl`) and is
|
|
692
|
+
// accepted in every mode, enforce included, so a local stack can be tested
|
|
693
|
+
// as it runs in production. Risk: nothing authenticates a loopback server,
|
|
694
|
+
// so while it is down any process of this user could bind its port and
|
|
695
|
+
// answer; use https wherever that matters.
|
|
717
696
|
|
|
718
697
|
return { ok: true, value: cfg };
|
|
719
698
|
}
|
|
@@ -73,6 +73,7 @@ import { DEFAULT_JEV_MODE, type JevConfig } from "./jev-config";
|
|
|
73
73
|
import * as jevThrottle from "./jev-throttle";
|
|
74
74
|
import { sessionProjectRoot } from "./session-root";
|
|
75
75
|
import type { PolicyOutcome, SemanticInput } from "./types";
|
|
76
|
+
import { JEV_CHECKS_MAX, JEV_CHECK_PROBES_MAX, JEV_PROBE_ID_RE, round2, type JevCheckRecord } from "../jev-activity";
|
|
76
77
|
|
|
77
78
|
/**
|
|
78
79
|
* T5's throttle as this file calls it. §7 declares only
|
|
@@ -110,7 +111,16 @@ const throttle: JevThrottle = jevThrottle;
|
|
|
110
111
|
* for a call.
|
|
111
112
|
*/
|
|
112
113
|
interface IntentStore {
|
|
113
|
-
|
|
114
|
+
/**
|
|
115
|
+
* `profile` is required here, unlike in `intent.ts`: the hook path always
|
|
116
|
+
* reads under the agent profile the prompt was captured under, never the
|
|
117
|
+
* flat store, so one profile's prompt can never be another's intent.
|
|
118
|
+
*/
|
|
119
|
+
readIntent(
|
|
120
|
+
sessionId: string | undefined,
|
|
121
|
+
now: number | undefined,
|
|
122
|
+
profile: intentStore.IntentProfile,
|
|
123
|
+
): { userSaid: string[]; agentLastMessage: string | null };
|
|
114
124
|
}
|
|
115
125
|
const intentReader: IntentStore = intentStore;
|
|
116
126
|
|
|
@@ -167,12 +177,26 @@ export interface JevCallContext {
|
|
|
167
177
|
permissionMode?: string;
|
|
168
178
|
sessionId?: string;
|
|
169
179
|
cli: string;
|
|
180
|
+
/**
|
|
181
|
+
* The runtime agent identity of the profile making the call (the handler's
|
|
182
|
+
* `readRuntimeAgentIdentity`; null when it has none). With `cli`, it names
|
|
183
|
+
* the intent store slot the human's prompts are read from, so they are the
|
|
184
|
+
* prompts typed into this profile and no other. Absent is read as null.
|
|
185
|
+
*/
|
|
186
|
+
agent?: AgentIdentity | null;
|
|
170
187
|
/**
|
|
171
188
|
* Set when FailproofAI Cloud's Jev mode asks (CONTRACT C10.2): the call goes
|
|
172
189
|
* to Cloud with this machine's id, and the deployment it was made under
|
|
173
190
|
* scopes the answer cache (a new deployment can change Cloud's checks).
|
|
174
191
|
*/
|
|
175
|
-
cloud?: {
|
|
192
|
+
cloud?: {
|
|
193
|
+
machineId: string;
|
|
194
|
+
deployment: number | null;
|
|
195
|
+
mode: "observe" | "enforce";
|
|
196
|
+
agent?: AgentIdentity | null;
|
|
197
|
+
/** Cloud policies whose Jev assignment here is `observe`: recorded, never applied. */
|
|
198
|
+
observedPolicies?: ReadonlyArray<string>;
|
|
199
|
+
};
|
|
176
200
|
/**
|
|
177
201
|
* A session pause is active. It suspends local policy, so no installed
|
|
178
202
|
* pack's check is asked; FailproofAI Cloud's checks are exempt from a pause
|
|
@@ -248,6 +272,66 @@ export function fallbackCode(reason: string): string {
|
|
|
248
272
|
return /^[a-z0-9][a-z0-9-]{0,63}$/.test(code) ? code : "error";
|
|
249
273
|
}
|
|
250
274
|
|
|
275
|
+
/** Why an outcome ended where it did (`JevCheckRecord.why`), from its own fields. */
|
|
276
|
+
function checkWhy(o: PolicyOutcome): JevCheckRecord["why"] {
|
|
277
|
+
switch (o.verdict) {
|
|
278
|
+
case "none":
|
|
279
|
+
// Both deciders: under `fire` nothing fired; at or over it only the exemption stops it.
|
|
280
|
+
return o.evidence < DEFAULT_THRESHOLDS_V1.fire ? "not-detected" : "exempt";
|
|
281
|
+
case "overridden":
|
|
282
|
+
return o.intent === "op-requested" ? "op-requested" : o.intent === "task-step" ? "task-step" : "user-asked";
|
|
283
|
+
case "deny":
|
|
284
|
+
return o.escalatedByInjection ? "injection" : "fired";
|
|
285
|
+
default:
|
|
286
|
+
if (o.intent === "downgraded-task-step") return "downgraded";
|
|
287
|
+
return o.targetScanIncomplete ? "scan-incomplete" : "fired";
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Who declared a check, as a registered-name prefix: `cloud/<id>@<v>` or `pack/<id>@<v>`. */
|
|
292
|
+
function checkPolicy(o: PolicyOutcome): string | undefined {
|
|
293
|
+
const packId = o.origin?.packId;
|
|
294
|
+
if (!packId) return undefined;
|
|
295
|
+
const at = o.origin?.packVersion ? `@${o.origin.packVersion}` : "";
|
|
296
|
+
return packId.startsWith("cloud:") ? `cloud/${packId.slice("cloud:".length)}${at}` : `pack/${packId}${at}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Every asked check's answer, for the activity row (`jevChecks`): verdict,
|
|
301
|
+
* why, evidence, exempt and each probe's probability. `answers` holds the
|
|
302
|
+
* machine's own answers and Cloud's for its checks (`<check>.<probe>`).
|
|
303
|
+
*/
|
|
304
|
+
export function checkRecords(
|
|
305
|
+
outcomes: ReadonlyArray<PolicyOutcome>,
|
|
306
|
+
answers: Readonly<Record<string, number>>,
|
|
307
|
+
watched: ReadonlyArray<PolicyOutcome> = [],
|
|
308
|
+
): JevCheckRecord[] {
|
|
309
|
+
const all = [...outcomes.map((o) => ({ o, watched: false })), ...watched.map((o) => ({ o, watched: true }))];
|
|
310
|
+
return all.slice(0, JEV_CHECKS_MAX).map(({ o, watched: isWatched }) => {
|
|
311
|
+
const probes: Record<string, number> = {};
|
|
312
|
+
const prefix = `${o.policy}.`;
|
|
313
|
+
for (const [id, p] of Object.entries(answers)) {
|
|
314
|
+
if (Object.keys(probes).length >= JEV_CHECK_PROBES_MAX) break;
|
|
315
|
+
if (!id.startsWith(prefix)) continue;
|
|
316
|
+
const part = id.slice(prefix.length);
|
|
317
|
+
// `exempt` has its own field; `user_asked` (v0) is a consent question, not a probe.
|
|
318
|
+
if (part === "exempt" || part === "user_asked" || !JEV_PROBE_ID_RE.test(part)) continue;
|
|
319
|
+
probes[part] = round2(p);
|
|
320
|
+
}
|
|
321
|
+
const policy = checkPolicy(o);
|
|
322
|
+
return {
|
|
323
|
+
check: o.policy,
|
|
324
|
+
...(policy ? { policy } : {}),
|
|
325
|
+
verdict: o.verdict,
|
|
326
|
+
why: checkWhy(o),
|
|
327
|
+
p: round2(o.evidence),
|
|
328
|
+
...(o.exempt !== null ? { exempt: round2(o.exempt) } : {}),
|
|
329
|
+
...(Object.keys(probes).length > 0 ? { probes } : {}),
|
|
330
|
+
...(isWatched ? { watched: true as const } : {}),
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
251
335
|
/**
|
|
252
336
|
* @param cached - the answer came from T5's cache, not the provider: it is
|
|
253
337
|
* applied like any other, but its ~0 ms is not a latency (`latencyMs`
|
|
@@ -269,7 +353,8 @@ export function toReview(outcome: SemanticOutcome, cached = false): JevReview {
|
|
|
269
353
|
// outcome is what clears a `both` policy's regex half. One it cannot vouch
|
|
270
354
|
// for — or a Cloud-origin outcome with no Cloud reply at all — means the
|
|
271
355
|
// picture is not one to clear anything on: the regex decides alone.
|
|
272
|
-
const
|
|
356
|
+
const cloudSummary = "cloud" in outcome ? (outcome as CloudSemanticOutcome & { status: "ok" }).cloud : null;
|
|
357
|
+
const cloudAsked = cloudSummary ? new Set(cloudSummary.asked) : null;
|
|
273
358
|
const unvouched = outcomes.some((o) => isCloudOutcome(o) && !(cloudAsked?.has(o.policy) ?? false));
|
|
274
359
|
if (unvouched) return { kind: "fallback", reason: "malformed", latencyMs, model: null };
|
|
275
360
|
// `null` when the injection probe was not in the request, which now means
|
|
@@ -280,9 +365,14 @@ export function toReview(outcome: SemanticOutcome, cached = false): JevReview {
|
|
|
280
365
|
// not downgrade from a deny (see "A check that fired without consent keeps
|
|
281
366
|
// the floor" in `combine.ts`).
|
|
282
367
|
const warned = (o: (typeof outcomes)[number]) => o.verdict === "instruct" && o.intent !== "downgraded-task-step";
|
|
368
|
+
const cloudAnswers = cloudSummary?.answers ?? {};
|
|
369
|
+
const watched = cloudSummary?.observedOutcomes ?? [];
|
|
283
370
|
return {
|
|
284
371
|
kind: "answered",
|
|
285
372
|
decision: outcome.verdict.decision,
|
|
373
|
+
// What each asked check concluded, for the activity row. `[]` when the
|
|
374
|
+
// request carried no check (globals only, or Cloud's short-circuit).
|
|
375
|
+
checks: sent ? checkRecords(outcomes, { ...outcome.answers, ...cloudAnswers }, watched) : [],
|
|
286
376
|
reason: outcome.verdict.reason,
|
|
287
377
|
policyName: jevPolicyName(outcome),
|
|
288
378
|
...(jevDecider(outcome)?.origin ? { origin: jevDecider(outcome)?.origin } : {}),
|
|
@@ -324,6 +414,18 @@ export function toReview(outcome: SemanticOutcome, cached = false): JevReview {
|
|
|
324
414
|
requestCut: outcome.requestCut && sent,
|
|
325
415
|
latencyMs,
|
|
326
416
|
model,
|
|
417
|
+
// An `observe` Cloud assignment's checks: asked, recorded, never applied.
|
|
418
|
+
...(sent && cloudSummary?.observedAsked?.length ? { observedAsked: cloudSummary.observedAsked } : {}),
|
|
419
|
+
...(cloudSummary?.observed?.length
|
|
420
|
+
? {
|
|
421
|
+
observedCloud: cloudSummary.observed.map((o) => ({
|
|
422
|
+
cloudPolicyId: o.cloudPolicyId,
|
|
423
|
+
cloudVersion: o.cloudVersion,
|
|
424
|
+
decision: o.decision,
|
|
425
|
+
reason: o.reason,
|
|
426
|
+
})),
|
|
427
|
+
}
|
|
428
|
+
: {}),
|
|
327
429
|
};
|
|
328
430
|
}
|
|
329
431
|
|
|
@@ -348,6 +450,7 @@ export function startJevReview(cfg: JevConfig, call: JevCallContext): TwoTierRev
|
|
|
348
450
|
if (!settled) controller.abort();
|
|
349
451
|
},
|
|
350
452
|
authorityOf,
|
|
453
|
+
...(call.cloud ? { cloud: true } : {}),
|
|
351
454
|
});
|
|
352
455
|
|
|
353
456
|
let route: ReturnType<typeof transportForConfig>;
|
|
@@ -380,7 +483,10 @@ export function startJevReview(cfg: JevConfig, call: JevCallContext): TwoTierRev
|
|
|
380
483
|
|
|
381
484
|
let intent: ReturnType<IntentStore["readIntent"]>;
|
|
382
485
|
try {
|
|
383
|
-
intent = intentReader.readIntent(call.sessionId
|
|
486
|
+
intent = intentReader.readIntent(call.sessionId, undefined, {
|
|
487
|
+
cli: call.cli,
|
|
488
|
+
agent: call.agent !== undefined ? call.agent : (call.cloud?.agent ?? null),
|
|
489
|
+
});
|
|
384
490
|
} catch {
|
|
385
491
|
intent = { userSaid: [], agentLastMessage: null };
|
|
386
492
|
}
|
|
@@ -429,6 +535,7 @@ export function startJevReview(cfg: JevConfig, call: JevCallContext): TwoTierRev
|
|
|
429
535
|
...options,
|
|
430
536
|
machineId: cloud.machineId,
|
|
431
537
|
agent: cloud.agent,
|
|
538
|
+
observedPolicies: cloud.observedPolicies ?? [],
|
|
432
539
|
// Paused: no installed pack's check is sent, only the globals, and
|
|
433
540
|
// Cloud's own checks are selected and decided on Cloud as ever.
|
|
434
541
|
...(call.localPaused ? { policies: [] } : {}),
|