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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Additive account, billing and integration commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from .. import _click_compat as click, client as api, permissions
|
|
10
|
+
from .._context import GLOBALS_EPILOG, require_auth
|
|
11
|
+
from . import _cloud, _write
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def request(ctx, method, path, body=None, params=None):
|
|
15
|
+
if method != "GET":
|
|
16
|
+
_write.confirm(ctx.obj, f"{method} {path}", assume_yes=False, destructive=method == "DELETE")
|
|
17
|
+
result = api.cloud_request(require_auth(ctx.obj), method, path, body=body, params=params)
|
|
18
|
+
if method != "GET":
|
|
19
|
+
_write.record_action("cloud_resource_changed", resource=path.split("/")[2], success=True, destructive=method == "DELETE")
|
|
20
|
+
_cloud.emit(ctx.obj, result, "Cloud", view=path, method=method, body=body, params=params)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def roles_list(ctx: typer.Context) -> None:
|
|
24
|
+
"""List named permission sets. Needs users:read."""
|
|
25
|
+
request(ctx, "GET", "/api/permission-sets")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def roles_show(ctx: typer.Context, name: str = typer.Argument(...)) -> None:
|
|
29
|
+
"""Show one permission set by name."""
|
|
30
|
+
request(ctx, "GET", f"/api/permission-sets/{name}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def grants(values):
|
|
34
|
+
if not values:
|
|
35
|
+
return []
|
|
36
|
+
try:
|
|
37
|
+
return permissions.parse_permission_tokens(values)
|
|
38
|
+
except permissions.PermissionTokenError as exc:
|
|
39
|
+
raise click.UsageError(str(exc)) from exc
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def roles_create(ctx: typer.Context, name: str = typer.Option(..., "--name"),
|
|
43
|
+
permission: Optional[List[str]] = typer.Option(None, "--permission", "--add"),
|
|
44
|
+
description: Optional[str] = typer.Option(None, "--description"),
|
|
45
|
+
file: Optional[str] = typer.Option(None, "--file")) -> None:
|
|
46
|
+
"""Create a custom permission set. Needs settings:write."""
|
|
47
|
+
if not 1 <= len(name.strip()) <= 64:
|
|
48
|
+
raise click.UsageError("Role name must be 1–64 characters")
|
|
49
|
+
body = _write.read_body(ctx.obj, file, {"permissions": grants(permission) if permission is not None else None, "description": description})
|
|
50
|
+
body["name"] = name.strip()
|
|
51
|
+
body.setdefault("permissions", [])
|
|
52
|
+
if not isinstance(body["permissions"], list):
|
|
53
|
+
raise click.UsageError("permissions must be an array")
|
|
54
|
+
body["permissions"] = grants(body["permissions"])
|
|
55
|
+
request(ctx, "POST", "/api/permission-sets", body)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def roles_update(ctx: typer.Context, name: str = typer.Argument(...),
|
|
59
|
+
permission: Optional[List[str]] = typer.Option(None, "--permission", help="Full replacement grants."),
|
|
60
|
+
add: Optional[List[str]] = typer.Option(None, "--add"),
|
|
61
|
+
remove: Optional[List[str]] = typer.Option(None, "--remove"),
|
|
62
|
+
description: Optional[str] = typer.Option(None, "--description"),
|
|
63
|
+
file: Optional[str] = typer.Option(None, "--file")) -> None:
|
|
64
|
+
"""Update a custom set; affected members receive the new grants."""
|
|
65
|
+
if permission is not None and (add or remove):
|
|
66
|
+
raise click.UsageError("Use --permission or --add/--remove, not both")
|
|
67
|
+
added, removed = grants(add or []), grants(remove or [])
|
|
68
|
+
body = _write.read_body(ctx.obj, file, {"permissions": grants(permission) if permission is not None else None, "description": description})
|
|
69
|
+
if file and (add or remove):
|
|
70
|
+
raise click.UsageError("Use --file or discrete flags")
|
|
71
|
+
if not body and not added and not removed:
|
|
72
|
+
raise click.UsageError("Pass --permission, --add, --remove, --description or --file")
|
|
73
|
+
current = api.cloud_request(require_auth(ctx.obj), "GET", f"/api/permission-sets/{name}")
|
|
74
|
+
if current.get("is_builtin"):
|
|
75
|
+
raise click.UsageError("Builtin permission sets are immutable")
|
|
76
|
+
body.setdefault("permissions", list(dict.fromkeys([v for v in current.get("permissions", []) if v not in removed] + added)))
|
|
77
|
+
body["permissions"] = grants(body["permissions"])
|
|
78
|
+
body.setdefault("description", current.get("description"))
|
|
79
|
+
request(ctx, "PUT", f"/api/permission-sets/{name}", body)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def roles_delete(ctx: typer.Context, name: str = typer.Argument(...)) -> None:
|
|
83
|
+
"""Delete a custom permission set; members keep materialized permissions."""
|
|
84
|
+
request(ctx, "DELETE", f"/api/permission-sets/{name}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def plan(ctx: typer.Context) -> None:
|
|
88
|
+
"""Show current offers and team-trial state."""
|
|
89
|
+
if ctx.invoked_subcommand:
|
|
90
|
+
return
|
|
91
|
+
_cloud.session_only(ctx.obj, "plan")
|
|
92
|
+
cctx = require_auth(ctx.obj)
|
|
93
|
+
data = {"offers": api.cloud_request(cctx, "GET", "/api/offers/me"),
|
|
94
|
+
"team_trial": api.cloud_request(cctx, "GET", "/api/team-trial/me")}
|
|
95
|
+
_cloud.emit(ctx.obj, data, "Plan")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def claim(ctx: typer.Context, offer: str = typer.Argument(...)) -> None:
|
|
99
|
+
"""Claim a named offer."""
|
|
100
|
+
_cloud.session_only(ctx.obj, "plan claim")
|
|
101
|
+
request(ctx, "POST", "/api/offers/claim", {"offer_key": offer})
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def otel_status(ctx: typer.Context) -> None:
|
|
105
|
+
"""Show Cloud OTEL ingest status."""
|
|
106
|
+
request(ctx, "GET", "/api/otel/status")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def otel_enable(ctx: typer.Context) -> None:
|
|
110
|
+
"""Enable OTEL ingestion for the Cloud org."""
|
|
111
|
+
request(ctx, "PUT", "/api/settings/otel.ingest_enabled", {"value": True})
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def otel_disable(ctx: typer.Context) -> None:
|
|
115
|
+
"""Disable OTEL ingestion for the Cloud org."""
|
|
116
|
+
request(ctx, "PUT", "/api/settings/otel.ingest_enabled", {"value": False})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def slack_status(ctx: typer.Context) -> None:
|
|
120
|
+
"""Show Slack connection status (session only)."""
|
|
121
|
+
_cloud.session_only(ctx.obj, "slack status")
|
|
122
|
+
request(ctx, "GET", "/api/integrations/slack")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def slack_disconnect(ctx: typer.Context) -> None:
|
|
126
|
+
"""Disconnect Slack (session only)."""
|
|
127
|
+
_cloud.session_only(ctx.obj, "slack disconnect")
|
|
128
|
+
request(ctx, "DELETE", "/api/integrations/slack")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def context_list(ctx: typer.Context) -> None:
|
|
132
|
+
"""List model context-window overrides."""
|
|
133
|
+
request(ctx, "GET", "/api/settings/model-context-windows")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def context_set(ctx: typer.Context, model: str = typer.Argument(...),
|
|
137
|
+
tokens: int = typer.Argument(...)) -> None:
|
|
138
|
+
"""Set an override; zero means unknown."""
|
|
139
|
+
if not 1 <= len(model.strip()) <= 256 or not 0 <= tokens <= 1_000_000:
|
|
140
|
+
raise click.UsageError("Model must be 1–256 characters; tokens must be 0–1000000")
|
|
141
|
+
request(ctx, "PUT", "/api/settings/model-context-windows", {"model_id": model.strip(), "context_window": tokens})
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def context_delete(ctx: typer.Context, model: str = typer.Argument(...)) -> None:
|
|
145
|
+
"""Remove a model override."""
|
|
146
|
+
if not model.strip():
|
|
147
|
+
raise click.UsageError("Model cannot be empty")
|
|
148
|
+
request(ctx, "DELETE", "/api/settings/model-context-windows", params={"model": model})
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def register_context_windows(settings_app):
|
|
152
|
+
group = typer.Typer(no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]})
|
|
153
|
+
for name, fn in (("list", context_list), ("set", context_set), ("delete", context_delete)):
|
|
154
|
+
group.command(name, epilog=GLOBALS_EPILOG)(fn)
|
|
155
|
+
settings_app.add_typer(group, name="context-windows")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def register(app):
|
|
159
|
+
for name, help_text, commands in (
|
|
160
|
+
("roles", "Manage permission sets.", (("list", roles_list), ("show", roles_show), ("create", roles_create), ("update", roles_update), ("delete", roles_delete))),
|
|
161
|
+
("otel", "Control Cloud OTEL ingest.", (("status", otel_status), ("enable", otel_enable), ("disable", otel_disable))),
|
|
162
|
+
("slack", "Manage the Slack integration (session only).", (("status", slack_status), ("disconnect", slack_disconnect))),
|
|
163
|
+
):
|
|
164
|
+
group = typer.Typer(no_args_is_help=True, help=help_text, context_settings={"help_option_names": ["-h", "--help"]})
|
|
165
|
+
for command, fn in commands:
|
|
166
|
+
group.command(command, epilog=GLOBALS_EPILOG)(fn)
|
|
167
|
+
app.add_typer(group, name=name)
|
|
168
|
+
group = typer.Typer(invoke_without_command=True, context_settings={"help_option_names": ["-h", "--help"]})
|
|
169
|
+
group.callback(epilog=GLOBALS_EPILOG)(plan)
|
|
170
|
+
group.command("claim", epilog=GLOBALS_EPILOG)(claim)
|
|
171
|
+
app.add_typer(group, name="plan")
|
|
@@ -21,10 +21,10 @@ import typer
|
|
|
21
21
|
|
|
22
22
|
from .. import _click_compat as click # the Click Typer is running; see _click_compat
|
|
23
23
|
from .. import client as api
|
|
24
|
-
from .. import output
|
|
24
|
+
from .. import cloud_output, output
|
|
25
25
|
from .._context import GLOBALS_EPILOG, AppState, deny_in_key_mode, require_auth
|
|
26
|
-
from ..errors import NotFoundError
|
|
27
|
-
from . import _write
|
|
26
|
+
from ..errors import FpCliError, NetworkError, NotFoundError
|
|
27
|
+
from . import _cloud, _write
|
|
28
28
|
|
|
29
29
|
# The assistant has NO server route at all: `/agent/chat` and `/agent/health` are
|
|
30
30
|
# implemented by the dashboard itself, and the conversation routes are per-human
|
|
@@ -279,6 +279,7 @@ def agent_ask(
|
|
|
279
279
|
chat: Optional[str] = typer.Option(None, "--chat", help="Continue this chat — its short id from `agent chats` (a prefix) or full id. Omit to start a NEW chat (its id is printed)."),
|
|
280
280
|
model: Optional[str] = typer.Option(None, "--model", help="Model to use (see `agent models`)."),
|
|
281
281
|
page_context: Optional[str] = typer.Option(None, "--page-context", help="Optional context string to ground the answer."),
|
|
282
|
+
approve: bool = typer.Option(False, "--approve", help="Explicitly approve requested writes. Without this, scripts/--json decline."),
|
|
282
283
|
) -> None:
|
|
283
284
|
"""Ask the assistant a question — every ask is saved to a chat.
|
|
284
285
|
|
|
@@ -301,7 +302,7 @@ def agent_ask(
|
|
|
301
302
|
* `fp agent ask "summarize p95 latency" --model claude-opus-4-7`
|
|
302
303
|
"""
|
|
303
304
|
state: AppState = ctx.obj
|
|
304
|
-
|
|
305
|
+
_cloud.session_only(state, "agent ask")
|
|
305
306
|
if not message:
|
|
306
307
|
message = sys.stdin.read().strip() if not sys.stdin.isatty() else typer.prompt("question", err=True)
|
|
307
308
|
if not message:
|
|
@@ -323,11 +324,49 @@ def agent_ask(
|
|
|
323
324
|
]
|
|
324
325
|
thread.append({"role": "user", "content": {"text": message}})
|
|
325
326
|
|
|
327
|
+
artifacts = []
|
|
328
|
+
|
|
329
|
+
def on_artifact(block):
|
|
330
|
+
rendered = run_artifact(cctx, block)
|
|
331
|
+
artifacts.append(rendered)
|
|
332
|
+
if not state.json:
|
|
333
|
+
render_artifact(rendered)
|
|
334
|
+
|
|
335
|
+
def on_ask_user(event):
|
|
336
|
+
decision = "reject"
|
|
337
|
+
if event.get("kind") == "approval":
|
|
338
|
+
if approve:
|
|
339
|
+
decision = "approve"
|
|
340
|
+
elif not state.json and sys.stdin.isatty():
|
|
341
|
+
output.info(str(event.get("question", "Approve this write?")))
|
|
342
|
+
if event.get("details"):
|
|
343
|
+
output.info(str(event["details"]))
|
|
344
|
+
if typer.confirm("Approve this change", default=False, err=True):
|
|
345
|
+
decision = "approve"
|
|
346
|
+
answer = {"callId": event["callId"], "decision": decision}
|
|
347
|
+
try:
|
|
348
|
+
api.cloud_request(cctx, "POST", "/api/agent/answer", body=answer)
|
|
349
|
+
except FpCliError as exc:
|
|
350
|
+
# Unwind the stream and Rich status before the central error renderer
|
|
351
|
+
# prints one line. Preserve the transport's exit code and request id.
|
|
352
|
+
reason = (
|
|
353
|
+
"cannot reach FailproofAI Cloud" if isinstance(exc, NetworkError)
|
|
354
|
+
else " ".join(exc.message.split()).rstrip(".")
|
|
355
|
+
)
|
|
356
|
+
exc.message = (
|
|
357
|
+
f"Assistant approval answer failed: {reason}; delivery unconfirmed."
|
|
358
|
+
)
|
|
359
|
+
exc.hint = None
|
|
360
|
+
raise exc from None
|
|
361
|
+
if decision == "reject" and not state.json:
|
|
362
|
+
output.hint("Declined assistant request; no write approved.")
|
|
363
|
+
|
|
326
364
|
# The reply streams server-side (an LLM turn + tool calls can take many seconds); show a
|
|
327
365
|
# themed spinner on stderr so the CLI doesn't look hung. No-op under --json / non-TTY.
|
|
328
366
|
with output.thinking("thinking…", enabled=not state.json):
|
|
329
367
|
result = api.agent_chat_oneshot(
|
|
330
|
-
cctx, messages=thread, conversation_id=full_chat_id, page_context=page_context, model=model
|
|
368
|
+
cctx, messages=thread, conversation_id=full_chat_id, page_context=page_context, model=model,
|
|
369
|
+
on_ask_user=on_ask_user, on_artifact=on_artifact,
|
|
331
370
|
)
|
|
332
371
|
|
|
333
372
|
chat_id = full_chat_id
|
|
@@ -337,7 +376,10 @@ def agent_ask(
|
|
|
337
376
|
# create-with-empty-title then a follow-up rename PATCH.
|
|
338
377
|
chat_id = (api.create_conversation(cctx, title=_title_from_question(message)) or {}).get("id")
|
|
339
378
|
if chat_id:
|
|
340
|
-
|
|
379
|
+
content = {"text": result.get("answer", "")}
|
|
380
|
+
if artifacts:
|
|
381
|
+
content["artifacts"] = [{k: v for k, v in a.items() if k != "result"} for a in artifacts]
|
|
382
|
+
full = thread + [{"role": "assistant", "content": content}]
|
|
341
383
|
api.replace_messages(cctx, chat_id, full)
|
|
342
384
|
if full_chat_id is not None and not prior:
|
|
343
385
|
# Continuing a chat that had no messages yet → title it from this first question
|
|
@@ -350,7 +392,7 @@ def agent_ask(
|
|
|
350
392
|
mode="continue" if chat else "new",
|
|
351
393
|
)
|
|
352
394
|
if state.json:
|
|
353
|
-
output.emit_json({**result, "chat_id": chat_id})
|
|
395
|
+
output.emit_json({**result, "chat_id": chat_id, **({"artifacts": artifacts} if artifacts else {})})
|
|
354
396
|
else:
|
|
355
397
|
for tool in result.get("tools", []):
|
|
356
398
|
output.agent_tool_used(tool)
|
|
@@ -404,4 +446,35 @@ def register(app: typer.Typer) -> None:
|
|
|
404
446
|
agent.command("show", epilog=GLOBALS_EPILOG)(agent_show)
|
|
405
447
|
agent.command("rename", epilog=GLOBALS_EPILOG)(agent_rename)
|
|
406
448
|
agent.command("delete", epilog=GLOBALS_EPILOG)(agent_delete)
|
|
449
|
+
agent.command("brief", epilog=GLOBALS_EPILOG)(agent_brief)
|
|
407
450
|
app.add_typer(agent, name="agent")
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def run_artifact(cctx, block):
|
|
454
|
+
"""Only execute through queries:run; never follow arbitrary URLs or actions."""
|
|
455
|
+
rendered = dict(block)
|
|
456
|
+
sql = block.get("sql")
|
|
457
|
+
if sql:
|
|
458
|
+
import re
|
|
459
|
+
if not isinstance(sql, str) or not re.match(r"^\s*(select|with)\b", sql, re.I):
|
|
460
|
+
raise click.UsageError("Assistant artifact SQL must be SELECT or WITH")
|
|
461
|
+
rendered["result"] = api.cloud_request(cctx, "POST", "/api/queries/run", body={"sql": sql, "params": []})
|
|
462
|
+
return rendered
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def render_artifact(block):
|
|
466
|
+
cloud_output.artifact(block)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def agent_brief(ctx: typer.Context, range: str = typer.Option("24h", "--range")) -> None:
|
|
470
|
+
"""Print the agent-curated home brief (24h or 7d). Session only."""
|
|
471
|
+
_cloud.session_only(ctx.obj, "agent brief")
|
|
472
|
+
if range not in ("24h", "7d"):
|
|
473
|
+
raise click.UsageError("--range must be 24h or 7d")
|
|
474
|
+
cctx = require_auth(ctx.obj)
|
|
475
|
+
data = api.cloud_request(cctx, "POST", "/api/agent/home-brief", body={"range": range})
|
|
476
|
+
blocks = [run_artifact(cctx, block) for block in data.get("blocks", [])]
|
|
477
|
+
if ctx.obj.json:
|
|
478
|
+
output.emit_json({**data, "blocks": blocks})
|
|
479
|
+
else:
|
|
480
|
+
cloud_output.brief({**data, "blocks": blocks})
|
|
@@ -442,4 +442,27 @@ def register(app: typer.Typer) -> None:
|
|
|
442
442
|
alerts_app.command("update", epilog=GLOBALS_EPILOG)(alerts_update)
|
|
443
443
|
alerts_app.command("delete", epilog=GLOBALS_EPILOG)(alerts_delete)
|
|
444
444
|
alerts_app.command("test", epilog=GLOBALS_EPILOG)(alerts_test)
|
|
445
|
+
alerts_app.command("recipients", epilog=GLOBALS_EPILOG)(alerts_recipients)
|
|
446
|
+
alerts_app.command("issues", epilog=GLOBALS_EPILOG)(alerts_issues)
|
|
445
447
|
app.add_typer(alerts_app, name="alerts")
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def alerts_recipients(ctx: typer.Context) -> None:
|
|
451
|
+
"""List available alert recipients."""
|
|
452
|
+
from .account_cmds import request
|
|
453
|
+
request(ctx, "GET", "/api/alerts/recipients")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def alerts_issues(ctx: typer.Context, alert_id: str = typer.Argument(...),
|
|
457
|
+
create: bool = typer.Option(False, "--create"),
|
|
458
|
+
title: Optional[str] = typer.Option(None, "--title"),
|
|
459
|
+
summary: Optional[str] = typer.Option(None, "--summary"),
|
|
460
|
+
file: Optional[str] = typer.Option(None, "--file")) -> None:
|
|
461
|
+
"""List alert issues, or --create one with --title/--summary or --file."""
|
|
462
|
+
from .account_cmds import request
|
|
463
|
+
if not create and any(v is not None for v in (title, summary, file)):
|
|
464
|
+
raise click.UsageError("Pass --create to use issue body flags")
|
|
465
|
+
body = _write.read_body(ctx.obj, file, {"title": title, "summary": summary}) if create else None
|
|
466
|
+
if create and (not isinstance(body.get("title"), str) or not body["title"].strip()):
|
|
467
|
+
raise click.UsageError("Creating an issue needs --title or a title in --file")
|
|
468
|
+
request(ctx, "POST" if create else "GET", f"/api/alerts/{alert_id}/issues", body)
|
|
@@ -1051,4 +1051,6 @@ def register(app: typer.Typer) -> None:
|
|
|
1051
1051
|
audits_app.command("resolve", epilog=GLOBALS_EPILOG)(findings_resolve)
|
|
1052
1052
|
audits_app.command("reopen", epilog=GLOBALS_EPILOG)(findings_reopen)
|
|
1053
1053
|
audits_app.command("assign", epilog=GLOBALS_EPILOG)(findings_assign)
|
|
1054
|
+
from . import contracts_cmds
|
|
1055
|
+
contracts_cmds.register(audits_app)
|
|
1054
1056
|
app.add_typer(audits_app, name="audits")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Org-wide agent contracts, alongside scheduled audits."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from .. import _click_compat as click, client as api
|
|
10
|
+
from .._context import GLOBALS_EPILOG, require_auth
|
|
11
|
+
from . import _write
|
|
12
|
+
from .account_cmds import request
|
|
13
|
+
|
|
14
|
+
PATH = "/api/audits/contracts"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def list_contracts(ctx: typer.Context) -> None:
|
|
18
|
+
"""List agent contracts."""
|
|
19
|
+
request(ctx, "GET", PATH)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def show(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
|
|
23
|
+
"""Show one contract, including its Markdown."""
|
|
24
|
+
request(ctx, "GET", f"{PATH}/{id}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def validate(body):
|
|
28
|
+
if set(body) - {"agent_id", "body", "enabled"}:
|
|
29
|
+
raise click.UsageError("Contracts accept agent_id, body and enabled")
|
|
30
|
+
if not isinstance(body.get("agent_id"), str) or not body["agent_id"].strip():
|
|
31
|
+
raise click.UsageError("Contract needs an agent_id from your telemetry")
|
|
32
|
+
if not isinstance(body.get("body"), str) or not body["body"].strip() or len(body["body"]) > 5000 or "\0" in body["body"]:
|
|
33
|
+
raise click.UsageError("Contract body must be 1–5000 characters without NUL")
|
|
34
|
+
if "enabled" in body and not isinstance(body["enabled"], bool):
|
|
35
|
+
raise click.UsageError("Contract enabled must be boolean")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create(ctx: typer.Context, agent_id: Optional[str] = typer.Option(None, "--agent-id"),
|
|
39
|
+
body: Optional[str] = typer.Option(None, "--body", help="Markdown or @file."),
|
|
40
|
+
file: Optional[str] = typer.Option(None, "--file", help="Contract API body JSON.")) -> None:
|
|
41
|
+
"""Write an agent contract."""
|
|
42
|
+
data = _write.read_body(ctx.obj, file, {"agent_id": agent_id, "body": _write.read_text_arg(body[1:]) if body and body.startswith("@") else body})
|
|
43
|
+
validate(data)
|
|
44
|
+
request(ctx, "POST", PATH, data)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def update(ctx: typer.Context, id: str = typer.Argument(...),
|
|
48
|
+
agent_id: Optional[str] = typer.Option(None, "--agent-id"),
|
|
49
|
+
body: Optional[str] = typer.Option(None, "--body"),
|
|
50
|
+
enabled: Optional[bool] = typer.Option(None, "--enabled/--disabled"),
|
|
51
|
+
file: Optional[str] = typer.Option(None, "--file")) -> None:
|
|
52
|
+
"""Update selected fields, preserving the rest."""
|
|
53
|
+
changes = _write.read_body(ctx.obj, file, {"agent_id": agent_id, "body": _write.read_text_arg(body[1:]) if body and body.startswith("@") else body, "enabled": enabled})
|
|
54
|
+
if not changes:
|
|
55
|
+
raise click.UsageError("Pass --agent-id, --body, --enabled/--disabled or --file")
|
|
56
|
+
current = api.cloud_request(require_auth(ctx.obj), "GET", f"{PATH}/{id}")
|
|
57
|
+
data = {k: current[k] for k in ("agent_id", "body", "enabled") if k in current}
|
|
58
|
+
data.update(changes)
|
|
59
|
+
validate(data)
|
|
60
|
+
request(ctx, "PUT", f"{PATH}/{id}", data)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def register(audits_app):
|
|
64
|
+
group = typer.Typer(no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]})
|
|
65
|
+
for name, fn in (("list", list_contracts), ("show", show), ("create", create), ("update", update)):
|
|
66
|
+
group.command(name, epilog=GLOBALS_EPILOG)(fn)
|
|
67
|
+
audits_app.add_typer(group, name="contracts")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Org dashboards and their saved-query tiles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from .. import _click_compat as click, client as api
|
|
10
|
+
from .._context import GLOBALS_EPILOG, require_auth, validate_choice
|
|
11
|
+
from . import _cloud, _write
|
|
12
|
+
|
|
13
|
+
CHARTS = ("line", "area", "bar", "stacked_bar", "pie", "kpi", "table")
|
|
14
|
+
TILE_FIELDS = ("query_id", "title", "chart_type", "chart_config", "param_bindings", "pos_x", "pos_y", "pos_w", "pos_h")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def request(ctx, method, path, body=None):
|
|
18
|
+
if method != "GET":
|
|
19
|
+
_write.confirm(ctx.obj, f"{method} {path}", assume_yes=False, destructive=method == "DELETE")
|
|
20
|
+
data = api.cloud_request(require_auth(ctx.obj), method, path, body=body)
|
|
21
|
+
if method != "GET":
|
|
22
|
+
_write.record_action("dashboard_changed", resource="dashboard", success=True, destructive=method == "DELETE")
|
|
23
|
+
_cloud.emit(ctx.obj, data, "Dashboards", view=path, method=method, body=body)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def list_dashboards(ctx: typer.Context) -> None:
|
|
27
|
+
"""List org dashboards."""
|
|
28
|
+
request(ctx, "GET", "/api/dashboards")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def show(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
|
|
32
|
+
"""Show a dashboard and its tiles."""
|
|
33
|
+
request(ctx, "GET", f"/api/dashboards/{id}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create(ctx: typer.Context, name: str = typer.Option(..., "--name"),
|
|
37
|
+
description: str = typer.Option("", "--description"),
|
|
38
|
+
file: Optional[str] = typer.Option(None, "--file", help="Dashboard definition JSON.")) -> None:
|
|
39
|
+
"""Create a dashboard (no tiles yet)."""
|
|
40
|
+
if not name.strip():
|
|
41
|
+
raise click.UsageError("--name cannot be empty")
|
|
42
|
+
definition = _write.read_body(ctx.obj, file) if file else {}
|
|
43
|
+
request(ctx, "POST", "/api/dashboards", {"name": name, "description": description, "definition": definition})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def update(ctx: typer.Context, id: str = typer.Argument(...),
|
|
47
|
+
name: Optional[str] = typer.Option(None, "--name"),
|
|
48
|
+
description: Optional[str] = typer.Option(None, "--description"),
|
|
49
|
+
file: Optional[str] = typer.Option(None, "--file", help="Replacement dashboard definition JSON.")) -> None:
|
|
50
|
+
"""Update selected fields, preserving the others."""
|
|
51
|
+
if name is None and description is None and file is None:
|
|
52
|
+
raise click.UsageError("Pass --name, --description or --file")
|
|
53
|
+
if name is not None and not name.strip():
|
|
54
|
+
raise click.UsageError("--name cannot be empty")
|
|
55
|
+
definition = _write.read_body(ctx.obj, file) if file else None
|
|
56
|
+
current = api.cloud_request(require_auth(ctx.obj), "GET", f"/api/dashboards/{id}")
|
|
57
|
+
# Show returns {dashboard, tiles}; older deployments return the row itself.
|
|
58
|
+
current = current.get("dashboard", current)
|
|
59
|
+
body = {k: current.get(k, {} if k == "definition" else "") for k in ("name", "description", "definition")}
|
|
60
|
+
body.update({k: v for k, v in {"name": name, "description": description, "definition": definition}.items() if v is not None})
|
|
61
|
+
request(ctx, "PUT", f"/api/dashboards/{id}", body)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def delete(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
|
|
65
|
+
"""Delete a dashboard (saved queries are retained)."""
|
|
66
|
+
request(ctx, "DELETE", f"/api/dashboards/{id}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def tiles(ctx: typer.Context, id: str = typer.Argument(..., metavar="DASHBOARD_ID")) -> None:
|
|
70
|
+
"""Manage tiles: `fp dashboards tiles ID list|add|update|remove|layout`."""
|
|
71
|
+
ctx.meta["dashboard_id"] = id
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def tile_path(ctx):
|
|
75
|
+
return f"/api/dashboards/{ctx.meta['dashboard_id']}/tiles"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def tile_list(ctx: typer.Context) -> None:
|
|
79
|
+
"""List tiles."""
|
|
80
|
+
request(ctx, "GET", tile_path(ctx))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def tile_add(ctx: typer.Context, query: str = typer.Option(..., "--query"),
|
|
84
|
+
chart: str = typer.Option(..., "--chart"), title: str = typer.Option("", "--title"),
|
|
85
|
+
file: Optional[str] = typer.Option(None, "--file", help="Tile config/geometry JSON.")) -> None:
|
|
86
|
+
"""Add a saved-query tile."""
|
|
87
|
+
validate_choice(chart, CHARTS, flag="--chart")
|
|
88
|
+
body = {"query_id": query, "title": title, "chart_type": chart, "chart_config": {},
|
|
89
|
+
"param_bindings": {}, "pos_x": 0, "pos_y": 0, "pos_w": 6, "pos_h": 4}
|
|
90
|
+
if file:
|
|
91
|
+
body.update(_write.read_body(ctx.obj, file))
|
|
92
|
+
validate_tile(body)
|
|
93
|
+
request(ctx, "POST", tile_path(ctx), body)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def validate_tile(body):
|
|
97
|
+
if set(body) - set(TILE_FIELDS):
|
|
98
|
+
raise click.UsageError("Unknown tile fields")
|
|
99
|
+
validate_choice(body.get("chart_type"), CHARTS, flag="--chart")
|
|
100
|
+
if not body.get("query_id") or not body.get("chart_type"):
|
|
101
|
+
raise click.UsageError("Tile needs query_id and chart_type")
|
|
102
|
+
for key in ("pos_x", "pos_y", "pos_w", "pos_h"):
|
|
103
|
+
value = body.get(key)
|
|
104
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < (1 if key in ("pos_w", "pos_h") else 0):
|
|
105
|
+
raise click.UsageError("Tile position must be non-negative with positive size")
|
|
106
|
+
for key in ("chart_config", "param_bindings"):
|
|
107
|
+
if not isinstance(body.get(key), dict):
|
|
108
|
+
raise click.UsageError(f"{key} must be a JSON object")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def tile_update(ctx: typer.Context, tile: str = typer.Argument(...),
|
|
112
|
+
query: Optional[str] = typer.Option(None, "--query"),
|
|
113
|
+
chart: Optional[str] = typer.Option(None, "--chart"),
|
|
114
|
+
title: Optional[str] = typer.Option(None, "--title"),
|
|
115
|
+
file: Optional[str] = typer.Option(None, "--file")) -> None:
|
|
116
|
+
"""Update a tile, preserving unmentioned config and geometry."""
|
|
117
|
+
body = _write.read_body(ctx.obj, file, {"query_id": query, "chart_type": chart, "title": title})
|
|
118
|
+
if not body:
|
|
119
|
+
raise click.UsageError("Pass --query, --chart, --title or --file")
|
|
120
|
+
data = api.cloud_request(require_auth(ctx.obj), "GET", tile_path(ctx))
|
|
121
|
+
matches = [r for r in _cloud.rows(data, "tiles") if str(r.get("id")) == tile]
|
|
122
|
+
if len(matches) != 1:
|
|
123
|
+
from ..errors import NotFoundError
|
|
124
|
+
raise NotFoundError(f"no tile {tile}; run `fp dashboards tiles {ctx.meta['dashboard_id']} list`")
|
|
125
|
+
merged = {k: matches[0][k] for k in TILE_FIELDS}
|
|
126
|
+
merged.update(body)
|
|
127
|
+
validate_tile(merged)
|
|
128
|
+
request(ctx, "PUT", f"{tile_path(ctx)}/{tile}", merged)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def tile_remove(ctx: typer.Context, tile: str = typer.Argument(...)) -> None:
|
|
132
|
+
"""Remove a tile."""
|
|
133
|
+
request(ctx, "DELETE", f"{tile_path(ctx)}/{tile}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def tile_layout(ctx: typer.Context, file: str = typer.Option(..., "--file")) -> None:
|
|
137
|
+
"""Apply geometry JSON: {tiles: [{id,pos_x,pos_y,pos_w,pos_h}]}."""
|
|
138
|
+
body = _write.read_body(ctx.obj, file)
|
|
139
|
+
if not isinstance(body.get("tiles"), list):
|
|
140
|
+
raise click.UsageError("Layout must contain a tiles array")
|
|
141
|
+
for row in body["tiles"]:
|
|
142
|
+
if not isinstance(row, dict) or not row.get("id"):
|
|
143
|
+
raise click.UsageError("Layout tiles need ids")
|
|
144
|
+
if set(row) != {"id", "pos_x", "pos_y", "pos_w", "pos_h"}:
|
|
145
|
+
raise click.UsageError("Layout tiles must contain id,pos_x,pos_y,pos_w,pos_h")
|
|
146
|
+
validate_tile({**{k: v for k, v in row.items() if k != "id"}, "query_id": "layout", "chart_type": "table", "chart_config": {}, "param_bindings": {}, "title": ""})
|
|
147
|
+
request(ctx, "PUT", f"{tile_path(ctx)}/layout", body)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def register(app: typer.Typer) -> None:
|
|
151
|
+
group = typer.Typer(no_args_is_help=True, help="Manage org dashboards and tiles.", context_settings={"help_option_names": ["-h", "--help"]})
|
|
152
|
+
for name, fn in (("list", list_dashboards), ("show", show), ("create", create), ("update", update), ("delete", delete)):
|
|
153
|
+
group.command(name, epilog=GLOBALS_EPILOG)(fn)
|
|
154
|
+
tile_group = typer.Typer(no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]})
|
|
155
|
+
tile_group.callback()(tiles)
|
|
156
|
+
for name, fn in (("list", tile_list), ("add", tile_add), ("update", tile_update), ("remove", tile_remove), ("layout", tile_layout)):
|
|
157
|
+
tile_group.command(name, epilog=GLOBALS_EPILOG)(fn)
|
|
158
|
+
group.add_typer(tile_group, name="tiles")
|
|
159
|
+
app.add_typer(group, name="dashboards")
|