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
|
@@ -28,20 +28,34 @@ 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 server's (and the machine's) cap on Jev declarations per policy version.
|
|
47
|
+
MAX_JEV_DECLARATIONS = 24
|
|
48
|
+
AGENT_INTEGRATIONS = frozenset({
|
|
49
|
+
"claude", "codex", "copilot", "cursor", "opencode", "pi",
|
|
50
|
+
"hermes", "openclaw", "factory", "devin", "antigravity", "goose",
|
|
51
|
+
})
|
|
52
|
+
AGENT_ID = re.compile(r"^agt_[0-9a-f]{16,32}$")
|
|
53
|
+
|
|
41
54
|
#: `id`, `id@3`, `id:observe`, `id@3:observe`. The id charset mirrors the
|
|
42
55
|
#: server's `safe_identifier`, so a ref this accepts is one the server will too
|
|
43
56
|
#: — a rejection should come from the policy not existing, not from parsing.
|
|
44
57
|
_REF = re.compile(r"^(?P<id>[A-Za-z0-9._-]{1,128})(?:@(?P<version>\d+))?(?:[:](?P<effect>[a-z]+))?$")
|
|
58
|
+
_POLICY_ID = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
|
|
45
59
|
|
|
46
60
|
|
|
47
61
|
class RefError(ValueError):
|
|
@@ -103,12 +117,17 @@ class DeployPlan:
|
|
|
103
117
|
changed: List[Tuple[PolicyRef, PolicyRef]]
|
|
104
118
|
unchanged: List[PolicyRef]
|
|
105
119
|
|
|
120
|
+
@property
|
|
121
|
+
def set_changes(self) -> bool:
|
|
122
|
+
"""Whether the POLICY SET changes — anything added, removed or moved."""
|
|
123
|
+
return bool(self.added or self.removed or self.changed)
|
|
124
|
+
|
|
106
125
|
@property
|
|
107
126
|
def is_noop(self) -> bool:
|
|
108
|
-
return not
|
|
127
|
+
return not self.set_changes
|
|
109
128
|
|
|
110
129
|
def to_dict(self) -> Dict[str, object]:
|
|
111
|
-
|
|
130
|
+
out: Dict[str, object] = {
|
|
112
131
|
"machineId": self.machine_id,
|
|
113
132
|
"base": self.base,
|
|
114
133
|
"result": [p.to_dict() for p in self.result],
|
|
@@ -118,6 +137,7 @@ class DeployPlan:
|
|
|
118
137
|
"unchanged": [p.to_dict() for p in self.unchanged],
|
|
119
138
|
"noop": self.is_noop,
|
|
120
139
|
}
|
|
140
|
+
return out
|
|
121
141
|
|
|
122
142
|
|
|
123
143
|
def latest_versions(policies: Iterable[PolicyVersion]) -> Dict[str, int]:
|
|
@@ -174,7 +194,10 @@ def resolve_ref(
|
|
|
174
194
|
)
|
|
175
195
|
if effect is None:
|
|
176
196
|
effect = existing.effect if existing else "enforce"
|
|
177
|
-
return PolicyRef(
|
|
197
|
+
return PolicyRef(
|
|
198
|
+
id=pid, version=version, effect=effect,
|
|
199
|
+
agent_targets=existing.agent_targets if existing else None,
|
|
200
|
+
)
|
|
178
201
|
|
|
179
202
|
|
|
180
203
|
def plan_deploy(
|
|
@@ -187,13 +210,23 @@ def plan_deploy(
|
|
|
187
210
|
replace: Optional[Sequence[str]] = None,
|
|
188
211
|
latest: Optional[Dict[str, int]] = None,
|
|
189
212
|
disabled: Optional[set] = None,
|
|
213
|
+
kinds: Optional[Dict[Tuple[str, int], str]] = None,
|
|
214
|
+
targets: Sequence[str] = (),
|
|
215
|
+
all_agents: Sequence[str] = (),
|
|
190
216
|
) -> DeployPlan:
|
|
191
217
|
"""Compute the full resulting set, plus the diff to show before writing.
|
|
192
218
|
|
|
193
219
|
`replace` (`--set`) is exclusive with `add`/`remove`: mixing "these exactly"
|
|
194
220
|
with "these as well" has no single obvious reading, and guessing one would
|
|
195
221
|
be guessing about somebody's fleet.
|
|
222
|
+
|
|
223
|
+
There is no Jev mode to set: FailproofAI Cloud derives a machine's Jev mode
|
|
224
|
+
from what is deployed to it (enforce while any `jev`/`both` policy is,
|
|
225
|
+
off otherwise), and a Jev rollout is observed per policy with an `observe`
|
|
226
|
+
effect like any other. `kinds` (`(id, version)` → kind) is accepted for
|
|
227
|
+
callers that already have it.
|
|
196
228
|
"""
|
|
229
|
+
del kinds
|
|
197
230
|
latest = latest or {}
|
|
198
231
|
current_list = list(current or [])
|
|
199
232
|
current_map = {p.id: p for p in current_list}
|
|
@@ -220,13 +253,41 @@ def plan_deploy(
|
|
|
220
253
|
ref = resolve_ref(token, latest=latest, current=current_map, disabled=disabled)
|
|
221
254
|
result_map[ref.id] = ref
|
|
222
255
|
|
|
256
|
+
grouped: Dict[str, List[Dict[str, str]]] = {}
|
|
257
|
+
for token in targets:
|
|
258
|
+
if "=" not in token:
|
|
259
|
+
raise RefUsageError(f"--target {token!r} must be POLICY=INTEGRATION[/agt_ID]")
|
|
260
|
+
pid, selector = token.split("=", 1)
|
|
261
|
+
if not _POLICY_ID.fullmatch(pid):
|
|
262
|
+
raise RefUsageError(f"--target policy id {pid!r} is invalid")
|
|
263
|
+
integration, _, instance_id = selector.partition("/")
|
|
264
|
+
if integration not in AGENT_INTEGRATIONS or ("/" in selector and not AGENT_ID.fullmatch(instance_id)):
|
|
265
|
+
raise RefUsageError(f"--target {token!r} has an unknown integration or invalid profile ID")
|
|
266
|
+
entry = {"integration": integration}
|
|
267
|
+
if instance_id:
|
|
268
|
+
entry["instanceId"] = instance_id
|
|
269
|
+
collection = grouped.setdefault(pid, [])
|
|
270
|
+
if entry in collection or len(collection) == 32:
|
|
271
|
+
raise RefUsageError("--target has a duplicate selector or more than 32 selectors for a policy")
|
|
272
|
+
collection.append(entry)
|
|
273
|
+
clears = set(all_agents)
|
|
274
|
+
if clears.intersection(grouped):
|
|
275
|
+
raise RefUsageError("--all-agents and --target cannot name the same policy")
|
|
276
|
+
for pid in clears:
|
|
277
|
+
if not _POLICY_ID.fullmatch(pid):
|
|
278
|
+
raise RefUsageError(f"--all-agents policy id {pid!r} is invalid")
|
|
279
|
+
for pid in grouped.keys() | clears:
|
|
280
|
+
if pid not in result_map:
|
|
281
|
+
raise RefUsageError(f"{pid!r} is not in the resulting set — add or keep it before targeting")
|
|
282
|
+
result_map[pid] = dataclass_replace(result_map[pid], agent_targets=grouped.get(pid))
|
|
283
|
+
|
|
223
284
|
result = sorted(result_map.values(), key=lambda p: p.id)
|
|
224
285
|
added, removed, changed, unchanged = [], [], [], []
|
|
225
286
|
for pid, ref in sorted(result_map.items()):
|
|
226
287
|
was = current_map.get(pid)
|
|
227
288
|
if was is None:
|
|
228
289
|
added.append(ref)
|
|
229
|
-
elif (was.version, was.effect) != (ref.version, ref.effect):
|
|
290
|
+
elif (was.version, was.effect, was.agent_targets) != (ref.version, ref.effect, ref.agent_targets):
|
|
230
291
|
changed.append((was, ref))
|
|
231
292
|
else:
|
|
232
293
|
unchanged.append(ref)
|
|
@@ -245,6 +306,63 @@ def plan_deploy(
|
|
|
245
306
|
)
|
|
246
307
|
|
|
247
308
|
|
|
309
|
+
def version_kinds(policies: Iterable[PolicyVersion]) -> Dict[Tuple[str, int], str]:
|
|
310
|
+
"""`{(id, version): kind}` for every published version."""
|
|
311
|
+
return {(p.id, p.version): p.kind for p in policies}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def resolve_kind(kind: Optional[str], *, has_source: bool, has_semantic: bool) -> str:
|
|
315
|
+
"""The kind a publish is, checked against what it was given.
|
|
316
|
+
|
|
317
|
+
Explicit wins; otherwise it follows from the inputs — JavaScript alone is
|
|
318
|
+
`regex` (what `publish` has always done), declarations alone `jev`, both
|
|
319
|
+
together `both`. A kind that contradicts its inputs is a usage error: a
|
|
320
|
+
`jev` policy has no JavaScript, and `regex` has no declarations.
|
|
321
|
+
"""
|
|
322
|
+
if kind is None:
|
|
323
|
+
kind = "both" if (has_source and has_semantic) else ("jev" if has_semantic else "regex")
|
|
324
|
+
if kind not in VALID_KINDS:
|
|
325
|
+
raise RefUsageError(f"--kind {kind!r} is not one of {', '.join(VALID_KINDS)}")
|
|
326
|
+
if kind == "regex" and has_semantic:
|
|
327
|
+
raise RefUsageError("--kind regex takes no --semantic; use --kind both for JavaScript plus Jev checks")
|
|
328
|
+
if kind == "jev" and has_source:
|
|
329
|
+
raise RefUsageError("--kind jev takes no JavaScript source; use --kind both for JavaScript plus Jev checks")
|
|
330
|
+
if kind in ("jev", "both") and not has_semantic:
|
|
331
|
+
raise RefUsageError(f"--kind {kind} needs --semantic <file.json> with its Jev declarations")
|
|
332
|
+
return kind
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def parse_semantic(text: str, *, what: str = "--semantic") -> List[Any]:
|
|
336
|
+
"""The Jev declarations in a `--semantic` file: a JSON array of declaration
|
|
337
|
+
objects (a `{"semantic": [...]}` wrapper is accepted too).
|
|
338
|
+
|
|
339
|
+
Deliberately a SHAPE check only — 1 to 24 objects, each with a name. What a
|
|
340
|
+
declaration may say (modes, probes, caps, reserved names) is checked by the
|
|
341
|
+
server, and authoritatively by each machine's own parser; a third copy of
|
|
342
|
+
those rules here is a third thing to drift. The server's refusal names the
|
|
343
|
+
declaration and the rule.
|
|
344
|
+
"""
|
|
345
|
+
try:
|
|
346
|
+
value = json.loads(text)
|
|
347
|
+
except ValueError as exc:
|
|
348
|
+
raise RefUsageError(f"{what} is not valid JSON: {exc}")
|
|
349
|
+
if isinstance(value, dict) and isinstance(value.get("semantic"), list):
|
|
350
|
+
value = value["semantic"]
|
|
351
|
+
if not isinstance(value, list):
|
|
352
|
+
raise RefUsageError(f"{what} must be a JSON array of Jev declarations")
|
|
353
|
+
if not value:
|
|
354
|
+
raise RefUsageError(f"{what} holds no Jev declarations")
|
|
355
|
+
if len(value) > MAX_JEV_DECLARATIONS:
|
|
356
|
+
raise RefUsageError(
|
|
357
|
+
f"{what} holds {len(value)} Jev declarations; a policy version carries at most "
|
|
358
|
+
f"{MAX_JEV_DECLARATIONS}"
|
|
359
|
+
)
|
|
360
|
+
for i, entry in enumerate(value):
|
|
361
|
+
if not isinstance(entry, dict) or not isinstance(entry.get("name"), str) or not entry["name"]:
|
|
362
|
+
raise RefUsageError(f"{what} declaration #{i} is not an object with a name")
|
|
363
|
+
return value
|
|
364
|
+
|
|
365
|
+
|
|
248
366
|
def check_race(base: Optional[int], returned: int) -> None:
|
|
249
367
|
"""Raise when a deploy landed on top of somebody else's.
|
|
250
368
|
|
|
@@ -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
|