failproofai 1.0.12-beta.0 → 1.0.12-beta.2
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 +3 -3
- 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 +22 -22
- 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 +1 -1
- 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 +4 -4
- 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 +7 -7
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0-ha5ydqq0mws._.js → [externals]__0vt9i6fqinjne._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__20xe05hm5u8_1._.js → [root-of-the-server]__0f_4g-vazrvxv._.js} +1 -1
- package/.next/standalone/.next/server/chunks/_0hf5q4xzyg40j._.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/_19011-u44143j._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +2 -2
- package/.next/standalone/.next/server/chunks/{_0kc-piux3hady._.js → _1y590mjzpkbos._.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 +5 -0
- package/.next/standalone/.next/server/chunks/src_hooks_builtin-policies_ts_1yq5ybgx1v98e._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3h94wnn._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_daemon-client_ts_0il_yu1zc0291._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn_4twyh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ts0fg5yih1c1._.js → [root-of-the-server]__00l2u6j86cb8y._.js} +2 -2
- 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]__1_xbg2kx30wp-._.js → [root-of-the-server]__0naevg4bcoh54._.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]__0pwj2h2efe5f-._.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]__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]__0vcyntk4snf0f._.js → [root-of-the-server]__1vofkb2oq7p7e._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bseae9u7g98r._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0mouhaflye1ro._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifpkowwub._.js → _0ouu886szh6yj._.js} +10 -9
- package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1ve8y151ua7qx._.js → _1fw0_nzkae5ao._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_20hn4gn87nsjc._.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_components_project-list_tsx_1ke6anr0vt64h._.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_00jxj8foxqz5t.js → node_modules_html-to-image_es_index_1fv03uigni8s5.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0janj0i6dqko7._.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_06kzv9ddabn3_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4l1tzrm._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_daemon-client_ts_0k41_i7jg6ndv._.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/{0pjxnp927y7_j.js → 0ay__-d2h1xb5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2znt1784j9gd9.js → 0tbm3vjm62h-2.js} +1 -1
- package/.next/standalone/.next/static/chunks/1c_jf6g5wnslk.js +1 -0
- package/.next/standalone/.next/static/chunks/{3tn1bzk094e8e.js → 1my3-5p47t7r-.js} +1 -1
- package/.next/standalone/.next/static/chunks/20py8k1gd_i1m.js +1 -0
- package/.next/standalone/.next/static/chunks/21hckpmsc4g70.js +6 -0
- package/.next/standalone/.next/static/chunks/2hmex93ppj1ke.js +1 -0
- package/.next/standalone/.next/static/chunks/2o6d37j425uci.js +1 -0
- package/.next/standalone/.next/static/chunks/40oworn_-x12p.js +1 -0
- package/.next/standalone/.next/static/chunks/{2qz5piiir-qqm.js → 42xptz88mtq3x.js} +1 -1
- 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 +32 -0
- package/.next/standalone/fp-cloud-cli/README.md +40 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +91 -12
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +78 -16
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +84 -18
- package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +124 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/models.py +89 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +189 -20
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +8 -5
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +2 -2
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +14 -7
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +527 -0
- package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +46 -0
- package/.next/standalone/fp-cloud-cli/tests/test_output.py +23 -0
- package/.next/standalone/hermes-plugin/README.md +6 -4
- package/.next/standalone/hermes-plugin/__init__.py +27 -0
- package/.next/standalone/hermes-plugin/client.py +5 -1
- 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/server.js +1 -1
- package/bin/failproofai.mjs +17 -1
- package/dist/cli.mjs +3507 -1250
- package/dist/worker.mjs +1779 -169
- package/hermes-plugin/README.md +6 -4
- package/hermes-plugin/__init__.py +27 -0
- package/hermes-plugin/client.py +5 -1
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +11 -10
- package/src/audit/open-browser.ts +6 -7
- package/src/hooks/agent-roster.ts +180 -0
- package/src/hooks/agent-skill.ts +203 -0
- package/src/hooks/agent-targets.ts +60 -0
- package/src/hooks/cloud-connection.ts +91 -45
- package/src/hooks/cloud-enrollment-cli.ts +7 -4
- package/src/hooks/cloud-login.ts +6 -3
- package/src/hooks/cloud-managed-policies.ts +430 -20
- package/src/hooks/cloud-policy-errors.ts +481 -0
- package/src/hooks/codex-system-hooks.ts +10 -4
- package/src/hooks/config-status.ts +10 -4
- package/src/hooks/configure-wizard.ts +54 -26
- package/src/hooks/custom-hooks-loader.ts +26 -3
- package/src/hooks/daemon-client.ts +9 -4
- package/src/hooks/daemon-service.ts +2 -1
- package/src/hooks/delivery-health.ts +47 -2
- package/src/hooks/effective-reviewers.ts +36 -1
- package/src/hooks/fp-config.ts +19 -0
- package/src/hooks/fp-home.ts +6 -0
- package/src/hooks/handler.ts +255 -19
- package/src/hooks/health-probe.ts +15 -0
- package/src/hooks/hook-activity-store.ts +7 -1
- package/src/hooks/integrations.ts +36 -14
- package/src/hooks/jev-activity.ts +117 -1
- package/src/hooks/jev-cli.ts +196 -1
- package/src/hooks/jev-cloud-connection.ts +92 -0
- package/src/hooks/manager.ts +174 -34
- package/src/hooks/pack-failclosed.ts +2 -0
- package/src/hooks/pack-manifest.ts +8 -1
- package/src/hooks/policy-evaluator.ts +22 -1
- package/src/hooks/policy-registry.ts +3 -2
- package/src/hooks/policy-reviewability.ts +23 -5
- package/src/hooks/semantic/cloud-jev-health.ts +227 -0
- package/src/hooks/semantic/cloud-jev.ts +868 -0
- package/src/hooks/semantic/combine.ts +26 -1
- package/src/hooks/semantic/jev-client.ts +19 -2
- package/src/hooks/semantic/jev-config.ts +179 -30
- package/src/hooks/semantic/jev-review.ts +217 -10
- package/src/hooks/semantic/pack-policies.ts +2 -1
- package/src/hooks/semantic/types.ts +8 -0
- package/src/hooks/session-pause-cli.ts +1 -1
- package/src/hooks/tui.ts +13 -4
- package/src/hooks/uninstall-cli.ts +12 -1
- package/src/hooks/worker-server.ts +5 -1
- package/.next/standalone/.next/server/chunks/src_hooks_1aveq0uhs2t1b._.js +0 -5
- package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7cyqlb2._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo55j16x._.js +0 -5
- package/.next/standalone/.next/static/chunks/0a-ccsp3xmypz.js +0 -1
- package/.next/standalone/.next/static/chunks/13jv-xvabeb62.js +0 -6
- package/.next/standalone/.next/static/chunks/2ank2u3apy8m6.js +0 -1
- package/.next/standalone/.next/static/chunks/2meqb9kygrncb.js +0 -1
- package/.next/standalone/.next/static/chunks/2ohktp_-lkv3x.js +0 -1
- package/.next/standalone/.next/static/chunks/2py0eldhwz8ay.js +0 -1
- /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_ssgManifest.js +0 -0
|
@@ -23,7 +23,7 @@ import re
|
|
|
23
23
|
import uuid
|
|
24
24
|
from dataclasses import dataclass
|
|
25
25
|
from enum import Enum
|
|
26
|
-
from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Union
|
|
26
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Tuple, Union
|
|
27
27
|
|
|
28
28
|
import httpx
|
|
29
29
|
|
|
@@ -487,9 +487,39 @@ def _raise_for_status(response: httpx.Response, ctx: ClientContext) -> None:
|
|
|
487
487
|
message or f"Request failed with status {response.status_code}.",
|
|
488
488
|
status=response.status_code,
|
|
489
489
|
request_id=request_id,
|
|
490
|
+
hint=_jev_budget_breakdown(response),
|
|
490
491
|
)
|
|
491
492
|
|
|
492
493
|
|
|
494
|
+
def _jev_budget_breakdown(response: httpx.Response) -> Optional[str]:
|
|
495
|
+
"""The per-policy breakdown of a deploy's ``jev_budget_exceeded`` refusal.
|
|
496
|
+
|
|
497
|
+
The message carries only the total (``used``/``budget``); the body also lists
|
|
498
|
+
every Jev policy version the machine would carry with its question
|
|
499
|
+
characters (``policies: [{id, version, chars}]``), which is what tells an
|
|
500
|
+
operator WHICH policy to leave off. Largest first. ``None`` for any other
|
|
501
|
+
error, and for a publish refusal, which names no other policies.
|
|
502
|
+
"""
|
|
503
|
+
try:
|
|
504
|
+
data = response.json()
|
|
505
|
+
except Exception:
|
|
506
|
+
return None
|
|
507
|
+
if not isinstance(data, dict) or data.get("code") != "jev_budget_exceeded":
|
|
508
|
+
return None
|
|
509
|
+
rows = [
|
|
510
|
+
(str(p.get("id", "")), p.get("version"), p["chars"])
|
|
511
|
+
for p in data.get("policies") or []
|
|
512
|
+
if isinstance(p, dict) and isinstance(p.get("chars"), int)
|
|
513
|
+
]
|
|
514
|
+
if not rows:
|
|
515
|
+
return None
|
|
516
|
+
rows.sort(key=lambda row: -row[2])
|
|
517
|
+
parts = [f"{pid}@{version} {chars:,}" if version is not None else f"{pid} {chars:,}"
|
|
518
|
+
for pid, version, chars in rows]
|
|
519
|
+
return ("Jev question characters per policy: " + " · ".join(parts)
|
|
520
|
+
+ " — deploy fewer of these Jev policies to this machine")
|
|
521
|
+
|
|
522
|
+
|
|
493
523
|
def _get_json(ctx: ClientContext, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
494
524
|
clean = {k: v for k, v in (params or {}).items() if v is not None}
|
|
495
525
|
url = _path(ctx, path) # chokepoint 1 of 4 for the /api -> /v1 rewrite
|
|
@@ -1643,22 +1673,43 @@ def list_policies(ctx: ClientContext) -> List[PolicyVersion]:
|
|
|
1643
1673
|
|
|
1644
1674
|
|
|
1645
1675
|
def publish_policy(
|
|
1646
|
-
ctx: ClientContext,
|
|
1676
|
+
ctx: ClientContext,
|
|
1677
|
+
policy_id: str,
|
|
1678
|
+
source: Optional[str],
|
|
1679
|
+
description: str = "",
|
|
1680
|
+
*,
|
|
1681
|
+
kind: Optional[str] = None,
|
|
1682
|
+
semantic: Optional[List[Any]] = None,
|
|
1647
1683
|
) -> PolicyVersion:
|
|
1648
1684
|
"""POST /api/enforcement/policies — mints a NEW VERSION; never edits in place.
|
|
1649
1685
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1686
|
+
``kind`` is ``regex`` (JavaScript ``source`` only — what an omitted kind
|
|
1687
|
+
means to the server), ``jev`` (Jev declarations in ``semantic``, no
|
|
1688
|
+
``source``) or ``both`` (the two together: the JavaScript is reviewable by
|
|
1689
|
+
exactly its own checks). ``authority``/``reviewedBy`` are derived by the
|
|
1690
|
+
server from the kind and are never sent.
|
|
1691
|
+
|
|
1692
|
+
Refuses Jev fields in the JAVASCRIPT before anything is sent, here because
|
|
1693
|
+
`policies publish` and `policies compose --publish` both route through it —
|
|
1694
|
+
and regardless of `--no-verify`, which only skips the syntax check. Jev
|
|
1695
|
+
checks belong in ``semantic``, where they take effect.
|
|
1653
1696
|
"""
|
|
1654
|
-
problem = cloud_publish_problem(source)
|
|
1697
|
+
problem = cloud_publish_problem(source or "")
|
|
1655
1698
|
if problem:
|
|
1656
1699
|
raise ApiError(
|
|
1657
1700
|
f"{policy_id} cannot be published as a cloud policy: {problem}",
|
|
1658
|
-
hint=("Jev checks
|
|
1659
|
-
"
|
|
1701
|
+
hint=("put Jev checks in the policy's Jev declarations — `--kind both --semantic "
|
|
1702
|
+
"checks.json` — or ship them in a failproofai pack with `failproofai publish`"),
|
|
1660
1703
|
)
|
|
1661
|
-
body = {"id": policy_id, "
|
|
1704
|
+
body: Dict[str, Any] = {"id": policy_id, "description": description}
|
|
1705
|
+
# `source` is omitted for a Jev-only policy: the server takes absent (or
|
|
1706
|
+
# empty) as "no JavaScript", which is what a `jev` version is.
|
|
1707
|
+
if source:
|
|
1708
|
+
body["source"] = source
|
|
1709
|
+
if kind is not None:
|
|
1710
|
+
body["kind"] = kind
|
|
1711
|
+
if semantic is not None:
|
|
1712
|
+
body["semantic"] = semantic
|
|
1662
1713
|
return PolicyVersion.from_dict(_post_json(ctx, "/api/enforcement/policies", body) or {})
|
|
1663
1714
|
|
|
1664
1715
|
|
|
@@ -1681,6 +1732,29 @@ def list_machines(ctx: ClientContext) -> List[Machine]:
|
|
|
1681
1732
|
return [Machine.from_dict(m) for m in items]
|
|
1682
1733
|
|
|
1683
1734
|
|
|
1735
|
+
def machine_agents(ctx: ClientContext, machine_id: str) -> List[Dict[str, Any]]:
|
|
1736
|
+
"""GET /api/enforcement/machines/{id}/agents — the agent profiles the daemon
|
|
1737
|
+
last reported: `[{instanceId, integration, profileLabel, scope, ...}]`."""
|
|
1738
|
+
data = _get_json(ctx, f"/api/enforcement/machines/{machine_id}/agents")
|
|
1739
|
+
items = data if isinstance(data, list) else (data or {}).get("agents", [])
|
|
1740
|
+
return [a for a in items if isinstance(a, dict)]
|
|
1741
|
+
|
|
1742
|
+
|
|
1743
|
+
def agent_labels(ctx: ClientContext, machine_id: str) -> Dict[str, str]:
|
|
1744
|
+
"""`{instanceId: profileLabel}` for one machine, or `{}` when unavailable.
|
|
1745
|
+
|
|
1746
|
+
Best effort on purpose: it only names the `agt_…` ids an exact-profile
|
|
1747
|
+
assignment carries. A server without the roster route, or a machine that
|
|
1748
|
+
never reported one, falls back to the bare id rather than failing the view.
|
|
1749
|
+
"""
|
|
1750
|
+
try:
|
|
1751
|
+
agents = machine_agents(ctx, machine_id)
|
|
1752
|
+
except Exception: # noqa: BLE001 — a missing label must never fail the read
|
|
1753
|
+
return {}
|
|
1754
|
+
return {str(a["instanceId"]): str(a["profileLabel"])
|
|
1755
|
+
for a in agents if a.get("instanceId") and a.get("profileLabel")}
|
|
1756
|
+
|
|
1757
|
+
|
|
1684
1758
|
def rename_machine(ctx: ClientContext, machine_id: str, label: str) -> Dict[str, Any]:
|
|
1685
1759
|
"""PATCH /api/enforcement/machines/{id} — a human label, not the id."""
|
|
1686
1760
|
path = f"/api/enforcement/machines/{machine_id}"
|
|
@@ -1708,11 +1782,16 @@ def get_deployment(ctx: ClientContext, machine_id: str) -> Optional[Deployment]:
|
|
|
1708
1782
|
|
|
1709
1783
|
|
|
1710
1784
|
def deploy_policies(
|
|
1711
|
-
ctx: ClientContext,
|
|
1785
|
+
ctx: ClientContext,
|
|
1786
|
+
machine_id: str,
|
|
1787
|
+
policies: Sequence[PolicyRef],
|
|
1712
1788
|
) -> Deployment:
|
|
1713
|
-
"""PUT /api/enforcement/deployments/{id} — REPLACES the machine's whole set.
|
|
1789
|
+
"""PUT /api/enforcement/deployments/{id} — REPLACES the machine's whole set.
|
|
1790
|
+
|
|
1791
|
+
Never carries a Jev mode: FailproofAI Cloud derives it from the set.
|
|
1792
|
+
"""
|
|
1714
1793
|
path = f"/api/enforcement/deployments/{machine_id}"
|
|
1715
|
-
body = {"policies": [p.to_dict() for p in policies]}
|
|
1794
|
+
body: Dict[str, Any] = {"policies": [p.to_dict() for p in policies]}
|
|
1716
1795
|
return Deployment.from_dict(_request_json(ctx, "PUT", path, json_body=body) or {})
|
|
1717
1796
|
|
|
1718
1797
|
|
|
@@ -32,8 +32,10 @@ from ..enforcement import (
|
|
|
32
32
|
disabled_ids,
|
|
33
33
|
latest_versions,
|
|
34
34
|
plan_deploy,
|
|
35
|
+
version_kinds,
|
|
35
36
|
)
|
|
36
|
-
from ..errors import ApiError, NotFoundError
|
|
37
|
+
from ..errors import ApiError, AuthError, FpCliError, NotFoundError
|
|
38
|
+
from ..models import PolicyRef
|
|
37
39
|
from . import _write
|
|
38
40
|
|
|
39
41
|
_KEY_MODE_REASON = (
|
|
@@ -62,6 +64,8 @@ def fleet_list(ctx: typer.Context) -> None:
|
|
|
62
64
|
`intended` is the generation deployed, `applied` is the one the machine last
|
|
63
65
|
collected, and `seen` is when it last reported anything — a machine can be
|
|
64
66
|
in sync and dead, or alive and behind, and those are different problems.
|
|
67
|
+
`state` reads `N errors` when the machine reported policies it could not
|
|
68
|
+
apply (`fp fleet show <machine>` lists them).
|
|
65
69
|
|
|
66
70
|
A machine appears from its very first check-in, including the poll that
|
|
67
71
|
finds nothing deployed — that is exactly the machine you are usually looking
|
|
@@ -101,10 +105,22 @@ def fleet_show(
|
|
|
101
105
|
machine can be told to run a policy and not yet have it; the policy list
|
|
102
106
|
alone cannot tell you which, and that is usually the question.
|
|
103
107
|
|
|
108
|
+
Shows the Jev mode FailproofAI Cloud sets on the machine (`local` when it
|
|
109
|
+
sets none) and the policy errors the machine last reported: a policy it
|
|
110
|
+
could not load, a Jev mode it cannot act on (`jev_unconfigured`: its key
|
|
111
|
+
does not carry Jev; `transcripts_disabled`: it was connected with
|
|
112
|
+
`--no-transcripts`), FailproofAI Cloud Jev refusing or failing its calls
|
|
113
|
+
(`jev_rate_limited`: over the organization's Jev rate limit;
|
|
114
|
+
`jev_unavailable`: it skips a failing Cloud Jev for a minute at a time), an
|
|
115
|
+
installed pack's check left out for the question budget (`jev_budget`) or
|
|
116
|
+
because an organization Cloud check has its name (`jev_name_clash`).
|
|
117
|
+
Anything listed there is not enforcing, whatever the deployment says.
|
|
118
|
+
|
|
104
119
|
Needs `policies:read`. With `--json`: `{machine, deployment}` — the machine
|
|
105
|
-
record (including `appliedDeployment`, `drifted`, `lastSeen
|
|
106
|
-
fields,
|
|
107
|
-
nothing is
|
|
120
|
+
record (including `appliedDeployment`, `drifted`, `lastSeen`, both label
|
|
121
|
+
fields, `jevMode` and `policyErrors`/`policyErrorsAt` when reported, with raw
|
|
122
|
+
timestamps) and the deployment, or `deployment: null` when nothing is
|
|
123
|
+
deployed.
|
|
108
124
|
|
|
109
125
|
Example:
|
|
110
126
|
|
|
@@ -129,7 +145,8 @@ def fleet_show(
|
|
|
129
145
|
"deployment": dep.to_dict() if dep else None,
|
|
130
146
|
})
|
|
131
147
|
return
|
|
132
|
-
output.render_machine_policies(machine_id, dep, machine
|
|
148
|
+
output.render_machine_policies(machine_id, dep, machine,
|
|
149
|
+
agent_labels=api.agent_labels(cctx, machine_id))
|
|
133
150
|
|
|
134
151
|
|
|
135
152
|
def fleet_deploy(
|
|
@@ -137,13 +154,22 @@ def fleet_deploy(
|
|
|
137
154
|
machine_id: str = typer.Argument(..., help="Machine id."),
|
|
138
155
|
add: Optional[List[str]] = typer.Option(
|
|
139
156
|
None, "--add",
|
|
140
|
-
help="Add or update a policy: `id`, `id@version`, `id:effect` or `id@version:effect`."
|
|
157
|
+
help="Add or update a policy: `id`, `id@version`, `id:effect` or `id@version:effect`. "
|
|
158
|
+
"`observe` records what the policy (its JavaScript and its Jev checks) would decide, and decides nothing.",
|
|
141
159
|
),
|
|
142
160
|
remove: Optional[List[str]] = typer.Option(None, "--remove", help="Remove a policy by id."),
|
|
143
161
|
replace: Optional[List[str]] = typer.Option(
|
|
144
162
|
None, "--set",
|
|
145
163
|
help="REPLACE the whole set with exactly these. Cannot be combined with --add/--remove.",
|
|
146
164
|
),
|
|
165
|
+
targets: Optional[List[str]] = typer.Option(
|
|
166
|
+
None, "--target",
|
|
167
|
+
help="Scope one assigned policy: POLICY=INTEGRATION or POLICY=INTEGRATION/agt_ID. Repeat to OR selectors.",
|
|
168
|
+
),
|
|
169
|
+
all_agents: Optional[List[str]] = typer.Option(
|
|
170
|
+
None, "--all-agents",
|
|
171
|
+
help="Clear one policy's targeting back to every agent on this machine.",
|
|
172
|
+
),
|
|
147
173
|
create: bool = typer.Option(
|
|
148
174
|
False, "--create",
|
|
149
175
|
help="Allow deploying to a machine id that has not checked in yet (pre-staging).",
|
|
@@ -159,6 +185,14 @@ def fleet_deploy(
|
|
|
159
185
|
|
|
160
186
|
`--set` replaces everything — the only way to drop policies you do not name.
|
|
161
187
|
|
|
188
|
+
Any kind deploys the same way. Jev checks run on FailproofAI Cloud; nothing
|
|
189
|
+
is installed on the machine: a `both` policy sends the machine its
|
|
190
|
+
JavaScript, and the machine asks FailproofAI Cloud about each checked tool
|
|
191
|
+
call. An `observe` effect applies to every kind: the policy's JavaScript
|
|
192
|
+
and its Jev checks are evaluated and recorded, and decide nothing. There is
|
|
193
|
+
no Jev mode to set: FailproofAI Cloud turns Jev on for a machine while any
|
|
194
|
+
`jev` or `both` policy is deployed to it, and off when none is.
|
|
195
|
+
|
|
162
196
|
**Concurrency.** The write is a full replace with no server-side lock, so the
|
|
163
197
|
CLI records the generation it read and refuses if the result is not exactly
|
|
164
198
|
one higher: that means somebody else deployed in between and a replace does
|
|
@@ -170,17 +204,18 @@ def fleet_deploy(
|
|
|
170
204
|
* `fp fleet deploy ci-runner-01 --add no-force-push`
|
|
171
205
|
* `fp fleet deploy ci-runner-01 --add prod-guard@1:observe --remove old-rule`
|
|
172
206
|
* `fp fleet deploy ci-runner-01 --set no-force-push --set no-secret-echo`
|
|
207
|
+
* `fp fleet deploy ci-runner-01 --add prod-db-intent:observe`
|
|
173
208
|
"""
|
|
174
209
|
state: AppState = ctx.obj
|
|
175
210
|
deny_in_key_mode(state, "fleet deploy", _KEY_MODE_REASON)
|
|
176
211
|
cctx = require_auth(state)
|
|
177
212
|
|
|
178
|
-
if not add and not remove and replace is None:
|
|
213
|
+
if not add and not remove and replace is None and not targets and not all_agents:
|
|
179
214
|
# Exit 2 for the same reason `--set` with `--add` is: no flag
|
|
180
215
|
# combination was given that this command can act on. Both are the
|
|
181
216
|
# caller's command line, not the server's answer.
|
|
182
217
|
raise click.UsageError(
|
|
183
|
-
"nothing to do — pass --add, --remove, or --
|
|
218
|
+
"nothing to do — pass --add, --remove, --set, --target, or --all-agents. "
|
|
184
219
|
"`fp fleet show <machine>` prints the current set."
|
|
185
220
|
)
|
|
186
221
|
|
|
@@ -212,6 +247,9 @@ def fleet_deploy(
|
|
|
212
247
|
replace=replace,
|
|
213
248
|
latest=latest,
|
|
214
249
|
disabled=disabled_ids(published),
|
|
250
|
+
kinds=version_kinds(published),
|
|
251
|
+
targets=targets or (),
|
|
252
|
+
all_agents=all_agents or (),
|
|
215
253
|
)
|
|
216
254
|
except RefUsageError as exc:
|
|
217
255
|
# Exit 2, like every other bad flag value in this CLI (`--since`,
|
|
@@ -314,8 +352,11 @@ def fleet_history(
|
|
|
314
352
|
"""List a machine's deployment generations, newest first.
|
|
315
353
|
|
|
316
354
|
A reissue — the server rewriting a deployment because a policy was disabled
|
|
317
|
-
— appears as an ordinary entry
|
|
318
|
-
`
|
|
355
|
+
— appears as an ordinary entry, and so does a Jev mode change. Each
|
|
356
|
+
generation shows the Jev mode FailproofAI Cloud set with it (`local` when it
|
|
357
|
+
set none), which is the mode a rollback to it restores. Needs
|
|
358
|
+
`policies:read`. With `--json`: `{machineId, history:[{deployment, policies,
|
|
359
|
+
updatedAt, jevMode}]}`.
|
|
319
360
|
|
|
320
361
|
Example:
|
|
321
362
|
|
|
@@ -329,7 +370,8 @@ def fleet_history(
|
|
|
329
370
|
if output.is_json():
|
|
330
371
|
output.emit_json({"machineId": machine_id, "history": entries})
|
|
331
372
|
return
|
|
332
|
-
output.render_deployment_history(machine_id, entries
|
|
373
|
+
output.render_deployment_history(machine_id, entries,
|
|
374
|
+
agent_labels=api.agent_labels(cctx, machine_id))
|
|
333
375
|
|
|
334
376
|
|
|
335
377
|
def fleet_rollback(
|
|
@@ -344,8 +386,12 @@ def fleet_rollback(
|
|
|
344
386
|
counter, so history stays append-only. A generation containing a policy that
|
|
345
387
|
has since been disabled or deleted cannot be reinstated; the server says so.
|
|
346
388
|
|
|
347
|
-
|
|
348
|
-
|
|
389
|
+
It restores that generation's Jev mode too, `local` included: the prompt
|
|
390
|
+
and the result say `jev mode <now> → <then>` when that changes it.
|
|
391
|
+
|
|
392
|
+
Needs `policies:write`. With `--json`: the resulting deployment plus
|
|
393
|
+
`jevModeBefore` (the mode it replaced), or `{"cancelled": true}` if you
|
|
394
|
+
decline.
|
|
349
395
|
|
|
350
396
|
Example:
|
|
351
397
|
|
|
@@ -356,9 +402,22 @@ def fleet_rollback(
|
|
|
356
402
|
cctx = require_auth(state)
|
|
357
403
|
_require_machine(cctx, machine_id)
|
|
358
404
|
current = api.get_deployment(cctx, machine_id)
|
|
405
|
+
# The mode that generation carried comes back with it, NULL (`local`)
|
|
406
|
+
# included, as the server reports it.
|
|
407
|
+
# Read it first, so the prompt says so rather than the fleet finding out.
|
|
408
|
+
target = next(
|
|
409
|
+
(e for e in api.deployment_history(cctx, machine_id) if e.get("deployment") == deployment), None)
|
|
410
|
+
mode_before = (current.jev_mode if current else None) or "local"
|
|
411
|
+
mode_target = (target.get("jevMode") or "local") if target is not None else None
|
|
412
|
+
consequence = "this REPLACES the machine's current set with the one from that generation"
|
|
413
|
+
if target is not None:
|
|
414
|
+
refs = [PolicyRef.from_dict(ref).label for ref in (target.get("policies") or [])]
|
|
415
|
+
consequence += "; assignments: " + ("; ".join(refs) if refs else "no policies")
|
|
416
|
+
if mode_target is not None and mode_target != mode_before:
|
|
417
|
+
consequence += f", and its Jev mode: jev mode {mode_before} → {mode_target}"
|
|
359
418
|
if not _write.confirm_destructive(
|
|
360
419
|
state, f"reinstate deployment #{deployment} on", machine_id,
|
|
361
|
-
consequence=
|
|
420
|
+
consequence=consequence,
|
|
362
421
|
assume_yes=yes,
|
|
363
422
|
):
|
|
364
423
|
if output.is_json():
|
|
@@ -369,9 +428,12 @@ def fleet_rollback(
|
|
|
369
428
|
result = api.rollback_deployment(cctx, machine_id, deployment)
|
|
370
429
|
check_race(current.deployment if current else None, result.deployment)
|
|
371
430
|
if output.is_json():
|
|
372
|
-
output.emit_json(result.to_dict())
|
|
431
|
+
output.emit_json({**result.to_dict(), "jevModeBefore": mode_before})
|
|
373
432
|
return
|
|
374
|
-
output.deployment_rolled_back(
|
|
433
|
+
output.deployment_rolled_back(
|
|
434
|
+
machine_id, deployment, result.deployment,
|
|
435
|
+
jev_from=mode_before, jev_to=result.jev_mode or "local",
|
|
436
|
+
)
|
|
375
437
|
|
|
376
438
|
|
|
377
439
|
def fleet_rename(
|
|
@@ -10,6 +10,7 @@ single most surprising thing here, so every success path says so.
|
|
|
10
10
|
"""
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import json
|
|
13
14
|
from typing import Optional
|
|
14
15
|
|
|
15
16
|
import typer
|
|
@@ -18,8 +19,8 @@ from .. import client as api
|
|
|
18
19
|
from .. import output, theme
|
|
19
20
|
from .._context import GLOBALS_EPILOG, AppState, deny_in_key_mode, require_auth
|
|
20
21
|
from .. import _click_compat as click # the Click Typer is running; see _click_compat
|
|
21
|
-
from ..enforcement import RefError, RefUsageError, read_source
|
|
22
|
-
from ..policy_check import check_syntax, run_policy
|
|
22
|
+
from ..enforcement import RefError, RefUsageError, parse_semantic, read_source, resolve_kind
|
|
23
|
+
from ..policy_check import SyntaxResult, check_syntax, run_policy
|
|
23
24
|
from ..errors import ApiError, NotFoundError
|
|
24
25
|
from . import _write
|
|
25
26
|
|
|
@@ -36,11 +37,15 @@ _KEY_MODE_REASON = (
|
|
|
36
37
|
def policies_list(ctx: typer.Context) -> None:
|
|
37
38
|
"""List every published policy version, newest of each policy first.
|
|
38
39
|
|
|
39
|
-
Shows `policy · version · state · description`, one row per VERSION —
|
|
40
|
+
Shows `policy · version · kind · state · description`, one row per VERSION —
|
|
40
41
|
versions are immutable and every one stays addressable, so a policy
|
|
41
42
|
published three times is three rows. The title counts distinct policies and
|
|
42
43
|
captions the version total, the way the dashboard's library does.
|
|
43
44
|
|
|
45
|
+
`kind` is `regex` (JavaScript), `jev` (Jev checks only) or `both`
|
|
46
|
+
(JavaScript its own Jev checks may clear); a `jev`/`both` row names its
|
|
47
|
+
checks and the characters they take of a machine's Jev question budget.
|
|
48
|
+
|
|
44
49
|
`state` is active, disabled (kept but not enforced) or archived (deleted;
|
|
45
50
|
machines already carrying it keep it until redeployed). Needs
|
|
46
51
|
`policies:read`. With `--json`: the server's policy list verbatim — also
|
|
@@ -64,9 +69,11 @@ def policies_show(
|
|
|
64
69
|
ctx: typer.Context,
|
|
65
70
|
policy_id: str = typer.Argument(..., help="Policy id."),
|
|
66
71
|
) -> None:
|
|
67
|
-
"""Show one policy, including its full source.
|
|
72
|
+
"""Show one policy, including its full source and its Jev checks.
|
|
68
73
|
|
|
69
|
-
Needs `policies:read`. With `--json`: the policy object with `source
|
|
74
|
+
Needs `policies:read`. With `--json`: the policy object with `source`,
|
|
75
|
+
`kind`, and for `jev`/`both` its `semantic` declarations, `semanticSha256`
|
|
76
|
+
and the server-derived `authority`/`reviewedBy`.
|
|
70
77
|
|
|
71
78
|
Example:
|
|
72
79
|
|
|
@@ -96,6 +103,8 @@ def policies_show(
|
|
|
96
103
|
source_bytes=len((match.source or "").encode("utf-8")))
|
|
97
104
|
if match.source:
|
|
98
105
|
output.info(match.source)
|
|
106
|
+
if match.semantic:
|
|
107
|
+
output.info(json.dumps(match.semantic, indent=2))
|
|
99
108
|
|
|
100
109
|
|
|
101
110
|
def policies_publish(
|
|
@@ -103,7 +112,19 @@ def policies_publish(
|
|
|
103
112
|
policy_id: str = typer.Argument(..., help="Policy id (letters, numbers, '.', '_', '-')."),
|
|
104
113
|
source: Optional[str] = typer.Argument(
|
|
105
114
|
None,
|
|
106
|
-
help="Path to the
|
|
115
|
+
help="Path to the JavaScript source, @path, or - for stdin. Omit to paste it. Same as --source.",
|
|
116
|
+
),
|
|
117
|
+
kind: Optional[str] = typer.Option(
|
|
118
|
+
None, "--kind",
|
|
119
|
+
help="regex (JavaScript), jev (Jev checks only) or both (JavaScript its own Jev checks may "
|
|
120
|
+
"clear). Default: follows from what you give — --semantic alone is jev, with source both.",
|
|
121
|
+
),
|
|
122
|
+
source_opt: Optional[str] = typer.Option(
|
|
123
|
+
None, "--source", help="The JavaScript source: a path, @path, or - for stdin.",
|
|
124
|
+
),
|
|
125
|
+
semantic_path: Optional[str] = typer.Option(
|
|
126
|
+
None, "--semantic",
|
|
127
|
+
help="A JSON file holding the Jev declarations (a JSON array): a path, @path, or - for stdin.",
|
|
107
128
|
),
|
|
108
129
|
description: str = typer.Option("", "--description", help="One-line description."),
|
|
109
130
|
no_verify: bool = typer.Option(
|
|
@@ -112,13 +133,29 @@ def policies_publish(
|
|
|
112
133
|
) -> None:
|
|
113
134
|
"""Publish a policy — mints a NEW VERSION; it never edits one in place.
|
|
114
135
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
136
|
+
A policy has a **kind**:
|
|
137
|
+
|
|
138
|
+
* `regex` — JavaScript (`--source`, or the positional source): what publish
|
|
139
|
+
has always done, and the default.
|
|
140
|
+
* `jev` — Jev checks only (`--semantic checks.json`): a JSON array of Jev
|
|
141
|
+
declarations, each `{name, title, appliesTo, mode, userCanOverride,
|
|
142
|
+
probes:[{id, instructions, criteria:{true,false}}], guidance, ...}` —
|
|
143
|
+
exactly a failproofai pack manifest's semantic entry. No JavaScript.
|
|
144
|
+
* `both` — the two together. FailproofAI Cloud makes the JavaScript's
|
|
145
|
+
verdict reviewable by exactly its own checks, and deploys them as one.
|
|
146
|
+
|
|
147
|
+
Jev checks run on FailproofAI Cloud; nothing is installed on the machine.
|
|
148
|
+
A machine gets a `both` policy's JavaScript, and asks FailproofAI Cloud
|
|
149
|
+
about each checked tool call while its Jev mode is `observe` or `enforce`.
|
|
150
|
+
|
|
151
|
+
The JavaScript is parse-checked with node before it is sent. Nothing
|
|
152
|
+
downstream does this: the server validates the id and a size ceiling, and a
|
|
153
|
+
broken policy otherwise fails on the machine at enforcement time.
|
|
154
|
+
`--no-verify` skips it; a host without node publishes with a warning rather
|
|
155
|
+
than a block. Jev fields in the JavaScript (`semanticPolicies.add`,
|
|
156
|
+
`authority: "reviewable"`) are refused regardless — put Jev checks in
|
|
157
|
+
`--semantic`. The server checks each declaration (limits, reserved
|
|
158
|
+
FailproofAI names, names unique in the org) and says which one it refused.
|
|
122
159
|
|
|
123
160
|
Source can come from a path, `@path`, a pipe, `-`, or an interactive paste
|
|
124
161
|
when you give none and stdin is a terminal.
|
|
@@ -134,21 +171,42 @@ def policies_publish(
|
|
|
134
171
|
* `fp policies publish no-force-push ./rule.mjs`
|
|
135
172
|
* `cat rule.mjs | fp policies publish no-force-push`
|
|
136
173
|
* `fp policies publish no-force-push -` — read stdin explicitly
|
|
174
|
+
* `fp policies publish prod-db-intent --kind jev --semantic ./checks.json`
|
|
175
|
+
* `fp policies publish no-prod-db --kind both --source ./rule.mjs --semantic ./checks.json`
|
|
137
176
|
"""
|
|
138
177
|
state: AppState = ctx.obj
|
|
139
178
|
deny_in_key_mode(state, "policies publish", _KEY_MODE_REASON)
|
|
140
179
|
cctx = require_auth(state)
|
|
141
180
|
|
|
181
|
+
if source is not None and source_opt is not None:
|
|
182
|
+
raise click.UsageError("give the JavaScript source once — positionally or with --source, not both")
|
|
183
|
+
source = source if source is not None else source_opt
|
|
184
|
+
if source == "-" and semantic_path == "-":
|
|
185
|
+
raise click.UsageError("only one of the JavaScript source and --semantic can come from stdin")
|
|
186
|
+
|
|
187
|
+
# Whether JavaScript is coming is decided WITHOUT reading stdin: a Jev-only
|
|
188
|
+
# publish must not sit waiting for a paste nobody is going to give.
|
|
189
|
+
wants_source = source is not None or kind in ("regex", "both") or (kind is None and semantic_path is None)
|
|
190
|
+
try:
|
|
191
|
+
resolved_kind = resolve_kind(kind, has_source=wants_source, has_semantic=semantic_path is not None)
|
|
192
|
+
except RefUsageError as exc:
|
|
193
|
+
raise click.UsageError(str(exc))
|
|
194
|
+
|
|
142
195
|
def _paste_prompt() -> None:
|
|
143
196
|
output.hint("paste the policy source, then press Ctrl-D")
|
|
144
197
|
|
|
198
|
+
text: Optional[str] = None
|
|
199
|
+
semantic = None
|
|
145
200
|
try:
|
|
146
|
-
|
|
201
|
+
if semantic_path is not None:
|
|
202
|
+
semantic = parse_semantic(read_source(semantic_path, isatty=False))
|
|
203
|
+
if wants_source:
|
|
204
|
+
text = read_source(source, prompt=_paste_prompt)
|
|
147
205
|
except RefUsageError as exc:
|
|
148
206
|
raise click.UsageError(str(exc))
|
|
149
207
|
except RefError as exc:
|
|
150
208
|
raise ApiError(str(exc))
|
|
151
|
-
if not text.strip():
|
|
209
|
+
if wants_source and not (text or "").strip():
|
|
152
210
|
raise ApiError("policy source is empty — nothing to publish")
|
|
153
211
|
|
|
154
212
|
# Nothing downstream parses this. The server checks the id and a size
|
|
@@ -156,7 +214,9 @@ def policies_publish(
|
|
|
156
214
|
# place for a syntax error to surface. `--no-verify` exists because a
|
|
157
215
|
# machine without node should still be able to publish, not because
|
|
158
216
|
# skipping is ever a good idea.
|
|
159
|
-
|
|
217
|
+
syn = SyntaxResult(ok=True, checked=False,
|
|
218
|
+
message="not checked" if text else "no JavaScript source")
|
|
219
|
+
if text and not no_verify:
|
|
160
220
|
syn = check_syntax(text)
|
|
161
221
|
if not syn.ok:
|
|
162
222
|
raise ApiError(
|
|
@@ -167,7 +227,13 @@ def policies_publish(
|
|
|
167
227
|
if not syn.checked and not output.is_json():
|
|
168
228
|
output.warn(syn.message)
|
|
169
229
|
|
|
170
|
-
created = api.publish_policy(
|
|
230
|
+
created = api.publish_policy(
|
|
231
|
+
cctx, policy_id, text, description,
|
|
232
|
+
# Sent only when it says something the server's default does not, so a
|
|
233
|
+
# plain JavaScript publish is the same request it has always been.
|
|
234
|
+
kind=None if resolved_kind == "regex" and kind is None else resolved_kind,
|
|
235
|
+
semantic=semantic,
|
|
236
|
+
)
|
|
171
237
|
|
|
172
238
|
# Which machines already carry this policy, and at which version. Publishing
|
|
173
239
|
# deploys nothing, so this is the one thing the card must not guess at: it
|
|
@@ -191,7 +257,7 @@ def policies_publish(
|
|
|
191
257
|
output.emit_json({**created.to_dict(), "carriers": carriers, "syntax": syn.to_dict()})
|
|
192
258
|
return
|
|
193
259
|
output.render_policy_published(created, carriers=carriers,
|
|
194
|
-
source_bytes=len(text.encode("utf-8")))
|
|
260
|
+
source_bytes=len((text or "").encode("utf-8")))
|
|
195
261
|
|
|
196
262
|
|
|
197
263
|
def policies_enable(
|