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.
Files changed (196) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +6 -6
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  17. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  24. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  27. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  37. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  38. package/.next/standalone/.next/server/app/audit/page.js +1 -1
  39. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/index.html +1 -1
  42. package/.next/standalone/.next/server/app/index.rsc +15 -15
  43. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  44. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  45. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  46. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  47. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  48. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  49. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  53. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +30 -18
  54. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  55. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  58. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  67. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  68. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
  75. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +2 -2
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ath6v_._.js → [root-of-the-server]__0ls0gsr._.js} +3 -3
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +2 -2
  81. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__170799-._.js → [root-of-the-server]__0spkm68._.js} +11 -11
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1psjybo._.js → [root-of-the-server]__10s_z4l._.js} +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0114ewg._.js → [root-of-the-server]__1j4u3yd._.js} +3 -3
  85. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0nb0syg._.js → [root-of-the-server]__1ky3im9._.js} +2 -2
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
  87. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  90. 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
  91. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
  93. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  94. package/.next/standalone/.next/server/pages/404.html +1 -1
  95. package/.next/standalone/.next/server/pages/500.html +1 -1
  96. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  97. package/.next/standalone/.next/server/server-reference-manifest.json +32 -20
  98. package/.next/standalone/.next/static/chunks/{3s7myk_co0gbh.js → 01h6lfjz1_oie.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 0a7k_x4rp_97z.js} +1 -1
  101. package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 0cvd6bcdwchc_.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 13-obgukzg_ke.js} +1 -1
  103. package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
  104. package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 1h0_yaoq5f3yx.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 29g8ykjk6sx_8.js} +1 -1
  106. package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 2rqm-klet_gbl.js} +1 -1
  107. package/.next/standalone/.next/static/chunks/{31feojxhjyjpl.js → 2uvyh8y0naofj.js} +1 -1
  108. package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
  109. package/.next/standalone/.next/static/chunks/3p-n_j80zlzi3.js +6 -0
  110. package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
  111. package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
  112. package/.next/standalone/Cargo.lock +1992 -198
  113. package/.next/standalone/Cargo.toml +1 -1
  114. package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
  115. package/.next/standalone/app/components/pause-notices.tsx +111 -0
  116. package/.next/standalone/app/policies/hooks-client.tsx +81 -6
  117. package/.next/standalone/crates/CLOUD_POLICIES.md +136 -0
  118. package/.next/standalone/crates/failproofaid/Cargo.toml +7 -0
  119. package/.next/standalone/crates/failproofaid/src/cloud_client.rs +484 -0
  120. package/.next/standalone/crates/failproofaid/src/cloud_policies.rs +921 -0
  121. package/.next/standalone/crates/failproofaid/src/main.rs +452 -3
  122. package/.next/standalone/crates/failproofaid/src/paths.rs +29 -0
  123. package/.next/standalone/crates/fpai-collect/Cargo.toml +43 -0
  124. package/.next/standalone/crates/fpai-collect/src/config.rs +412 -0
  125. package/.next/standalone/crates/fpai-collect/src/cursor.rs +520 -0
  126. package/.next/standalone/crates/fpai-collect/src/delivery.rs +472 -0
  127. package/.next/standalone/crates/fpai-collect/src/filetail.rs +484 -0
  128. package/.next/standalone/crates/fpai-collect/src/health.rs +369 -0
  129. package/.next/standalone/crates/fpai-collect/src/lib.rs +34 -0
  130. package/.next/standalone/crates/fpai-collect/src/redact.rs +516 -0
  131. package/.next/standalone/crates/fpai-collect/src/sources/claude/mod.rs +279 -0
  132. package/.next/standalone/crates/fpai-collect/src/sources/claude/transform.rs +619 -0
  133. package/.next/standalone/crates/fpai-collect/src/sources/codex/mod.rs +156 -0
  134. package/.next/standalone/crates/fpai-collect/src/sources/codex/transform.rs +715 -0
  135. package/.next/standalone/crates/fpai-collect/src/sources/copilot/mod.rs +158 -0
  136. package/.next/standalone/crates/fpai-collect/src/sources/copilot/transform.rs +512 -0
  137. package/.next/standalone/crates/fpai-collect/src/sources/goose/mod.rs +511 -0
  138. package/.next/standalone/crates/fpai-collect/src/sources/goose/transform.rs +586 -0
  139. package/.next/standalone/crates/fpai-collect/src/sources/hermes/mod.rs +318 -0
  140. package/.next/standalone/crates/fpai-collect/src/sources/hermes/transform.rs +579 -0
  141. package/.next/standalone/crates/fpai-collect/src/sources/hooks/mod.rs +310 -0
  142. package/.next/standalone/crates/fpai-collect/src/sources/hooks/transform.rs +448 -0
  143. package/.next/standalone/crates/fpai-collect/src/sources/mod.rs +23 -0
  144. package/.next/standalone/crates/fpai-collect/src/sources/openclaw/mod.rs +176 -0
  145. package/.next/standalone/crates/fpai-collect/src/sources/openclaw/transform.rs +500 -0
  146. package/.next/standalone/crates/fpai-collect/src/sources/opencode/mod.rs +315 -0
  147. package/.next/standalone/crates/fpai-collect/src/sources/opencode/transform.rs +493 -0
  148. package/.next/standalone/crates/fpai-collect/src/sources/pi/mod.rs +201 -0
  149. package/.next/standalone/crates/fpai-collect/src/sources/pi/transform.rs +533 -0
  150. package/.next/standalone/crates/fpai-collect/src/spool.rs +458 -0
  151. package/.next/standalone/crates/fpai-collect/src/sqlitepoll.rs +387 -0
  152. package/.next/standalone/crates/fpai-collect/src/supervisor.rs +423 -0
  153. package/.next/standalone/crates/fpai-collect/src/uploader.rs +600 -0
  154. package/.next/standalone/crates/fpai-collect/tests/claude_source.rs +738 -0
  155. package/.next/standalone/crates/fpai-collect/tests/claude_subagents.rs +393 -0
  156. package/.next/standalone/crates/fpai-collect/tests/codex_source.rs +975 -0
  157. package/.next/standalone/crates/fpai-collect/tests/config.rs +286 -0
  158. package/.next/standalone/crates/fpai-collect/tests/copilot_source.rs +1019 -0
  159. package/.next/standalone/crates/fpai-collect/tests/delivery.rs +281 -0
  160. package/.next/standalone/crates/fpai-collect/tests/goose_source.rs +1008 -0
  161. package/.next/standalone/crates/fpai-collect/tests/hermes_source.rs +1129 -0
  162. package/.next/standalone/crates/fpai-collect/tests/hooks_source.rs +621 -0
  163. package/.next/standalone/crates/fpai-collect/tests/openclaw_source.rs +854 -0
  164. package/.next/standalone/crates/fpai-collect/tests/opencode_source.rs +1478 -0
  165. package/.next/standalone/crates/fpai-collect/tests/pi_source.rs +1191 -0
  166. package/.next/standalone/crates/fpai-collect/tests/supervisor.rs +290 -0
  167. package/.next/standalone/crates/fpai-collect/tests/uploader.rs +506 -0
  168. package/.next/standalone/package.json +2 -2
  169. package/.next/standalone/server.js +1 -1
  170. package/bin/failproofai.mjs +129 -0
  171. package/dist/cli.mjs +2195 -770
  172. package/dist/worker.mjs +365 -52
  173. package/package.json +2 -2
  174. package/src/hooks/builtin-policies.ts +139 -0
  175. package/src/hooks/cloud-connection.ts +205 -0
  176. package/src/hooks/cloud-enrollment-cli.ts +227 -0
  177. package/src/hooks/cloud-enrollment.ts +173 -0
  178. package/src/hooks/cloud-managed-policies.ts +138 -0
  179. package/src/hooks/collector-config.ts +226 -0
  180. package/src/hooks/configure-wizard.ts +221 -5
  181. package/src/hooks/custom-hooks-loader.ts +69 -25
  182. package/src/hooks/daemon-client.ts +18 -0
  183. package/src/hooks/handler.ts +144 -6
  184. package/src/hooks/hook-activity-store.ts +50 -0
  185. package/src/hooks/loader-utils.ts +6 -4
  186. package/src/hooks/policy-types.ts +35 -0
  187. package/src/hooks/session-pause-cli.ts +157 -0
  188. package/src/hooks/session-pause.ts +236 -0
  189. package/src/hooks/tui.ts +115 -0
  190. package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
  191. package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
  192. package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
  193. package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
  194. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_buildManifest.js +0 -0
  195. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_clientMiddlewareManifest.js +0 -0
  196. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_ssgManifest.js +0 -0
