failproofai 1.0.4 → 1.0.5-beta.1

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 (168) 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 +4 -4
  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 +6 -6
  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/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  32. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  34. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  35. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/index.html +1 -1
  38. package/.next/standalone/.next/server/app/index.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  40. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  41. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  42. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  43. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  44. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  47. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  48. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  51. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  58. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  62. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  63. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +3 -3
  66. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  67. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  69. package/.next/standalone/.next/server/chunks/{[externals]__0-9ti7z._.js → [externals]__1_bftcl._.js} +1 -1
  70. package/.next/standalone/.next/server/chunks/{[externals]__0u3wrvq._.js → [externals]__1msfs-h._.js} +1 -1
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0faqz9x._.js +22 -0
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zyc_nm._.js +3 -0
  75. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p-qi2t._.js → [root-of-the-server]__112b4go._.js} +22 -3
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1l71g3y._.js +22 -0
  77. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/{_1-akg6w._.js → _1c3k-8x._.js} +2 -2
  80. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/lib_openclaw-projects_ts_0gc-p24._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00y9ww2._.js +22 -0
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  87. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09flfsx._.js → [root-of-the-server]__05g1k7_._.js} +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0sz_5na._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kaotix._.js +22 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u6sn69._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/{_02yoeej._.js → _0iv28v5._.js} +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/{_1if9ixi._.js → _14c6q09._.js} +21 -2
  101. package/.next/standalone/.next/server/chunks/ssr/{_04rbnaq._.js → _17noq1a._.js} +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/lib_codex-projects_ts_0pqlw37._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/lib_openclaw-projects_ts_0qxq3bp._.js +1 -1
  111. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  112. package/.next/standalone/.next/server/pages/404.html +1 -1
  113. package/.next/standalone/.next/server/pages/500.html +1 -1
  114. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  115. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  116. package/.next/standalone/.next/static/chunks/{3tjji451hrgc0.js → 074xcclgow3yh.js} +1 -1
  117. package/.next/standalone/.next/static/chunks/{31u94yukw9fz-.js → 0qytob94jbzs6.js} +1 -1
  118. package/.next/standalone/.next/static/chunks/25cn9xpv4r1ow.js +1 -0
  119. package/.next/standalone/.next/static/chunks/{1j6o9min_6qa9.js → 26tf_lpco5n-v.js} +1 -1
  120. package/.next/standalone/.next/static/chunks/{0-tyrpwuc3lla.js → 2b014kj28p0vz.js} +1 -1
  121. package/.next/standalone/.next/static/chunks/{2lwipmv5etc8m.js → 3_fnbc-v5r-9e.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/{1c6aptmytnu6q.js → 3kltzmx9k_t6x.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/{27cl8h-c2w7cd.js → 3pmi7qhtmniku.js} +1 -1
  124. package/.next/standalone/.next/static/chunks/{2u8vh73tpw28_.js → 3q1rji38vuvr4.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/{0ca7octhod0g2.js → 3qxl_gibq3bas.js} +1 -1
  126. package/.next/standalone/.next/static/chunks/{3p9_8x5ezce-9.js → 3vjaz4hld9oo8.js} +1 -1
  127. package/.next/standalone/.next/static/chunks/{2zafgs90r_leq.js → 41ejnuj2d9g7_.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{turbopack-3p73obe0xmrxm.js → turbopack-1xgsp63yhrdu5.js} +1 -1
  129. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +7 -0
  130. package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -1
  131. package/.next/standalone/lib/download-session.ts +58 -13
  132. package/.next/standalone/lib/openclaw-db.ts +271 -0
  133. package/.next/standalone/lib/openclaw-projects.ts +54 -18
  134. package/.next/standalone/lib/openclaw-sessions.ts +75 -17
  135. package/.next/standalone/package.json +9 -9
  136. package/.next/standalone/sdk/python/CHANGELOG.md +38 -8
  137. package/.next/standalone/sdk/python/failproofai_sdk/_events.py +20 -0
  138. package/.next/standalone/sdk/python/failproofai_sdk/_redact.py +233 -0
  139. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  140. package/.next/standalone/sdk/python/failproofai_sdk/_writer.py +4 -1
  141. package/.next/standalone/sdk/python/failproofai_sdk/integrations/llama_index.py +5 -5
  142. package/.next/standalone/sdk/python/tests/integrations/test_llama_index.py +2 -2
  143. package/.next/standalone/sdk/python/tests/test_redaction.py +140 -0
  144. package/.next/standalone/sdk/python/tests/test_sdk.py +87 -0
  145. package/.next/standalone/sdk/python/tests/test_server_contract.py +60 -0
  146. package/.next/standalone/sdk/python/tests/test_site_docs.py +2 -2
  147. package/.next/standalone/server.js +1 -1
  148. package/dist/cli.mjs +466 -255
  149. package/dist/worker.mjs +301 -90
  150. package/lib/download-session.ts +58 -13
  151. package/lib/openclaw-db.ts +271 -0
  152. package/lib/openclaw-projects.ts +54 -18
  153. package/lib/openclaw-sessions.ts +75 -17
  154. package/openclaw-plugin/index.js +10 -6
  155. package/openclaw-plugin/instruct-retry-gate.js +89 -0
  156. package/package.json +9 -9
  157. package/src/hooks/policy-evaluator.ts +22 -3
  158. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +0 -3
  159. package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +0 -3
  160. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +0 -3
  161. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00-s7h8._.js +0 -3
  162. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__190won8._.js +0 -3
  163. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +0 -3
  164. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1sc84bb._.js +0 -3
  165. package/.next/standalone/.next/static/chunks/2pd16tn5qmx13.js +0 -1
  166. /package/.next/standalone/.next/static/{dg8VQ7Ejlo0Qsh_x92oIt → Qexks15mjEoPt6cN3D2jb}/_buildManifest.js +0 -0
  167. /package/.next/standalone/.next/static/{dg8VQ7Ejlo0Qsh_x92oIt → Qexks15mjEoPt6cN3D2jb}/_clientMiddlewareManifest.js +0 -0
  168. /package/.next/standalone/.next/static/{dg8VQ7Ejlo0Qsh_x92oIt → Qexks15mjEoPt6cN3D2jb}/_ssgManifest.js +0 -0
@@ -0,0 +1,140 @@
1
+ import json
2
+
3
+ import pytest
4
+
5
+ from failproofai_sdk._redact import redact_json_line, redaction_enabled, scrub_string
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ ("raw", "marker"),
10
+ [
11
+ ("sk-ant-api03-AbCdEfGhIjKlMnOpQrStUv", "anthropic-key"),
12
+ ("sk-proj-abcdefghijklmnopqrstuvwxyz", "openai-key"),
13
+ ("ghp_abcdefghijklmnopqrstuvwxyz0123", "github-token"),
14
+ ("AKIAIOSFODNN7EXAMPLE0000", "aws-access-key-id"),
15
+ (
16
+ "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sIgNaTuRe0123456789ab",
17
+ "jwt",
18
+ ),
19
+ ("Authorization: Bearer tok-🔑🔑🔑-SECRETTAIL", "bearer-token"),
20
+ ("Authorization: Bearer 🔑🔑", "bearer-token"),
21
+ ("API_TOKEN=пароль-очень-ENDOFSECRET", "secret-assignment"),
22
+ ("API_TOKEN=密码密码密码", "secret-assignment"),
23
+ ],
24
+ )
25
+ def test_minimal_redaction_matches_daemon_secret_shapes(raw, marker):
26
+ scrubbed, count = scrub_string(raw)
27
+ assert count == 1
28
+ assert scrubbed == f"[redacted:{marker}]" or scrubbed.endswith(f"[redacted:{marker}]")
29
+ assert "SECRETTAIL" not in scrubbed
30
+ assert "ENDOFSECRET" not in scrubbed
31
+
32
+
33
+ @pytest.mark.parametrize(
34
+ "value",
35
+ [
36
+ "this is a risk-averse approach",
37
+ "AWS_REGION=us-east-1",
38
+ "key=someLongIdentifier",
39
+ "api_key=$OPENAI_API_KEY",
40
+ "--token=<your-token-here>",
41
+ ],
42
+ )
43
+ def test_minimal_redaction_leaves_known_false_positives_alone(value):
44
+ assert scrub_string(value) == (value, 0)
45
+
46
+
47
+ def test_redaction_preserves_json_structure_and_is_deterministic():
48
+ encoded = json.dumps(
49
+ {
50
+ "type": "tool_use",
51
+ "input": {"command": 'API_KEY="abcdefghijklmnop" && echo done'},
52
+ "nested": [{"output": "ghp_abcdefghijklmnopqrstuvwxyz0123"}],
53
+ }
54
+ )
55
+ first = redact_json_line(encoded)
56
+ second = redact_json_line(encoded)
57
+ assert first == second
58
+ event = json.loads(first)
59
+ assert event["type"] == "tool_use"
60
+ assert event["input"]["command"] == 'API_KEY="[redacted:secret-assignment]" && echo done'
61
+ assert event["nested"][0]["output"] == "[redacted:github-token]"
62
+
63
+
64
+ @pytest.mark.parametrize(
65
+ "field",
66
+ ["password", "client_secret", "api_key", "access_token", "apiKey", "accessToken"],
67
+ )
68
+ def test_secret_named_fields_redact_opaque_values(field):
69
+ encoded = json.dumps({"nested": {field: "abcdefghijklmnop"}})
70
+ event = json.loads(redact_json_line(encoded))
71
+ assert event["nested"][field] == "[redacted:secret-assignment]"
72
+
73
+
74
+ @pytest.mark.parametrize(
75
+ "field",
76
+ ["password", "client_secret", "api_key", "access_token", "apiKey", "accessToken"],
77
+ )
78
+ def test_secret_named_arrays_redact_opaque_elements(field):
79
+ encoded = json.dumps(
80
+ {"nested": {field: ["abcdefghijklmnop", ["qrstuvwxyzabcdef"], "short", 7, None]}}
81
+ )
82
+ event = json.loads(redact_json_line(encoded))
83
+ assert event["nested"][field] == [
84
+ "[redacted:secret-assignment]",
85
+ ["[redacted:secret-assignment]"],
86
+ "short",
87
+ 7,
88
+ None,
89
+ ]
90
+
91
+
92
+ def test_arrays_under_ordinary_fields_are_left_alone():
93
+ encoded = json.dumps({"messages": ["an ordinary sentence of text", ["another long value"]]})
94
+ assert redact_json_line(encoded) == encoded
95
+
96
+
97
+ def test_credential_shaped_dictionary_keys_are_redacted_without_colliding():
98
+ first = "API_KEY=abcdefghijklmnop"
99
+ second = "API_KEY=qrstuvwxyzabcdef"
100
+ event = json.loads(redact_json_line(json.dumps({"nested": {first: 1, second: 2}})))
101
+ keys = list(event["nested"])
102
+ assert first not in keys
103
+ assert second not in keys
104
+ assert len(keys) == 2
105
+ assert sorted(event["nested"].values()) == [1, 2]
106
+
107
+
108
+ @pytest.mark.parametrize(
109
+ ("raw", "expected"),
110
+ [
111
+ (
112
+ "PASSWORD='abcdefghijkL\"mnopQRST'",
113
+ "PASSWORD='[redacted:secret-assignment]'",
114
+ ),
115
+ (
116
+ 'PASSWORD="abcdefghijkL\'mnopQRST"',
117
+ 'PASSWORD="[redacted:secret-assignment]"',
118
+ ),
119
+ ],
120
+ )
121
+ def test_quoted_assignment_stops_only_at_its_matching_quote(raw, expected):
122
+ assert scrub_string(raw) == (expected, 1)
123
+
124
+
125
+ def test_plain_string_scan_does_not_copy_every_remaining_suffix():
126
+ class NoTailSlices(str):
127
+ def __getitem__(self, item):
128
+ if isinstance(item, slice) and item.stop is None and (item.start or 0) > 0:
129
+ raise AssertionError(f"copied the remaining tail at {item.start}")
130
+ return super().__getitem__(item)
131
+
132
+ value = NoTailSlices("ordinary payload text " * 100)
133
+ assert scrub_string(value) == (value, 0)
134
+
135
+
136
+ @pytest.mark.parametrize("config", [None, [], {"collector": None}, {"collector": "minimal"}])
137
+ def test_malformed_redaction_config_fails_closed(tmp_path, config):
138
+ (tmp_path / "config.json").write_text(json.dumps(config), encoding="utf-8")
139
+
140
+ assert redaction_enabled(tmp_path / "custom-agents") is True
@@ -622,6 +622,93 @@ def test_writer_multiple_events_in_one_file(tmp_path):
622
622
  resolver.set_base_dir(original)
623
623
 
624
624
 
625
+ def test_writer_redacts_credentials_before_the_spool_reaches_disk(tmp_path):
626
+ import failproofai_sdk._resolver as resolver
627
+ original = resolver._base_dir
628
+ try:
629
+ resolver.set_base_dir(tmp_path)
630
+ writer = EventWriter(flush_interval=60)
631
+ writer.submit(
632
+ {
633
+ "timestamp": "t",
634
+ "session_id": "s1",
635
+ "agent_id": "a1",
636
+ "type": "tool_use",
637
+ "input": {
638
+ "command": "API_KEY=abcdefghijklmnop",
639
+ "password": "qrstuvwxyzabcdef",
640
+ "API_KEY=secretvalue123456": True,
641
+ },
642
+ }
643
+ )
644
+ writer.flush_now()
645
+
646
+ raw = next((tmp_path / "events").glob("*.jsonl")).read_text()
647
+ assert "abcdefghijklmnop" not in raw
648
+ assert "qrstuvwxyzabcdef" not in raw
649
+ assert "API_KEY=secretvalue123456" not in raw
650
+ assert "API_KEY=[redacted:secret-assignment]" in raw
651
+ finally:
652
+ resolver.set_base_dir(original)
653
+
654
+
655
+ def test_writer_redacts_opaque_secrets_in_secret_named_arrays(tmp_path):
656
+ import failproofai_sdk._resolver as resolver
657
+ original = resolver._base_dir
658
+ try:
659
+ resolver.set_base_dir(tmp_path)
660
+ writer = EventWriter(flush_interval=60)
661
+ writer.submit(
662
+ {
663
+ "timestamp": "t",
664
+ "session_id": "s1",
665
+ "agent_id": "a1",
666
+ "type": "tool_use",
667
+ "input": {
668
+ "password": ["ordinarysecretvalue1"],
669
+ "client_secret": ["ordinarysecretvalue2"],
670
+ "api_key": ["ordinarysecretvalue3"],
671
+ "access_token": [["ordinarysecretvalue4"]],
672
+ },
673
+ }
674
+ )
675
+ writer.flush_now()
676
+
677
+ raw = next((tmp_path / "events").glob("*.jsonl")).read_text()
678
+ assert "ordinarysecretvalue" not in raw
679
+ assert raw.count("[redacted:secret-assignment]") == 4
680
+ finally:
681
+ resolver.set_base_dir(original)
682
+
683
+
684
+ def test_writer_honours_collector_redact_off(tmp_path):
685
+ import failproofai_sdk._resolver as resolver
686
+ original = resolver._base_dir
687
+ try:
688
+ base_dir = tmp_path / "custom-agents"
689
+ (tmp_path / "config.json").write_text(
690
+ json.dumps({"collector": {"redact": "off"}}), encoding="utf-8"
691
+ )
692
+ resolver.set_base_dir(base_dir)
693
+ writer = EventWriter(flush_interval=60)
694
+ writer.submit(
695
+ {
696
+ "timestamp": "t",
697
+ "session_id": "s1",
698
+ "agent_id": "a1",
699
+ "type": "tool_use",
700
+ "input": {"command": "API_KEY=abcdefghijklmnop"},
701
+ }
702
+ )
703
+ writer.flush_now()
704
+
705
+ raw = next((base_dir / "events").glob("*.jsonl")).read_text()
706
+ assert "API_KEY=abcdefghijklmnop" in raw
707
+ assert "[redacted:" not in raw
708
+ finally:
709
+ resolver.set_base_dir(original)
710
+
711
+
625
712
  def test_writer_coerces_unserializable_payload_values(tmp_path):
626
713
  import failproofai_sdk._resolver as resolver
627
714
  original = resolver._base_dir
@@ -427,6 +427,66 @@ def test_the_promoted_numeric_set_matches_the_one_ingest_lifts():
427
427
  assert _events._PROMOTED_NUMERIC == PROMOTED_NUMERIC
428
428
 
429
429
 
430
+ # ─────────────────────────────────────────────────────────────────────────────
431
+ # A promoted column the caller does not have costs a warning, not the event
432
+ # ─────────────────────────────────────────────────────────────────────────────
433
+
434
+
435
+ @pytest.mark.parametrize("name", sorted(_events._PROMOTED_STRING))
436
+ def test_a_promoted_string_passed_as_none_is_omitted_not_refused(name):
437
+ """None means "I have no value", and that must not cost the whole event.
438
+
439
+ `agent_end(error_type=None)` is the shape every successful run produces: the
440
+ field is populated only on a failing outcome, so the ordinary success path
441
+ passes None. Refusing it raised inside the caller's emit helper, and a helper
442
+ that swallows telemetry errors — which every one of them does, because
443
+ telemetry must not break a run — turned that into a silently missing event.
444
+ """
445
+ recorder = _Recorder()
446
+ EventNamespace(recorder).agent_start(session_id="s", agent_id="a", **{name: None})
447
+ assert name not in recorder.entries[0]
448
+
449
+
450
+ @pytest.mark.parametrize("name", sorted(_events._PROMOTED_NUMERIC))
451
+ def test_a_promoted_numeric_passed_as_none_in_fields_is_omitted_too(name):
452
+ """The named-parameter path already dropped None; `**fields` did not.
453
+
454
+ `_build` omits None only from a dataclass's own `specifics` — `extra` is
455
+ merged verbatim — so the same value went to the wire as an explicit JSON
456
+ null depending only on which door it came through.
457
+ """
458
+ recorder = _Recorder()
459
+ EventNamespace(recorder).agent_start(session_id="s", agent_id="a", **{name: None})
460
+ assert name not in recorder.entries[0]
461
+
462
+
463
+ def test_a_dropped_promoted_column_is_warned_about(caplog):
464
+ """Silently dropping it would hide a real mistake; refusing it costs the event."""
465
+ recorder = _Recorder()
466
+ with caplog.at_level("WARNING", logger="failproofai_sdk._events"):
467
+ EventNamespace(recorder).agent_end(session_id="s", agent_id="a", error_type=None)
468
+ assert "error_type" in caplog.text
469
+
470
+
471
+ def test_agent_end_on_a_successful_run_is_still_emitted():
472
+ """The regression this all exists for, in the shape the caller actually sends."""
473
+ recorder = _Recorder()
474
+ EventNamespace(recorder).agent_end(
475
+ session_id="s", agent_id="a", outcome="success", summary=None, error_type=None
476
+ )
477
+ assert [e["type"] for e in recorder.entries] == ["agent_end"]
478
+ assert recorder.entries[0]["outcome"] == "success"
479
+ assert "error_type" not in recorder.entries[0]
480
+
481
+
482
+ @pytest.mark.parametrize("name", sorted(_events._PROMOTED_STRING))
483
+ def test_a_promoted_string_with_a_real_value_still_goes_through(name):
484
+ """Dropping None must not also drop the values that matter."""
485
+ recorder = _Recorder()
486
+ EventNamespace(recorder).agent_start(session_id="s", agent_id="a", **{name: "real"})
487
+ assert recorder.entries[0][name] == "real"
488
+
489
+
430
490
  # ─────────────────────────────────────────────────────────────────────────────
431
491
  # The MEASURED duration is bound by the same u32 range a caller is held to
432
492
  # ─────────────────────────────────────────────────────────────────────────────
@@ -315,8 +315,8 @@ def test_no_cross_adapter_page_presents_one_adapters_option_as_universal():
315
315
  and CrewAI has none — and `instrument()` drops options an adapter does not
316
316
  read, so `instrument("crewai", capture_content=False)` raised nothing and
317
317
  changed nothing. A reader on regulated data shipped believing prompts and
318
- completions had stopped being recorded, and `collector.redact` explicitly
319
- does not apply to SDK events, so nothing was behind it.
318
+ completions had stopped being recorded. Minimal credential redaction is not
319
+ a substitute: arbitrary regulated content need not resemble a secret.
320
320
 
321
321
  The four per-framework pages are checked elsewhere; these are the pages that
322
322
  speak about all of them at once and so must name the difference.
@@ -9,7 +9,7 @@ const currentPort = parseInt(process.env.PORT, 10) || 3000
9
9
  const hostname = process.env.HOSTNAME || '0.0.0.0'
10
10
 
11
11
  let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
12
- const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"1.0.4"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"instrumentationClientInject":[],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","enablePrerenderSourceMaps":true,"cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":true,"coldCacheBadge":false,"devValidationWorker":true,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"dynamicOnHover":false,"useOffline":false,"varyParams":true,"optimisticRouting":true,"instrumentationClientRouterTransitionEvents":false,"prefetchInlining":{"maxSize":2048,"maxBundleSize":10240},"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptOperationCache":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"devMemoryThresholdRestart":true,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"requestInsights":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"useTypeScriptCli":true,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":true,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"serverComponentsHmrCancellation":false,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":true,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","turbopackMemoryEvictionMode":"auto","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"useCacheTimeout":54,"instantInsights":{"validationLevel":"warning"},"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["node_modules/@img/**","node_modules/sharp/**"]},"turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"repoRoot":"/home/runner/work/failproofai/failproofai","distDirRoot":".next"}
12
+ const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"1.0.5-beta.1"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"instrumentationClientInject":[],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","enablePrerenderSourceMaps":true,"cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":true,"coldCacheBadge":false,"devValidationWorker":true,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"dynamicOnHover":false,"useOffline":false,"varyParams":true,"optimisticRouting":true,"instrumentationClientRouterTransitionEvents":false,"prefetchInlining":{"maxSize":2048,"maxBundleSize":10240},"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptOperationCache":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"devMemoryThresholdRestart":true,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"requestInsights":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"useTypeScriptCli":true,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":true,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"serverComponentsHmrCancellation":false,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":true,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","turbopackMemoryEvictionMode":"auto","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"useCacheTimeout":54,"instantInsights":{"validationLevel":"warning"},"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["node_modules/@img/**","node_modules/sharp/**"]},"turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"repoRoot":"/home/runner/work/failproofai/failproofai","distDirRoot":".next"}
13
13
 
14
14
  process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
15
15