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
|
@@ -30,20 +30,26 @@ import { dirname, resolve, sep } from "node:path";
|
|
|
30
30
|
import { CORE_SOURCE } from "./pack-store";
|
|
31
31
|
|
|
32
32
|
import {
|
|
33
|
+
BACK,
|
|
33
34
|
collapseAfter,
|
|
34
35
|
colorsEnabled,
|
|
35
36
|
multiSelect,
|
|
36
37
|
OPEN_SOURCE,
|
|
37
38
|
optsFor,
|
|
38
39
|
paint,
|
|
40
|
+
promptBrowserLogin,
|
|
39
41
|
promptCloudKey,
|
|
40
42
|
screenKit,
|
|
43
|
+
selectOne,
|
|
41
44
|
type CloudKeyAnswer,
|
|
42
45
|
type MultiChoice,
|
|
43
46
|
type OpenSource,
|
|
47
|
+
type SelectChoice,
|
|
44
48
|
type TTYIn,
|
|
45
49
|
type TTYOut,
|
|
46
50
|
} from "./tui";
|
|
51
|
+
import { claimLogin, machineOs, pollLogin, startLogin, type LoginOrg } from "./cloud-login";
|
|
52
|
+
import { canOpenBrowser, openUrl } from "../audit/open-browser";
|
|
47
53
|
import {
|
|
48
54
|
DEFAULT_INGEST_URL,
|
|
49
55
|
readIngestCredential,
|
|
@@ -56,6 +62,7 @@ import {
|
|
|
56
62
|
} from "./integrations";
|
|
57
63
|
import { INTEGRATION_TYPES, type IntegrationType, type HookScope } from "./types";
|
|
58
64
|
import { enforcingPolicyCount, installHooks, notEnforcingReason } from "./manager";
|
|
65
|
+
import { installAgentSkill } from "./agent-skill";
|
|
59
66
|
import {
|
|
60
67
|
CODEX_UNTRUSTED_NOTICE,
|
|
61
68
|
codexHookTrust,
|
|
@@ -998,6 +1005,195 @@ export async function runConfigureWizard(
|
|
|
998
1005
|
stdout,
|
|
999
1006
|
});
|
|
1000
1007
|
|
|
1008
|
+
// A key typed after picking "Paste an API key" from the menu. Same field as
|
|
1009
|
+
// above minus the saved key, which the menu already offered as "keep"; Tab is
|
|
1010
|
+
// the way back to the menu, so it never means open source here.
|
|
1011
|
+
const askKey = (): Promise<CloudKeyAnswer | OpenSource | null> =>
|
|
1012
|
+
promptCloudKey({
|
|
1013
|
+
message: "Connect to cloud",
|
|
1014
|
+
meta: source ? `${host} · from ${source}` : host,
|
|
1015
|
+
tabHint: "go back",
|
|
1016
|
+
emptyError: "Paste a key, or press tab to go back.",
|
|
1017
|
+
check: async (key) => {
|
|
1018
|
+
if (!url) return { ok: false, line: badUrl ?? "No usable endpoint." };
|
|
1019
|
+
const r = await checkCloudKey(url, key);
|
|
1020
|
+
if (r.ok) {
|
|
1021
|
+
typedOrg = r.org;
|
|
1022
|
+
return { ok: true, line: r.note };
|
|
1023
|
+
}
|
|
1024
|
+
return { ok: false, line: r.refused ? `That key was refused: ${r.reason}.` : `Couldn't check the key: ${r.reason}.` };
|
|
1025
|
+
},
|
|
1026
|
+
collapsed: (answer) => (answer === OPEN_SOURCE ? [] : [...cloudLines(planFor(answer)), ""]),
|
|
1027
|
+
stdin,
|
|
1028
|
+
stdout,
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// The browser login. Resolves the approval, BACK for the menu of other
|
|
1032
|
+
// options (Tab / Esc, or a cloud that cannot do this), or null when the person
|
|
1033
|
+
// cancelled the whole run.
|
|
1034
|
+
type Approval = Extract<CloudPlan, { kind: "login" }>;
|
|
1035
|
+
// Set once the cloud says it has no browser login; the menu then drops it.
|
|
1036
|
+
let loginUnsupported = false;
|
|
1037
|
+
const askLogin = (endpoint: string): Promise<{ value: Approval } | typeof BACK | null> => {
|
|
1038
|
+
let pollSecret = "";
|
|
1039
|
+
return promptBrowserLogin<Approval>({
|
|
1040
|
+
message: "Connect to cloud",
|
|
1041
|
+
meta: source ? `${host} · from ${source}` : host,
|
|
1042
|
+
machineLabel,
|
|
1043
|
+
start: async () => {
|
|
1044
|
+
const r = await startLogin(endpoint, { label: machineLabel, os: machineOs(), cliVersion });
|
|
1045
|
+
if (r.kind === "ok") {
|
|
1046
|
+
pollSecret = r.pollSecret;
|
|
1047
|
+
void emit("configure_cloud_login", { step: "started" });
|
|
1048
|
+
return { ok: true, url: r.verificationUrl, expiresIn: r.expiresIn, interval: r.interval };
|
|
1049
|
+
}
|
|
1050
|
+
if (r.kind === "unsupported") {
|
|
1051
|
+
void emit("configure_cloud_login", { step: "unsupported" });
|
|
1052
|
+
loginUnsupported = true;
|
|
1053
|
+
return { ok: false, retryable: false, line: `${host} doesn't support logging in from here yet.` };
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
ok: false,
|
|
1057
|
+
retryable: true,
|
|
1058
|
+
line: r.kind === "refused" ? `Couldn't start the login: ${r.reason}.` : `Couldn't reach ${host}: ${r.reason}.`,
|
|
1059
|
+
};
|
|
1060
|
+
},
|
|
1061
|
+
poll: async () => {
|
|
1062
|
+
const p = await pollLogin(endpoint, pollSecret);
|
|
1063
|
+
switch (p.kind) {
|
|
1064
|
+
case "approved":
|
|
1065
|
+
void emit("configure_cloud_login", { step: "approved" });
|
|
1066
|
+
return {
|
|
1067
|
+
state: "approved",
|
|
1068
|
+
value: {
|
|
1069
|
+
kind: "login",
|
|
1070
|
+
pollSecret,
|
|
1071
|
+
org: p.org,
|
|
1072
|
+
approvedBy: p.approvedBy,
|
|
1073
|
+
keyName: p.keyName,
|
|
1074
|
+
permissions: p.permissions,
|
|
1075
|
+
},
|
|
1076
|
+
};
|
|
1077
|
+
case "pending":
|
|
1078
|
+
return { state: "pending", interval: p.interval };
|
|
1079
|
+
case "slow_down":
|
|
1080
|
+
return { state: "slow_down" };
|
|
1081
|
+
case "denied":
|
|
1082
|
+
void emit("configure_cloud_login", { step: "denied" });
|
|
1083
|
+
return { state: "denied" };
|
|
1084
|
+
case "unreachable":
|
|
1085
|
+
return { state: "unreachable", line: p.reason };
|
|
1086
|
+
// Expired, already claimed, or swept: in every case this link is done.
|
|
1087
|
+
default:
|
|
1088
|
+
return { state: "expired" };
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
open: (link) => {
|
|
1092
|
+
if (!canOpenBrowser()) return false;
|
|
1093
|
+
openUrl(link);
|
|
1094
|
+
return true;
|
|
1095
|
+
},
|
|
1096
|
+
collapsed: (plan) => [...cloudLines(plan), ""],
|
|
1097
|
+
stdin,
|
|
1098
|
+
stdout,
|
|
1099
|
+
});
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
// Whether the saved key was refused when the CONNECT step checked it — still
|
|
1103
|
+
// true at the end of the run if a login there created no key.
|
|
1104
|
+
let savedRefused = false;
|
|
1105
|
+
|
|
1106
|
+
// The interactive CONNECT step. A machine with no connection, or whose saved
|
|
1107
|
+
// key is refused, starts the browser login straight away — Tab or Esc there
|
|
1108
|
+
// opens the other options. A machine that is connected with a working key gets
|
|
1109
|
+
// the menu, "keep" first, so re-running config never opens a browser by itself.
|
|
1110
|
+
const chooseConnection = async (): Promise<CloudPlan | null> => {
|
|
1111
|
+
const verdict =
|
|
1112
|
+
saved && !saved.splitKeys
|
|
1113
|
+
? await transient("Checking your saved key…", () => checkCloudKey(saved.url, saved.token))
|
|
1114
|
+
: null;
|
|
1115
|
+
const savedBroken = verdict !== null && !verdict.ok && verdict.refused;
|
|
1116
|
+
savedRefused = savedBroken;
|
|
1117
|
+
const keptPlan: CloudPlan =
|
|
1118
|
+
verdict && verdict.ok
|
|
1119
|
+
? { kind: "kept", how: "saved", verified: true }
|
|
1120
|
+
: verdict && !verdict.refused
|
|
1121
|
+
? { kind: "kept", how: "saved", verified: false }
|
|
1122
|
+
: { kind: "kept", how: "skip", verified: false };
|
|
1123
|
+
const keepHint = !saved
|
|
1124
|
+
? ""
|
|
1125
|
+
: saved.splitKeys
|
|
1126
|
+
? "leave both keys as they are"
|
|
1127
|
+
: verdict?.ok
|
|
1128
|
+
? `${verdict.org ?? saved.org ?? hostOf(saved.url)} · key ${maskToken(saved.token)}`
|
|
1129
|
+
: savedBroken
|
|
1130
|
+
? "its key no longer works"
|
|
1131
|
+
: "its key couldn't be checked just now";
|
|
1132
|
+
|
|
1133
|
+
type Pick = "login" | "key" | "oss" | "keep";
|
|
1134
|
+
const login: SelectChoice<Pick> = {
|
|
1135
|
+
label: saved ? "Log in again" : "Log in to FailproofAI Cloud",
|
|
1136
|
+
value: "login",
|
|
1137
|
+
hint: savedBroken
|
|
1138
|
+
? "recommended · your saved key no longer works"
|
|
1139
|
+
: saved
|
|
1140
|
+
? "opens your browser and makes a new key"
|
|
1141
|
+
: "recommended · opens your browser",
|
|
1142
|
+
};
|
|
1143
|
+
const key: SelectChoice<Pick> = { label: "Paste an API key", value: "key", hint: "one you already have" };
|
|
1144
|
+
const oss: SelectChoice<Pick> = { label: "Use open source", value: "oss", hint: "skip the cloud" };
|
|
1145
|
+
const keep: SelectChoice<Pick> = { label: "Keep current connection", value: "keep", hint: keepHint };
|
|
1146
|
+
// No initial-cursor option on selectOne: the default is whatever is first.
|
|
1147
|
+
const choices = !saved ? [login, key, oss] : savedBroken ? [login, key, keep] : [keep, login, key];
|
|
1148
|
+
const leftLogin = (result: Awaited<ReturnType<typeof askLogin>>): CloudPlan | null | typeof BACK => {
|
|
1149
|
+
if (result === null || result === BACK) {
|
|
1150
|
+
if (result === BACK && loginUnsupported) {
|
|
1151
|
+
write([kit.caution(`${host} doesn't support logging in from here yet. Paste an API key instead.`), ""]);
|
|
1152
|
+
}
|
|
1153
|
+
return result;
|
|
1154
|
+
}
|
|
1155
|
+
return result.value;
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
if (url && (!saved || savedBroken)) {
|
|
1159
|
+
if (savedBroken && verdict && !verdict.ok) {
|
|
1160
|
+
write([kit.caution(`Your saved key no longer works: ${verdict.reason}. Logging in again.`)]);
|
|
1161
|
+
}
|
|
1162
|
+
void emit("configure_cloud_choice", { choice: "login", connected: Boolean(saved), auto: true });
|
|
1163
|
+
const first = leftLogin(await askLogin(url));
|
|
1164
|
+
if (first !== BACK) return first;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
for (;;) {
|
|
1168
|
+
const pick = await selectOne<Pick>({
|
|
1169
|
+
message: "Connect to cloud",
|
|
1170
|
+
meta: source ? `${host} · from ${source}` : host,
|
|
1171
|
+
choices: loginUnsupported ? choices.filter((c) => c.value !== "login") : choices,
|
|
1172
|
+
collapsed: (p) =>
|
|
1173
|
+
p === "keep" ? [...cloudLines(keptPlan), ""] : p === "oss" ? [...cloudLines({ kind: "oss" }), ""] : [],
|
|
1174
|
+
stdin,
|
|
1175
|
+
stdout,
|
|
1176
|
+
});
|
|
1177
|
+
if (pick === null) return null;
|
|
1178
|
+
void emit("configure_cloud_choice", { choice: pick, connected: Boolean(saved) });
|
|
1179
|
+
if (pick === "keep") return keptPlan;
|
|
1180
|
+
if (pick === "oss") return { kind: "oss" };
|
|
1181
|
+
if (pick === "key") {
|
|
1182
|
+
const answer = await askKey();
|
|
1183
|
+
if (answer === null) return null;
|
|
1184
|
+
if (answer === OPEN_SOURCE) continue;
|
|
1185
|
+
return planFor(answer);
|
|
1186
|
+
}
|
|
1187
|
+
if (!url) {
|
|
1188
|
+
write([kit.fail(badUrl ?? "No usable endpoint."), ""]);
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
const result = leftLogin(await askLogin(url));
|
|
1192
|
+
if (result === BACK) continue;
|
|
1193
|
+
return result;
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1001
1197
|
let cloud: CloudPlan;
|
|
1002
1198
|
if (answers.oss) {
|
|
1003
1199
|
cloud = saved ? { kind: "kept", how: "skip", verified: false } : { kind: "oss" };
|
|
@@ -1032,11 +1228,12 @@ export async function runConfigureWizard(
|
|
|
1032
1228
|
cloud = saved ? { kind: "kept", how: "skip", verified: false } : { kind: "oss" };
|
|
1033
1229
|
write([...cloudLines(cloud), ""]);
|
|
1034
1230
|
} else {
|
|
1035
|
-
// Said above the
|
|
1231
|
+
// Said above the menu, which stays usable for keeping a saved key and for
|
|
1232
|
+
// open source, neither of which sends anything anywhere.
|
|
1036
1233
|
if (badUrl) write([kit.fail(badUrl)]);
|
|
1037
|
-
const
|
|
1038
|
-
if (
|
|
1039
|
-
cloud =
|
|
1234
|
+
const chosen = await chooseConnection();
|
|
1235
|
+
if (chosen === null) return cancel();
|
|
1236
|
+
cloud = chosen;
|
|
1040
1237
|
}
|
|
1041
1238
|
|
|
1042
1239
|
// ── AGENTS ──────────────────────────────────────────────────────────────────
|
|
@@ -1049,8 +1246,10 @@ export async function runConfigureWizard(
|
|
|
1049
1246
|
// Whether the daemon will be collecting once this run ends: a key is already
|
|
1050
1247
|
// on disk, or this run writes one. Removal copy and the backfill request both
|
|
1051
1248
|
// depend on it.
|
|
1052
|
-
|
|
1053
|
-
const
|
|
1249
|
+
// A browser login connects exactly like a pasted key, one step later.
|
|
1250
|
+
const connecting = cloud.kind === "connect" || cloud.kind === "login";
|
|
1251
|
+
const collectingAfter = connecting || readIngestCredential() !== null;
|
|
1252
|
+
const sessionsOn = connecting ? answers.noTranscripts !== true : fp.collector.sessions === true;
|
|
1054
1253
|
|
|
1055
1254
|
let selected: IntegrationType[];
|
|
1056
1255
|
if (answers.agents && answers.agents.length > 0) {
|
|
@@ -1105,6 +1304,37 @@ export async function runConfigureWizard(
|
|
|
1105
1304
|
// 5. The connection last.
|
|
1106
1305
|
const done: string[] = [];
|
|
1107
1306
|
let daemonInstalled = daemonAlreadyRunning;
|
|
1307
|
+
// The `sudo -v` at the top cached root for sudo's timeout — five minutes by
|
|
1308
|
+
// default on macOS, fifteen on most Linux. A browser login, or simply a slow
|
|
1309
|
+
// agents screen, can outlast it, and the install below runs `sudo -n`, which
|
|
1310
|
+
// would then fail AFTER the person did everything else. Ask once more instead.
|
|
1311
|
+
// The stale-unit refresh runs `sudo -n` too, so it is asked for the same way —
|
|
1312
|
+
// but, as at the top, losing root there costs the scheduled audit, not setup.
|
|
1313
|
+
// Unattended runs never primed anything, so there is nothing to have expired.
|
|
1314
|
+
if ((daemonWanted || daemonUnitStale) && !unattended) {
|
|
1315
|
+
let stillRoot = false;
|
|
1316
|
+
try {
|
|
1317
|
+
stillRoot = canElevate();
|
|
1318
|
+
} catch {
|
|
1319
|
+
stillRoot = false;
|
|
1320
|
+
}
|
|
1321
|
+
if (!stillRoot && daemonWanted) {
|
|
1322
|
+
if (!elevate("Your sudo session expired while you were away. Installing failproofaid needs root again.")) {
|
|
1323
|
+
write([
|
|
1324
|
+
kit.fail("Couldn't get root, so setup stopped before changing anything."),
|
|
1325
|
+
` Re-run once you can use sudo: ${kit.cmd("failproofai config")}`,
|
|
1326
|
+
]);
|
|
1327
|
+
void emit("configure_aborted", { reason: "needs_root" });
|
|
1328
|
+
return { applied: false, abort: "needs_root" };
|
|
1329
|
+
}
|
|
1330
|
+
} else if (
|
|
1331
|
+
!stillRoot &&
|
|
1332
|
+
!elevate("Your sudo session expired while you were away. Refreshing failproofaid's service definition needs root again.")
|
|
1333
|
+
) {
|
|
1334
|
+
done.push(kit.caution("No root, so its service definition stays as it is. Scheduled audits stay off until it is refreshed."));
|
|
1335
|
+
daemonUnitStale = false;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1108
1338
|
if (daemonWanted) {
|
|
1109
1339
|
// A unit that is running but cannot answer is torn down before it is
|
|
1110
1340
|
// rebuilt: it holds the singleton flock the replacement needs, so
|
|
@@ -1214,24 +1444,6 @@ export async function runConfigureWizard(
|
|
|
1214
1444
|
pruneOldDaemonBinaries();
|
|
1215
1445
|
}
|
|
1216
1446
|
|
|
1217
|
-
const connectedAfterPlan = cloud.kind === "connect" || cloud.kind === "kept";
|
|
1218
|
-
// Telemetry runs concurrently with the rest of apply (never rejects,
|
|
1219
|
-
// 5s-bounded) and is awaited before returning. `cli` carries the selected
|
|
1220
|
-
// agent ids, as it always has (and as hooks_installed does): a fixed list of
|
|
1221
|
-
// product ids, never user content. The agents_* fields add counts only.
|
|
1222
|
-
const applied = emit("configure_applied", {
|
|
1223
|
-
target: "user",
|
|
1224
|
-
scopes: ["user"],
|
|
1225
|
-
cli: selected,
|
|
1226
|
-
cli_count: selected.length,
|
|
1227
|
-
policy_count: (readScopedHooksConfig("user", cwd).enabledPolicies ?? []).length,
|
|
1228
|
-
connected: cloud.kind === "connect",
|
|
1229
|
-
mode: connectedAfterPlan ? "cloud" : "oss",
|
|
1230
|
-
agents_selected: selected.length,
|
|
1231
|
-
agents_detected: plan.detected.size,
|
|
1232
|
-
agents_changed: added.length + removed.length > 0,
|
|
1233
|
-
});
|
|
1234
|
-
|
|
1235
1447
|
// Hooks, at user scope, for the chosen agents. Whatever policies are enabled
|
|
1236
1448
|
// there are carried through untouched: `replace: true` would otherwise switch
|
|
1237
1449
|
// off policies the user had turned on, and setup must never reduce
|
|
@@ -1259,6 +1471,11 @@ export async function runConfigureWizard(
|
|
|
1259
1471
|
cwd,
|
|
1260
1472
|
);
|
|
1261
1473
|
|
|
1474
|
+
// The failproofai skill, in every agent this machine has and any ticked
|
|
1475
|
+
// without being detected — traced or not, an agent asked about FailproofAI
|
|
1476
|
+
// should know it. A pointer to the skill online (see agent-skill.ts).
|
|
1477
|
+
installAgentSkill(INTEGRATION_TYPES.filter((id) => plan.detected.has(id) || selected.includes(id)));
|
|
1478
|
+
|
|
1262
1479
|
// Agents traced again start fresh (see `requestFreshStart`). Only on a
|
|
1263
1480
|
// rerun: its baseline is the selection the daemon actually filtered on. With
|
|
1264
1481
|
// no saved list (upgrade, first) the daemon was collecting every agent, so an
|
|
@@ -1273,8 +1490,38 @@ export async function runConfigureWizard(
|
|
|
1273
1490
|
// a key on disk doing nothing. `connectToCloud` re-verifies each capability
|
|
1274
1491
|
// and writes only what works.
|
|
1275
1492
|
let connected = false;
|
|
1276
|
-
|
|
1277
|
-
|
|
1493
|
+
// A browser login has an approval, not a key: claim it NOW, with the daemon
|
|
1494
|
+
// in, so a run that stopped earlier left nothing minted anywhere. The claim
|
|
1495
|
+
// is single-use — the server mints the key and hands it over exactly once.
|
|
1496
|
+
let token: string | null = cloud.kind === "connect" ? cloud.token : null;
|
|
1497
|
+
let orgLabel: string | undefined = cloud.kind === "connect" ? cloud.org : undefined;
|
|
1498
|
+
let mintedName: string | undefined;
|
|
1499
|
+
let claimedOrg: LoginOrg | undefined;
|
|
1500
|
+
if (cloud.kind === "login" && url) {
|
|
1501
|
+
const claimed = await transient("Creating this machine's key…", () => claimLogin(url, cloud.pollSecret));
|
|
1502
|
+
void emit("configure_cloud_login", { step: "claimed", ok: claimed.kind === "ok", reason: claimed.kind });
|
|
1503
|
+
if (claimed.kind === "ok") {
|
|
1504
|
+
token = claimed.key;
|
|
1505
|
+
orgLabel = loginOrgLabel(claimed.org);
|
|
1506
|
+
mintedName = claimed.keyName;
|
|
1507
|
+
claimedOrg = claimed.org;
|
|
1508
|
+
} else {
|
|
1509
|
+
hookLogWarn(`cloud login could not be completed: ${claimed.kind}`);
|
|
1510
|
+
done.push(
|
|
1511
|
+
kit.caution(
|
|
1512
|
+
claimed.kind === "expired"
|
|
1513
|
+
? "The login took too long to finish, so no key was created. Everything else was applied."
|
|
1514
|
+
: claimed.kind === "revoked"
|
|
1515
|
+
? "Whoever approved the login can no longer create keys in that org, so no key was created."
|
|
1516
|
+
: claimed.kind === "unreachable"
|
|
1517
|
+
? `Couldn't reach ${host} to create the key (${claimed.reason}); everything else was applied.`
|
|
1518
|
+
: `The key couldn't be created: ${claimed.reason}. Everything else was applied.`,
|
|
1519
|
+
"failproofai config",
|
|
1520
|
+
),
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (token && url) {
|
|
1278
1525
|
try {
|
|
1279
1526
|
const outcome = await transient("Connecting…", () =>
|
|
1280
1527
|
connectToCloud({
|
|
@@ -1284,11 +1531,18 @@ export async function runConfigureWizard(
|
|
|
1284
1531
|
machineLabel,
|
|
1285
1532
|
// Both streams unless the run said `--no-transcripts`.
|
|
1286
1533
|
sessions: answers.noTranscripts !== true,
|
|
1534
|
+
// The org the login was approved into, for when introspect can't say.
|
|
1535
|
+
org: claimedOrg,
|
|
1287
1536
|
}),
|
|
1288
1537
|
);
|
|
1289
1538
|
connected = outcome.anyConfigured;
|
|
1290
1539
|
done.push(...(outcome.changes ?? []));
|
|
1291
|
-
done.push(...connectDoneLines(kit, outcome, host,
|
|
1540
|
+
done.push(...connectDoneLines(kit, outcome, host, orgLabel));
|
|
1541
|
+
// Logging in again makes a NEW key; the one this machine held keeps
|
|
1542
|
+
// working until someone revokes it. Said here, where it is still useful.
|
|
1543
|
+
if (cloud.kind === "login" && saved && connected) {
|
|
1544
|
+
done.push(` Your previous key for this machine still works until you revoke it on ${host}'s keys page.`);
|
|
1545
|
+
}
|
|
1292
1546
|
if (outcome.ingest.ok && answers.noTranscripts === true) {
|
|
1293
1547
|
done.push(" Decisions only: session transcripts are not sent (--no-transcripts).");
|
|
1294
1548
|
}
|
|
@@ -1298,13 +1552,49 @@ export async function runConfigureWizard(
|
|
|
1298
1552
|
hookLogWarn(`cloud connection was not written: ${err instanceof Error ? err.message : String(err)}`);
|
|
1299
1553
|
done.push(kit.caution("Couldn't connect; everything else was applied.", "failproofai config"));
|
|
1300
1554
|
}
|
|
1555
|
+
// The claim spent the approval and the key exists, but nothing here uses
|
|
1556
|
+
// it: say which one, so it can be revoked rather than left live.
|
|
1557
|
+
if (cloud.kind === "login" && !connected) {
|
|
1558
|
+
done.push(` The key ${mintedName ? `"${mintedName}" ` : ""}was created but isn't in use here; revoke it on ${host}'s keys page.`);
|
|
1559
|
+
}
|
|
1301
1560
|
} else if (cloud.kind === "kept" && cloud.how === "saved" && fp.mode !== "cloud") {
|
|
1302
1561
|
// A home migrated from layout 1 carries keys but no mode, and would read
|
|
1303
1562
|
// as open source everywhere that asks.
|
|
1304
1563
|
updateConfig({ mode: "cloud" });
|
|
1305
1564
|
}
|
|
1306
1565
|
|
|
1307
|
-
|
|
1566
|
+
// Logging in again on a connected machine, with nothing connected at the end
|
|
1567
|
+
// of it — no key, or a key `connectToCloud` could not use, which writes
|
|
1568
|
+
// nothing — changed nothing about the connection: the previous one is still
|
|
1569
|
+
// there (never removed from this step, decision D4) — but not as if it worked
|
|
1570
|
+
// when its key was refused.
|
|
1571
|
+
const keptOld = cloud.kind === "login" && !connected && Boolean(saved);
|
|
1572
|
+
if (keptOld) {
|
|
1573
|
+
done.push(
|
|
1574
|
+
savedRefused
|
|
1575
|
+
? " This machine keeps its previous connection, but its key no longer works. Run failproofai config again to log in."
|
|
1576
|
+
: " This machine keeps its previous connection.",
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
const cloudAfter = connected || cloud.kind === "kept" || keptOld;
|
|
1580
|
+
|
|
1581
|
+
// Sent once the connection's outcome is known — a login whose claim then
|
|
1582
|
+
// failed must not count as connected. Never rejects, 5s-bounded. `cli`
|
|
1583
|
+
// carries the selected agent ids, as it always has (and as hooks_installed
|
|
1584
|
+
// does): a fixed list of product ids, never user content. The agents_*
|
|
1585
|
+
// fields add counts only.
|
|
1586
|
+
await emit("configure_applied", {
|
|
1587
|
+
target: "user",
|
|
1588
|
+
scopes: ["user"],
|
|
1589
|
+
cli: selected,
|
|
1590
|
+
cli_count: selected.length,
|
|
1591
|
+
policy_count: policies.length,
|
|
1592
|
+
connected,
|
|
1593
|
+
mode: cloudAfter ? "cloud" : "oss",
|
|
1594
|
+
agents_selected: selected.length,
|
|
1595
|
+
agents_detected: plan.detected.size,
|
|
1596
|
+
agents_changed: added.length + removed.length > 0,
|
|
1597
|
+
});
|
|
1308
1598
|
// Only now — a completed apply — is the launcher considered "seen", so
|
|
1309
1599
|
// first-run onboarding stops offering itself on every command.
|
|
1310
1600
|
markLauncherSeen();
|
|
@@ -1321,7 +1611,7 @@ export async function runConfigureWizard(
|
|
|
1321
1611
|
done,
|
|
1322
1612
|
cwd,
|
|
1323
1613
|
agents: selected.length,
|
|
1324
|
-
connected:
|
|
1614
|
+
connected: cloudAfter,
|
|
1325
1615
|
codexTrust,
|
|
1326
1616
|
}),
|
|
1327
1617
|
);
|
|
@@ -1334,7 +1624,7 @@ export async function runConfigureWizard(
|
|
|
1334
1624
|
policies,
|
|
1335
1625
|
daemonInstalled,
|
|
1336
1626
|
connected,
|
|
1337
|
-
mode:
|
|
1627
|
+
mode: cloudAfter ? "cloud" : "oss",
|
|
1338
1628
|
agents: { selected, added, removed },
|
|
1339
1629
|
};
|
|
1340
1630
|
}
|
|
@@ -1345,7 +1635,19 @@ export async function runConfigureWizard(
|
|
|
1345
1635
|
type CloudPlan =
|
|
1346
1636
|
| { kind: "oss" }
|
|
1347
1637
|
| { kind: "kept"; how: "skip" | "saved"; verified: boolean }
|
|
1348
|
-
| { kind: "connect"; token: string; org?: string; note?: string }
|
|
1638
|
+
| { kind: "connect"; token: string; org?: string; note?: string }
|
|
1639
|
+
/**
|
|
1640
|
+
* Approved in the browser, not yet claimed. The key does not exist until
|
|
1641
|
+
* apply claims it — after the daemon is in — so a run that stops before then
|
|
1642
|
+
* leaves no key behind anywhere.
|
|
1643
|
+
*/
|
|
1644
|
+
| { kind: "login"; pollSecret: string; org: LoginOrg; approvedBy?: string; keyName?: string; permissions: string[] };
|
|
1645
|
+
|
|
1646
|
+
/** "Acme (acme)", or whichever half the server sent. */
|
|
1647
|
+
function loginOrgLabel(org: LoginOrg): string {
|
|
1648
|
+
if (org.name && org.slug) return `${org.name} (${org.slug})`;
|
|
1649
|
+
return org.name ?? org.slug ?? "your org";
|
|
1650
|
+
}
|
|
1349
1651
|
|
|
1350
1652
|
/**
|
|
1351
1653
|
* The connection already on this machine, from either credential: the policy
|
|
@@ -1394,7 +1696,15 @@ export function resolveCloudUrl(
|
|
|
1394
1696
|
const env = process.env.FAILPROOFAI_CLOUD_URL?.trim();
|
|
1395
1697
|
const fromFlag = flag?.trim();
|
|
1396
1698
|
const override = fromFlag || env;
|
|
1397
|
-
if (!override)
|
|
1699
|
+
if (!override) {
|
|
1700
|
+
if (!existing) return { ok: true, url: cloudBaseFor(DEFAULT_INGEST_URL) };
|
|
1701
|
+
// Checked like a typed one: login sends its secret and receives a key here
|
|
1702
|
+
// before anything else would, and the file it came from can be edited.
|
|
1703
|
+
const saved = validateCloudUrl(existing);
|
|
1704
|
+
return saved.ok
|
|
1705
|
+
? { ok: true, url: existing }
|
|
1706
|
+
: { ok: false, message: `the saved cloud address "${existing}" cannot be used: ${saved.reason}` };
|
|
1707
|
+
}
|
|
1398
1708
|
const source = fromFlag && fromFlag !== env ? "--url" : "FAILPROOFAI_CLOUD_URL";
|
|
1399
1709
|
const validated = validateCloudUrl(cloudBaseFor(override));
|
|
1400
1710
|
if (!validated.ok) {
|
|
@@ -1430,6 +1740,11 @@ export async function checkCloudKey(
|
|
|
1430
1740
|
url: string,
|
|
1431
1741
|
key: string,
|
|
1432
1742
|
): Promise<{ ok: true; org?: string; note?: string } | { ok: false; refused: boolean; reason: string }> {
|
|
1743
|
+
// Checked here rather than trusted: a saved address comes from a file that
|
|
1744
|
+
// can be edited, and an http one to another host would carry the key in
|
|
1745
|
+
// the clear. Not a verdict on the key, so not `refused`.
|
|
1746
|
+
const target = validateCloudUrl(url);
|
|
1747
|
+
if (!target.ok) return { ok: false, refused: false, reason: `the address ${url} can't be used: ${target.reason}` };
|
|
1433
1748
|
const identity = await introspectKey(url, key);
|
|
1434
1749
|
if (identity.kind === "rejected") return { ok: false, refused: true, reason: "the server did not accept it" };
|
|
1435
1750
|
if (identity.kind === "unreachable") return { ok: false, refused: false, reason: identity.reason };
|
|
@@ -1477,6 +1792,15 @@ export function cloudCollapsedLines(
|
|
|
1477
1792
|
kit.ok(plan.org ? `Key accepted for ${plan.org} on ${at.host}` : `Key accepted by ${at.host}`),
|
|
1478
1793
|
...(plan.note ? [kit.caution(plan.note)] : []),
|
|
1479
1794
|
];
|
|
1795
|
+
case "login":
|
|
1796
|
+
// Who approved it is said out loud: a link someone else approved into
|
|
1797
|
+
// THEIR org would otherwise connect this machine there unnoticed.
|
|
1798
|
+
return [
|
|
1799
|
+
head,
|
|
1800
|
+
kit.ok(
|
|
1801
|
+
`Logged in${plan.approvedBy ? ` as ${plan.approvedBy}` : ""} · ${loginOrgLabel(plan.org)} on ${at.host}`,
|
|
1802
|
+
),
|
|
1803
|
+
];
|
|
1480
1804
|
}
|
|
1481
1805
|
}
|
|
1482
1806
|
|
|
@@ -23,11 +23,20 @@
|
|
|
23
23
|
* `is_auto_retryable()` excludes it from the retry pass — that is correct, and
|
|
24
24
|
* re-litigating it here would burn the budget of batches that could succeed.
|
|
25
25
|
* The gap was never the detection. It was that the detection was mute.
|
|
26
|
+
*
|
|
27
|
+
* A credential refusal is only evidence about the CURRENT key while nothing
|
|
28
|
+
* has been accepted since. A key regenerated on the dashboard leaves the
|
|
29
|
+
* running collector on the old secret until the daemon's next config tick, so
|
|
30
|
+
* a batch or two is refused while the new key already works. Those parked
|
|
31
|
+
* before the collector's last accepted delivery (`delivery.last_ok_ts`) are
|
|
32
|
+
* counted as `resending`, not refused: the daemon re-sends them with the key
|
|
33
|
+
* that works (`Delivery::redrive_refused`) and drops one only if that key
|
|
34
|
+
* refuses it too.
|
|
26
35
|
*/
|
|
27
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
36
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
28
37
|
import { join } from "node:path";
|
|
29
38
|
|
|
30
|
-
import { failedDir } from "./fp-home";
|
|
39
|
+
import { collectorHealthFile, failedDir } from "./fp-home";
|
|
31
40
|
import { readCredentials } from "./fp-config";
|
|
32
41
|
|
|
33
42
|
/**
|
|
@@ -106,6 +115,13 @@ export interface DeliveryHealth {
|
|
|
106
115
|
credentialRejected: number;
|
|
107
116
|
/** Count per client status, e.g. `{ 401: 26 }`. */
|
|
108
117
|
byStatus: Record<number, number>;
|
|
118
|
+
/**
|
|
119
|
+
* Credential refusals parked BEFORE the last delivery the server accepted.
|
|
120
|
+
* The key in use now works, so these are not counted as refused: the daemon
|
|
121
|
+
* re-sends them with that key (`Delivery::redrive_refused`, fpai-collect)
|
|
122
|
+
* and drops one only if the current key refuses it too.
|
|
123
|
+
*/
|
|
124
|
+
resending: number;
|
|
109
125
|
/** Age of the oldest parked batch, in ms. Undefined when none are parked. */
|
|
110
126
|
oldestAgeMs?: number;
|
|
111
127
|
}
|
|
@@ -115,8 +131,23 @@ const EMPTY: DeliveryHealth = {
|
|
|
115
131
|
rejected: 0,
|
|
116
132
|
credentialRejected: 0,
|
|
117
133
|
byStatus: {},
|
|
134
|
+
resending: 0,
|
|
118
135
|
};
|
|
119
136
|
|
|
137
|
+
/**
|
|
138
|
+
* `delivery.last_ok_ts` from the collector's health file, in ms, or undefined.
|
|
139
|
+
* The last upload the server ACCEPTED (`UploadMetrics::last_ok_ts`).
|
|
140
|
+
*/
|
|
141
|
+
export function readLastDeliveredAt(home?: string): number | undefined {
|
|
142
|
+
try {
|
|
143
|
+
const raw = JSON.parse(readFileSync(collectorHealthFile(home), "utf8")) as { delivery?: { last_ok_ts?: unknown } };
|
|
144
|
+
const ts = raw?.delivery?.last_ok_ts;
|
|
145
|
+
return typeof ts === "number" && Number.isFinite(ts) && ts > 0 ? ts * 1000 : undefined;
|
|
146
|
+
} catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
120
151
|
/**
|
|
121
152
|
* Summarise `failed/` without touching it.
|
|
122
153
|
*
|
|
@@ -127,6 +158,7 @@ export function deliveryHealth(
|
|
|
127
158
|
home?: string,
|
|
128
159
|
now: number = Date.now(),
|
|
129
160
|
connectedAt: number | undefined = home ? undefined : readCredentials().connectedAt,
|
|
161
|
+
lastDeliveredAt: number | undefined = home ? undefined : readLastDeliveredAt(),
|
|
130
162
|
): DeliveryHealth {
|
|
131
163
|
const dir = failedDir(home);
|
|
132
164
|
if (!existsSync(dir)) return EMPTY;
|
|
@@ -144,6 +176,7 @@ export function deliveryHealth(
|
|
|
144
176
|
let total = 0;
|
|
145
177
|
let rejected = 0;
|
|
146
178
|
let credentialRejected = 0;
|
|
179
|
+
let resending = 0;
|
|
147
180
|
let oldestMtime: number | undefined;
|
|
148
181
|
|
|
149
182
|
for (const name of entries) {
|
|
@@ -162,6 +195,17 @@ export function deliveryHealth(
|
|
|
162
195
|
CREDENTIAL_STATUSES.has(parsed.clientStatus) &&
|
|
163
196
|
mtime < connectedAt
|
|
164
197
|
) continue;
|
|
198
|
+
// Refused with a key that has worked since: the latest delivery attempt
|
|
199
|
+
// was accepted, so the CURRENT key is not the one being refused.
|
|
200
|
+
if (
|
|
201
|
+
lastDeliveredAt !== undefined &&
|
|
202
|
+
parsed.clientStatus !== undefined &&
|
|
203
|
+
CREDENTIAL_STATUSES.has(parsed.clientStatus) &&
|
|
204
|
+
mtime <= lastDeliveredAt
|
|
205
|
+
) {
|
|
206
|
+
resending += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
165
209
|
total += 1;
|
|
166
210
|
if (parsed.clientStatus !== undefined) {
|
|
167
211
|
rejected += 1;
|
|
@@ -177,6 +221,7 @@ export function deliveryHealth(
|
|
|
177
221
|
rejected,
|
|
178
222
|
credentialRejected,
|
|
179
223
|
byStatus,
|
|
224
|
+
resending,
|
|
180
225
|
...(oldestMtime !== undefined ? { oldestAgeMs: Math.max(0, now - oldestMtime) } : {}),
|
|
181
226
|
};
|
|
182
227
|
}
|
|
@@ -49,6 +49,8 @@ import { cloudReviewerNames, readCloudAuthorityInputs, readCloudJevMode } from "
|
|
|
49
49
|
import type { AgentIdentity } from "./agent-targets";
|
|
50
50
|
|
|
51
51
|
let cached: ReadonlySet<string> | null = null;
|
|
52
|
+
/** Whether the installed packs alone (not Cloud) declared any check, for the cached answer. */
|
|
53
|
+
let cachedPackChecks = false;
|
|
52
54
|
let cachedContested: ReadonlyMap<string, string[]> = new Map();
|
|
53
55
|
/** The agent the current registration pass is for; see {@link forgetEffectiveReviewerNames}. */
|
|
54
56
|
let reviewerCli: string | undefined;
|
|
@@ -185,9 +187,11 @@ export function effectiveReviewerNames(): ReadonlySet<string> {
|
|
|
185
187
|
if (cached) return cached;
|
|
186
188
|
let names: ReadonlySet<string> = NO_REVIEWERS;
|
|
187
189
|
cachedContested = new Map();
|
|
190
|
+
cachedPackChecks = false;
|
|
188
191
|
try {
|
|
189
192
|
const packs = jevPacks(readInstalledPacks().packs, reviewerCli);
|
|
190
193
|
names = reviewerNamesFor(packs);
|
|
194
|
+
cachedPackChecks = names.size > 0;
|
|
191
195
|
cachedContested = contestedSemanticNames(packs);
|
|
192
196
|
} catch {
|
|
193
197
|
// See above: an unreadable manifest declares no checks.
|
|
@@ -224,6 +228,16 @@ export function jevChecksInstalled(): boolean {
|
|
|
224
228
|
return effectiveReviewerNames().size > 0;
|
|
225
229
|
}
|
|
226
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Whether an installed pack (not FailproofAI Cloud) gives Jev a check for this
|
|
233
|
+
* agent: the part of {@link jevChecksInstalled} a Cloud review needs to know
|
|
234
|
+
* when deciding whether a call has anything of the machine's own to ask.
|
|
235
|
+
*/
|
|
236
|
+
export function packJevChecksInstalled(): boolean {
|
|
237
|
+
effectiveReviewerNames();
|
|
238
|
+
return cachedPackChecks;
|
|
239
|
+
}
|
|
240
|
+
|
|
227
241
|
/**
|
|
228
242
|
* The same question for a CLI command, read fresh rather than from the
|
|
229
243
|
* registration cache (which only the hook path fills): any agent, packs as
|