@@ -0,0 +1,286 @@
1
+ //! Configuration contract tests.
2
+ //!
3
+ //! Two of these guard a security property rather than a behaviour: the ingest
4
+ //! credential must never be written into a world-readable file, and the home
5
+ //! directory holding it must not stay world-traversable. `policies-config.json`
6
+ //! is 0664 inside a 0775 `~/.failproofai` on a normal machine, which is exactly
7
+ //! why the credential lives in its own file instead.
8
+
9
+ use std::fs;
10
+ use std::path::PathBuf;
11
+ use std::time::{SystemTime, UNIX_EPOCH};
12
+
13
+ use fpai_collect::config::{self, Ingest};
14
+ use fpai_collect::{DEFAULT_INGEST_URL, HooksVerbosity, Redact};
15
+
16
+ fn tmp_home(name: &str) -> PathBuf {
17
+ let d = std::env::temp_dir().join(format!(
18
+ "fpai-cfg-{}-{}-{}",
19
+ name,
20
+ std::process::id(),
21
+ SystemTime::now()
22
+ .duration_since(UNIX_EPOCH)
23
+ .unwrap()
24
+ .as_nanos()
25
+ ));
26
+ fs::create_dir_all(&d).unwrap();
27
+ d
28
+ }
29
+
30
+ /// The env vars this module reads are process-global, so tests that touch them
31
+ /// must not run concurrently with tests that don't expect them.
32
+ fn without_env_overrides<T>(f: impl FnOnce() -> T) -> T {
33
+ // SAFETY: single-threaded within the closure; see the serial guard below.
34
+ unsafe {
35
+ std::env::remove_var("FAILPROOFAI_INGEST_KEY");
36
+ std::env::remove_var("FAILPROOFAI_INGEST_URL");
37
+ }
38
+ f()
39
+ }
40
+
41
+ #[test]
42
+ fn absent_config_means_collection_is_off() {
43
+ let home = tmp_home("absent");
44
+ let cfg = without_env_overrides(|| config::load(&home).unwrap());
45
+ assert!(cfg.ingest.is_none());
46
+ assert!(
47
+ !cfg.is_enabled(),
48
+ "with no credential the collector must start nothing at all"
49
+ );
50
+ fs::remove_dir_all(&home).ok();
51
+ }
52
+
53
+ #[test]
54
+ fn a_key_alone_does_not_enable_session_collection() {
55
+ // The privacy default, and the one most likely to be eroded by accident.
56
+ // Transcripts carry prompts, file contents and pasted credentials, so
57
+ // configuring ingest must not start shipping them.
58
+ let home = tmp_home("optin");
59
+ config::write_ingest(
60
+ &home,
61
+ &Ingest {
62
+ url: DEFAULT_INGEST_URL.into(),
63
+ key: "k".into(),
64
+ },
65
+ )
66
+ .unwrap();
67
+
68
+ let cfg = without_env_overrides(|| config::load(&home).unwrap());
69
+ assert!(cfg.ingest.is_some());
70
+ assert!(!cfg.settings.sessions, "sessions must be opt-in");
71
+ assert!(
72
+ cfg.settings.hooks,
73
+ "hooks carry no file contents and default on"
74
+ );
75
+ assert!(
76
+ cfg.is_enabled(),
77
+ "hooks alone is enough to run the collector"
78
+ );
79
+ fs::remove_dir_all(&home).ok();
80
+ }
81
+
82
+ #[test]
83
+ fn the_credential_file_is_written_owner_only() {
84
+ #[cfg(unix)]
85
+ {
86
+ use std::os::unix::fs::PermissionsExt;
87
+ let home = tmp_home("mode");
88
+ let path = config::write_ingest(
89
+ &home,
90
+ &Ingest {
91
+ url: DEFAULT_INGEST_URL.into(),
92
+ key: "secret".into(),
93
+ },
94
+ )
95
+ .unwrap();
96
+
97
+ let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
98
+ assert_eq!(
99
+ mode, 0o600,
100
+ "the ingest credential must not be readable by other local users, got {mode:o}"
101
+ );
102
+ fs::remove_dir_all(&home).ok();
103
+ }
104
+ }
105
+
106
+ #[test]
107
+ fn writing_the_credential_tightens_a_world_traversable_home() {
108
+ // A 0600 file inside a 0775 directory is still reachable by every local
109
+ // user. ~/.failproofai really is 0775 on a normal machine.
110
+ #[cfg(unix)]
111
+ {
112
+ use std::os::unix::fs::PermissionsExt;
113
+ let home = tmp_home("tighten");
114
+ fs::set_permissions(&home, fs::Permissions::from_mode(0o775)).unwrap();
115
+
116
+ config::write_ingest(
117
+ &home,
118
+ &Ingest {
119
+ url: DEFAULT_INGEST_URL.into(),
120
+ key: "s".into(),
121
+ },
122
+ )
123
+ .unwrap();
124
+
125
+ let mode = fs::metadata(&home).unwrap().permissions().mode() & 0o777;
126
+ assert_eq!(
127
+ mode, 0o700,
128
+ "the home holding a credential must be owner-only, got {mode:o}"
129
+ );
130
+ fs::remove_dir_all(&home).ok();
131
+ }
132
+ }
133
+
134
+ #[test]
135
+ fn rewriting_an_already_permissive_credential_file_fixes_its_mode() {
136
+ // `OpenOptions::mode()` applies only when the file is CREATED, so an
137
+ // existing 0644 file would silently keep its mode without the explicit
138
+ // set_permissions afterwards.
139
+ #[cfg(unix)]
140
+ {
141
+ use std::os::unix::fs::PermissionsExt;
142
+ let home = tmp_home("rewrite");
143
+ let path = home.join("ingest.json");
144
+ fs::write(&path, "{}").unwrap();
145
+ fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
146
+
147
+ config::write_ingest(
148
+ &home,
149
+ &Ingest {
150
+ url: DEFAULT_INGEST_URL.into(),
151
+ key: "s".into(),
152
+ },
153
+ )
154
+ .unwrap();
155
+
156
+ let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
157
+ assert_eq!(
158
+ mode, 0o600,
159
+ "an existing permissive file must be tightened, got {mode:o}"
160
+ );
161
+ fs::remove_dir_all(&home).ok();
162
+ }
163
+ }
164
+
165
+ #[test]
166
+ fn the_endpoint_defaults_to_the_hosted_one_and_round_trips() {
167
+ let home = tmp_home("url");
168
+ // A file with only a key is the common case — the wizard need not write a
169
+ // url when the user accepts the default.
170
+ fs::write(home.join("ingest.json"), r#"{"key":"abc"}"#).unwrap();
171
+
172
+ let cfg = without_env_overrides(|| config::load(&home).unwrap());
173
+ let ingest = cfg.ingest.unwrap();
174
+ assert_eq!(ingest.url, DEFAULT_INGEST_URL);
175
+ assert_eq!(ingest.key, "abc");
176
+
177
+ // A self-hoster replaces the whole endpoint, path included.
178
+ fs::write(
179
+ home.join("ingest.json"),
180
+ r#"{"key":"abc","url":"http://localhost:8080/events"}"#,
181
+ )
182
+ .unwrap();
183
+ let cfg = without_env_overrides(|| config::load(&home).unwrap());
184
+ assert_eq!(cfg.ingest.unwrap().url, "http://localhost:8080/events");
185
+ fs::remove_dir_all(&home).ok();
186
+ }
187
+
188
+ #[test]
189
+ fn an_empty_key_is_treated_as_unconfigured_rather_than_sent() {
190
+ // Otherwise every request goes out as `Authorization: Bearer ` and the
191
+ // spool fills with 401s that look like a server problem.
192
+ let home = tmp_home("emptykey");
193
+ fs::write(home.join("ingest.json"), r#"{"key":" "}"#).unwrap();
194
+ let cfg = without_env_overrides(|| config::load(&home).unwrap());
195
+ assert!(cfg.ingest.is_none());
196
+ assert!(!cfg.is_enabled());
197
+ fs::remove_dir_all(&home).ok();
198
+ }
199
+
200
+ #[test]
201
+ fn malformed_json_is_an_error_not_a_silent_disable() {
202
+ // Quietly disabling collection because of a stray comma is precisely the
203
+ // silent failure this project exists to remove.
204
+ let home = tmp_home("malformed");
205
+ fs::write(home.join("ingest.json"), r#"{"key": "abc",}"#).unwrap();
206
+ let err = without_env_overrides(|| config::load(&home)).unwrap_err();
207
+ assert!(
208
+ format!("{err}").contains("not valid JSON"),
209
+ "expected a JSON error, got: {err}"
210
+ );
211
+ fs::remove_dir_all(&home).ok();
212
+ }
213
+
214
+ #[test]
215
+ fn settings_come_from_the_policies_config_collector_block() {
216
+ let home = tmp_home("settings");
217
+ fs::write(home.join("ingest.json"), r#"{"key":"abc"}"#).unwrap();
218
+ fs::write(
219
+ home.join("policies-config.json"),
220
+ r#"{"enabledPolicies":["block-sudo"],
221
+ "collector":{"sessions":true,"hooksVerbosity":"all","redact":"off","environment":"ci"}}"#,
222
+ )
223
+ .unwrap();
224
+
225
+ let cfg = without_env_overrides(|| config::load(&home).unwrap());
226
+ assert!(cfg.settings.sessions);
227
+ assert_eq!(cfg.settings.hooks_verbosity, HooksVerbosity::All);
228
+ assert_eq!(cfg.settings.redact, Redact::Off);
229
+ assert_eq!(cfg.settings.environment, "ci");
230
+ fs::remove_dir_all(&home).ok();
231
+ }
232
+
233
+ #[test]
234
+ fn a_policies_config_with_no_collector_block_uses_defaults() {
235
+ // Every existing install is this case; it must not error or change.
236
+ let home = tmp_home("nocollector");
237
+ fs::write(home.join("ingest.json"), r#"{"key":"abc"}"#).unwrap();
238
+ fs::write(
239
+ home.join("policies-config.json"),
240
+ r#"{"enabledPolicies":[]}"#,
241
+ )
242
+ .unwrap();
243
+
244
+ let cfg = without_env_overrides(|| config::load(&home).unwrap());
245
+ assert!(!cfg.settings.sessions);
246
+ assert!(cfg.settings.hooks);
247
+ assert_eq!(cfg.settings.hooks_verbosity, HooksVerbosity::Decisions);
248
+ fs::remove_dir_all(&home).ok();
249
+ }
250
+
251
+ #[test]
252
+ fn a_comma_in_environment_is_rejected_rather_than_dropped_downstream() {
253
+ // The ingest endpoint skips any line whose `environment` contains a comma.
254
+ // Accepting it here would mean every event from this machine vanished
255
+ // server-side with nothing on this end to show for it.
256
+ let home = tmp_home("comma");
257
+ fs::write(home.join("ingest.json"), r#"{"key":"abc"}"#).unwrap();
258
+ fs::write(
259
+ home.join("policies-config.json"),
260
+ r#"{"collector":{"environment":"local,ci"}}"#,
261
+ )
262
+ .unwrap();
263
+
264
+ let err = without_env_overrides(|| config::load(&home)).unwrap_err();
265
+ assert!(format!("{err}").contains("comma"), "got: {err}");
266
+ fs::remove_dir_all(&home).ok();
267
+ }
268
+
269
+ #[test]
270
+ fn the_sdk_spool_directory_is_watched_alongside_our_own() {
271
+ // This is what lets failproofaid supersede agenteye-collector without
272
+ // every Python SDK user reconfiguring anything.
273
+ let home = tmp_home("spooldirs");
274
+ let cfg = without_env_overrides(|| config::load(&home).unwrap());
275
+
276
+ assert_eq!(cfg.spool_dirs[0], cfg.own_spool_dir);
277
+ assert_eq!(cfg.own_spool_dir, home.join("spool"));
278
+ assert!(
279
+ cfg.spool_dirs
280
+ .iter()
281
+ .any(|d| d.ends_with("events") && d.to_string_lossy().contains("agenteye")),
282
+ "the SDK's drop directory must be watched too, got {:?}",
283
+ cfg.spool_dirs
284
+ );
285
+ fs::remove_dir_all(&home).ok();
286
+ }