failproofai 1.0.12-beta.1 → 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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00l2u6j86cb8y._.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]__0bigvy9xmv2ka._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__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]__0b78b0j8iyv6l._.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/{_1a3tubds7s7ow._.js → _0ouu886szh6yj._.js} +4 -4
- 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/_1fw0_nzkae5ao._.js +3 -0
- 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_1i8oid1gc7s1f.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_0agdmptetetdt._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589gp4p60j._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/{2-tf6_7u623kd.js → 0ay__-d2h1xb5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{18433qz-8njv3.js → 0tbm3vjm62h-2.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1b32-afqacaen.js → 1c_jf6g5wnslk.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0_zfad4co-t2z.js → 1my3-5p47t7r-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0jutamwqz8pg5.js → 20py8k1gd_i1m.js} +1 -1
- 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/{42e8zab314654.js → 2o6d37j425uci.js} +1 -1
- package/.next/standalone/.next/static/chunks/40oworn_-x12p.js +1 -0
- package/.next/standalone/.next/static/chunks/{24w3x2da9sk49.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 +9 -18
- package/.next/standalone/fp-cloud-cli/README.md +7 -9
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +1 -27
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +12 -216
- package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +7 -53
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +2 -71
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +1 -1
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +6 -7
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +32 -197
- package/.next/standalone/hermes-plugin/__init__.py +11 -0
- package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
- package/.next/standalone/package.json +11 -10
- package/.next/standalone/sdk/python/uv.lock +31 -8
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +1686 -612
- package/dist/worker.mjs +354 -49
- package/hermes-plugin/__init__.py +11 -0
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +11 -10
- package/src/audit/open-browser.ts +24 -7
- package/src/hooks/agent-roster.ts +9 -1
- package/src/hooks/agent-skill.ts +203 -0
- package/src/hooks/cloud-connection.ts +27 -51
- package/src/hooks/cloud-introspect.ts +5 -2
- package/src/hooks/cloud-login.ts +248 -0
- package/src/hooks/cloud-managed-policies.ts +54 -4
- package/src/hooks/codex-system-hooks.ts +10 -4
- package/src/hooks/config-status.ts +9 -3
- package/src/hooks/configure-wizard.ts +356 -32
- package/src/hooks/delivery-health.ts +47 -2
- package/src/hooks/effective-reviewers.ts +14 -0
- package/src/hooks/fp-config.ts +19 -0
- package/src/hooks/handler.ts +21 -2
- package/src/hooks/hook-activity-store.ts +7 -1
- package/src/hooks/integrations.ts +18 -3
- package/src/hooks/jev-activity.ts +117 -1
- package/src/hooks/jev-cli.ts +7 -9
- package/src/hooks/manager.ts +80 -46
- package/src/hooks/policy-evaluator.ts +17 -0
- package/src/hooks/policy-reviewability.ts +4 -2
- package/src/hooks/semantic/cloud-jev.ts +219 -28
- package/src/hooks/semantic/combine.ts +26 -1
- package/src/hooks/semantic/jev-client.ts +1 -1
- package/src/hooks/semantic/jev-config.ts +6 -27
- package/src/hooks/semantic/jev-review.ts +90 -2
- package/src/hooks/tui.ts +280 -0
- package/src/hooks/uninstall-cli.ts +12 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0588st-tpiuia._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_128ebxr83nk6g._.js +0 -3
- package/.next/standalone/.next/static/chunks/114hbolvrv94e.js +0 -1
- package/.next/standalone/.next/static/chunks/1p-hsi58i-6h0.js +0 -6
- package/.next/standalone/.next/static/chunks/3-pku9qwv1yfu.js +0 -1
- /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → LHCtFxVG0mdvd4lsXAOFC}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → LHCtFxVG0mdvd4lsXAOFC}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → LHCtFxVG0mdvd4lsXAOFC}/_ssgManifest.js +0 -0
|
@@ -490,7 +490,7 @@ _TOP_LEVEL_GROUPS = [
|
|
|
490
490
|
]),
|
|
491
491
|
("ENFORCE", [
|
|
492
492
|
("policies", "Write cloud-managed policies.", "list show publish test compose enable disable delete"),
|
|
493
|
-
("fleet", "Deploy policies to machines.", "list show deploy
|
|
493
|
+
("fleet", "Deploy policies to machines.", "list show deploy diff history rollback rename"),
|
|
494
494
|
("guardrails", "What enforcement actually blocked.", "summary timeline"),
|
|
495
495
|
]),
|
|
496
496
|
("MANAGE", [
|
|
@@ -6312,16 +6312,6 @@ def render_deploy_plan(plan: Any, *, applied: bool = False) -> None:
|
|
|
6312
6312
|
footer.append(f"polic{'y' if n == 1 else 'ies'} after this change", style=theme.LABEL)
|
|
6313
6313
|
lines.append(Text())
|
|
6314
6314
|
lines.append(footer)
|
|
6315
|
-
if getattr(plan, "jev_mode", None) is not None:
|
|
6316
|
-
before = plan.jev_mode_before or "local"
|
|
6317
|
-
after = plan.jev_mode_after or "local"
|
|
6318
|
-
jev = Text(" jev mode ", style=theme.LABEL)
|
|
6319
|
-
if plan.jev_mode_changes:
|
|
6320
|
-
jev.append(f"{before} → ", style=theme.FAINT)
|
|
6321
|
-
jev.append(after, style=f"bold {theme.ACCENT}")
|
|
6322
|
-
else:
|
|
6323
|
-
jev.append(f"{after} (unchanged)", style=theme.TEXT_DIM)
|
|
6324
|
-
lines.append(jev)
|
|
6325
6315
|
|
|
6326
6316
|
head = Text(plan.machine_id, style=f"bold {theme.TEXT}")
|
|
6327
6317
|
if plan.base is not None:
|
|
@@ -6482,65 +6472,6 @@ def deployment_rolled_back(
|
|
|
6482
6472
|
_notice_box(body, color=theme.SUCCESS, title="rolled back")
|
|
6483
6473
|
|
|
6484
6474
|
|
|
6485
|
-
def render_jev_mode_plan(rows: Sequence[dict]) -> None:
|
|
6486
|
-
"""The mode-only change, per machine: ``from → to``, the set untouched."""
|
|
6487
|
-
out = []
|
|
6488
|
-
for r in rows:
|
|
6489
|
-
changed = bool(r.get("changed"))
|
|
6490
|
-
mode = Text()
|
|
6491
|
-
if changed:
|
|
6492
|
-
mode.append(f"{r.get('from')} → ", style=theme.FAINT)
|
|
6493
|
-
mode.append(str(r.get("to")), style=f"bold {theme.ACCENT}")
|
|
6494
|
-
else:
|
|
6495
|
-
mode.append(f"{r.get('to')} (unchanged)", style=theme.TEXT_DIM)
|
|
6496
|
-
out.append([Text(str(r.get("machineId")), style=theme.TEXT), mode])
|
|
6497
|
-
n = sum(1 for r in rows if r.get("changed"))
|
|
6498
|
-
title = Text()
|
|
6499
|
-
title.append("jev mode", style=f"bold {theme.ACCENT}")
|
|
6500
|
-
title.append(" · ", style=theme.FAINT)
|
|
6501
|
-
title.append(f"{n} of {len(rows)} to change", style=theme.LABEL)
|
|
6502
|
-
title.append(" · policy sets untouched", style=theme.FAINT)
|
|
6503
|
-
render_list_panel("jev mode", header=["machine", "jev mode"], rows=out, days=set(), order=None,
|
|
6504
|
-
empty_message="no machines", title=title)
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
def jev_mode_applied(rows: Sequence[dict]) -> None:
|
|
6508
|
-
"""``✓ jev mode observe → enforce on N machines``, and any that failed."""
|
|
6509
|
-
done = [r for r in rows if r.get("changed") and not r.get("error")]
|
|
6510
|
-
failed = [r for r in rows if r.get("error")]
|
|
6511
|
-
for r in done:
|
|
6512
|
-
body = Text()
|
|
6513
|
-
body.append("✓ ", style=theme.SUCCESS)
|
|
6514
|
-
body.append("jev mode ", style=theme.TEXT)
|
|
6515
|
-
body.append(f"{r.get('from')} → ", style=theme.FAINT)
|
|
6516
|
-
body.append(str(r.get("to")), style=f"bold {theme.ACCENT}")
|
|
6517
|
-
body.append(" on ", style=theme.TEXT)
|
|
6518
|
-
body.append(str(r.get("machineId")), style=theme.ACCENT)
|
|
6519
|
-
if r.get("deployment") is not None:
|
|
6520
|
-
body.append(" · ", style=theme.FAINT)
|
|
6521
|
-
body.append(f"now on deployment #{r.get('deployment')}", style=theme.LABEL)
|
|
6522
|
-
_notice_box(body, color=theme.SUCCESS, title="jev mode")
|
|
6523
|
-
for r in failed:
|
|
6524
|
-
body = Text()
|
|
6525
|
-
body.append("✗ ", style=theme.ERROR)
|
|
6526
|
-
body.append(str(r.get("machineId")), style=theme.ACCENT)
|
|
6527
|
-
body.append(" · ", style=theme.FAINT)
|
|
6528
|
-
body.append(str(r.get("error")), style=theme.TEXT)
|
|
6529
|
-
_notice_box(body, color=theme.ERROR, title="jev mode not changed")
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
def jev_mode_unchanged(machine_ids: Sequence[str], mode: str) -> None:
|
|
6533
|
-
"""Every machine already has the mode. Calm, like a no-op deploy."""
|
|
6534
|
-
body = Text()
|
|
6535
|
-
body.append("= ", style=theme.FAINT)
|
|
6536
|
-
body.append(", ".join(machine_ids) if len(machine_ids) <= 3 else f"{len(machine_ids)} machines",
|
|
6537
|
-
style=theme.ACCENT)
|
|
6538
|
-
body.append(f" already {'has' if len(machine_ids) == 1 else 'have'} jev mode {mode}", style=theme.TEXT)
|
|
6539
|
-
body.append(" · ", style=theme.FAINT)
|
|
6540
|
-
body.append("nothing changed", style=theme.LABEL)
|
|
6541
|
-
_notice_box(body, color=theme.ACCENT, title="no change")
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
6475
|
def machine_renamed(machine_id: str, label: str) -> None:
|
|
6545
6476
|
body = Text()
|
|
6546
6477
|
body.append("✓ ", style=theme.SUCCESS)
|
|
@@ -6819,7 +6750,7 @@ def render_deployment_history(machine_id: str, entries: Sequence[dict],
|
|
|
6819
6750
|
[("+", f"{i} → {scope[i]}") for i in sorted(set(cur) - set(prev))]
|
|
6820
6751
|
+ [("-", i) for i in sorted(set(prev) - set(cur))]
|
|
6821
6752
|
+ [("~", f"{i} → {scope[i]}") for i in sorted(set(cur) & set(prev)) if cur[i] != prev[i]]
|
|
6822
|
-
# A mode-only generation (
|
|
6753
|
+
# A mode-only generation (a legacy jev-mode change) read as "no
|
|
6823
6754
|
# change", which is exactly the change a rollback brings back.
|
|
6824
6755
|
+ ([("~", f"jev {prev_mode}→{mode}")] if mode != prev_mode else [])
|
|
6825
6756
|
)
|
|
@@ -197,7 +197,7 @@ you need a flag you don't already know.
|
|
|
197
197
|
|
|
198
198
|
**Enforce (cloud-managed policy, session-only — see §2):**
|
|
199
199
|
- `policies list|show|publish|enable|disable|delete|test|compose` — policy versions. `publish` mints a version of a kind — `regex` (a local `.mjs`), `jev` (`--semantic checks.json`: Jev declarations, no JavaScript) or `both` (the two, the JavaScript reviewable by its own checks); list/show print the kind and the Jev check names; `test` runs one against a synthetic context locally (it applies each policy's `match` filter, so a policy that does not cover the `--event`/`--tool` you pass is reported `skipped`, not run). `enable`/`disable`/`delete` take `--yes`.
|
|
200
|
-
- `fleet list|show|deploy|
|
|
200
|
+
- `fleet list|show|deploy|diff|history|rollback|rename` — which machines run which policies, and the policy errors each machine reports. **`deploy` REPLACES a machine's whole set** (`--add`/`--remove` amend it, `--set` replaces, `--create` mints a deployment). There is no Jev mode to set: FailproofAI Cloud turns Jev on for a machine while a `jev`/`both` policy is deployed to it (off otherwise); trial one with `:observe`. `deploy` prints the plan and asks **only on an interactive terminal without `--json`** — under `--json` or with stdin redirected it applies immediately, so read `fleet show` first if you want review.
|
|
201
201
|
- `guardrails summary|timeline` — what enforcement actually did; `--since 1h|6h|24h|7d`, `--machine`.
|
|
202
202
|
|
|
203
203
|
**Analytics & assistant:**
|
|
@@ -235,14 +235,14 @@ Cloud-managed enforcement, split the way the dashboard splits it: `policies` wri
|
|
|
235
235
|
|
|
236
236
|
### fleet
|
|
237
237
|
- `fleet list` — `machine · label · pol · intended · applied · seen · events · state`. `intended` is the generation deployed, `applied` what the machine last collected (they differ until it polls), and `seen` how long since it last reported anything — a machine can be in sync and dead, or alive and behind, which are different problems. `state` reads `N errors` when the machine reported policies it could not apply. JSON `{machines, deployments}` with raw epoch-ms timestamps plus the computed `drifted`; a machine carries `policyErrors`/`policyErrorsAt` and `jevMode` when the server has them.
|
|
238
|
-
- `fleet show <machine>` — the set the machine is told to run, **and whether it has collected it**. Reads both the deployment and the machine record, so it reports `not yet collected` / `machine is on #N` / `collected` alongside who deployed it, when, and last-seen. A machine can be told to run a policy it has never picked up; the policy list alone cannot tell you which. Also the **Jev mode** FailproofAI Cloud
|
|
239
|
-
- `fleet deploy <machine> [--add REF]… [--remove ID]… [--set REF]… [--
|
|
238
|
+
- `fleet show <machine>` — the set the machine is told to run, **and whether it has collected it**. Reads both the deployment and the machine record, so it reports `not yet collected` / `machine is on #N` / `collected` alongside who deployed it, when, and last-seen. A machine can be told to run a policy it has never picked up; the policy list alone cannot tell you which. Also the **Jev mode** FailproofAI Cloud derived for it from its policies (`local` when none) and the **policy errors** the machine last reported — `[{id, version, kind: regex|jev|both|daemon, message}]`: a policy that did not load, `jev_unconfigured` (a Jev mode, and the machine's key does not carry Jev), `transcripts_disabled` (a Jev mode on a machine connected with `--no-transcripts`, which never asks), `jev_budget: dropped <name>` (an installed pack's check FailproofAI Cloud left out of a request for the question budget). Whatever is listed there is not enforcing. JSON `{machine, deployment}` with raw timestamps and both label fields; `deployment: null` when nothing is deployed.
|
|
239
|
+
- `fleet deploy <machine> [--add REF]… [--remove ID]… [--set REF]… [--create] [-y]`
|
|
240
240
|
|
|
241
241
|
**A deploy REPLACES the whole set.** The endpoint takes the full list and does not merge. `--add`/`--remove` are a read-modify-write: the CLI reads the current set, applies the delta, prints the complete result, writes that. `--set` replaces everything and is refused alongside `--add`/`--remove`.
|
|
242
242
|
|
|
243
|
-
REF is `id`, `id@version`, `id:effect`, or `id@version:effect`. Effect is `enforce` (default) or `observe`. A bare `--add` of an already-deployed policy **keeps its pinned version** — pass `id@version` to move it. Every kind deploys the same way (a `both` policy's JavaScript and its checks go together)
|
|
243
|
+
REF is `id`, `id@version`, `id:effect`, or `id@version:effect`. Effect is `enforce` (default) or `observe`. A bare `--add` of an already-deployed policy **keeps its pinned version** — pass `id@version` to move it. Every kind deploys the same way (a `both` policy's JavaScript and its checks go together), and `observe` applies to every kind: a `jev` policy's checks, or both halves of a `both` policy, are asked and recorded and decide nothing.
|
|
244
244
|
|
|
245
|
-
|
|
245
|
+
There is no Jev mode to set: FailproofAI Cloud turns Jev on for a machine while a `jev` or `both` policy is deployed to it, and off when none is. Jev checks run on FailproofAI Cloud; nothing is installed on the machine. The machine sends each checked tool call of an agent such a policy targets to FailproofAI Cloud on its Cloud Jev credential; without one it reports `jev_unconfigured`, and connected with `--no-transcripts` it never asks and reports `transcripts_disabled`.
|
|
246
246
|
|
|
247
247
|
Deploying to an id that has never checked in is refused (a typo would mint a machine); `--create` allows it for pre-staging.
|
|
248
248
|
|
|
@@ -252,10 +252,9 @@ Cloud-managed enforcement, split the way the dashboard splits it: `policies` wri
|
|
|
252
252
|
|
|
253
253
|
**Exit codes.** A malformed ref (`bad ref!!`, `id:banana`, empty), or `--set` combined with `--add`/`--remove`, is a usage error → **exit 2**, like every other bad flag value. A ref that parses but names something that does not exist (`--add ghost-policy`, an unpublished `@version`) is **exit 1**; an unknown machine is **exit 6**. Branch on these rather than on the message.
|
|
254
254
|
|
|
255
|
-
JSON `{plan:{result,added,removed,changed,unchanged,noop
|
|
256
|
-
- `fleet jev-mode <machine>… <mode>` · `fleet jev-mode --all <mode> [-y]` — set the Jev mode FailproofAI Cloud gives machines (`off|observe|enforce|local`) and **nothing else**: the policy set is never read back or rewritten. A machine already on the mode is skipped; `--all` covers every machine with a deployment and skips the rest; a named machine with no deployment is refused before anything is written (`fleet deploy <machine> --jev-mode <mode>` gives it its first). Each change is a new generation in `fleet history`. One machine's failure does not stop the others; the exit code is 1 and the JSON row carries `error`. A bad mode, or `--all` with machine ids, is **exit 2**. JSON `{mode, applied, machines:[{machineId, from, to, changed, deployment?, error?}]}`.
|
|
255
|
+
JSON `{plan:{result,added,removed,changed,unchanged,noop}, deployment, applied}` — the plan is included so a harness does not recompute the diff.
|
|
257
256
|
- `fleet diff [machine]` — intent vs delivery per machine, with a `drifted` flag. A machine id nobody has reported under is refused (exit 6), not rendered as an empty fleet.
|
|
258
|
-
- `fleet history <machine>` · `fleet rollback <machine> <generation> [-y]` — rollback mints a NEW generation carrying the old set; history stays append-only. The `change` column uses the deploy plan's vocabulary: `+` added, `-` removed, `~` same policy at a different version or effect (an enforce → observe flip is a policy that stopped blocking, so it is never "no change"), and `~ jev <from>→<to>` for a Jev mode change. The `jev` column is the mode FailproofAI Cloud
|
|
257
|
+
- `fleet history <machine>` · `fleet rollback <machine> <generation> [-y]` — rollback mints a NEW generation carrying the old set; history stays append-only. The `change` column uses the deploy plan's vocabulary: `+` added, `-` removed, `~` same policy at a different version or effect (an enforce → observe flip is a policy that stopped blocking, so it is never "no change"), and `~ jev <from>→<to>` for a Jev mode change. The `jev` column is the mode FailproofAI Cloud had with each generation (`local` = none); a rollback's prompt and result say `jev mode <now> → <then>` when it differs, and the JSON adds `jevModeBefore`.
|
|
259
258
|
- `fleet rename <machine> "<label>"` — a human label; the id never changes. The server stores it as an override beside the machine's self-asserted label. An empty label **clears** the override (the machine falls back to its own label, else its id) and the CLI says so rather than reporting a rename to nothing. A machine that has never checked in cannot be renamed → exit 6.
|
|
260
259
|
|
|
261
260
|
### guardrails
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""FailproofAI Cloud Jev / regex / both policies in `fp` (CONTRACT C4, C6).
|
|
2
2
|
|
|
3
3
|
`fp policies publish --kind regex|jev|both [--source] [--semantic]`, list/show
|
|
4
|
-
carrying the kind and the Jev check names,
|
|
5
|
-
machines showing the policy errors they report.
|
|
4
|
+
carrying the kind and the Jev check names, deploys (no Jev mode: Cloud derives
|
|
5
|
+
it), and machines showing the policy errors they report.
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
@@ -144,7 +144,7 @@ def test_semantic_refuses_what_is_not_a_declaration_list(text, needle):
|
|
|
144
144
|
parse_semantic(text)
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
# ── deploy plan:
|
|
147
|
+
# ── deploy plan: no Jev mode ──────────────────────────────────────────────────
|
|
148
148
|
|
|
149
149
|
|
|
150
150
|
def _plan(**kw):
|
|
@@ -153,38 +153,16 @@ def _plan(**kw):
|
|
|
153
153
|
return plan_deploy("m1", **base)
|
|
154
154
|
|
|
155
155
|
|
|
156
|
-
def
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def test_the_same_jev_mode_again_is_a_noop():
|
|
163
|
-
assert _plan(jev_mode="observe", current_jev_mode="observe").is_noop
|
|
164
|
-
assert _plan(jev_mode="local").is_noop # already local
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def test_local_hands_the_mode_back_to_the_machine():
|
|
168
|
-
plan = _plan(jev_mode="local", current_jev_mode="enforce")
|
|
169
|
-
assert plan.jev_mode_changes and plan.jev_mode_after is None
|
|
170
|
-
assert plan.to_dict()["jevMode"] == {"from": "enforce", "to": "local", "changed": True}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def test_no_jev_mode_says_nothing_and_changes_nothing():
|
|
174
|
-
plan = _plan(add=["a"], current_jev_mode="enforce")
|
|
156
|
+
def test_a_plan_never_carries_a_jev_mode():
|
|
157
|
+
with pytest.raises(TypeError):
|
|
158
|
+
_plan(jev_mode="observe")
|
|
159
|
+
plan = _plan(add=["a"])
|
|
175
160
|
assert plan.is_noop and "jevMode" not in plan.to_dict()
|
|
176
161
|
|
|
177
162
|
|
|
178
|
-
def
|
|
179
|
-
with pytest.raises(RefUsageError, match="not one of off, observe, enforce, local"):
|
|
180
|
-
_plan(jev_mode="shadow")
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def test_observe_on_a_jev_policy_is_refused_with_the_mode_that_does_it():
|
|
163
|
+
def test_observe_applies_to_a_jev_policy_like_any_other():
|
|
184
164
|
kinds = version_kinds([PolicyVersion.from_dict(dict(SERVER_JEV, id="j", version=2))])
|
|
185
|
-
|
|
186
|
-
_plan(add=["j:observe"], kinds=kinds)
|
|
187
|
-
# enforce is fine, and so is observe on a `both` policy (it applies to the JS half).
|
|
165
|
+
assert _plan(add=["j:observe"], kinds=kinds).added[0].effect == "observe"
|
|
188
166
|
assert _plan(add=["j"], kinds=kinds).added[0].effect == "enforce"
|
|
189
167
|
both = version_kinds([PolicyVersion.from_dict(dict(SERVER_BOTH, id="j", version=2))])
|
|
190
168
|
assert _plan(add=["j:observe"], kinds=both).added[0].effect == "observe"
|
|
@@ -213,14 +191,13 @@ def test_publish_still_refuses_jev_fields_in_the_javascript(monkeypatch):
|
|
|
213
191
|
assert "--kind both --semantic" in (exc.value.hint or "")
|
|
214
192
|
|
|
215
193
|
|
|
216
|
-
def
|
|
194
|
+
def test_deploy_never_sends_a_jev_mode(monkeypatch):
|
|
217
195
|
bodies = []
|
|
218
196
|
monkeypatch.setattr(client, "_request_json",
|
|
219
197
|
lambda ctx, method, path, json_body=None: bodies.append(json_body) or {})
|
|
220
198
|
client.deploy_policies(None, "m1", [PolicyRef("a", 1)])
|
|
221
|
-
client.deploy_policies(None, "m1", [PolicyRef("a", 1)], jev_mode="local")
|
|
222
199
|
assert "jevMode" not in bodies[0]
|
|
223
|
-
assert
|
|
200
|
+
assert not hasattr(client, "set_jev_mode")
|
|
224
201
|
|
|
225
202
|
|
|
226
203
|
# ── the commands, over HTTP ───────────────────────────────────────────────────
|
|
@@ -365,25 +342,7 @@ def _fleet_routes(current_policies, jev_mode=None, errors=None):
|
|
|
365
342
|
|
|
366
343
|
|
|
367
344
|
@respx.mock
|
|
368
|
-
def
|
|
369
|
-
# CONTRACT C9.1: a mode change sent as a deploy rewrote the set the CLI had
|
|
370
|
-
# read — which leaves out disabled policies' assignments, so they were lost.
|
|
371
|
-
_fleet_routes([{"id": "no-force-push", "version": 2, "effect": "enforce"}])
|
|
372
|
-
deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1")
|
|
373
|
-
mode = respx.put(f"{BASE}/api/enforcement/deployments/m1/jev-mode").mock(return_value=httpx.Response(
|
|
374
|
-
200, json={"machineId": "m1", "deployment": 6, "jevMode": "observe", "changed": True,
|
|
375
|
-
"policies": [{"id": "no-force-push", "version": 2, "effect": "enforce"}]}))
|
|
376
|
-
result = runner.invoke(app, ["--json", "fleet", "deploy", "m1", "--jev-mode", "observe", "--yes"])
|
|
377
|
-
assert result.exit_code == 0, result.output
|
|
378
|
-
assert not deploy.called
|
|
379
|
-
assert json.loads(mode.calls.last.request.content) == {"jevMode": "observe"}
|
|
380
|
-
out = json.loads(result.stdout)
|
|
381
|
-
assert out["applied"] is True and out["jevModeOnly"] is True
|
|
382
|
-
assert out["plan"]["jevMode"]["to"] == "observe" and out["deployment"]["deployment"] == 6
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
@respx.mock
|
|
386
|
-
def test_fleet_deploy_adds_a_jev_policy_and_leaves_the_mode_alone_without_the_flag(logged_in, runner):
|
|
345
|
+
def test_fleet_deploy_adds_a_jev_policy_and_sends_no_mode(logged_in, runner):
|
|
387
346
|
_fleet_routes([], jev_mode="enforce")
|
|
388
347
|
put = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
|
|
389
348
|
200, json={"machineId": "m1", "deployment": 6, "jevMode": "enforce",
|
|
@@ -395,13 +354,26 @@ def test_fleet_deploy_adds_a_jev_policy_and_leaves_the_mode_alone_without_the_fl
|
|
|
395
354
|
|
|
396
355
|
|
|
397
356
|
@respx.mock
|
|
398
|
-
def
|
|
357
|
+
def test_fleet_deploy_observes_a_jev_policy(logged_in, runner):
|
|
399
358
|
_fleet_routes([])
|
|
400
|
-
put = respx.put(f"{BASE}/api/enforcement/deployments/m1")
|
|
359
|
+
put = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
|
|
360
|
+
200, json={"machineId": "m1", "deployment": 6,
|
|
361
|
+
"policies": [{"id": "secrets", "version": 1, "effect": "observe"}]}))
|
|
401
362
|
result = runner.invoke(app, ["fleet", "deploy", "m1", "--add", "secrets:observe", "--yes"])
|
|
402
|
-
assert result.exit_code ==
|
|
403
|
-
assert
|
|
404
|
-
|
|
363
|
+
assert result.exit_code == 0, result.output
|
|
364
|
+
assert json.loads(put.calls.last.request.content) == {
|
|
365
|
+
"policies": [{"id": "secrets", "version": 1, "effect": "observe"}]}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@pytest.mark.parametrize("argv", [
|
|
369
|
+
["fleet", "jev-mode", "m1", "observe"],
|
|
370
|
+
["fleet", "deploy", "m1", "--jev-mode", "observe"],
|
|
371
|
+
])
|
|
372
|
+
def test_the_jev_mode_cannot_be_set_by_hand(logged_in, runner, argv):
|
|
373
|
+
with respx.mock(assert_all_called=False) as mock:
|
|
374
|
+
result = runner.invoke(app, argv)
|
|
375
|
+
assert result.exit_code == 2, result.output
|
|
376
|
+
assert not mock.calls
|
|
405
377
|
|
|
406
378
|
|
|
407
379
|
@respx.mock
|
|
@@ -424,7 +396,7 @@ def test_fleet_show_and_list_show_the_jev_mode_and_the_reported_errors(logged_in
|
|
|
424
396
|
assert js["deployment"]["jevMode"] == "observe"
|
|
425
397
|
|
|
426
398
|
|
|
427
|
-
# ──
|
|
399
|
+
# ── Rollback and history show the mode Cloud derived (m6) ────────────────────
|
|
428
400
|
|
|
429
401
|
|
|
430
402
|
def _fleet_many(deployments, machines=None):
|
|
@@ -443,133 +415,6 @@ def _fleet_many(deployments, machines=None):
|
|
|
443
415
|
return_value=httpx.Response(200, json=[SERVER_BOTH, SERVER_JEV, SERVER_REGEX_OLD]))
|
|
444
416
|
|
|
445
417
|
|
|
446
|
-
def _mode_route(machine_id, mode, deployment=6, status=200):
|
|
447
|
-
body = ({"machineId": machine_id, "deployment": deployment, "jevMode": None if mode == "local" else mode,
|
|
448
|
-
"changed": True, "policies": []} if status == 200
|
|
449
|
-
else {"error": f"machine {machine_id} has no deployment", "code": "no_deployment"})
|
|
450
|
-
return respx.put(f"{BASE}/api/enforcement/deployments/{machine_id}/jev-mode").mock(
|
|
451
|
-
return_value=httpx.Response(status, json=body))
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
def test_set_jev_mode_sends_only_the_mode(monkeypatch):
|
|
455
|
-
seen = {}
|
|
456
|
-
|
|
457
|
-
def fake(ctx, method, path, json_body=None, **kw):
|
|
458
|
-
seen.update(method=method, path=path, body=json_body)
|
|
459
|
-
return {"machineId": "m1", "deployment": 9, "jevMode": "enforce", "changed": False, "policies": []}
|
|
460
|
-
|
|
461
|
-
monkeypatch.setattr(client, "_request_json", fake)
|
|
462
|
-
dep, changed = client.set_jev_mode(object(), "m1", "enforce")
|
|
463
|
-
assert seen == {"method": "PUT", "path": "/api/enforcement/deployments/m1/jev-mode", "body": {"jevMode": "enforce"}}
|
|
464
|
-
assert (dep.deployment, dep.jev_mode, changed) == (9, "enforce", False)
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
@respx.mock
|
|
468
|
-
def test_fleet_deploy_jev_mode_on_a_machine_with_no_deployment_deploys_its_first(logged_in, runner):
|
|
469
|
-
respx.get(f"{BASE}/api/enforcement/machines").mock(return_value=httpx.Response(
|
|
470
|
-
200, json=[{"machineId": "m1", "deployment": None, "deployed": False}]))
|
|
471
|
-
respx.get(f"{BASE}/api/enforcement/deployments").mock(return_value=httpx.Response(200, json=[]))
|
|
472
|
-
respx.get(f"{BASE}/api/enforcement/policies").mock(return_value=httpx.Response(200, json=[SERVER_JEV]))
|
|
473
|
-
mode = respx.put(f"{BASE}/api/enforcement/deployments/m1/jev-mode")
|
|
474
|
-
deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
|
|
475
|
-
200, json={"machineId": "m1", "deployment": 1, "jevMode": "observe", "policies": []}))
|
|
476
|
-
result = runner.invoke(app, ["--json", "fleet", "deploy", "m1", "--jev-mode", "observe", "--yes"])
|
|
477
|
-
assert result.exit_code == 0, result.output
|
|
478
|
-
assert not mode.called
|
|
479
|
-
assert json.loads(deploy.calls.last.request.content) == {"policies": [], "jevMode": "observe"}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
@respx.mock
|
|
483
|
-
def test_fleet_deploy_with_a_set_change_and_a_mode_still_deploys_both_together(logged_in, runner):
|
|
484
|
-
_fleet_routes([{"id": "no-force-push", "version": 2, "effect": "enforce"}])
|
|
485
|
-
mode = respx.put(f"{BASE}/api/enforcement/deployments/m1/jev-mode")
|
|
486
|
-
deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
|
|
487
|
-
200, json={"machineId": "m1", "deployment": 6, "jevMode": "enforce", "policies": []}))
|
|
488
|
-
result = runner.invoke(app, ["fleet", "deploy", "m1", "--add", "secrets", "--jev-mode", "enforce", "--yes"])
|
|
489
|
-
assert result.exit_code == 0, result.output
|
|
490
|
-
assert not mode.called
|
|
491
|
-
assert json.loads(deploy.calls.last.request.content)["jevMode"] == "enforce"
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
@respx.mock
|
|
495
|
-
def test_fleet_jev_mode_sets_named_machines_and_skips_one_already_there(logged_in, runner):
|
|
496
|
-
_fleet_many({"m1": None, "m2": "enforce", "m3": "observe"})
|
|
497
|
-
r1, r2, r3 = _mode_route("m1", "enforce"), _mode_route("m2", "enforce"), _mode_route("m3", "enforce", 7)
|
|
498
|
-
deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1")
|
|
499
|
-
result = runner.invoke(app, ["--json", "fleet", "jev-mode", "m1", "m2", "m3", "enforce"])
|
|
500
|
-
assert result.exit_code == 0, result.output
|
|
501
|
-
assert r1.called and r3.called and not r2.called and not deploy.called
|
|
502
|
-
assert json.loads(r1.calls.last.request.content) == {"jevMode": "enforce"}
|
|
503
|
-
out = json.loads(result.stdout)
|
|
504
|
-
assert out["mode"] == "enforce" and out["applied"] is True
|
|
505
|
-
rows = {r["machineId"]: r for r in out["machines"]}
|
|
506
|
-
assert rows["m1"]["from"] == "local" and rows["m1"]["to"] == "enforce" and rows["m1"]["deployment"] == 6
|
|
507
|
-
assert rows["m2"]["changed"] is False
|
|
508
|
-
assert rows["m3"]["from"] == "observe" and rows["m3"]["deployment"] == 7
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
@respx.mock
|
|
512
|
-
def test_fleet_jev_mode_all_covers_every_machine_with_a_deployment_only(logged_in, runner):
|
|
513
|
-
_fleet_many({"m1": "enforce", "m2": "off"}, machines=["m1", "m2", "m-new"])
|
|
514
|
-
r1, r2 = _mode_route("m1", "local"), _mode_route("m2", "local")
|
|
515
|
-
fresh = respx.put(f"{BASE}/api/enforcement/deployments/m-new/jev-mode")
|
|
516
|
-
result = runner.invoke(app, ["fleet", "jev-mode", "--all", "local"])
|
|
517
|
-
assert result.exit_code == 0, result.output
|
|
518
|
-
assert r1.called and r2.called and not fresh.called
|
|
519
|
-
assert json.loads(r1.calls.last.request.content) == {"jevMode": "local"}
|
|
520
|
-
assert "enforce → local" in result.stdout and "off → local" in result.stdout
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
@respx.mock
|
|
524
|
-
def test_fleet_jev_mode_when_nothing_changes_writes_nothing(logged_in, runner):
|
|
525
|
-
_fleet_many({"m1": "observe"})
|
|
526
|
-
route = _mode_route("m1", "observe")
|
|
527
|
-
result = runner.invoke(app, ["fleet", "jev-mode", "m1", "observe"])
|
|
528
|
-
assert result.exit_code == 0, result.output
|
|
529
|
-
assert not route.called
|
|
530
|
-
assert "already has jev mode observe" in result.output + (result.stderr or "")
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
@pytest.mark.parametrize("argv, needle", [
|
|
534
|
-
(["fleet", "jev-mode"], "pass the mode"),
|
|
535
|
-
(["fleet", "jev-mode", "m1", "shadow"], "is not a Jev mode"),
|
|
536
|
-
(["fleet", "jev-mode", "observe"], "name at least one machine"),
|
|
537
|
-
(["fleet", "jev-mode", "--all", "m1", "observe"], "do not name machines"),
|
|
538
|
-
])
|
|
539
|
-
def test_fleet_jev_mode_usage_errors_exit_2_before_any_request(logged_in, runner, argv, needle):
|
|
540
|
-
with respx.mock(assert_all_called=False) as mock:
|
|
541
|
-
result = runner.invoke(app, argv)
|
|
542
|
-
assert result.exit_code == 2, result.output
|
|
543
|
-
assert needle in result.output + (result.stderr or "")
|
|
544
|
-
assert not mock.calls
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
@respx.mock
|
|
548
|
-
def test_fleet_jev_mode_refuses_a_named_machine_with_no_deployment_before_writing(logged_in, runner):
|
|
549
|
-
_fleet_many({"m1": None}, machines=["m1", "m2"])
|
|
550
|
-
r1 = _mode_route("m1", "observe")
|
|
551
|
-
result = runner.invoke(app, ["fleet", "jev-mode", "m1", "m2", "observe"])
|
|
552
|
-
assert result.exit_code == 1, result.output
|
|
553
|
-
text = result.output + (result.stderr or "")
|
|
554
|
-
assert "m2 has no deployment" in text and "fp fleet deploy <machine> --jev-mode observe" in text
|
|
555
|
-
assert not r1.called
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
@respx.mock
|
|
559
|
-
def test_fleet_jev_mode_goes_on_past_a_machine_that_fails_and_names_it(logged_in, runner):
|
|
560
|
-
_fleet_many({"m1": None, "m2": None})
|
|
561
|
-
_mode_route("m1", "observe", status=404)
|
|
562
|
-
r2 = _mode_route("m2", "observe")
|
|
563
|
-
result = runner.invoke(app, ["--json", "fleet", "jev-mode", "m1", "m2", "observe"])
|
|
564
|
-
assert result.exit_code == 1, result.output
|
|
565
|
-
assert r2.called
|
|
566
|
-
# The result document, then the error document the exit code comes with.
|
|
567
|
-
out, end = json.JSONDecoder().raw_decode(result.stdout)
|
|
568
|
-
rows = {r["machineId"]: r for r in out["machines"]}
|
|
569
|
-
assert "error" in rows["m1"] and rows["m2"]["deployment"] == 6 and out["applied"] is False
|
|
570
|
-
assert "could not be changed on m1" in result.stdout[end:] + (result.stderr or "")
|
|
571
|
-
|
|
572
|
-
|
|
573
418
|
@respx.mock
|
|
574
419
|
def test_fleet_rollback_says_which_jev_mode_it_restores(logged_in, runner):
|
|
575
420
|
_fleet_many({"m1": "off"})
|
|
@@ -607,15 +452,6 @@ def test_fleet_history_shows_each_generations_jev_mode_and_a_mode_only_change(lo
|
|
|
607
452
|
assert any("local" in ln for ln in lines if "#5" in ln)
|
|
608
453
|
|
|
609
454
|
|
|
610
|
-
def test_the_help_says_enforce_blocks_and_that_the_mode_is_machine_wide(runner):
|
|
611
|
-
for argv in (["fleet", "deploy", "--help"], ["fleet", "jev-mode", "--help"]):
|
|
612
|
-
result = runner.invoke(app, argv)
|
|
613
|
-
assert result.exit_code == 0, result.output
|
|
614
|
-
flat = " ".join(result.stdout.split())
|
|
615
|
-
assert "BLOCK" in flat, argv
|
|
616
|
-
assert "installed" in flat and "pack" in flat, argv
|
|
617
|
-
|
|
618
|
-
|
|
619
455
|
# ── CONTRACT C10.6: Jev checks run on FailproofAI Cloud (help text only) ──────
|
|
620
456
|
|
|
621
457
|
|
|
@@ -623,7 +459,6 @@ def test_the_help_says_enforce_blocks_and_that_the_mode_is_machine_wide(runner):
|
|
|
623
459
|
"argv",
|
|
624
460
|
[
|
|
625
461
|
["fleet", "deploy", "--help"],
|
|
626
|
-
["fleet", "jev-mode", "--help"],
|
|
627
462
|
["policies", "publish", "--help"],
|
|
628
463
|
],
|
|
629
464
|
)
|
|
@@ -637,7 +472,7 @@ def test_the_help_says_jev_checks_run_on_cloud_and_nothing_is_installed(runner,
|
|
|
637
472
|
assert "AgentEye" not in flat, argv
|
|
638
473
|
|
|
639
474
|
|
|
640
|
-
@pytest.mark.parametrize("argv", [["fleet", "show", "--help"]
|
|
475
|
+
@pytest.mark.parametrize("argv", [["fleet", "show", "--help"]])
|
|
641
476
|
def test_the_help_names_the_reasons_a_machine_cannot_ask(runner, argv):
|
|
642
477
|
flat = " ".join(runner.invoke(app, argv).stdout.split())
|
|
643
478
|
assert "transcripts_disabled" in flat, argv
|
|
@@ -104,6 +104,17 @@ def _profile_name() -> str:
|
|
|
104
104
|
|
|
105
105
|
def _profile_settings_path(profile: str) -> str | None:
|
|
106
106
|
"""Evidence of the profile running this plugin, not a transcript guess."""
|
|
107
|
+
try:
|
|
108
|
+
# The host's own resolver first: it honours a per-task profile
|
|
109
|
+
# override as well as HERMES_HOME, so each profile of a multi-profile
|
|
110
|
+
# gateway reports its own config (and roster identity), not the
|
|
111
|
+
# process-wide one.
|
|
112
|
+
from hermes_constants import get_hermes_home
|
|
113
|
+
|
|
114
|
+
hosted = Path(get_hermes_home()).expanduser() / "config.yaml"
|
|
115
|
+
return str(hosted.absolute()) if hosted.is_file() else None
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
107
118
|
configured = os.environ.get("HERMES_HOME", "").strip()
|
|
108
119
|
if configured:
|
|
109
120
|
selected = Path(configured).expanduser() / "config.yaml"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "1.0.12-beta.
|
|
3
|
+
"version": "1.0.12-beta.2",
|
|
4
4
|
"description": "Observability and enforcement for AI agent harnesses. 40 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs",
|
|
@@ -118,22 +118,23 @@
|
|
|
118
118
|
"postcss": "8.5.26",
|
|
119
119
|
"eslint-plugin-react-hooks": "7.0.1",
|
|
120
120
|
"vite": "8.1.6",
|
|
121
|
-
"undici": "7.
|
|
121
|
+
"undici": "7.30.1",
|
|
122
|
+
"zod": "4.6.6",
|
|
122
123
|
"brace-expansion": "5.0.12",
|
|
123
124
|
"sharp": "0.35.5",
|
|
124
125
|
"source-map-js": "1.2.2",
|
|
125
126
|
"browserslist": "4.28.8"
|
|
126
127
|
},
|
|
127
128
|
"optionalDependencies": {
|
|
128
|
-
"@failproofai/failproofaid-linux-x64": "1.0.12-beta.
|
|
129
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.12-beta.
|
|
130
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.12-beta.
|
|
131
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.12-beta.
|
|
129
|
+
"@failproofai/failproofaid-linux-x64": "1.0.12-beta.2",
|
|
130
|
+
"@failproofai/failproofaid-linux-arm64": "1.0.12-beta.2",
|
|
131
|
+
"@failproofai/failproofaid-darwin-x64": "1.0.12-beta.2",
|
|
132
|
+
"@failproofai/failproofaid-darwin-arm64": "1.0.12-beta.2"
|
|
132
133
|
},
|
|
133
134
|
"failproofaidBinaries": {
|
|
134
|
-
"linux-x64": "
|
|
135
|
-
"linux-arm64": "
|
|
136
|
-
"darwin-x64": "
|
|
137
|
-
"darwin-arm64": "
|
|
135
|
+
"linux-x64": "4c09117fdb80d87792d1d2b77d9359cdbd0c522b5a6222ab7b927659130ddf54",
|
|
136
|
+
"linux-arm64": "dd390d76561267fc519ceb96fb90be340044e2f564bef36a44e284f227d93f29",
|
|
137
|
+
"darwin-x64": "62381f7ffe21d02289c36c4b205166b2867dbde986444d1ea185241b94fd04ef",
|
|
138
|
+
"darwin-arm64": "9ad798817d3048a5126e80dc71883538d37c1b6b6443e9c19389dc2583c8f675"
|
|
138
139
|
}
|
|
139
140
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 5
|
|
3
3
|
requires-python = ">=3.10"
|
|
4
4
|
resolution-markers = [
|
|
5
5
|
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
|
@@ -1091,11 +1091,33 @@ provides-extras = ["dev", "langchain", "langgraph", "crewai", "llamaindex", "pyd
|
|
|
1091
1091
|
|
|
1092
1092
|
[[package]]
|
|
1093
1093
|
name = "filelock"
|
|
1094
|
-
version = "
|
|
1094
|
+
version = "4.1.0"
|
|
1095
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1096
|
+
resolution-markers = [
|
|
1097
|
+
"python_full_version < '3.11' and sys_platform == 'win32'",
|
|
1098
|
+
"python_full_version < '3.11' and sys_platform != 'win32'",
|
|
1099
|
+
]
|
|
1100
|
+
sdist = { url = "https://files.pythonhosted.org/packages/4c/58/6fd434bec86eff7c38a3168454cb132b762b2bea9b3ac094101a2f7bc32a/filelock-4.1.0.tar.gz", hash = "sha256:ad7f724afef953e731b1cc39bcd3a09166d72ed7fcdf29e6e88b1c3235c6715d", size = 561277, upload-time = "2026-10-09T19:57:20.34Z" }
|
|
1101
|
+
wheels = [
|
|
1102
|
+
{ url = "https://files.pythonhosted.org/packages/ee/86/032133892a5de43b5a98200b01aadcad68cc255e274a762f08b8a76d2912/filelock-4.1.0-py3-none-any.whl", hash = "sha256:2ce9818e3e2d8f284c1a964414447ef148d42a5fd5e2a477a7118e574b293ec1", size = 133003, upload-time = "2026-10-09T19:57:18.716Z" },
|
|
1103
|
+
]
|
|
1104
|
+
|
|
1105
|
+
[[package]]
|
|
1106
|
+
name = "filelock"
|
|
1107
|
+
version = "4.2.4"
|
|
1095
1108
|
source = { registry = "https://pypi.org/simple" }
|
|
1096
|
-
|
|
1109
|
+
resolution-markers = [
|
|
1110
|
+
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
|
1111
|
+
"python_full_version >= '3.14' and sys_platform != 'win32'",
|
|
1112
|
+
"python_full_version == '3.13.*'",
|
|
1113
|
+
"python_full_version == '3.12.*' and sys_platform == 'win32'",
|
|
1114
|
+
"python_full_version == '3.12.*' and sys_platform != 'win32'",
|
|
1115
|
+
"python_full_version == '3.11.*' and sys_platform == 'win32'",
|
|
1116
|
+
"python_full_version == '3.11.*' and sys_platform != 'win32'",
|
|
1117
|
+
]
|
|
1118
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/28/3893324bcadf8f89b1616637468d766af3f625008c0735bf704fdfa86617/filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d", size = 652444, upload-time = "2026-10-19T01:57:51.001Z" }
|
|
1097
1119
|
wheels = [
|
|
1098
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1120
|
+
{ url = "https://files.pythonhosted.org/packages/e0/1b/5e74289a1878e666bb4b65fe5805a04cafcac2aa5f519ef1ead67ef91c56/filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e", size = 162475, upload-time = "2026-10-19T01:57:49.419Z" },
|
|
1099
1121
|
]
|
|
1100
1122
|
|
|
1101
1123
|
[[package]]
|
|
@@ -1679,7 +1701,8 @@ version = "1.28.0"
|
|
|
1679
1701
|
source = { registry = "https://pypi.org/simple" }
|
|
1680
1702
|
dependencies = [
|
|
1681
1703
|
{ name = "click" },
|
|
1682
|
-
{ name = "filelock" },
|
|
1704
|
+
{ name = "filelock", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
1705
|
+
{ name = "filelock", version = "4.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
1683
1706
|
{ name = "fsspec" },
|
|
1684
1707
|
{ name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
|
|
1685
1708
|
{ name = "httpx" },
|
|
@@ -4967,11 +4990,11 @@ wheels = [
|
|
|
4967
4990
|
|
|
4968
4991
|
[[package]]
|
|
4969
4992
|
name = "setuptools"
|
|
4970
|
-
version = "84.
|
|
4993
|
+
version = "84.2.1"
|
|
4971
4994
|
source = { registry = "https://pypi.org/simple" }
|
|
4972
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
4995
|
+
sdist = { url = "https://files.pythonhosted.org/packages/70/b3/c19426f783614d625699a4b247faf628ea9ab1db228674491767b417f9e1/setuptools-84.2.1.tar.gz", hash = "sha256:3ff8b3b5492a0aa66960f4419365a86b84e013070d4313e4d4a6894f5d64d6bc", size = 1230289, upload-time = "2026-10-20T19:01:37.742Z" }
|
|
4973
4996
|
wheels = [
|
|
4974
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
4997
|
+
{ url = "https://files.pythonhosted.org/packages/ef/55/6dc6d6bb953bdfc52519a0059592c52b6e3b8ad51cf8256dffceb55b0610/setuptools-84.2.1-py3-none-any.whl", hash = "sha256:ca140b45d5325a9478f880e351484f62fef15f336462e449ca0c03c873d589c2", size = 876263, upload-time = "2026-10-20T19:01:35.678Z" },
|
|
4975
4998
|
]
|
|
4976
4999
|
|
|
4977
5000
|
[[package]]
|