failproofai 1.0.0-beta.2 → 1.0.0-beta.3
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 +6 -6
- 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/build-manifest.json +3 -3
- 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 +7 -7
- 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 +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- 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 +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- 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/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- 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/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/reminder/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/build-manifest.json +3 -3
- 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 +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +30 -18
- 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/build-manifest.json +3 -3
- 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/build-manifest.json +3 -3
- 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/build-manifest.json +3 -3
- 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/chunks/[root-of-the-server]__0id_xf7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ath6v_._.js → [root-of-the-server]__0ls0gsr._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__170799-._.js → [root-of-the-server]__0spkm68._.js} +11 -11
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1psjybo._.js → [root-of-the-server]__10s_z4l._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0114ewg._.js → [root-of-the-server]__1j4u3yd._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0nb0syg._.js → [root-of-the-server]__1ky3im9._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0oy_-36.js → node_modules_html-to-image_es_index_0jfts82.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- 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/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +32 -20
- package/.next/standalone/.next/static/chunks/{3s7myk_co0gbh.js → 01h6lfjz1_oie.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 0a7k_x4rp_97z.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 0cvd6bcdwchc_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 13-obgukzg_ke.js} +1 -1
- package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
- package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 1h0_yaoq5f3yx.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 29g8ykjk6sx_8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 2rqm-klet_gbl.js} +1 -1
- package/.next/standalone/.next/static/chunks/{31feojxhjyjpl.js → 2uvyh8y0naofj.js} +1 -1
- package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
- package/.next/standalone/.next/static/chunks/3p-n_j80zlzi3.js +6 -0
- package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
- package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
- package/.next/standalone/Cargo.lock +1992 -198
- package/.next/standalone/Cargo.toml +1 -1
- package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
- package/.next/standalone/app/components/pause-notices.tsx +111 -0
- package/.next/standalone/app/policies/hooks-client.tsx +81 -6
- package/.next/standalone/crates/CLOUD_POLICIES.md +136 -0
- package/.next/standalone/crates/failproofaid/Cargo.toml +7 -0
- package/.next/standalone/crates/failproofaid/src/cloud_client.rs +484 -0
- package/.next/standalone/crates/failproofaid/src/cloud_policies.rs +921 -0
- package/.next/standalone/crates/failproofaid/src/main.rs +452 -3
- package/.next/standalone/crates/failproofaid/src/paths.rs +29 -0
- package/.next/standalone/crates/fpai-collect/Cargo.toml +43 -0
- package/.next/standalone/crates/fpai-collect/src/config.rs +412 -0
- package/.next/standalone/crates/fpai-collect/src/cursor.rs +520 -0
- package/.next/standalone/crates/fpai-collect/src/delivery.rs +472 -0
- package/.next/standalone/crates/fpai-collect/src/filetail.rs +484 -0
- package/.next/standalone/crates/fpai-collect/src/health.rs +369 -0
- package/.next/standalone/crates/fpai-collect/src/lib.rs +34 -0
- package/.next/standalone/crates/fpai-collect/src/redact.rs +516 -0
- package/.next/standalone/crates/fpai-collect/src/sources/claude/mod.rs +279 -0
- package/.next/standalone/crates/fpai-collect/src/sources/claude/transform.rs +619 -0
- package/.next/standalone/crates/fpai-collect/src/sources/codex/mod.rs +156 -0
- package/.next/standalone/crates/fpai-collect/src/sources/codex/transform.rs +715 -0
- package/.next/standalone/crates/fpai-collect/src/sources/copilot/mod.rs +158 -0
- package/.next/standalone/crates/fpai-collect/src/sources/copilot/transform.rs +512 -0
- package/.next/standalone/crates/fpai-collect/src/sources/goose/mod.rs +511 -0
- package/.next/standalone/crates/fpai-collect/src/sources/goose/transform.rs +586 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hermes/mod.rs +318 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hermes/transform.rs +579 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hooks/mod.rs +310 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hooks/transform.rs +448 -0
- package/.next/standalone/crates/fpai-collect/src/sources/mod.rs +23 -0
- package/.next/standalone/crates/fpai-collect/src/sources/openclaw/mod.rs +176 -0
- package/.next/standalone/crates/fpai-collect/src/sources/openclaw/transform.rs +500 -0
- package/.next/standalone/crates/fpai-collect/src/sources/opencode/mod.rs +315 -0
- package/.next/standalone/crates/fpai-collect/src/sources/opencode/transform.rs +493 -0
- package/.next/standalone/crates/fpai-collect/src/sources/pi/mod.rs +201 -0
- package/.next/standalone/crates/fpai-collect/src/sources/pi/transform.rs +533 -0
- package/.next/standalone/crates/fpai-collect/src/spool.rs +458 -0
- package/.next/standalone/crates/fpai-collect/src/sqlitepoll.rs +387 -0
- package/.next/standalone/crates/fpai-collect/src/supervisor.rs +423 -0
- package/.next/standalone/crates/fpai-collect/src/uploader.rs +600 -0
- package/.next/standalone/crates/fpai-collect/tests/claude_source.rs +738 -0
- package/.next/standalone/crates/fpai-collect/tests/claude_subagents.rs +393 -0
- package/.next/standalone/crates/fpai-collect/tests/codex_source.rs +975 -0
- package/.next/standalone/crates/fpai-collect/tests/config.rs +286 -0
- package/.next/standalone/crates/fpai-collect/tests/copilot_source.rs +1019 -0
- package/.next/standalone/crates/fpai-collect/tests/delivery.rs +281 -0
- package/.next/standalone/crates/fpai-collect/tests/goose_source.rs +1008 -0
- package/.next/standalone/crates/fpai-collect/tests/hermes_source.rs +1129 -0
- package/.next/standalone/crates/fpai-collect/tests/hooks_source.rs +621 -0
- package/.next/standalone/crates/fpai-collect/tests/openclaw_source.rs +854 -0
- package/.next/standalone/crates/fpai-collect/tests/opencode_source.rs +1478 -0
- package/.next/standalone/crates/fpai-collect/tests/pi_source.rs +1191 -0
- package/.next/standalone/crates/fpai-collect/tests/supervisor.rs +290 -0
- package/.next/standalone/crates/fpai-collect/tests/uploader.rs +506 -0
- package/.next/standalone/package.json +2 -2
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +129 -0
- package/dist/cli.mjs +2195 -770
- package/dist/worker.mjs +365 -52
- package/package.json +2 -2
- package/src/hooks/builtin-policies.ts +139 -0
- package/src/hooks/cloud-connection.ts +205 -0
- package/src/hooks/cloud-enrollment-cli.ts +227 -0
- package/src/hooks/cloud-enrollment.ts +173 -0
- package/src/hooks/cloud-managed-policies.ts +138 -0
- package/src/hooks/collector-config.ts +226 -0
- package/src/hooks/configure-wizard.ts +221 -5
- package/src/hooks/custom-hooks-loader.ts +69 -25
- package/src/hooks/daemon-client.ts +18 -0
- package/src/hooks/handler.ts +144 -6
- package/src/hooks/hook-activity-store.ts +50 -0
- package/src/hooks/loader-utils.ts +6 -4
- package/src/hooks/policy-types.ts +35 -0
- package/src/hooks/session-pause-cli.ts +157 -0
- package/src/hooks/session-pause.ts +236 -0
- package/src/hooks/tui.ts +115 -0
- package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
- package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
- package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
- package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import { listActivePauses } from "@/src/hooks/session-pause";
|
|
4
|
+
import type { ActivePause } from "@/src/hooks/session-pause";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sessions whose enforcement is paused right now.
|
|
8
|
+
*
|
|
9
|
+
* Read live rather than derived from activity rows: a pause set seconds ago has
|
|
10
|
+
* produced no rows yet, and that is exactly the moment someone needs to be told
|
|
11
|
+
* the machine is unguarded. Expiry is applied at read time, so an expired pause
|
|
12
|
+
* simply stops appearing.
|
|
13
|
+
*/
|
|
14
|
+
export async function getActivePausesAction(): Promise<ActivePause[]> {
|
|
15
|
+
return listActivePauses();
|
|
16
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The dashboard's rendering of a paused machine.
|
|
5
|
+
*
|
|
6
|
+
* Split out of `hooks-client.tsx` because these are the pieces that keep the
|
|
7
|
+
* activity view honest: a row evaluated during a pause looks identical to one
|
|
8
|
+
* where every policy ran and allowed, and without saying so the log asserts a
|
|
9
|
+
* clean window over exactly the window that was not enforced.
|
|
10
|
+
*/
|
|
11
|
+
import React, { useEffect, useState } from "react";
|
|
12
|
+
import { ShieldAlert, TriangleAlert } from "lucide-react";
|
|
13
|
+
import type { ActivePause } from "@/src/hooks/session-pause";
|
|
14
|
+
|
|
15
|
+
/** Compact "time left" for a future timestamp. */
|
|
16
|
+
export function formatRemaining(ms: number): string {
|
|
17
|
+
if (ms <= 0) return "expiring now";
|
|
18
|
+
// Sub-minute is checked before rounding: Math.round(30s) is "1m", which tells
|
|
19
|
+
// someone they have more time than they do. On a countdown to enforcement
|
|
20
|
+
// coming back, never round up.
|
|
21
|
+
if (ms < 60_000) return "under a minute";
|
|
22
|
+
const minutes = Math.round(ms / 60_000);
|
|
23
|
+
if (minutes < 60) return `${minutes}m`;
|
|
24
|
+
const hours = Math.floor(minutes / 60);
|
|
25
|
+
const rest = minutes % 60;
|
|
26
|
+
return rest === 0 ? `${hours}h` : `${hours}h${rest}m`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Live state, not history: enforcement is paused RIGHT NOW.
|
|
31
|
+
*
|
|
32
|
+
* The rows below cannot carry this on their own — a pause set seconds ago has
|
|
33
|
+
* produced none yet, and that is precisely when someone needs telling that the
|
|
34
|
+
* machine is unguarded. An absent banner has to mean "enforcing", so this is fed
|
|
35
|
+
* from live pause state rather than inferred from whatever is on screen.
|
|
36
|
+
*/
|
|
37
|
+
export function PausedBanner({ pauses, now: nowProp }: { pauses: ActivePause[]; now?: number }) {
|
|
38
|
+
const [tick, setTick] = useState(() => Date.now());
|
|
39
|
+
// Re-render on a timer so "22m left" does not sit frozen while the pause
|
|
40
|
+
// silently drains away.
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const id = setInterval(() => setTick(Date.now()), 30_000);
|
|
43
|
+
return () => clearInterval(id);
|
|
44
|
+
}, []);
|
|
45
|
+
const now = nowProp ?? tick;
|
|
46
|
+
|
|
47
|
+
// Filter again here rather than trusting the fetch: the list was accurate
|
|
48
|
+
// when it arrived, and a short pause can expire between polls.
|
|
49
|
+
const live = pauses.filter((p) => p.expiresAt > now);
|
|
50
|
+
if (live.length === 0) return null;
|
|
51
|
+
const soonest = live.reduce((a, b) => (a.expiresAt < b.expiresAt ? a : b));
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
role="status"
|
|
56
|
+
className="mb-4 flex items-start gap-2.5 rounded border border-amber-500/40 bg-amber-500/[0.07] px-3 py-2.5"
|
|
57
|
+
>
|
|
58
|
+
<ShieldAlert className="h-4 w-4 shrink-0 text-amber-500 mt-0.5" aria-hidden="true" />
|
|
59
|
+
<div className="text-xs leading-relaxed">
|
|
60
|
+
<p className="font-medium text-foreground">
|
|
61
|
+
Enforcement is paused for {live.length} session{live.length === 1 ? "" : "s"} —{" "}
|
|
62
|
+
{formatRemaining(soonest.expiresAt - now)} left
|
|
63
|
+
{live.length > 1 ? " on the next to expire" : ""}.
|
|
64
|
+
</p>
|
|
65
|
+
<p className="text-muted-foreground mt-0.5">
|
|
66
|
+
Builtin, custom and convention policies are suspended for{" "}
|
|
67
|
+
{live.length === 1 ? "that session" : "those sessions"}; cloud-managed policies keep
|
|
68
|
+
enforcing. It lifts on its own — or run{" "}
|
|
69
|
+
<span className="font-mono text-foreground">failproofai config --resume</span>.
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Marks a row that was evaluated while enforcement was paused. */
|
|
77
|
+
export function PausedPill() {
|
|
78
|
+
return (
|
|
79
|
+
<span
|
|
80
|
+
title="Enforcement was paused for this session — local policies did not run"
|
|
81
|
+
className="ml-1.5 inline-flex items-center rounded px-1.5 py-0.5 text-[0.6rem] font-medium uppercase tracking-wide border border-amber-500/40 bg-amber-500/10 text-amber-600 dark:text-amber-500"
|
|
82
|
+
>
|
|
83
|
+
paused
|
|
84
|
+
</span>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Why an `allow` on this row proves nothing. */
|
|
89
|
+
export function PausedNote({
|
|
90
|
+
item,
|
|
91
|
+
}: {
|
|
92
|
+
item: { pausedBy?: string; pauseExpiresAt?: number };
|
|
93
|
+
}) {
|
|
94
|
+
if (!item.pausedBy) return null;
|
|
95
|
+
const lifted =
|
|
96
|
+
typeof item.pauseExpiresAt === "number"
|
|
97
|
+
? new Date(item.pauseExpiresAt).toLocaleTimeString()
|
|
98
|
+
: null;
|
|
99
|
+
return (
|
|
100
|
+
<div className="sm:col-span-2 lg:col-span-3 mt-1 flex items-start gap-2 rounded border border-amber-500/30 bg-amber-500/5 px-2.5 py-2">
|
|
101
|
+
<TriangleAlert className="h-3.5 w-3.5 shrink-0 text-amber-500/80 mt-0.5" aria-hidden="true" />
|
|
102
|
+
<p className="text-[0.7rem] leading-relaxed text-muted-foreground">
|
|
103
|
+
<span className="font-medium text-foreground">Not enforced — paused.</span> Enforcement was
|
|
104
|
+
suspended for this session
|
|
105
|
+
{item.pausedBy === "cli" ? " from the CLI" : ` by ${item.pausedBy}`}
|
|
106
|
+
{lifted ? `, lifting at ${lifted}` : ""}. Builtin, custom and convention policies did not
|
|
107
|
+
run for this event; cloud-managed policies did.
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -7,6 +7,9 @@ import { Check, ChevronDown, Code, Copy, Settings, Shield, ShieldAlert, ShieldCh
|
|
|
7
7
|
import PaginationControls from "@/app/components/pagination-controls";
|
|
8
8
|
import { getHookActivityAction, searchHookActivityAction } from "@/app/actions/get-hook-activity";
|
|
9
9
|
import type { HookActivityPayload } from "@/app/actions/get-hook-activity";
|
|
10
|
+
import { getActivePausesAction } from "@/app/actions/get-active-pauses";
|
|
11
|
+
import type { ActivePause } from "@/src/hooks/session-pause";
|
|
12
|
+
import { PausedBanner, PausedNote, PausedPill } from "@/app/components/pause-notices";
|
|
10
13
|
import { getHooksConfigAction } from "@/app/actions/get-hooks-config";
|
|
11
14
|
import type { HooksConfigPayload, PolicyInfo, CustomPolicyInfo } from "@/app/actions/get-hooks-config";
|
|
12
15
|
import type { IntegrationType } from "@/src/hooks/types";
|
|
@@ -386,7 +389,27 @@ function DetailPanel({
|
|
|
386
389
|
event detail
|
|
387
390
|
</span>
|
|
388
391
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-2">
|
|
392
|
+
<PausedNote item={item} />
|
|
389
393
|
<EnforcementNote item={item} />
|
|
394
|
+
{item.policySource && (
|
|
395
|
+
<div>
|
|
396
|
+
<span className="text-muted-foreground">Decided by: </span>
|
|
397
|
+
<span className="font-mono text-foreground">
|
|
398
|
+
{item.policySource === "cloud" && item.cloudPolicyId
|
|
399
|
+
? `cloud · ${item.cloudPolicyId} rev ${item.cloudRevision}`
|
|
400
|
+
: item.policySource}
|
|
401
|
+
</span>
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
{item.cloudGeneration !== undefined && (
|
|
405
|
+
<div>
|
|
406
|
+
{/* Present on every row of a managed machine, not just cloud
|
|
407
|
+
decisions — it is what separates a rollout that changed no
|
|
408
|
+
outcomes from one that never arrived. */}
|
|
409
|
+
<span className="text-muted-foreground">Cloud generation: </span>
|
|
410
|
+
<span className="font-mono text-foreground">{item.cloudGeneration}</span>
|
|
411
|
+
</div>
|
|
412
|
+
)}
|
|
390
413
|
<div>
|
|
391
414
|
<span className="text-muted-foreground">Session ID: </span>
|
|
392
415
|
<span className="font-mono text-foreground">
|
|
@@ -443,6 +466,7 @@ function ActivityTab({
|
|
|
443
466
|
|
|
444
467
|
const [page, setPage] = useState(() => paramToPage(url.get("page")));
|
|
445
468
|
const [data, setData] = useState<HookActivityPayload | null>(null);
|
|
469
|
+
const [activePauses, setActivePauses] = useState<ActivePause[]>([]);
|
|
446
470
|
const [expandedRow, setExpandedRow] = useState<number | null>(null);
|
|
447
471
|
|
|
448
472
|
const [filterDecision, setFilterDecision] = useState<"" | "allow" | "deny" | "instruct">(() => {
|
|
@@ -456,10 +480,14 @@ function ActivityTab({
|
|
|
456
480
|
const v = url.get("cli");
|
|
457
481
|
return isKnownCli(v) ? v : "";
|
|
458
482
|
});
|
|
483
|
+
const [filterSource, setFilterSource] = useState<"" | "builtin" | "custom" | "convention" | "cloud">(() => {
|
|
484
|
+
const v = url.get("source");
|
|
485
|
+
return v === "builtin" || v === "custom" || v === "convention" || v === "cloud" ? v : "";
|
|
486
|
+
});
|
|
459
487
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
460
488
|
const filterTelemetryFirstRunRef = useRef(true);
|
|
461
|
-
const filtersRef = useRef({ filterDecision, filterEventType, filterPolicy, filterSessionId, filterCli });
|
|
462
|
-
filtersRef.current = { filterDecision, filterEventType, filterPolicy, filterSessionId, filterCli };
|
|
489
|
+
const filtersRef = useRef({ filterDecision, filterEventType, filterPolicy, filterSessionId, filterCli, filterSource });
|
|
490
|
+
filtersRef.current = { filterDecision, filterEventType, filterPolicy, filterSessionId, filterCli, filterSource };
|
|
463
491
|
|
|
464
492
|
useEffect(() => {
|
|
465
493
|
if (!mountedRef.current) {
|
|
@@ -472,17 +500,18 @@ function ActivityTab({
|
|
|
472
500
|
policy: filterPolicy || undefined,
|
|
473
501
|
session: filterSessionId || undefined,
|
|
474
502
|
cli: filterCli || undefined,
|
|
503
|
+
source: filterSource || undefined,
|
|
475
504
|
page: pageToParam(page),
|
|
476
505
|
});
|
|
477
506
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
478
|
-
}, [filterDecision, filterEventType, filterPolicy, filterSessionId, filterCli, page]);
|
|
507
|
+
}, [filterDecision, filterEventType, filterPolicy, filterSessionId, filterCli, filterSource, page]);
|
|
479
508
|
|
|
480
|
-
const hasActiveFilters = filterDecision !== "" || filterEventType !== "" || filterPolicy !== "" || filterSessionId !== "" || filterCli !== "";
|
|
509
|
+
const hasActiveFilters = filterDecision !== "" || filterEventType !== "" || filterPolicy !== "" || filterSessionId !== "" || filterCli !== "" || filterSource !== "";
|
|
481
510
|
|
|
482
511
|
const fetchData = useCallback(async (p: number) => {
|
|
483
512
|
try {
|
|
484
|
-
const { filterDecision: fd, filterEventType: fe, filterPolicy: fp, filterSessionId: fs, filterCli: fc } = filtersRef.current;
|
|
485
|
-
const active = fd !== "" || fe !== "" || fp !== "" || fs !== "" || fc !== "";
|
|
513
|
+
const { filterDecision: fd, filterEventType: fe, filterPolicy: fp, filterSessionId: fs, filterCli: fc, filterSource: fsrc } = filtersRef.current;
|
|
514
|
+
const active = fd !== "" || fe !== "" || fp !== "" || fs !== "" || fc !== "" || fsrc !== "";
|
|
486
515
|
let result: HookActivityPayload;
|
|
487
516
|
if (active) {
|
|
488
517
|
result = await searchHookActivityAction(
|
|
@@ -492,6 +521,7 @@ function ActivityTab({
|
|
|
492
521
|
policyName: fp || undefined,
|
|
493
522
|
sessionId: fs || undefined,
|
|
494
523
|
integration: fc || undefined,
|
|
524
|
+
source: fsrc || undefined,
|
|
495
525
|
},
|
|
496
526
|
p,
|
|
497
527
|
);
|
|
@@ -511,6 +541,22 @@ function ActivityTab({
|
|
|
511
541
|
return () => clearInterval(id);
|
|
512
542
|
}, [page, fetchData, intervalSec]);
|
|
513
543
|
|
|
544
|
+
// Pause state is polled independently of the activity page: it is live
|
|
545
|
+
// machine state, not a property of whichever rows are on screen, and it must
|
|
546
|
+
// keep updating while the user sits on page 3 of history.
|
|
547
|
+
useEffect(() => {
|
|
548
|
+
let cancelled = false;
|
|
549
|
+
const load = () => {
|
|
550
|
+
getActivePausesAction()
|
|
551
|
+
.then((p) => { if (!cancelled) setActivePauses(p); })
|
|
552
|
+
.catch(() => { /* non-critical: the banner simply stays hidden */ });
|
|
553
|
+
};
|
|
554
|
+
load();
|
|
555
|
+
const ms = intervalSec > 0 ? intervalSec * 1000 : 5000;
|
|
556
|
+
const id = setInterval(load, ms);
|
|
557
|
+
return () => { cancelled = true; clearInterval(id); };
|
|
558
|
+
}, [intervalSec]);
|
|
559
|
+
|
|
514
560
|
useEffect(() => {
|
|
515
561
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
516
562
|
debounceRef.current = setTimeout(() => {
|
|
@@ -565,6 +611,10 @@ function ActivityTab({
|
|
|
565
611
|
|
|
566
612
|
return (
|
|
567
613
|
<>
|
|
614
|
+
{/* Above the stats, deliberately: a paused machine is the most important
|
|
615
|
+
thing on this screen, and the numbers below it are being produced with
|
|
616
|
+
local enforcement switched off. */}
|
|
617
|
+
<PausedBanner pauses={activePauses} />
|
|
568
618
|
{data?.stats && data.stats.totalEvents > 0 && (
|
|
569
619
|
<div style={{ marginBottom: 18 }}>
|
|
570
620
|
<StatsBar stats={data.stats} />
|
|
@@ -593,6 +643,29 @@ function ActivityTab({
|
|
|
593
643
|
<option value="PermissionRequest">PermissionRequest</option>
|
|
594
644
|
</select>
|
|
595
645
|
</div>
|
|
646
|
+
<div className="filter-group">
|
|
647
|
+
{/* "What did my organization's policies decide here?" is the
|
|
648
|
+
question cloud rollout reporting rests on, and it is
|
|
649
|
+
unanswerable while the source is only a prefix on a name. */}
|
|
650
|
+
<span className="filter-label">source</span>
|
|
651
|
+
<select
|
|
652
|
+
value={filterSource}
|
|
653
|
+
onChange={(e) => {
|
|
654
|
+
const v = e.target.value;
|
|
655
|
+
setFilterSource(
|
|
656
|
+
v === "builtin" || v === "custom" || v === "convention" || v === "cloud" ? v : "",
|
|
657
|
+
);
|
|
658
|
+
}}
|
|
659
|
+
className="filter-input"
|
|
660
|
+
aria-label="Filter by policy source"
|
|
661
|
+
>
|
|
662
|
+
<option value="">all sources</option>
|
|
663
|
+
<option value="builtin">builtin</option>
|
|
664
|
+
<option value="custom">custom</option>
|
|
665
|
+
<option value="convention">convention</option>
|
|
666
|
+
<option value="cloud">cloud</option>
|
|
667
|
+
</select>
|
|
668
|
+
</div>
|
|
596
669
|
<div className="filter-group">
|
|
597
670
|
<span className="filter-label">cli</span>
|
|
598
671
|
<select
|
|
@@ -642,6 +715,7 @@ function ActivityTab({
|
|
|
642
715
|
setFilterCli("");
|
|
643
716
|
setFilterPolicy("");
|
|
644
717
|
setFilterSessionId("");
|
|
718
|
+
setFilterSource("");
|
|
645
719
|
}}
|
|
646
720
|
aria-label="Clear all filters"
|
|
647
721
|
>
|
|
@@ -744,6 +818,7 @@ function ActivityTab({
|
|
|
744
818
|
</td>
|
|
745
819
|
<td className="px-3 py-2">
|
|
746
820
|
<DecisionBadge decision={item.decision} />
|
|
821
|
+
{item.pausedBy && <PausedPill />}
|
|
747
822
|
</td>
|
|
748
823
|
<td className="px-3 py-2">
|
|
749
824
|
<EventTypeBadge eventType={item.eventType} />
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Cloud-managed policy generations
|
|
2
|
+
|
|
3
|
+
This is the contract between the Failproof Cloud HTTP transport and the
|
|
4
|
+
`failproofaid` policy worker. Polling and integrity maintenance run outside the
|
|
5
|
+
hook path; hooks evaluate only the last verified local generation.
|
|
6
|
+
|
|
7
|
+
## Layout
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
~/.failproofai/policies/cloud-managed/
|
|
11
|
+
├── desired-state.json
|
|
12
|
+
├── active.json
|
|
13
|
+
├── artifacts/
|
|
14
|
+
│ └── <sha256>.mjs
|
|
15
|
+
└── generations/
|
|
16
|
+
└── <generation>/
|
|
17
|
+
├── manifest.json
|
|
18
|
+
└── <policy-id>.mjs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `desired-state.json` is the last complete desired-state snapshot received
|
|
22
|
+
from cloud. A later slice must authenticate it with a publisher signature;
|
|
23
|
+
SHA-256 alone proves byte identity, not publisher identity.
|
|
24
|
+
- `artifacts/` is a content-addressed cache.
|
|
25
|
+
- `generations/` contains complete materialized policy sets. New generations
|
|
26
|
+
are built without modifying the active one.
|
|
27
|
+
- `active.json` is an atomically replaced pointer to one complete generation.
|
|
28
|
+
It is derived state and is reconstructed when it is deleted, malformed, or
|
|
29
|
+
disagrees with `desired-state.json`.
|
|
30
|
+
|
|
31
|
+
## Desired-state schema
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"schemaVersion": 1,
|
|
36
|
+
"generation": 184,
|
|
37
|
+
"policies": [
|
|
38
|
+
{
|
|
39
|
+
"id": "block-secret-exfiltration",
|
|
40
|
+
"revision": 7,
|
|
41
|
+
"sha256": "<64 lowercase hex characters>",
|
|
42
|
+
"artifactUrl": "https://..."
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The artifact URL is opaque to the store. A cloud client supplies downloaded
|
|
49
|
+
bytes through `ArtifactFetcher`; the reconciler trusts none of those bytes
|
|
50
|
+
until their SHA-256 matches the desired state.
|
|
51
|
+
|
|
52
|
+
## Cloud transport
|
|
53
|
+
|
|
54
|
+
Enrol with the CLI:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
failproofai config --connect https://be.failproof.ai \
|
|
58
|
+
--token <org-scoped policies:pull key> \
|
|
59
|
+
--machine-id prod-runner-01 # defaults to this host's name
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That verifies the credentials against the server before storing anything, then
|
|
63
|
+
writes `~/.failproofai/cloud.json` (mode 0600). `--disconnect` removes it,
|
|
64
|
+
`--status` reports the connection with the token masked.
|
|
65
|
+
|
|
66
|
+
**The credential must not go in the service unit.** `daemon-service.ts` installs
|
|
67
|
+
`/etc/systemd/system/failproofaid@<user>.service` at mode 0644 — root-owned and
|
|
68
|
+
world-readable — and the launchd plist likewise. An
|
|
69
|
+
`Environment="FAILPROOFAI_CLOUD_TOKEN=…"` line there hands an organization-scoped
|
|
70
|
+
key to every local user, and `systemctl show` prints it back with no privilege
|
|
71
|
+
at all. Keeping it in a file also means enrolment, rotation and disconnect need
|
|
72
|
+
no root, and an already-installed daemon can be connected without reinstalling.
|
|
73
|
+
|
|
74
|
+
The daemon re-resolves enrolment on **every poll**, not at startup, so all three
|
|
75
|
+
take effect within one interval with nothing to restart — which matters because
|
|
76
|
+
restarting a system unit needs root, the very thing this avoids.
|
|
77
|
+
|
|
78
|
+
Environment variables still take precedence over the file, for CI, containers
|
|
79
|
+
and tests:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
FAILPROOFAI_CLOUD_URL=https://be.failproof.ai
|
|
83
|
+
FAILPROOFAI_CLOUD_TOKEN=<org-scoped policies:pull key>
|
|
84
|
+
FAILPROOFAI_MACHINE_ID=<deployment machine id>
|
|
85
|
+
FAILPROOFAI_CLOUD_CREDENTIALS=<path> # overrides ~/.failproofai/cloud.json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`FAILPROOFAI_CLOUD_POLICY_POLL_MS` controls the interval (30 seconds by
|
|
89
|
+
default, clamped to at least 100 ms). The client sends Bearer authentication
|
|
90
|
+
to both desired-state and artifact endpoints. Relative artifact locators are
|
|
91
|
+
resolved against the configured base URL; cross-origin locators are rejected
|
|
92
|
+
before the token is sent.
|
|
93
|
+
|
|
94
|
+
An HTTP failure, invalid desired-state response, bad digest, or incomplete
|
|
95
|
+
generation leaves the previous generation active.
|
|
96
|
+
|
|
97
|
+
## Activation transaction
|
|
98
|
+
|
|
99
|
+
1. Validate schema, policy IDs, unique IDs, digests, and monotonic generation.
|
|
100
|
+
2. Reuse a verified cache/generation copy or fetch missing bytes.
|
|
101
|
+
3. Verify every artifact digest.
|
|
102
|
+
4. Materialize the complete generation and `fsync` its files/directories.
|
|
103
|
+
5. Persist `desired-state.json`.
|
|
104
|
+
6. Atomically replace `active.json`.
|
|
105
|
+
|
|
106
|
+
Any failure before step 6 leaves the previous generation active. The worker
|
|
107
|
+
loads only paths named by `active.json` and independently verifies every digest
|
|
108
|
+
immediately before importing JavaScript.
|
|
109
|
+
|
|
110
|
+
## Integrity maintenance
|
|
111
|
+
|
|
112
|
+
`failproofaid` runs a maintenance thread outside the hook path. It hashes the
|
|
113
|
+
active generation periodically (30 seconds by default) and repairs either a
|
|
114
|
+
modified generation copy or a modified content-addressed artifact from the
|
|
115
|
+
other verified copy. If both are gone, it retains the active manifest and
|
|
116
|
+
reports that a cloud re-fetch is required.
|
|
117
|
+
|
|
118
|
+
`FAILPROOFAI_CLOUD_POLICY_DIR` overrides the root for tests and development.
|
|
119
|
+
When cloud polling is disabled, `FAILPROOFAI_CLOUD_POLICY_RECONCILE_MS`
|
|
120
|
+
overrides the standalone integrity interval, clamped to at least 100 ms. With
|
|
121
|
+
cloud polling enabled, integrity repair runs on each poll.
|
|
122
|
+
|
|
123
|
+
## Current security boundary
|
|
124
|
+
|
|
125
|
+
PR #632 runs the daemon as the same OS user as the governed agent. This layer
|
|
126
|
+
provides deterministic deployment, drift detection, and self-healing, but it
|
|
127
|
+
does not make policies tamper-proof against that user. The user can stop the
|
|
128
|
+
service or delete both verified copies. Publisher signatures, deployment
|
|
129
|
+
acknowledgement, and a stronger service identity are separate follow-up layers.
|
|
130
|
+
The current machine credential is an org-scoped Bearer key transported over
|
|
131
|
+
HTTPS.
|
|
132
|
+
|
|
133
|
+
Downloaded JavaScript executes in the existing TypeScript policy worker with
|
|
134
|
+
the user's authority. Cloud authorization must therefore treat assigning an
|
|
135
|
+
arbitrary JavaScript policy as remote code execution until a sandboxed policy
|
|
136
|
+
runtime exists.
|
|
@@ -13,6 +13,13 @@ path = "src/main.rs"
|
|
|
13
13
|
|
|
14
14
|
[dependencies]
|
|
15
15
|
fpai-ipc = { path = "../fpai-ipc" }
|
|
16
|
+
fpai-collect = { path = "../fpai-collect" }
|
|
17
|
+
# Without a subscriber every `tracing::` call in fpai-collect is silently
|
|
18
|
+
# discarded — including the uploader's "the server stored NONE of its events".
|
|
19
|
+
tracing = "0.1"
|
|
20
|
+
tracing-subscriber = { version = "0.3", default-features = false, features = ["std", "fmt", "env-filter"] }
|
|
16
21
|
serde = { version = "1", features = ["derive"] }
|
|
17
22
|
serde_json = "1"
|
|
18
23
|
libc = "0.2"
|
|
24
|
+
sha2 = "0.10"
|
|
25
|
+
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|