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
|
@@ -22,7 +22,7 @@ from .. import _click_compat as click # the Click Typer is running; see _click_
|
|
|
22
22
|
from ..enforcement import RefError, RefUsageError, parse_semantic, read_source, resolve_kind
|
|
23
23
|
from ..policy_check import SyntaxResult, check_syntax, run_policy
|
|
24
24
|
from ..errors import ApiError, NotFoundError
|
|
25
|
-
from . import _write
|
|
25
|
+
from . import _cloud, _write
|
|
26
26
|
|
|
27
27
|
#: Every command here is session-only. These endpoints are ROOT-ONLY on the
|
|
28
28
|
#: server — deliberately absent from `/v1`, because `/v1` is internet-facing and
|
|
@@ -565,4 +565,146 @@ def register(app: typer.Typer) -> None:
|
|
|
565
565
|
policies_app.command("delete", epilog=GLOBALS_EPILOG)(policies_delete)
|
|
566
566
|
policies_app.command("test", epilog=GLOBALS_EPILOG)(policies_test)
|
|
567
567
|
policies_app.command("compose", epilog=GLOBALS_EPILOG)(policies_compose)
|
|
568
|
+
policies_app.command("author", epilog=GLOBALS_EPILOG)(policies_author)
|
|
569
|
+
policies_app.command("backtest", epilog=GLOBALS_EPILOG)(policies_backtest)
|
|
570
|
+
policies_app.command("candidacy", epilog=GLOBALS_EPILOG)(policies_candidacy)
|
|
568
571
|
app.add_typer(policies_app, name="policies")
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def policies_author(
|
|
575
|
+
ctx: typer.Context,
|
|
576
|
+
intent: str = typer.Argument(...),
|
|
577
|
+
kind: Optional[str] = typer.Option(
|
|
578
|
+
None, "--kind",
|
|
579
|
+
help="regex | jev | both. Default: the assistant writes a regex (JavaScript) policy.",
|
|
580
|
+
),
|
|
581
|
+
) -> None:
|
|
582
|
+
"""Author a vetted policy from an intent (session; does not publish).
|
|
583
|
+
|
|
584
|
+
`--kind jev` asks for Jev checks only, `--kind both` for JavaScript plus the
|
|
585
|
+
Jev checks that may clear it. Publish the result with `fp policies publish`.
|
|
586
|
+
"""
|
|
587
|
+
_cloud.session_only(ctx.obj, "policies author")
|
|
588
|
+
if kind is not None and kind not in ("regex", "jev", "both"):
|
|
589
|
+
raise click.UsageError("--kind must be regex, jev or both")
|
|
590
|
+
body = {"issue": {"title": intent, "source": "manual"}}
|
|
591
|
+
if kind in ("jev", "both"):
|
|
592
|
+
# The dashboard proxy treats anything else as regex, exactly as before kinds.
|
|
593
|
+
body["kind"] = kind
|
|
594
|
+
events = list(api.cloud_stream(require_auth(ctx.obj), "/api/agent/policy-author", body))
|
|
595
|
+
for event in events:
|
|
596
|
+
if event.get("type") == "error":
|
|
597
|
+
raise ApiError(str(event.get("message") or event.get("error") or event.get("reason") or "Policy author failed"))
|
|
598
|
+
result = next((e.get("result", e) for e in reversed(events) if e.get("type") in ("result", "done")), None)
|
|
599
|
+
if result is None:
|
|
600
|
+
raise ApiError("Policy author returned no result; nothing was published")
|
|
601
|
+
_cloud.emit(ctx.obj, result, "Policy draft")
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def policies_backtest(
|
|
605
|
+
ctx: typer.Context,
|
|
606
|
+
file_or_id: Optional[str] = typer.Argument(None, metavar="FILE_OR_ID"),
|
|
607
|
+
kind: Optional[str] = typer.Option(
|
|
608
|
+
None, "--kind",
|
|
609
|
+
help="regex | jev | both. Default: a published policy's own kind; for a file, "
|
|
610
|
+
"regex, or jev/both when --semantic is given.",
|
|
611
|
+
),
|
|
612
|
+
semantic_path: Optional[str] = typer.Option(
|
|
613
|
+
None, "--semantic", help="Jev checks JSON (the same file `policies publish --semantic` takes).",
|
|
614
|
+
),
|
|
615
|
+
hours: Optional[float] = typer.Option(None, "--hours", help="How far back to replay (default 168 for jev/both)."),
|
|
616
|
+
agent_id: Optional[str] = typer.Option(None, "--agent-id", help="Replay only this agent's calls."),
|
|
617
|
+
env: Optional[str] = typer.Option(None, "--env", help="Replay only this environment's calls."),
|
|
618
|
+
run: bool = typer.Option(
|
|
619
|
+
False, "--run",
|
|
620
|
+
help="jev/both: actually ask Jev (spends the org's Jev allowance). Default is a free estimate.",
|
|
621
|
+
),
|
|
622
|
+
sample: Optional[int] = typer.Option(None, "--sample", help="jev/both with --run: calls to ask Jev about (max 50)."),
|
|
623
|
+
) -> None:
|
|
624
|
+
"""Replay local or published policy source against stored events (session).
|
|
625
|
+
|
|
626
|
+
A regex policy (a .mjs file or a published regex id) is replayed module-only,
|
|
627
|
+
as before. A jev/both policy — a published one, or `--semantic checks.json`
|
|
628
|
+
with or without a source file — runs the every-kind backtest: an estimate
|
|
629
|
+
by default, `--run` to spend Jev allowance on up to `--sample` calls.
|
|
630
|
+
Needs policies:write and events:read.
|
|
631
|
+
"""
|
|
632
|
+
from pathlib import Path
|
|
633
|
+
_cloud.session_only(ctx.obj, "policies backtest")
|
|
634
|
+
if kind is not None and kind not in ("regex", "jev", "both"):
|
|
635
|
+
raise click.UsageError("--kind must be regex, jev or both")
|
|
636
|
+
if file_or_id is None and semantic_path is None:
|
|
637
|
+
raise click.UsageError("give a policy file or published id, or --semantic checks.json")
|
|
638
|
+
if hours is not None and not hours > 0:
|
|
639
|
+
raise click.UsageError("--hours must be a positive number")
|
|
640
|
+
if sample is not None and not 1 <= sample <= 50:
|
|
641
|
+
raise click.UsageError("--sample must be between 1 and 50")
|
|
642
|
+
if file_or_id == "-" and semantic_path == "-":
|
|
643
|
+
raise click.UsageError("only one of the source and --semantic can come from stdin")
|
|
644
|
+
cctx = require_auth(ctx.obj)
|
|
645
|
+
source: Optional[str] = None
|
|
646
|
+
semantic = None
|
|
647
|
+
published_kind = None
|
|
648
|
+
if file_or_id is not None:
|
|
649
|
+
if file_or_id == "-" or file_or_id.startswith("@") or Path(file_or_id).is_file():
|
|
650
|
+
source = _write.read_text_arg(file_or_id.removeprefix("@"))
|
|
651
|
+
else:
|
|
652
|
+
data = api.cloud_request(cctx, "GET", "/api/enforcement/policies")
|
|
653
|
+
versions = [p for p in _cloud.rows(data, "policies") if p.get("id") == file_or_id]
|
|
654
|
+
if not versions:
|
|
655
|
+
raise NotFoundError(f"no policy named {file_or_id}")
|
|
656
|
+
newest = max(versions, key=lambda p: p.get("version", 0))
|
|
657
|
+
published_kind = newest.get("kind") or "regex"
|
|
658
|
+
source = newest.get("source") or None
|
|
659
|
+
if semantic_path is None and isinstance(newest.get("semantic"), list):
|
|
660
|
+
semantic = newest["semantic"]
|
|
661
|
+
if semantic_path is not None:
|
|
662
|
+
try:
|
|
663
|
+
semantic = parse_semantic(read_source(semantic_path, isatty=False))
|
|
664
|
+
except RefUsageError as exc:
|
|
665
|
+
raise click.UsageError(str(exc))
|
|
666
|
+
except RefError as exc:
|
|
667
|
+
raise ApiError(str(exc))
|
|
668
|
+
resolved = kind or published_kind or ("regex" if not semantic else ("both" if source else "jev"))
|
|
669
|
+
if resolved == "regex":
|
|
670
|
+
if semantic_path is not None:
|
|
671
|
+
raise click.UsageError("--semantic needs --kind jev or both")
|
|
672
|
+
if not isinstance(source, str) or not source.strip():
|
|
673
|
+
raise click.UsageError("Policy source is empty")
|
|
674
|
+
body = {"source": source}
|
|
675
|
+
if agent_id:
|
|
676
|
+
body["agentId"] = agent_id
|
|
677
|
+
if env:
|
|
678
|
+
body["environment"] = env
|
|
679
|
+
if hours is not None:
|
|
680
|
+
body["hours"] = hours
|
|
681
|
+
_cloud.emit(ctx.obj, api.cloud_request(cctx, "POST", "/api/agent/policy-backtest",
|
|
682
|
+
body=body), "Policy backtest")
|
|
683
|
+
return
|
|
684
|
+
if resolved == "both" and not (source or "").strip():
|
|
685
|
+
raise click.UsageError("a both backtest needs the JavaScript source too")
|
|
686
|
+
if not semantic:
|
|
687
|
+
raise click.UsageError(f"a {resolved} backtest needs Jev checks: pass --semantic checks.json")
|
|
688
|
+
body = {"kind": resolved, "semantic": semantic, "hours": hours if hours is not None else 168,
|
|
689
|
+
"mode": "run" if run else "estimate"}
|
|
690
|
+
if resolved == "both":
|
|
691
|
+
body["source"] = source
|
|
692
|
+
if agent_id:
|
|
693
|
+
body["agentId"] = agent_id
|
|
694
|
+
if env:
|
|
695
|
+
body["environment"] = env
|
|
696
|
+
if sample is not None:
|
|
697
|
+
body["sample"] = sample
|
|
698
|
+
if run and not ctx.obj.json:
|
|
699
|
+
output.warn("--run asks Jev about stored calls and spends the org's Jev allowance.")
|
|
700
|
+
_cloud.emit(ctx.obj, api.cloud_request(cctx, "POST", "/api/agent/policy-backtest-all",
|
|
701
|
+
body=body), "Policy backtest")
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def policies_candidacy(ctx: typer.Context, issue: str = typer.Option(..., "--issue")) -> None:
|
|
705
|
+
"""Ask whether a policy could address an issue (session; read-only)."""
|
|
706
|
+
_cloud.session_only(ctx.obj, "policies candidacy")
|
|
707
|
+
cctx = require_auth(ctx.obj)
|
|
708
|
+
row = api.cloud_request(cctx, "GET", f"/api/issues/{issue}")
|
|
709
|
+
_cloud.emit(ctx.obj, api.cloud_request(cctx, "POST", "/api/agent/policy-candidacy",
|
|
710
|
+
body={"issue": row}), "Policy candidacy")
|
|
@@ -17,6 +17,7 @@ from .._context import GLOBALS_EPILOG, AppState, require_auth, resolve_fields, v
|
|
|
17
17
|
from ..errors import NotFoundError
|
|
18
18
|
from ..models import SavedQuery
|
|
19
19
|
from . import _write
|
|
20
|
+
from . import _cloud
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _humanize_list(items: List[str]) -> str:
|
|
@@ -375,4 +376,73 @@ def register(app: typer.Typer) -> None:
|
|
|
375
376
|
query_app.command("delete", epilog=GLOBALS_EPILOG)(query_delete)
|
|
376
377
|
query_app.command("run", epilog=GLOBALS_EPILOG)(query_run)
|
|
377
378
|
query_app.command("schema", epilog=GLOBALS_EPILOG)(query_schema)
|
|
379
|
+
query_app.command("compose", epilog=GLOBALS_EPILOG)(query_compose)
|
|
380
|
+
query_app.command("export", epilog=GLOBALS_EPILOG)(query_export)
|
|
378
381
|
app.add_typer(query_app, name="query")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def query_compose(ctx: typer.Context, question: str = typer.Argument(...),
|
|
385
|
+
run: bool = typer.Option(False, "--run")) -> None:
|
|
386
|
+
"""AI-compose SQL (session only); optionally run it."""
|
|
387
|
+
_cloud.session_only(ctx.obj, "query compose")
|
|
388
|
+
cctx = require_auth(ctx.obj)
|
|
389
|
+
draft = None
|
|
390
|
+
for event in api.cloud_stream(cctx, "/api/agent/compose-sql", {"intent": question}):
|
|
391
|
+
if event.get("type") == "error":
|
|
392
|
+
from ..errors import ApiError
|
|
393
|
+
raise ApiError(str(event.get("error") or event.get("reason") or "SQL composer failed"))
|
|
394
|
+
if event.get("type") in ("result", "done"):
|
|
395
|
+
draft = event.get("result", event)
|
|
396
|
+
if not isinstance(draft, dict):
|
|
397
|
+
raise click.UsageError("Cloud did not return composed SQL")
|
|
398
|
+
sql = draft.get("sql") or draft.get("sql_text")
|
|
399
|
+
if not isinstance(sql, str) or not sql.strip():
|
|
400
|
+
raise click.UsageError("Cloud did not return composed SQL")
|
|
401
|
+
if run:
|
|
402
|
+
data = api.cloud_request(cctx, "POST", "/api/queries/run", body={"sql": sql, "params": []})
|
|
403
|
+
_cloud.emit(ctx.obj, {"sql": sql, "result": data}, "Composed query")
|
|
404
|
+
elif ctx.obj.json:
|
|
405
|
+
output.emit_json(draft)
|
|
406
|
+
else:
|
|
407
|
+
_cloud.emit(ctx.obj, {"sql": sql}, "Composed query")
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def query_export(ctx: typer.Context, id: Optional[str] = typer.Argument(None),
|
|
411
|
+
sql: Optional[str] = typer.Option(None, "--sql"),
|
|
412
|
+
format: str = typer.Option("csv", "--format"),
|
|
413
|
+
out: Optional[str] = typer.Option(None, "--out")) -> None:
|
|
414
|
+
"""Export a saved or ad-hoc query as CSV/JSON (all returned rows).
|
|
415
|
+
|
|
416
|
+
--json wraps CSV in {format,content} unless --out writes the file.
|
|
417
|
+
"""
|
|
418
|
+
import csv
|
|
419
|
+
import io
|
|
420
|
+
import json
|
|
421
|
+
if (id is None) == (sql is None):
|
|
422
|
+
raise click.UsageError("Pass a saved query id or --sql, exactly one")
|
|
423
|
+
if format not in ("csv", "json"):
|
|
424
|
+
raise click.UsageError("--format must be csv or json")
|
|
425
|
+
data = api.cloud_request(require_auth(ctx.obj), "POST", "/api/queries/run",
|
|
426
|
+
body={"query_id": id, "sql": _read_sql(sql) if sql is not None else None, "params": []})
|
|
427
|
+
if format == "json":
|
|
428
|
+
content = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
|
|
429
|
+
else:
|
|
430
|
+
buf = io.StringIO()
|
|
431
|
+
writer = csv.writer(buf)
|
|
432
|
+
writer.writerow([c.get("name", "") if isinstance(c, dict) else c for c in data.get("columns", [])])
|
|
433
|
+
# Spreadsheet-safe export of untrusted event cells.
|
|
434
|
+
writer.writerows([
|
|
435
|
+
["'" + v if isinstance(v, str) and v.startswith(("=", "+", "-", "@", "\t", "\r")) else v for v in row]
|
|
436
|
+
for row in data.get("rows", [])
|
|
437
|
+
])
|
|
438
|
+
content = buf.getvalue()
|
|
439
|
+
if out:
|
|
440
|
+
_cloud.write_file(out, content)
|
|
441
|
+
_cloud.emit(ctx.obj, {"out": out, "format": format}, "Query exported")
|
|
442
|
+
elif ctx.obj.json:
|
|
443
|
+
output.emit_json(data if format == "json" else {"format": format, "content": content})
|
|
444
|
+
elif format == "json":
|
|
445
|
+
from .. import cloud_output
|
|
446
|
+
cloud_output.query_result(data, "Query export")
|
|
447
|
+
else:
|
|
448
|
+
print(content, end="")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Conversation sentiment summary, messages and sessions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from .. import client as api
|
|
10
|
+
from .._context import GLOBALS_EPILOG, require_auth, resolve_dates, validate_limit
|
|
11
|
+
from . import _cloud
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read(ctx, suffix, since=None, limit=None, session_id=None):
|
|
15
|
+
validate_limit(limit)
|
|
16
|
+
frm, to = resolve_dates(since, None, None)
|
|
17
|
+
data = api.cloud_request(require_auth(ctx.obj), "GET", f"/api/events/sentiment{suffix}",
|
|
18
|
+
params={"ts_from": frm, "ts_to": to, "limit": limit, "session_id": session_id})
|
|
19
|
+
_cloud.emit(ctx.obj, data, "Sentiment", view=suffix.strip("/") or "summary")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def summary(ctx: typer.Context, since: Optional[str] = typer.Option(None, "--since")) -> None:
|
|
23
|
+
"""Summarize conversation sentiment."""
|
|
24
|
+
read(ctx, "", since)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def messages(ctx: typer.Context, since: Optional[str] = typer.Option(None, "--since"),
|
|
28
|
+
limit: int = typer.Option(50, "--limit"),
|
|
29
|
+
session_id: Optional[str] = typer.Option(None, "--session-id")) -> None:
|
|
30
|
+
"""List sentiment-scored messages."""
|
|
31
|
+
read(ctx, "/messages", since, limit, session_id)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def sessions(ctx: typer.Context, since: Optional[str] = typer.Option(None, "--since"),
|
|
35
|
+
limit: int = typer.Option(50, "--limit")) -> None:
|
|
36
|
+
"""List sentiment-scored sessions."""
|
|
37
|
+
read(ctx, "/sessions", since, limit)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def session(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
|
|
41
|
+
"""Show one session's sentiment."""
|
|
42
|
+
read(ctx, f"/sessions/{id}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def register(app: typer.Typer) -> None:
|
|
46
|
+
group = typer.Typer(no_args_is_help=True, help="Inspect conversation sentiment.", context_settings={"help_option_names": ["-h", "--help"]})
|
|
47
|
+
for name, fn in (("summary", summary), ("messages", messages), ("sessions", sessions), ("session", session)):
|
|
48
|
+
group.command(name, epilog=GLOBALS_EPILOG)(fn)
|
|
49
|
+
app.add_typer(group, name="sentiment")
|
|
@@ -18,7 +18,9 @@ from .._context import (
|
|
|
18
18
|
validate_choice,
|
|
19
19
|
validate_limit,
|
|
20
20
|
)
|
|
21
|
+
from .. import _click_compat as click
|
|
21
22
|
from ..models import Session
|
|
23
|
+
from . import _cloud, _write
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
def sessions(
|
|
@@ -70,6 +72,8 @@ def sessions(
|
|
|
70
72
|
* `fp sessions --agents` — expand each multi-agent session to its full agent roster
|
|
71
73
|
* `fp --json sessions --all --fields session_id,agents | jq '.sessions'` — every session's full agent roster as JSON
|
|
72
74
|
"""
|
|
75
|
+
if ctx.invoked_subcommand:
|
|
76
|
+
return
|
|
73
77
|
state: AppState = ctx.obj
|
|
74
78
|
cctx = require_auth(state)
|
|
75
79
|
validate_limit(limit)
|
|
@@ -147,5 +151,95 @@ def sessions(
|
|
|
147
151
|
output.recheck_filters_hint(active_filters)
|
|
148
152
|
|
|
149
153
|
|
|
154
|
+
def re_evaluate(ctx: typer.Context, session_id: str = typer.Argument(...)) -> None:
|
|
155
|
+
"""Re-run evaluators on one session. Needs evaluations:trigger."""
|
|
156
|
+
_write.confirm(ctx.obj, f"Re-evaluate session {session_id}", assume_yes=False)
|
|
157
|
+
_cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "POST",
|
|
158
|
+
f"/api/sessions/{session_id}/re-evaluate"), "Session re-evaluation", body=session_id)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def import_sessions(
|
|
162
|
+
ctx: typer.Context,
|
|
163
|
+
files: List[str] = typer.Argument(..., metavar="FILE...", help="Event JSONL, Claude Code transcripts or Codex rollouts (.gz ok)."),
|
|
164
|
+
format: str = typer.Option("auto", "--format", help="auto | failproofai (FailproofAI event JSONL) | claude | codex."),
|
|
165
|
+
env: Optional[str] = typer.Option(None, "--env", help="Environment for events that name none (server default: upload)."),
|
|
166
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Convert and report; store nothing."),
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Import sessions from files, as the dashboard's Import sessions dialog does.
|
|
169
|
+
|
|
170
|
+
Each file is gzipped and sent on its own. Sessions already in the org are skipped
|
|
171
|
+
whole; secrets are redacted the way the collector redacts them live. Claude Code
|
|
172
|
+
and Codex files take the session id from the file name, so pass the original files.
|
|
173
|
+
Needs events:add. With `--json`: `{"files": [{file, result}]}`.
|
|
174
|
+
"""
|
|
175
|
+
import gzip
|
|
176
|
+
import time
|
|
177
|
+
from pathlib import Path
|
|
178
|
+
validate_choice(format, ("auto", "failproofai", "agenteye", "claude", "codex"), flag="--format")
|
|
179
|
+
# The server names FailproofAI's own event JSONL `agenteye`; `failproofai` is its public name.
|
|
180
|
+
wire_format = "agenteye" if format == "failproofai" else format
|
|
181
|
+
payloads = []
|
|
182
|
+
for name in files:
|
|
183
|
+
path = Path(name)
|
|
184
|
+
try:
|
|
185
|
+
raw = path.read_bytes()
|
|
186
|
+
except OSError as exc:
|
|
187
|
+
raise click.BadParameter(f"cannot read '{name}': {exc.strerror or exc}", param_hint="FILE")
|
|
188
|
+
rel = "/".join(path.resolve().parts[-2:])
|
|
189
|
+
payloads.append((name, rel, raw if raw[:2] == b"\x1f\x8b" else gzip.compress(raw)))
|
|
190
|
+
if not dry_run:
|
|
191
|
+
_write.confirm(ctx.obj, f"Import {len(files)} file{'s' if len(files) != 1 else ''}", assume_yes=False)
|
|
192
|
+
cctx = require_auth(ctx.obj)
|
|
193
|
+
# One cutoff for the whole run, so a later file of a session this run created
|
|
194
|
+
# is not skipped as "already present".
|
|
195
|
+
before = int(time.time() * 1000)
|
|
196
|
+
results = []
|
|
197
|
+
for name, rel, data in payloads:
|
|
198
|
+
result = api.import_events_file(cctx, data, {
|
|
199
|
+
"format": wire_format, "path": rel, "environment": env,
|
|
200
|
+
"dry_run": "true" if dry_run else None, "existing_before": before,
|
|
201
|
+
})
|
|
202
|
+
results.append({"file": name, "result": result})
|
|
203
|
+
if not dry_run:
|
|
204
|
+
_write.record_action("sessions_imported", resource="events", success=True)
|
|
205
|
+
_cloud.emit(ctx.obj, {"files": results}, "Session import")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def show(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
|
|
209
|
+
"""Show a session overview and its event timeline."""
|
|
210
|
+
cctx = require_auth(ctx.obj)
|
|
211
|
+
overview = api.cloud_request(cctx, "GET", "/api/sessions", params={"session_id": id, "limit": 1})
|
|
212
|
+
items = _cloud.rows(overview, "sessions")
|
|
213
|
+
if not items:
|
|
214
|
+
from ..errors import NotFoundError
|
|
215
|
+
raise NotFoundError(f"no session {id}")
|
|
216
|
+
timeline = api.cloud_request(cctx, "GET", "/api/events/summary", params={"session_id": id, "limit": 200, "order": "asc"})
|
|
217
|
+
_cloud.emit(ctx.obj, {"overview": items[0], "timeline": timeline}, "Session")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def export(ctx: typer.Context, id: str = typer.Argument(...),
|
|
221
|
+
out: Optional[str] = typer.Option(None, "--out")) -> None:
|
|
222
|
+
"""Export a full session as JSON."""
|
|
223
|
+
import json
|
|
224
|
+
data = api.cloud_request(require_auth(ctx.obj), "GET", f"/api/sessions/{id}/export")
|
|
225
|
+
if out:
|
|
226
|
+
_cloud.write_file(out, json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
|
227
|
+
_cloud.emit(ctx.obj, {"out": out}, "Session exported")
|
|
228
|
+
else:
|
|
229
|
+
_cloud.emit(ctx.obj, data, "Session")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def history(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
|
|
233
|
+
"""Show a session's evaluation history."""
|
|
234
|
+
_cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "GET", f"/api/sessions/{id}/evaluation-history"), "Evaluation history")
|
|
235
|
+
|
|
236
|
+
|
|
150
237
|
def register(app: typer.Typer) -> None:
|
|
151
|
-
|
|
238
|
+
group = typer.Typer(invoke_without_command=True, rich_markup_mode="markdown", context_settings={"help_option_names": ["-h", "--help"]})
|
|
239
|
+
group.callback(epilog=GLOBALS_EPILOG)(sessions)
|
|
240
|
+
group.command("re-evaluate", epilog=GLOBALS_EPILOG)(re_evaluate)
|
|
241
|
+
group.command("show", epilog=GLOBALS_EPILOG)(show)
|
|
242
|
+
group.command("export", epilog=GLOBALS_EPILOG)(export)
|
|
243
|
+
group.command("history", epilog=GLOBALS_EPILOG)(history)
|
|
244
|
+
group.command("import", epilog=GLOBALS_EPILOG)(import_sessions)
|
|
245
|
+
app.add_typer(group, name="sessions")
|
|
@@ -147,4 +147,6 @@ def register(app: typer.Typer) -> None:
|
|
|
147
147
|
settings_app.command("list", epilog=GLOBALS_EPILOG)(settings_list)
|
|
148
148
|
settings_app.command("schema", epilog=GLOBALS_EPILOG)(settings_schema)
|
|
149
149
|
settings_app.command("set", epilog=GLOBALS_EPILOG)(settings_set)
|
|
150
|
+
from .account_cmds import register_context_windows
|
|
151
|
+
register_context_windows(settings_app)
|
|
150
152
|
app.add_typer(settings_app, name="settings")
|
|
@@ -23,6 +23,8 @@ def usage(ctx: typer.Context) -> None:
|
|
|
23
23
|
* `fp usage`
|
|
24
24
|
* `fp --json usage | jq '.usage.events_ingested'`
|
|
25
25
|
"""
|
|
26
|
+
if ctx.invoked_subcommand:
|
|
27
|
+
return
|
|
26
28
|
state: AppState = ctx.obj
|
|
27
29
|
data = api.get_usage(require_auth(state))
|
|
28
30
|
if state.json:
|
|
@@ -32,4 +34,22 @@ def usage(ctx: typer.Context) -> None:
|
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
def register(app: typer.Typer) -> None:
|
|
35
|
-
|
|
37
|
+
group = typer.Typer(invoke_without_command=True, rich_markup_mode="markdown", context_settings={"help_option_names": ["-h", "--help"]})
|
|
38
|
+
group.callback(epilog=GLOBALS_EPILOG)(usage)
|
|
39
|
+
group.command("windows", epilog=GLOBALS_EPILOG)(usage_windows)
|
|
40
|
+
app.add_typer(group, name="usage")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def usage_windows(ctx: typer.Context) -> None:
|
|
44
|
+
"""List usage metering windows. API key only (the dashboard has no session route)."""
|
|
45
|
+
from .account_cmds import request
|
|
46
|
+
from ..client import AuthMode
|
|
47
|
+
from ..errors import KeyModeUnsupportedError
|
|
48
|
+
if ctx.obj.auth_mode is not AuthMode.API_KEY:
|
|
49
|
+
# GET /usage/windows is served on the versioned API only; a signed-in
|
|
50
|
+
# session would reach a dashboard 404 and print a misleading message.
|
|
51
|
+
raise KeyModeUnsupportedError(
|
|
52
|
+
"`fp usage windows` needs an API key — the dashboard has no session route for it.",
|
|
53
|
+
hint="pass --api-key (or FP_API_KEY) with a usage:read key",
|
|
54
|
+
)
|
|
55
|
+
request(ctx, "GET", "/api/usage/windows")
|
|
@@ -401,4 +401,18 @@ def register(app: typer.Typer) -> None:
|
|
|
401
401
|
users_app.command("update", epilog=GLOBALS_EPILOG)(users_update)
|
|
402
402
|
users_app.command("disable", epilog=GLOBALS_EPILOG)(users_disable)
|
|
403
403
|
users_app.command("enable", epilog=GLOBALS_EPILOG)(users_enable)
|
|
404
|
+
users_app.command("delete", epilog=GLOBALS_EPILOG)(users_delete)
|
|
405
|
+
users_app.command("defaults", epilog=GLOBALS_EPILOG)(users_defaults)
|
|
404
406
|
app.add_typer(users_app, name="users")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def users_delete(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
|
|
410
|
+
"""Delete a member by id (not email)."""
|
|
411
|
+
from .account_cmds import request
|
|
412
|
+
request(ctx, "DELETE", f"/api/users/{id}")
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def users_defaults(ctx: typer.Context) -> None:
|
|
416
|
+
"""Show defaults for new members."""
|
|
417
|
+
from .account_cmds import request
|
|
418
|
+
request(ctx, "GET", "/api/users/defaults")
|
|
@@ -43,10 +43,6 @@ VALID_EFFECTS = ("enforce", "observe")
|
|
|
43
43
|
#: JavaScript reviewable by its own Jev checks (`both`).
|
|
44
44
|
VALID_KINDS = ("regex", "jev", "both")
|
|
45
45
|
|
|
46
|
-
#: The Jev modes a deployment may set. `local` stops FailproofAI Cloud
|
|
47
|
-
#: overriding the machine's own mode (the server stores it as no value).
|
|
48
|
-
JEV_MODES = ("off", "observe", "enforce", "local")
|
|
49
|
-
|
|
50
46
|
#: The server's (and the machine's) cap on Jev declarations per policy version.
|
|
51
47
|
MAX_JEV_DECLARATIONS = 24
|
|
52
48
|
AGENT_INTEGRATIONS = frozenset({
|
|
@@ -120,22 +116,6 @@ class DeployPlan:
|
|
|
120
116
|
removed: List[PolicyRef]
|
|
121
117
|
changed: List[Tuple[PolicyRef, PolicyRef]]
|
|
122
118
|
unchanged: List[PolicyRef]
|
|
123
|
-
#: `--jev-mode` as given (`off|observe|enforce|local`), or None when the
|
|
124
|
-
#: command said nothing about Jev — which the server reads as "keep it".
|
|
125
|
-
jev_mode: Optional[str] = None
|
|
126
|
-
#: The mode FailproofAI Cloud sets on the machine now (None = local).
|
|
127
|
-
jev_mode_before: Optional[str] = None
|
|
128
|
-
|
|
129
|
-
@property
|
|
130
|
-
def jev_mode_after(self) -> Optional[str]:
|
|
131
|
-
"""What the machine's Cloud-set mode will be (None = local)."""
|
|
132
|
-
if self.jev_mode is None:
|
|
133
|
-
return self.jev_mode_before
|
|
134
|
-
return None if self.jev_mode == "local" else self.jev_mode
|
|
135
|
-
|
|
136
|
-
@property
|
|
137
|
-
def jev_mode_changes(self) -> bool:
|
|
138
|
-
return self.jev_mode is not None and self.jev_mode_after != self.jev_mode_before
|
|
139
119
|
|
|
140
120
|
@property
|
|
141
121
|
def set_changes(self) -> bool:
|
|
@@ -144,7 +124,7 @@ class DeployPlan:
|
|
|
144
124
|
|
|
145
125
|
@property
|
|
146
126
|
def is_noop(self) -> bool:
|
|
147
|
-
return not
|
|
127
|
+
return not self.set_changes
|
|
148
128
|
|
|
149
129
|
def to_dict(self) -> Dict[str, object]:
|
|
150
130
|
out: Dict[str, object] = {
|
|
@@ -157,13 +137,6 @@ class DeployPlan:
|
|
|
157
137
|
"unchanged": [p.to_dict() for p in self.unchanged],
|
|
158
138
|
"noop": self.is_noop,
|
|
159
139
|
}
|
|
160
|
-
if self.jev_mode is not None:
|
|
161
|
-
# `local` on both sides: the wire word, not the stored NULL.
|
|
162
|
-
out["jevMode"] = {
|
|
163
|
-
"from": self.jev_mode_before or "local",
|
|
164
|
-
"to": self.jev_mode_after or "local",
|
|
165
|
-
"changed": self.jev_mode_changes,
|
|
166
|
-
}
|
|
167
140
|
return out
|
|
168
141
|
|
|
169
142
|
|
|
@@ -237,8 +210,6 @@ def plan_deploy(
|
|
|
237
210
|
replace: Optional[Sequence[str]] = None,
|
|
238
211
|
latest: Optional[Dict[str, int]] = None,
|
|
239
212
|
disabled: Optional[set] = None,
|
|
240
|
-
jev_mode: Optional[str] = None,
|
|
241
|
-
current_jev_mode: Optional[str] = None,
|
|
242
213
|
kinds: Optional[Dict[Tuple[str, int], str]] = None,
|
|
243
214
|
targets: Sequence[str] = (),
|
|
244
215
|
all_agents: Sequence[str] = (),
|
|
@@ -249,16 +220,13 @@ def plan_deploy(
|
|
|
249
220
|
with "these as well" has no single obvious reading, and guessing one would
|
|
250
221
|
be guessing about somebody's fleet.
|
|
251
222
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
223
|
+
There is no Jev mode to set: FailproofAI Cloud derives a machine's Jev mode
|
|
224
|
+
from what is deployed to it (enforce while any `jev`/`both` policy is,
|
|
225
|
+
off otherwise), and a Jev rollout is observed per policy with an `observe`
|
|
226
|
+
effect like any other. `kinds` (`(id, version)` → kind) is accepted for
|
|
227
|
+
callers that already have it.
|
|
257
228
|
"""
|
|
258
|
-
|
|
259
|
-
raise RefUsageError(
|
|
260
|
-
f"--jev-mode {jev_mode!r} is not one of {', '.join(JEV_MODES)}"
|
|
261
|
-
)
|
|
229
|
+
del kinds
|
|
262
230
|
latest = latest or {}
|
|
263
231
|
current_list = list(current or [])
|
|
264
232
|
current_map = {p.id: p for p in current_list}
|
|
@@ -314,13 +282,6 @@ def plan_deploy(
|
|
|
314
282
|
result_map[pid] = dataclass_replace(result_map[pid], agent_targets=grouped.get(pid))
|
|
315
283
|
|
|
316
284
|
result = sorted(result_map.values(), key=lambda p: p.id)
|
|
317
|
-
for ref in result:
|
|
318
|
-
if ref.effect == "observe" and (kinds or {}).get((ref.id, ref.version)) == "jev":
|
|
319
|
-
raise RefUsageError(
|
|
320
|
-
f"{ref.id}@{ref.version} is a Jev policy, and an effect does not apply to Jev "
|
|
321
|
-
"checks — deploy it as enforce and use --jev-mode observe to watch Jev without "
|
|
322
|
-
"it deciding anything"
|
|
323
|
-
)
|
|
324
285
|
added, removed, changed, unchanged = [], [], [], []
|
|
325
286
|
for pid, ref in sorted(result_map.items()):
|
|
326
287
|
was = current_map.get(pid)
|
|
@@ -342,16 +303,9 @@ def plan_deploy(
|
|
|
342
303
|
removed=removed,
|
|
343
304
|
changed=changed,
|
|
344
305
|
unchanged=unchanged,
|
|
345
|
-
jev_mode=jev_mode,
|
|
346
|
-
jev_mode_before=current_jev_mode,
|
|
347
306
|
)
|
|
348
307
|
|
|
349
308
|
|
|
350
|
-
def jev_mode_after(mode: str) -> Optional[str]:
|
|
351
|
-
"""What a `--jev-mode`/`jev-mode` word leaves stored: `local` is None (Cloud stops overriding)."""
|
|
352
|
-
return None if mode == "local" else mode
|
|
353
|
-
|
|
354
|
-
|
|
355
309
|
def version_kinds(policies: Iterable[PolicyVersion]) -> Dict[Tuple[str, int], str]:
|
|
356
310
|
"""`{(id, version): kind}` for every published version."""
|
|
357
311
|
return {(p.id, p.version): p.kind for p in policies}
|
|
@@ -85,6 +85,9 @@ class NotFoundError(FpCliError):
|
|
|
85
85
|
"""The requested resource does not exist."""
|
|
86
86
|
|
|
87
87
|
exit_code = 6
|
|
88
|
+
# The HTTP response may explicitly distinguish an absent endpoint from a
|
|
89
|
+
# missing record. None means callers must fall back to the request shape.
|
|
90
|
+
route_unknown: Optional[bool] = None
|
|
88
91
|
|
|
89
92
|
|
|
90
93
|
class ApiError(FpCliError):
|