failproofai 1.0.0-beta.2 → 1.0.0-beta.4
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 +2 -2
- 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 +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 +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 +1 -1
- 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 +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/[externals]__0h9_83k._.js +3 -0
- package/.next/standalone/.next/server/chunks/[externals]__1ifpkx-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-dh5jy._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0f8nu8d._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0h3lfmp._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l4vuqm._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0longh-._.js → [root-of-the-server]__0szqzl8._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__10oa-v9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__18d-9oi._.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/{[root-of-the-server]__16le-kd._.js → [root-of-the-server]__1p7s8-d._.js} +2 -2
- 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/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[externals]__1aq139v._.js +3 -0
- 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]__0nb0syg._.js → [root-of-the-server]__0cmdyn_._.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]__0uno39i._.js → [root-of-the-server]__0n_uvv0._.js} +2 -2
- 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]__01od0m6._.js → [root-of-the-server]__0rag5dg._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apb7k_._.js → [root-of-the-server]__0rtdfof._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1psjybo._.js → [root-of-the-server]__0sdrz-j._.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]__0114ewg._.js → [root-of-the-server]__1j4u3yd._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +1 -1
- 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_1q9xxca.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -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/{18rvdw5v72364.js → 06jakt_xhoolk.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
- package/.next/standalone/.next/static/chunks/0oe2-hp0_2tzs.js +6 -0
- package/.next/standalone/.next/static/chunks/{1hdhteui932bq.js → 0qmo2t50kpb30.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 0ue9ak_xexiqh.js} +1 -1
- package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
- package/.next/standalone/.next/static/chunks/1fw3pxobk_otl.js +1 -0
- package/.next/standalone/.next/static/chunks/23kev26by1e4f.js +1 -0
- package/.next/standalone/.next/static/chunks/2c90h1g6x8z8y.js +1 -0
- package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 2zb3m1tzq6h98.js} +1 -1
- package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
- package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 31cqiv_cpgj9m.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 3h5_3u46gu9jx.js} +1 -1
- package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
- package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 43a1ywqsg9bq9.js} +1 -1
- 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 +15 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +129 -0
- package/dist/cli.mjs +2255 -779
- package/dist/worker.mjs +365 -52
- package/package.json +15 -9
- package/scripts/build-daemon-packages.mjs +210 -0
- package/scripts/daemon-platforms.mjs +42 -0
- package/scripts/prune-standalone.mjs +6 -0
- package/scripts/publish-aliases.mjs +62 -41
- 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/daemon-download.ts +137 -27
- package/src/hooks/daemon-service.ts +23 -8
- 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/server/chunks/[externals]__14odj07._.js +0 -3
- package/.next/standalone/.next/server/chunks/[externals]__1s61mel._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[externals]__1_g_b3t._.js +0 -3
- package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
- package/.next/standalone/.next/static/chunks/2mazwb1qmur1p.js +0 -1
- package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
- package/.next/standalone/.next/static/chunks/31feojxhjyjpl.js +0 -1
- package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
- package/.next/standalone/.next/static/chunks/3s7myk_co0gbh.js +0 -1
- package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
use crate::cloud_policies::{DesiredPolicy, DesiredState, PolicyStore};
|
|
2
|
+
use reqwest::Url;
|
|
3
|
+
use reqwest::blocking::Client;
|
|
4
|
+
use std::sync::Arc;
|
|
5
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
6
|
+
use std::thread::JoinHandle;
|
|
7
|
+
use std::time::{Duration, Instant};
|
|
8
|
+
|
|
9
|
+
const DEFAULT_POLL_MS: u64 = 30_000;
|
|
10
|
+
const MINIMUM_POLL_MS: u64 = 100;
|
|
11
|
+
|
|
12
|
+
#[derive(Clone)]
|
|
13
|
+
pub struct CloudClient {
|
|
14
|
+
base_url: Url,
|
|
15
|
+
token: String,
|
|
16
|
+
machine_id: String,
|
|
17
|
+
client: Client,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// On-disk enrolment, written by `failproofai config --connect`.
|
|
21
|
+
///
|
|
22
|
+
/// The credential deliberately does NOT live in the service unit: that file is
|
|
23
|
+
/// installed world-readable (0644, `/etc/systemd/system`), so a token there
|
|
24
|
+
/// would be readable by every local user and echoed by `systemctl show`.
|
|
25
|
+
#[derive(serde::Deserialize)]
|
|
26
|
+
struct StoredCredentials {
|
|
27
|
+
#[serde(rename = "schemaVersion")]
|
|
28
|
+
schema_version: u32,
|
|
29
|
+
url: String,
|
|
30
|
+
#[serde(rename = "machineId")]
|
|
31
|
+
machine_id: String,
|
|
32
|
+
token: String,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// `~/.failproofai/cloud.json`, or `FAILPROOFAI_CLOUD_CREDENTIALS`.
|
|
36
|
+
/// The same two rules the TS side applies, so the pair cannot drift.
|
|
37
|
+
pub fn credentials_path() -> Option<std::path::PathBuf> {
|
|
38
|
+
if let Some(path) = std::env::var_os("FAILPROOFAI_CLOUD_CREDENTIALS") {
|
|
39
|
+
return Some(std::path::PathBuf::from(path));
|
|
40
|
+
}
|
|
41
|
+
let home = std::env::var_os("HOME")?;
|
|
42
|
+
Some(
|
|
43
|
+
std::path::PathBuf::from(home)
|
|
44
|
+
.join(".failproofai")
|
|
45
|
+
.join("cloud.json"),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
impl CloudClient {
|
|
50
|
+
/// Environment first, then the credential file.
|
|
51
|
+
///
|
|
52
|
+
/// Env wins so CI, containers and tests keep working unchanged, and so an
|
|
53
|
+
/// operator who prefers env-only configuration loses nothing.
|
|
54
|
+
pub fn from_env_or_file() -> Result<Option<Self>, String> {
|
|
55
|
+
if let Some(client) = Self::from_env()? {
|
|
56
|
+
return Ok(Some(client));
|
|
57
|
+
}
|
|
58
|
+
Self::from_file()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub fn from_env() -> Result<Option<Self>, String> {
|
|
62
|
+
let Some(base_url) = env_value("FAILPROOFAI_CLOUD_URL") else {
|
|
63
|
+
return Ok(None);
|
|
64
|
+
};
|
|
65
|
+
let token = env_value("FAILPROOFAI_CLOUD_TOKEN")
|
|
66
|
+
.ok_or("FAILPROOFAI_CLOUD_TOKEN is required when FAILPROOFAI_CLOUD_URL is set")?;
|
|
67
|
+
let machine_id = env_value("FAILPROOFAI_MACHINE_ID")
|
|
68
|
+
.ok_or("FAILPROOFAI_MACHINE_ID is required when FAILPROOFAI_CLOUD_URL is set")?;
|
|
69
|
+
Self::new(&base_url, token, machine_id).map(Some)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// A missing file means "not enrolled" — not an error. A malformed one IS
|
|
73
|
+
/// an error: it was written by us, so bad content means something is wrong
|
|
74
|
+
/// that the operator should see rather than a silently unenrolled machine.
|
|
75
|
+
pub fn from_file() -> Result<Option<Self>, String> {
|
|
76
|
+
let Some(path) = credentials_path() else {
|
|
77
|
+
return Ok(None);
|
|
78
|
+
};
|
|
79
|
+
let bytes = match std::fs::read(&path) {
|
|
80
|
+
Ok(bytes) => bytes,
|
|
81
|
+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
|
82
|
+
Err(err) => return Err(format!("failed to read {}: {err}", path.display())),
|
|
83
|
+
};
|
|
84
|
+
let stored: StoredCredentials = serde_json::from_slice(&bytes)
|
|
85
|
+
.map_err(|err| format!("invalid credentials in {}: {err}", path.display()))?;
|
|
86
|
+
if stored.schema_version != 1 {
|
|
87
|
+
return Err(format!(
|
|
88
|
+
"unsupported credentials schema {} in {}",
|
|
89
|
+
stored.schema_version,
|
|
90
|
+
path.display()
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
if stored.token.is_empty() {
|
|
94
|
+
return Err(format!("empty token in {}", path.display()));
|
|
95
|
+
}
|
|
96
|
+
Self::new(&stored.url, stored.token, stored.machine_id).map(Some)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fn new(base_url: &str, token: String, machine_id: String) -> Result<Self, String> {
|
|
100
|
+
let mut base_url =
|
|
101
|
+
Url::parse(base_url).map_err(|err| format!("invalid FAILPROOFAI_CLOUD_URL: {err}"))?;
|
|
102
|
+
if !matches!(base_url.scheme(), "http" | "https") {
|
|
103
|
+
return Err("FAILPROOFAI_CLOUD_URL must use http or https".to_string());
|
|
104
|
+
}
|
|
105
|
+
if !base_url.path().ends_with('/') {
|
|
106
|
+
base_url.set_path(&format!("{}/", base_url.path()));
|
|
107
|
+
}
|
|
108
|
+
if machine_id.is_empty() {
|
|
109
|
+
return Err("FAILPROOFAI_MACHINE_ID cannot be empty".to_string());
|
|
110
|
+
}
|
|
111
|
+
let client = Client::builder()
|
|
112
|
+
.connect_timeout(Duration::from_secs(5))
|
|
113
|
+
.timeout(Duration::from_secs(15))
|
|
114
|
+
.build()
|
|
115
|
+
.map_err(|err| format!("failed to build cloud HTTP client: {err}"))?;
|
|
116
|
+
Ok(Self {
|
|
117
|
+
base_url,
|
|
118
|
+
token,
|
|
119
|
+
machine_id,
|
|
120
|
+
client,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pub fn desired_state(&self) -> Result<DesiredState, String> {
|
|
125
|
+
let mut url = self
|
|
126
|
+
.base_url
|
|
127
|
+
.join("enforcement/v1/desired-state")
|
|
128
|
+
.map_err(|err| format!("failed to build desired-state URL: {err}"))?;
|
|
129
|
+
url.query_pairs_mut()
|
|
130
|
+
.append_pair("machineId", &self.machine_id);
|
|
131
|
+
self.client
|
|
132
|
+
.get(url)
|
|
133
|
+
.bearer_auth(&self.token)
|
|
134
|
+
.send()
|
|
135
|
+
.and_then(|response| response.error_for_status())
|
|
136
|
+
.map_err(|err| format!("desired-state request failed: {err}"))?
|
|
137
|
+
.json()
|
|
138
|
+
.map_err(|err| format!("invalid desired-state response: {err}"))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn artifact(&self, policy: &DesiredPolicy) -> Result<Vec<u8>, String> {
|
|
142
|
+
let url = self
|
|
143
|
+
.base_url
|
|
144
|
+
.join(&policy.artifact_url)
|
|
145
|
+
.map_err(|err| format!("invalid artifact URL: {err}"))?;
|
|
146
|
+
if url.origin() != self.base_url.origin() {
|
|
147
|
+
return Err("artifact URL points outside the configured cloud origin".to_string());
|
|
148
|
+
}
|
|
149
|
+
self.client
|
|
150
|
+
.get(url)
|
|
151
|
+
.bearer_auth(&self.token)
|
|
152
|
+
.send()
|
|
153
|
+
.and_then(|response| response.error_for_status())
|
|
154
|
+
.map_err(|err| format!("artifact request failed: {err}"))?
|
|
155
|
+
.bytes()
|
|
156
|
+
.map(|bytes| bytes.to_vec())
|
|
157
|
+
.map_err(|err| format!("failed to read artifact response: {err}"))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// One maintenance lane that re-resolves enrolment on every tick.
|
|
162
|
+
///
|
|
163
|
+
/// Enrolment is deliberately NOT read once at startup. `failproofai config
|
|
164
|
+
/// --connect` writes a credential file without root, and the service is a
|
|
165
|
+
/// SYSTEM unit — so requiring a restart to notice it would put `sudo systemctl
|
|
166
|
+
/// restart` back into the flow and undo the reason the credential lives in a
|
|
167
|
+
/// file at all. Re-resolving per tick also makes token rotation and
|
|
168
|
+
/// `--disconnect` take effect within one interval, with nothing to restart.
|
|
169
|
+
///
|
|
170
|
+
/// Resolution failures degrade to integrity-only rather than killing the lane:
|
|
171
|
+
/// a machine that was pulling policy keeps its last known-good generation and
|
|
172
|
+
/// keeps repairing tampering while its credentials are broken.
|
|
173
|
+
///
|
|
174
|
+
/// Two intervals, chosen per tick, so both documented knobs keep their meaning
|
|
175
|
+
/// now that one lane serves both cases: `FAILPROOFAI_CLOUD_POLICY_POLL_MS` when
|
|
176
|
+
/// enrolled, `FAILPROOFAI_CLOUD_POLICY_RECONCILE_MS` when not.
|
|
177
|
+
pub fn spawn_maintenance(
|
|
178
|
+
store: PolicyStore,
|
|
179
|
+
shutdown: Arc<AtomicBool>,
|
|
180
|
+
poll_interval: Duration,
|
|
181
|
+
idle_interval: Duration,
|
|
182
|
+
) -> JoinHandle<()> {
|
|
183
|
+
std::thread::spawn(move || {
|
|
184
|
+
let mut last_state: Option<bool> = None;
|
|
185
|
+
while !shutdown.load(Ordering::Relaxed) {
|
|
186
|
+
let cloud = match CloudClient::from_env_or_file() {
|
|
187
|
+
Ok(client) => client,
|
|
188
|
+
Err(err) => {
|
|
189
|
+
eprintln!("[failproofaid] cloud enrolment error: {err}");
|
|
190
|
+
None
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Log only on transition, so a disconnected machine does not print
|
|
195
|
+
// a line every 30 seconds forever.
|
|
196
|
+
let enrolled = cloud.is_some();
|
|
197
|
+
if last_state != Some(enrolled) {
|
|
198
|
+
eprintln!(
|
|
199
|
+
"[failproofaid] cloud-managed policy polling {}",
|
|
200
|
+
if enrolled { "enabled" } else { "disabled" }
|
|
201
|
+
);
|
|
202
|
+
last_state = Some(enrolled);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if let Some(cloud) = cloud.as_ref() {
|
|
206
|
+
poll_once(&store, cloud);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Runs whether or not cloud is reachable: poll failures never
|
|
210
|
+
// discard the last known-good generation, and local tampering is
|
|
211
|
+
// still repaired while the cloud is offline or unconfigured.
|
|
212
|
+
if let Err(err) = store.repair_active_from_cache() {
|
|
213
|
+
eprintln!("[failproofaid] cloud policy integrity error: {err}");
|
|
214
|
+
}
|
|
215
|
+
wait_until_shutdown(
|
|
216
|
+
&shutdown,
|
|
217
|
+
if enrolled {
|
|
218
|
+
poll_interval
|
|
219
|
+
} else {
|
|
220
|
+
idle_interval
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fn poll_once(store: &PolicyStore, cloud: &CloudClient) {
|
|
228
|
+
match cloud.desired_state() {
|
|
229
|
+
Ok(desired) => {
|
|
230
|
+
match store.reconcile(&desired, &|policy: &DesiredPolicy| cloud.artifact(policy)) {
|
|
231
|
+
Ok(outcome)
|
|
232
|
+
if outcome.activated || outcome.downloaded > 0 || outcome.repaired > 0 =>
|
|
233
|
+
{
|
|
234
|
+
eprintln!(
|
|
235
|
+
"[failproofaid] cloud policy generation {} active (downloaded {}, repaired {})",
|
|
236
|
+
outcome.generation, outcome.downloaded, outcome.repaired
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
Ok(_) => {}
|
|
240
|
+
Err(err) => eprintln!("[failproofaid] cloud policy reconcile error: {err}"),
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
Err(err) => eprintln!("[failproofaid] cloud policy poll error: {err}"),
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
pub fn poll_interval_from_env() -> Duration {
|
|
248
|
+
let milliseconds = env_value("FAILPROOFAI_CLOUD_POLICY_POLL_MS")
|
|
249
|
+
.and_then(|value| value.parse::<u64>().ok())
|
|
250
|
+
.unwrap_or(DEFAULT_POLL_MS)
|
|
251
|
+
.max(MINIMUM_POLL_MS);
|
|
252
|
+
Duration::from_millis(milliseconds)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fn env_value(name: &str) -> Option<String> {
|
|
256
|
+
std::env::var(name)
|
|
257
|
+
.ok()
|
|
258
|
+
.map(|value| value.trim().to_string())
|
|
259
|
+
.filter(|value| !value.is_empty())
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
fn wait_until_shutdown(shutdown: &AtomicBool, interval: Duration) {
|
|
263
|
+
let deadline = Instant::now() + interval;
|
|
264
|
+
while !shutdown.load(Ordering::Relaxed) && Instant::now() < deadline {
|
|
265
|
+
let remaining = deadline.saturating_duration_since(Instant::now());
|
|
266
|
+
std::thread::sleep(remaining.min(Duration::from_millis(50)));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#[cfg(test)]
|
|
271
|
+
mod tests {
|
|
272
|
+
use super::*;
|
|
273
|
+
// Only the fixtures construct an effect explicitly.
|
|
274
|
+
use crate::cloud_policies::PolicyEffect;
|
|
275
|
+
|
|
276
|
+
// std::env::set_var is process-global, so these must not interleave with
|
|
277
|
+
// each other or with anything else reading the same variables.
|
|
278
|
+
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
279
|
+
|
|
280
|
+
/// Clears the vars it set and removes its scratch directory. Kept as a
|
|
281
|
+
/// guard so a failing assertion cannot leak process-global env into the
|
|
282
|
+
/// next test.
|
|
283
|
+
struct EnvGuard(std::path::PathBuf);
|
|
284
|
+
impl Drop for EnvGuard {
|
|
285
|
+
fn drop(&mut self) {
|
|
286
|
+
unsafe {
|
|
287
|
+
std::env::remove_var("FAILPROOFAI_CLOUD_CREDENTIALS");
|
|
288
|
+
std::env::remove_var("FAILPROOFAI_CLOUD_URL");
|
|
289
|
+
std::env::remove_var("FAILPROOFAI_CLOUD_TOKEN");
|
|
290
|
+
std::env::remove_var("FAILPROOFAI_MACHINE_ID");
|
|
291
|
+
}
|
|
292
|
+
let _ = fs::remove_dir_all(&self.0);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
static SCRATCH_SEQ: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
|
|
297
|
+
|
|
298
|
+
fn with_credentials_file(contents: Option<&str>) -> EnvGuard {
|
|
299
|
+
let seq = SCRATCH_SEQ.fetch_add(1, Ordering::Relaxed);
|
|
300
|
+
let dir =
|
|
301
|
+
std::env::temp_dir().join(format!("failproofaid-creds-{}-{seq}", std::process::id()));
|
|
302
|
+
fs::create_dir_all(&dir).unwrap();
|
|
303
|
+
let path = dir.join("cloud.json");
|
|
304
|
+
if let Some(contents) = contents {
|
|
305
|
+
fs::write(&path, contents).unwrap();
|
|
306
|
+
}
|
|
307
|
+
unsafe {
|
|
308
|
+
std::env::set_var("FAILPROOFAI_CLOUD_CREDENTIALS", &path);
|
|
309
|
+
std::env::remove_var("FAILPROOFAI_CLOUD_URL");
|
|
310
|
+
std::env::remove_var("FAILPROOFAI_CLOUD_TOKEN");
|
|
311
|
+
std::env::remove_var("FAILPROOFAI_MACHINE_ID");
|
|
312
|
+
}
|
|
313
|
+
EnvGuard(dir)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const GOOD: &str =
|
|
317
|
+
r#"{"schemaVersion":1,"url":"https://cloud.example","machineId":"m-1","token":"secret"}"#;
|
|
318
|
+
|
|
319
|
+
#[test]
|
|
320
|
+
fn no_credentials_file_means_not_enrolled_rather_than_an_error() {
|
|
321
|
+
let _lock = ENV_LOCK.lock().unwrap();
|
|
322
|
+
let _guard = with_credentials_file(None);
|
|
323
|
+
assert!(CloudClient::from_file().unwrap().is_none());
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#[test]
|
|
327
|
+
fn reads_a_valid_credentials_file() {
|
|
328
|
+
let _lock = ENV_LOCK.lock().unwrap();
|
|
329
|
+
let _guard = with_credentials_file(Some(GOOD));
|
|
330
|
+
let client = CloudClient::from_file().unwrap().expect("enrolled");
|
|
331
|
+
assert_eq!(client.machine_id, "m-1");
|
|
332
|
+
assert_eq!(client.token, "secret");
|
|
333
|
+
assert_eq!(client.base_url.host_str(), Some("cloud.example"));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#[test]
|
|
337
|
+
fn a_malformed_credentials_file_is_an_error_not_a_silent_disconnect() {
|
|
338
|
+
// We wrote this file. Bad content means something is wrong that the
|
|
339
|
+
// operator needs to see — reporting "not enrolled" would leave a
|
|
340
|
+
// machine quietly unmanaged while looking healthy.
|
|
341
|
+
let _lock = ENV_LOCK.lock().unwrap();
|
|
342
|
+
let _guard = with_credentials_file(Some("{ not json"));
|
|
343
|
+
let err = CloudClient::from_file()
|
|
344
|
+
.err()
|
|
345
|
+
.expect("malformed file must error");
|
|
346
|
+
assert!(err.contains("invalid credentials"), "{err}");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#[test]
|
|
350
|
+
fn rejects_an_unknown_schema_version() {
|
|
351
|
+
let _lock = ENV_LOCK.lock().unwrap();
|
|
352
|
+
let _guard = with_credentials_file(Some(
|
|
353
|
+
r#"{"schemaVersion":99,"url":"https://c.example","machineId":"m","token":"t"}"#,
|
|
354
|
+
));
|
|
355
|
+
let err = CloudClient::from_file()
|
|
356
|
+
.err()
|
|
357
|
+
.expect("unknown schema must error");
|
|
358
|
+
assert!(err.contains("unsupported credentials schema"), "{err}");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#[test]
|
|
362
|
+
fn rejects_an_empty_token_and_an_empty_machine_id() {
|
|
363
|
+
let _lock = ENV_LOCK.lock().unwrap();
|
|
364
|
+
{
|
|
365
|
+
let _guard = with_credentials_file(Some(
|
|
366
|
+
r#"{"schemaVersion":1,"url":"https://c.example","machineId":"m","token":""}"#,
|
|
367
|
+
));
|
|
368
|
+
let err = CloudClient::from_file()
|
|
369
|
+
.err()
|
|
370
|
+
.expect("empty token must error");
|
|
371
|
+
assert!(err.contains("empty token"), "{err}");
|
|
372
|
+
}
|
|
373
|
+
let _guard = with_credentials_file(Some(
|
|
374
|
+
r#"{"schemaVersion":1,"url":"https://c.example","machineId":"","token":"t"}"#,
|
|
375
|
+
));
|
|
376
|
+
assert!(CloudClient::from_file().is_err());
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#[test]
|
|
380
|
+
fn environment_wins_over_the_credentials_file() {
|
|
381
|
+
// CI, containers and the existing tests configure by env; enrolment
|
|
382
|
+
// must not silently override them.
|
|
383
|
+
let _lock = ENV_LOCK.lock().unwrap();
|
|
384
|
+
let _guard = with_credentials_file(Some(GOOD));
|
|
385
|
+
unsafe {
|
|
386
|
+
std::env::set_var("FAILPROOFAI_CLOUD_URL", "https://env.example");
|
|
387
|
+
std::env::set_var("FAILPROOFAI_CLOUD_TOKEN", "env-token");
|
|
388
|
+
std::env::set_var("FAILPROOFAI_MACHINE_ID", "env-machine");
|
|
389
|
+
}
|
|
390
|
+
let client = CloudClient::from_env_or_file().unwrap().expect("enrolled");
|
|
391
|
+
assert_eq!(client.machine_id, "env-machine");
|
|
392
|
+
assert_eq!(client.token, "env-token");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
#[test]
|
|
396
|
+
fn falls_back_to_the_file_when_only_some_env_vars_are_set() {
|
|
397
|
+
// FAILPROOFAI_CLOUD_URL is the switch: without it, from_env returns
|
|
398
|
+
// None and the file is consulted rather than erroring.
|
|
399
|
+
let _lock = ENV_LOCK.lock().unwrap();
|
|
400
|
+
let _guard = with_credentials_file(Some(GOOD));
|
|
401
|
+
unsafe {
|
|
402
|
+
std::env::set_var("FAILPROOFAI_CLOUD_TOKEN", "stray");
|
|
403
|
+
}
|
|
404
|
+
let client = CloudClient::from_env_or_file().unwrap().expect("enrolled");
|
|
405
|
+
assert_eq!(client.machine_id, "m-1");
|
|
406
|
+
}
|
|
407
|
+
use sha2::{Digest, Sha256};
|
|
408
|
+
use std::fs;
|
|
409
|
+
use std::io::{Read, Write};
|
|
410
|
+
use std::net::TcpListener;
|
|
411
|
+
|
|
412
|
+
#[test]
|
|
413
|
+
fn fetches_desired_state_and_artifact_into_the_store() {
|
|
414
|
+
let artifact = b"export default 'managed';\n".to_vec();
|
|
415
|
+
let sha = format!("{:x}", Sha256::digest(&artifact));
|
|
416
|
+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
|
417
|
+
let address = listener.local_addr().unwrap();
|
|
418
|
+
let expected_sha = sha.clone();
|
|
419
|
+
let expected_artifact = artifact.clone();
|
|
420
|
+
let server = std::thread::spawn(move || {
|
|
421
|
+
for _ in 0..2 {
|
|
422
|
+
let (mut stream, _) = listener.accept().unwrap();
|
|
423
|
+
let mut request = [0_u8; 4096];
|
|
424
|
+
let read = stream.read(&mut request).unwrap();
|
|
425
|
+
let request = String::from_utf8_lossy(&request[..read]);
|
|
426
|
+
assert!(
|
|
427
|
+
request.contains("Authorization: Bearer test-token")
|
|
428
|
+
|| request.contains("authorization: Bearer test-token")
|
|
429
|
+
);
|
|
430
|
+
let body = if request
|
|
431
|
+
.starts_with("GET /enforcement/v1/desired-state?machineId=machine-1")
|
|
432
|
+
{
|
|
433
|
+
format!(r#"{{"schemaVersion":1,"generation":7,"policies":[{{"id":"guard","revision":2,"sha256":"{expected_sha}","artifactUrl":"/enforcement/v1/artifacts/{expected_sha}"}}]}}"#).into_bytes()
|
|
434
|
+
} else {
|
|
435
|
+
expected_artifact.clone()
|
|
436
|
+
};
|
|
437
|
+
let content_type = if request.contains("desired-state") {
|
|
438
|
+
"application/json"
|
|
439
|
+
} else {
|
|
440
|
+
"text/javascript"
|
|
441
|
+
};
|
|
442
|
+
write!(stream, "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: {}\r\nConnection: close\r\n\r\n", body.len(), content_type).unwrap();
|
|
443
|
+
stream.write_all(&body).unwrap();
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
let cloud = CloudClient::new(
|
|
448
|
+
&format!("http://{address}"),
|
|
449
|
+
"test-token".into(),
|
|
450
|
+
"machine-1".into(),
|
|
451
|
+
)
|
|
452
|
+
.unwrap();
|
|
453
|
+
let desired = cloud.desired_state().unwrap();
|
|
454
|
+
let root =
|
|
455
|
+
std::env::temp_dir().join(format!("failproofaid-http-test-{}", std::process::id()));
|
|
456
|
+
let _ = fs::remove_dir_all(&root);
|
|
457
|
+
let store = PolicyStore::new(root.clone());
|
|
458
|
+
let outcome = store
|
|
459
|
+
.reconcile(&desired, &|policy: &DesiredPolicy| cloud.artifact(policy))
|
|
460
|
+
.unwrap();
|
|
461
|
+
assert_eq!(outcome.generation, 7);
|
|
462
|
+
assert_eq!(outcome.downloaded, 1);
|
|
463
|
+
assert_eq!(
|
|
464
|
+
fs::read(root.join("generations/7/guard.mjs")).unwrap(),
|
|
465
|
+
artifact
|
|
466
|
+
);
|
|
467
|
+
server.join().unwrap();
|
|
468
|
+
fs::remove_dir_all(root).unwrap();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#[test]
|
|
472
|
+
fn rejects_cross_origin_artifacts_before_sending_the_token() {
|
|
473
|
+
let cloud =
|
|
474
|
+
CloudClient::new("https://cloud.example", "secret".into(), "machine".into()).unwrap();
|
|
475
|
+
let policy = DesiredPolicy {
|
|
476
|
+
id: "guard".into(),
|
|
477
|
+
revision: 1,
|
|
478
|
+
sha256: "0".repeat(64),
|
|
479
|
+
artifact_url: "https://evil.example/artifact".into(),
|
|
480
|
+
effect: PolicyEffect::Enforce,
|
|
481
|
+
};
|
|
482
|
+
assert!(cloud.artifact(&policy).unwrap_err().contains("outside"));
|
|
483
|
+
}
|
|
484
|
+
}
|