failproofai 1.0.12-beta.0 → 1.0.12-beta.1
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 +1 -1
- 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]__0588st-tpiuia._.js +3 -0
- 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]__0vcyntk4snf0f._.js → [root-of-the-server]__0b78b0j8iyv6l._.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]__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/_0eoura3_9gjb-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_128ebxr83nk6g._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifpkowwub._.js → _1a3tubds7s7ow._.js} +10 -9
- 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 +1 -1
- 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_1i8oid1gc7s1f.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/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/{3tn1bzk094e8e.js → 0_zfad4co-t2z.js} +1 -1
- package/.next/standalone/.next/static/chunks/0jutamwqz8pg5.js +1 -0
- package/.next/standalone/.next/static/chunks/{2meqb9kygrncb.js → 114hbolvrv94e.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2znt1784j9gd9.js → 18433qz-8njv3.js} +1 -1
- package/.next/standalone/.next/static/chunks/1b32-afqacaen.js +1 -0
- package/.next/standalone/.next/static/chunks/{13jv-xvabeb62.js → 1p-hsi58i-6h0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0pjxnp927y7_j.js → 2-tf6_7u623kd.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2qz5piiir-qqm.js → 24w3x2da9sk49.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0a-ccsp3xmypz.js → 3-pku9qwv1yfu.js} +1 -1
- package/.next/standalone/.next/static/chunks/42e8zab314654.js +1 -0
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +41 -0
- package/.next/standalone/fp-cloud-cli/README.md +42 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +117 -12
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +285 -19
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +84 -18
- package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +170 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/models.py +89 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +259 -21
- 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 +17 -9
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +692 -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 +16 -0
- package/.next/standalone/hermes-plugin/client.py +5 -1
- package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +17 -1
- package/dist/cli.mjs +2930 -1747
- package/dist/worker.mjs +1450 -145
- package/hermes-plugin/README.md +6 -4
- package/hermes-plugin/__init__.py +16 -0
- package/hermes-plugin/client.py +5 -1
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +9 -9
- package/src/audit/open-browser.ts +1 -19
- package/src/hooks/agent-roster.ts +172 -0
- package/src/hooks/agent-targets.ts +60 -0
- package/src/hooks/cloud-connection.ts +97 -27
- package/src/hooks/cloud-enrollment-cli.ts +7 -4
- package/src/hooks/cloud-introspect.ts +2 -5
- package/src/hooks/cloud-managed-policies.ts +380 -20
- package/src/hooks/cloud-policy-errors.ts +481 -0
- package/src/hooks/config-status.ts +1 -1
- package/src/hooks/configure-wizard.ts +15 -311
- 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/effective-reviewers.ts +22 -1
- package/src/hooks/fp-home.ts +6 -0
- package/src/hooks/handler.ts +235 -18
- package/src/hooks/health-probe.ts +15 -0
- package/src/hooks/integrations.ts +18 -11
- package/src/hooks/jev-cli.ts +197 -0
- package/src/hooks/jev-cloud-connection.ts +92 -0
- package/src/hooks/manager.ts +122 -16
- package/src/hooks/pack-failclosed.ts +2 -0
- package/src/hooks/pack-manifest.ts +8 -1
- package/src/hooks/policy-evaluator.ts +5 -1
- package/src/hooks/policy-registry.ts +3 -2
- package/src/hooks/policy-reviewability.ts +21 -5
- package/src/hooks/semantic/cloud-jev-health.ts +227 -0
- package/src/hooks/semantic/cloud-jev.ts +677 -0
- package/src/hooks/semantic/jev-client.ts +18 -1
- package/src/hooks/semantic/jev-config.ts +173 -3
- package/src/hooks/semantic/jev-review.ts +129 -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 +0 -271
- 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/[root-of-the-server]__0ts0fg5yih1c1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1ve8y151ua7qx._.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/2ank2u3apy8m6.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/src/hooks/cloud-login.ts +0 -245
- /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → xq8UgF5yFxty1VWxY2hyn}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → xq8UgF5yFxty1VWxY2hyn}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → xq8UgF5yFxty1VWxY2hyn}/_ssgManifest.js +0 -0
|
@@ -28,20 +28,38 @@ re-reads rather than reporting a success that erased somebody.
|
|
|
28
28
|
"""
|
|
29
29
|
from __future__ import annotations
|
|
30
30
|
|
|
31
|
+
import json
|
|
31
32
|
import re
|
|
32
33
|
import sys
|
|
33
|
-
from dataclasses import dataclass
|
|
34
|
-
from typing import Dict, Iterable, List, Optional, Sequence, Tuple
|
|
34
|
+
from dataclasses import dataclass, replace as dataclass_replace
|
|
35
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
35
36
|
|
|
36
37
|
from .errors import ApiError
|
|
37
38
|
from .models import PolicyRef, PolicyVersion
|
|
38
39
|
|
|
39
40
|
VALID_EFFECTS = ("enforce", "observe")
|
|
40
41
|
|
|
42
|
+
#: What a policy version carries: JavaScript (`regex`), Jev checks (`jev`), or
|
|
43
|
+
#: JavaScript reviewable by its own Jev checks (`both`).
|
|
44
|
+
VALID_KINDS = ("regex", "jev", "both")
|
|
45
|
+
|
|
46
|
+
#: The Jev modes a deployment may set. `local` stops FailproofAI Cloud
|
|
47
|
+
#: overriding the machine's own mode (the server stores it as no value).
|
|
48
|
+
JEV_MODES = ("off", "observe", "enforce", "local")
|
|
49
|
+
|
|
50
|
+
#: The server's (and the machine's) cap on Jev declarations per policy version.
|
|
51
|
+
MAX_JEV_DECLARATIONS = 24
|
|
52
|
+
AGENT_INTEGRATIONS = frozenset({
|
|
53
|
+
"claude", "codex", "copilot", "cursor", "opencode", "pi",
|
|
54
|
+
"hermes", "openclaw", "factory", "devin", "antigravity", "goose",
|
|
55
|
+
})
|
|
56
|
+
AGENT_ID = re.compile(r"^agt_[0-9a-f]{16,32}$")
|
|
57
|
+
|
|
41
58
|
#: `id`, `id@3`, `id:observe`, `id@3:observe`. The id charset mirrors the
|
|
42
59
|
#: server's `safe_identifier`, so a ref this accepts is one the server will too
|
|
43
60
|
#: — a rejection should come from the policy not existing, not from parsing.
|
|
44
61
|
_REF = re.compile(r"^(?P<id>[A-Za-z0-9._-]{1,128})(?:@(?P<version>\d+))?(?:[:](?P<effect>[a-z]+))?$")
|
|
62
|
+
_POLICY_ID = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
|
|
45
63
|
|
|
46
64
|
|
|
47
65
|
class RefError(ValueError):
|
|
@@ -102,13 +120,34 @@ class DeployPlan:
|
|
|
102
120
|
removed: List[PolicyRef]
|
|
103
121
|
changed: List[Tuple[PolicyRef, PolicyRef]]
|
|
104
122
|
unchanged: List[PolicyRef]
|
|
123
|
+
#: `--jev-mode` as given (`off|observe|enforce|local`), or None when the
|
|
124
|
+
#: command said nothing about Jev — which the server reads as "keep it".
|
|
125
|
+
jev_mode: Optional[str] = None
|
|
126
|
+
#: The mode FailproofAI Cloud sets on the machine now (None = local).
|
|
127
|
+
jev_mode_before: Optional[str] = None
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def jev_mode_after(self) -> Optional[str]:
|
|
131
|
+
"""What the machine's Cloud-set mode will be (None = local)."""
|
|
132
|
+
if self.jev_mode is None:
|
|
133
|
+
return self.jev_mode_before
|
|
134
|
+
return None if self.jev_mode == "local" else self.jev_mode
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def jev_mode_changes(self) -> bool:
|
|
138
|
+
return self.jev_mode is not None and self.jev_mode_after != self.jev_mode_before
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def set_changes(self) -> bool:
|
|
142
|
+
"""Whether the POLICY SET changes — anything added, removed or moved."""
|
|
143
|
+
return bool(self.added or self.removed or self.changed)
|
|
105
144
|
|
|
106
145
|
@property
|
|
107
146
|
def is_noop(self) -> bool:
|
|
108
|
-
return not (self.
|
|
147
|
+
return not (self.set_changes or self.jev_mode_changes)
|
|
109
148
|
|
|
110
149
|
def to_dict(self) -> Dict[str, object]:
|
|
111
|
-
|
|
150
|
+
out: Dict[str, object] = {
|
|
112
151
|
"machineId": self.machine_id,
|
|
113
152
|
"base": self.base,
|
|
114
153
|
"result": [p.to_dict() for p in self.result],
|
|
@@ -118,6 +157,14 @@ class DeployPlan:
|
|
|
118
157
|
"unchanged": [p.to_dict() for p in self.unchanged],
|
|
119
158
|
"noop": self.is_noop,
|
|
120
159
|
}
|
|
160
|
+
if self.jev_mode is not None:
|
|
161
|
+
# `local` on both sides: the wire word, not the stored NULL.
|
|
162
|
+
out["jevMode"] = {
|
|
163
|
+
"from": self.jev_mode_before or "local",
|
|
164
|
+
"to": self.jev_mode_after or "local",
|
|
165
|
+
"changed": self.jev_mode_changes,
|
|
166
|
+
}
|
|
167
|
+
return out
|
|
121
168
|
|
|
122
169
|
|
|
123
170
|
def latest_versions(policies: Iterable[PolicyVersion]) -> Dict[str, int]:
|
|
@@ -174,7 +221,10 @@ def resolve_ref(
|
|
|
174
221
|
)
|
|
175
222
|
if effect is None:
|
|
176
223
|
effect = existing.effect if existing else "enforce"
|
|
177
|
-
return PolicyRef(
|
|
224
|
+
return PolicyRef(
|
|
225
|
+
id=pid, version=version, effect=effect,
|
|
226
|
+
agent_targets=existing.agent_targets if existing else None,
|
|
227
|
+
)
|
|
178
228
|
|
|
179
229
|
|
|
180
230
|
def plan_deploy(
|
|
@@ -187,13 +237,28 @@ def plan_deploy(
|
|
|
187
237
|
replace: Optional[Sequence[str]] = None,
|
|
188
238
|
latest: Optional[Dict[str, int]] = None,
|
|
189
239
|
disabled: Optional[set] = None,
|
|
240
|
+
jev_mode: Optional[str] = None,
|
|
241
|
+
current_jev_mode: Optional[str] = None,
|
|
242
|
+
kinds: Optional[Dict[Tuple[str, int], str]] = None,
|
|
243
|
+
targets: Sequence[str] = (),
|
|
244
|
+
all_agents: Sequence[str] = (),
|
|
190
245
|
) -> DeployPlan:
|
|
191
246
|
"""Compute the full resulting set, plus the diff to show before writing.
|
|
192
247
|
|
|
193
248
|
`replace` (`--set`) is exclusive with `add`/`remove`: mixing "these exactly"
|
|
194
249
|
with "these as well" has no single obvious reading, and guessing one would
|
|
195
250
|
be guessing about somebody's fleet.
|
|
251
|
+
|
|
252
|
+
`jev_mode` is `--jev-mode`; `current_jev_mode` what Cloud sets now (None =
|
|
253
|
+
local). `kinds` maps `(id, version)` to the version's kind, so an `observe`
|
|
254
|
+
effect on a Jev-only version — which the server refuses, because a Jev
|
|
255
|
+
rollout is observed through the Jev MODE — is refused here, before a plan is
|
|
256
|
+
drawn and confirmed for a write that cannot happen.
|
|
196
257
|
"""
|
|
258
|
+
if jev_mode is not None and jev_mode not in JEV_MODES:
|
|
259
|
+
raise RefUsageError(
|
|
260
|
+
f"--jev-mode {jev_mode!r} is not one of {', '.join(JEV_MODES)}"
|
|
261
|
+
)
|
|
197
262
|
latest = latest or {}
|
|
198
263
|
current_list = list(current or [])
|
|
199
264
|
current_map = {p.id: p for p in current_list}
|
|
@@ -220,13 +285,48 @@ def plan_deploy(
|
|
|
220
285
|
ref = resolve_ref(token, latest=latest, current=current_map, disabled=disabled)
|
|
221
286
|
result_map[ref.id] = ref
|
|
222
287
|
|
|
288
|
+
grouped: Dict[str, List[Dict[str, str]]] = {}
|
|
289
|
+
for token in targets:
|
|
290
|
+
if "=" not in token:
|
|
291
|
+
raise RefUsageError(f"--target {token!r} must be POLICY=INTEGRATION[/agt_ID]")
|
|
292
|
+
pid, selector = token.split("=", 1)
|
|
293
|
+
if not _POLICY_ID.fullmatch(pid):
|
|
294
|
+
raise RefUsageError(f"--target policy id {pid!r} is invalid")
|
|
295
|
+
integration, _, instance_id = selector.partition("/")
|
|
296
|
+
if integration not in AGENT_INTEGRATIONS or ("/" in selector and not AGENT_ID.fullmatch(instance_id)):
|
|
297
|
+
raise RefUsageError(f"--target {token!r} has an unknown integration or invalid profile ID")
|
|
298
|
+
entry = {"integration": integration}
|
|
299
|
+
if instance_id:
|
|
300
|
+
entry["instanceId"] = instance_id
|
|
301
|
+
collection = grouped.setdefault(pid, [])
|
|
302
|
+
if entry in collection or len(collection) == 32:
|
|
303
|
+
raise RefUsageError("--target has a duplicate selector or more than 32 selectors for a policy")
|
|
304
|
+
collection.append(entry)
|
|
305
|
+
clears = set(all_agents)
|
|
306
|
+
if clears.intersection(grouped):
|
|
307
|
+
raise RefUsageError("--all-agents and --target cannot name the same policy")
|
|
308
|
+
for pid in clears:
|
|
309
|
+
if not _POLICY_ID.fullmatch(pid):
|
|
310
|
+
raise RefUsageError(f"--all-agents policy id {pid!r} is invalid")
|
|
311
|
+
for pid in grouped.keys() | clears:
|
|
312
|
+
if pid not in result_map:
|
|
313
|
+
raise RefUsageError(f"{pid!r} is not in the resulting set — add or keep it before targeting")
|
|
314
|
+
result_map[pid] = dataclass_replace(result_map[pid], agent_targets=grouped.get(pid))
|
|
315
|
+
|
|
223
316
|
result = sorted(result_map.values(), key=lambda p: p.id)
|
|
317
|
+
for ref in result:
|
|
318
|
+
if ref.effect == "observe" and (kinds or {}).get((ref.id, ref.version)) == "jev":
|
|
319
|
+
raise RefUsageError(
|
|
320
|
+
f"{ref.id}@{ref.version} is a Jev policy, and an effect does not apply to Jev "
|
|
321
|
+
"checks — deploy it as enforce and use --jev-mode observe to watch Jev without "
|
|
322
|
+
"it deciding anything"
|
|
323
|
+
)
|
|
224
324
|
added, removed, changed, unchanged = [], [], [], []
|
|
225
325
|
for pid, ref in sorted(result_map.items()):
|
|
226
326
|
was = current_map.get(pid)
|
|
227
327
|
if was is None:
|
|
228
328
|
added.append(ref)
|
|
229
|
-
elif (was.version, was.effect) != (ref.version, ref.effect):
|
|
329
|
+
elif (was.version, was.effect, was.agent_targets) != (ref.version, ref.effect, ref.agent_targets):
|
|
230
330
|
changed.append((was, ref))
|
|
231
331
|
else:
|
|
232
332
|
unchanged.append(ref)
|
|
@@ -242,9 +342,73 @@ def plan_deploy(
|
|
|
242
342
|
removed=removed,
|
|
243
343
|
changed=changed,
|
|
244
344
|
unchanged=unchanged,
|
|
345
|
+
jev_mode=jev_mode,
|
|
346
|
+
jev_mode_before=current_jev_mode,
|
|
245
347
|
)
|
|
246
348
|
|
|
247
349
|
|
|
350
|
+
def jev_mode_after(mode: str) -> Optional[str]:
|
|
351
|
+
"""What a `--jev-mode`/`jev-mode` word leaves stored: `local` is None (Cloud stops overriding)."""
|
|
352
|
+
return None if mode == "local" else mode
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def version_kinds(policies: Iterable[PolicyVersion]) -> Dict[Tuple[str, int], str]:
|
|
356
|
+
"""`{(id, version): kind}` for every published version."""
|
|
357
|
+
return {(p.id, p.version): p.kind for p in policies}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def resolve_kind(kind: Optional[str], *, has_source: bool, has_semantic: bool) -> str:
|
|
361
|
+
"""The kind a publish is, checked against what it was given.
|
|
362
|
+
|
|
363
|
+
Explicit wins; otherwise it follows from the inputs — JavaScript alone is
|
|
364
|
+
`regex` (what `publish` has always done), declarations alone `jev`, both
|
|
365
|
+
together `both`. A kind that contradicts its inputs is a usage error: a
|
|
366
|
+
`jev` policy has no JavaScript, and `regex` has no declarations.
|
|
367
|
+
"""
|
|
368
|
+
if kind is None:
|
|
369
|
+
kind = "both" if (has_source and has_semantic) else ("jev" if has_semantic else "regex")
|
|
370
|
+
if kind not in VALID_KINDS:
|
|
371
|
+
raise RefUsageError(f"--kind {kind!r} is not one of {', '.join(VALID_KINDS)}")
|
|
372
|
+
if kind == "regex" and has_semantic:
|
|
373
|
+
raise RefUsageError("--kind regex takes no --semantic; use --kind both for JavaScript plus Jev checks")
|
|
374
|
+
if kind == "jev" and has_source:
|
|
375
|
+
raise RefUsageError("--kind jev takes no JavaScript source; use --kind both for JavaScript plus Jev checks")
|
|
376
|
+
if kind in ("jev", "both") and not has_semantic:
|
|
377
|
+
raise RefUsageError(f"--kind {kind} needs --semantic <file.json> with its Jev declarations")
|
|
378
|
+
return kind
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def parse_semantic(text: str, *, what: str = "--semantic") -> List[Any]:
|
|
382
|
+
"""The Jev declarations in a `--semantic` file: a JSON array of declaration
|
|
383
|
+
objects (a `{"semantic": [...]}` wrapper is accepted too).
|
|
384
|
+
|
|
385
|
+
Deliberately a SHAPE check only — 1 to 24 objects, each with a name. What a
|
|
386
|
+
declaration may say (modes, probes, caps, reserved names) is checked by the
|
|
387
|
+
server, and authoritatively by each machine's own parser; a third copy of
|
|
388
|
+
those rules here is a third thing to drift. The server's refusal names the
|
|
389
|
+
declaration and the rule.
|
|
390
|
+
"""
|
|
391
|
+
try:
|
|
392
|
+
value = json.loads(text)
|
|
393
|
+
except ValueError as exc:
|
|
394
|
+
raise RefUsageError(f"{what} is not valid JSON: {exc}")
|
|
395
|
+
if isinstance(value, dict) and isinstance(value.get("semantic"), list):
|
|
396
|
+
value = value["semantic"]
|
|
397
|
+
if not isinstance(value, list):
|
|
398
|
+
raise RefUsageError(f"{what} must be a JSON array of Jev declarations")
|
|
399
|
+
if not value:
|
|
400
|
+
raise RefUsageError(f"{what} holds no Jev declarations")
|
|
401
|
+
if len(value) > MAX_JEV_DECLARATIONS:
|
|
402
|
+
raise RefUsageError(
|
|
403
|
+
f"{what} holds {len(value)} Jev declarations; a policy version carries at most "
|
|
404
|
+
f"{MAX_JEV_DECLARATIONS}"
|
|
405
|
+
)
|
|
406
|
+
for i, entry in enumerate(value):
|
|
407
|
+
if not isinstance(entry, dict) or not isinstance(entry.get("name"), str) or not entry["name"]:
|
|
408
|
+
raise RefUsageError(f"{what} declaration #{i} is not an object with a name")
|
|
409
|
+
return value
|
|
410
|
+
|
|
411
|
+
|
|
248
412
|
def check_race(base: Optional[int], returned: int) -> None:
|
|
249
413
|
"""Raise when a deploy landed on top of somebody else's.
|
|
250
414
|
|
|
@@ -724,7 +724,17 @@ class AuditFinding:
|
|
|
724
724
|
|
|
725
725
|
@dataclass
|
|
726
726
|
class PolicyVersion:
|
|
727
|
-
"""One published version of a policy. Versions are minted, never edited.
|
|
727
|
+
"""One published version of a policy. Versions are minted, never edited.
|
|
728
|
+
|
|
729
|
+
``kind`` is what the version carries: ``regex`` (JavaScript only — every
|
|
730
|
+
version published before kinds existed), ``jev`` (Jev checks only, no
|
|
731
|
+
JavaScript) or ``both`` (JavaScript whose verdict its own Jev checks may
|
|
732
|
+
clear). ``semantic`` is the list of Jev declarations for ``jev``/``both``.
|
|
733
|
+
``authority``/``reviewed_by`` are DERIVED by the server from the kind and
|
|
734
|
+
never sent by a client. ``jev_chars`` is what its Jev checks take of a
|
|
735
|
+
machine's Jev question budget, measured by the server the way the machine
|
|
736
|
+
measures it; ``None`` for ``regex``.
|
|
737
|
+
"""
|
|
728
738
|
|
|
729
739
|
id: str
|
|
730
740
|
version: int
|
|
@@ -735,9 +745,18 @@ class PolicyVersion:
|
|
|
735
745
|
created_by: Optional[str]
|
|
736
746
|
disabled: bool
|
|
737
747
|
archived: bool
|
|
748
|
+
kind: str = "regex"
|
|
749
|
+
semantic: Optional[List[Any]] = None
|
|
750
|
+
semantic_sha256: Optional[str] = None
|
|
751
|
+
authority: Optional[str] = None
|
|
752
|
+
reviewed_by: Optional[List[str]] = None
|
|
753
|
+
jev_chars: Optional[int] = None
|
|
738
754
|
|
|
739
755
|
@classmethod
|
|
740
756
|
def from_dict(cls, d: Dict[str, Any]) -> "PolicyVersion":
|
|
757
|
+
semantic = d.get("semantic")
|
|
758
|
+
reviewed = d.get("reviewedBy", d.get("reviewed_by"))
|
|
759
|
+
chars = d.get("jevChars", d.get("jev_chars"))
|
|
741
760
|
return cls(
|
|
742
761
|
id=str(d.get("id", "")),
|
|
743
762
|
version=_as_int(d.get("version"), 0),
|
|
@@ -748,18 +767,46 @@ class PolicyVersion:
|
|
|
748
767
|
created_by=d.get("createdBy", d.get("created_by")),
|
|
749
768
|
disabled=bool(d.get("disabled", False)),
|
|
750
769
|
archived=bool(d.get("archived", False)),
|
|
770
|
+
# A server that predates kinds sends none, and everything it has is
|
|
771
|
+
# JavaScript — `regex` is not a guess, it is the only thing it had.
|
|
772
|
+
kind=str(d.get("kind") or "regex"),
|
|
773
|
+
semantic=semantic if isinstance(semantic, list) else None,
|
|
774
|
+
semantic_sha256=d.get("semanticSha256", d.get("semantic_sha256")),
|
|
775
|
+
authority=d.get("authority"),
|
|
776
|
+
reviewed_by=[str(n) for n in reviewed] if isinstance(reviewed, list) else None,
|
|
777
|
+
jev_chars=chars if isinstance(chars, int) and not isinstance(chars, bool) else None,
|
|
751
778
|
)
|
|
752
779
|
|
|
780
|
+
@property
|
|
781
|
+
def jev_names(self) -> List[str]:
|
|
782
|
+
"""The names of this version's Jev checks, in declared order."""
|
|
783
|
+
return [
|
|
784
|
+
str(entry["name"])
|
|
785
|
+
for entry in (self.semantic or [])
|
|
786
|
+
if isinstance(entry, dict) and isinstance(entry.get("name"), str)
|
|
787
|
+
]
|
|
788
|
+
|
|
753
789
|
def to_dict(self) -> Dict[str, Any]:
|
|
754
790
|
"""The server's own shape. `vars()` would leak Python snake_case into a
|
|
755
791
|
contract that is camelCase everywhere else, which is a difference a
|
|
756
792
|
harness discovers at runtime rather than in review."""
|
|
757
|
-
|
|
793
|
+
out: Dict[str, Any] = {
|
|
758
794
|
"id": self.id, "version": self.version, "description": self.description,
|
|
759
795
|
"sha256": self.sha256, "source": self.source, "createdAt": self.created_at,
|
|
760
796
|
"createdBy": self.created_by, "disabled": self.disabled,
|
|
761
|
-
"archived": self.archived,
|
|
797
|
+
"archived": self.archived, "kind": self.kind,
|
|
762
798
|
}
|
|
799
|
+
if self.semantic is not None:
|
|
800
|
+
out["semantic"] = self.semantic
|
|
801
|
+
if self.semantic_sha256:
|
|
802
|
+
out["semanticSha256"] = self.semantic_sha256
|
|
803
|
+
if self.authority:
|
|
804
|
+
out["authority"] = self.authority
|
|
805
|
+
if self.reviewed_by is not None:
|
|
806
|
+
out["reviewedBy"] = self.reviewed_by
|
|
807
|
+
if self.jev_chars is not None:
|
|
808
|
+
out["jevChars"] = self.jev_chars
|
|
809
|
+
return out
|
|
763
810
|
|
|
764
811
|
|
|
765
812
|
@dataclass
|
|
@@ -774,6 +821,9 @@ class PolicyRef:
|
|
|
774
821
|
id: str
|
|
775
822
|
version: int
|
|
776
823
|
effect: str = "enforce"
|
|
824
|
+
#: None means every agent; a nonempty selector list narrows this
|
|
825
|
+
#: machine's assignment, never the immutable published policy version.
|
|
826
|
+
agent_targets: Optional[List[Dict[str, str]]] = None
|
|
777
827
|
|
|
778
828
|
@classmethod
|
|
779
829
|
def from_dict(cls, d: Dict[str, Any]) -> "PolicyRef":
|
|
@@ -781,14 +831,23 @@ class PolicyRef:
|
|
|
781
831
|
id=str(d.get("id", "")),
|
|
782
832
|
version=_as_int(d.get("version"), 0),
|
|
783
833
|
effect=str(d.get("effect") or "enforce"),
|
|
834
|
+
agent_targets=[dict(target) for target in d["agentTargets"]]
|
|
835
|
+
if isinstance(d.get("agentTargets"), list) else None,
|
|
784
836
|
)
|
|
785
837
|
|
|
786
838
|
def to_dict(self) -> Dict[str, Any]:
|
|
787
|
-
|
|
839
|
+
out: Dict[str, Any] = {"id": self.id, "version": self.version, "effect": self.effect}
|
|
840
|
+
if self.agent_targets is not None:
|
|
841
|
+
out["agentTargets"] = self.agent_targets
|
|
842
|
+
return out
|
|
788
843
|
|
|
789
844
|
@property
|
|
790
845
|
def label(self) -> str:
|
|
791
|
-
|
|
846
|
+
scope = "" if not self.agent_targets else " → " + ", ".join(
|
|
847
|
+
f"{t['integration']}/{t['instanceId']}" if t.get("instanceId") else f"all {t['integration']}"
|
|
848
|
+
for t in self.agent_targets
|
|
849
|
+
)
|
|
850
|
+
return f"{self.id}@{self.version}:{self.effect}{scope}"
|
|
792
851
|
|
|
793
852
|
|
|
794
853
|
@dataclass
|
|
@@ -806,6 +865,9 @@ class Deployment:
|
|
|
806
865
|
policies: List[PolicyRef]
|
|
807
866
|
updated_at: str
|
|
808
867
|
updated_by: Optional[str]
|
|
868
|
+
#: The Jev mode FailproofAI Cloud sets on this machine (`off|observe|enforce`),
|
|
869
|
+
#: or None when Cloud does not override the machine's own (`local`).
|
|
870
|
+
jev_mode: Optional[str] = None
|
|
809
871
|
|
|
810
872
|
@classmethod
|
|
811
873
|
def from_dict(cls, d: Dict[str, Any]) -> "Deployment":
|
|
@@ -815,14 +877,18 @@ class Deployment:
|
|
|
815
877
|
policies=[PolicyRef.from_dict(p) for p in (d.get("policies") or [])],
|
|
816
878
|
updated_at=str(d.get("updatedAt", d.get("updated_at", "")) or ""),
|
|
817
879
|
updated_by=d.get("updatedBy", d.get("updated_by")),
|
|
880
|
+
jev_mode=d.get("jevMode", d.get("jev_mode")) or None,
|
|
818
881
|
)
|
|
819
882
|
|
|
820
883
|
def to_dict(self) -> Dict[str, Any]:
|
|
821
|
-
|
|
884
|
+
out: Dict[str, Any] = {
|
|
822
885
|
"machineId": self.machine_id, "deployment": self.deployment,
|
|
823
886
|
"policies": [p.to_dict() for p in self.policies],
|
|
824
887
|
"updatedAt": self.updated_at, "updatedBy": self.updated_by,
|
|
825
888
|
}
|
|
889
|
+
if self.jev_mode:
|
|
890
|
+
out["jevMode"] = self.jev_mode
|
|
891
|
+
return out
|
|
826
892
|
|
|
827
893
|
|
|
828
894
|
@dataclass
|
|
@@ -851,6 +917,13 @@ class Machine:
|
|
|
851
917
|
deployed: bool
|
|
852
918
|
policy_count: int
|
|
853
919
|
event_count: int
|
|
920
|
+
#: What the machine last reported it could not apply — `[{id, version, kind,
|
|
921
|
+
#: message}]` — or None when it has never reported (an empty list means it
|
|
922
|
+
#: reported and all is well).
|
|
923
|
+
policy_errors: Optional[List[Dict[str, Any]]] = None
|
|
924
|
+
policy_errors_at: Optional[Any] = None
|
|
925
|
+
#: The Jev mode FailproofAI Cloud sets on this machine, or None (`local`).
|
|
926
|
+
jev_mode: Optional[str] = None
|
|
854
927
|
|
|
855
928
|
@classmethod
|
|
856
929
|
def from_dict(cls, d: Dict[str, Any]) -> "Machine":
|
|
@@ -858,6 +931,8 @@ class Machine:
|
|
|
858
931
|
v = d.get(key)
|
|
859
932
|
return int(v) if isinstance(v, (int, float)) else None
|
|
860
933
|
|
|
934
|
+
errors = d.get("policyErrors", d.get("policy_errors"))
|
|
935
|
+
|
|
861
936
|
return cls(
|
|
862
937
|
machine_id=str(d.get("machineId", d.get("machine_id", "")) or ""),
|
|
863
938
|
label=d.get("label"),
|
|
@@ -870,6 +945,9 @@ class Machine:
|
|
|
870
945
|
deployed=bool(d.get("deployed", False)),
|
|
871
946
|
policy_count=_as_int(d.get("policyCount"), 0),
|
|
872
947
|
event_count=_as_int(d.get("eventCount"), 0),
|
|
948
|
+
policy_errors=[e for e in errors if isinstance(e, dict)] if isinstance(errors, list) else None,
|
|
949
|
+
policy_errors_at=d.get("policyErrorsAt", d.get("policy_errors_at")),
|
|
950
|
+
jev_mode=d.get("jevMode", d.get("jev_mode")) or None,
|
|
873
951
|
)
|
|
874
952
|
|
|
875
953
|
@property
|
|
@@ -892,6 +970,11 @@ class Machine:
|
|
|
892
970
|
"appliedAt": self.applied_at, "deployed": self.deployed,
|
|
893
971
|
"policyCount": self.policy_count, "eventCount": self.event_count,
|
|
894
972
|
"drifted": self.drifted,
|
|
973
|
+
# Emitted only when the server sent them, so a server that predates
|
|
974
|
+
# them reads exactly as it always did.
|
|
975
|
+
**({"policyErrors": self.policy_errors} if self.policy_errors is not None else {}),
|
|
976
|
+
**({"policyErrorsAt": self.policy_errors_at} if self.policy_errors_at is not None else {}),
|
|
977
|
+
**({"jevMode": self.jev_mode} if self.jev_mode else {}),
|
|
895
978
|
}
|
|
896
979
|
|
|
897
980
|
@property
|