failproofai 1.0.4 → 1.0.5-beta.0

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 (112) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  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/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  29. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  31. package/.next/standalone/.next/server/app/index.html +1 -1
  32. package/.next/standalone/.next/server/app/index.rsc +14 -14
  33. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  34. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  35. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  36. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  37. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  40. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  53. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  55. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  56. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  57. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  67. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09flfsx._.js → [root-of-the-server]__0x1aj1_._.js} +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/ssr/{_04rbnaq._.js → _0u3xxgc._.js} +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/{_02yoeej._.js → _0zs_4uu._.js} +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  79. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  80. package/.next/standalone/.next/server/pages/404.html +1 -1
  81. package/.next/standalone/.next/server/pages/500.html +1 -1
  82. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  83. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  84. package/.next/standalone/.next/static/chunks/{27cl8h-c2w7cd.js → 0hw6-c206lsma.js} +1 -1
  85. package/.next/standalone/.next/static/chunks/{3p9_8x5ezce-9.js → 0m9sx3khjrl1q.js} +1 -1
  86. package/.next/standalone/.next/static/chunks/{0-tyrpwuc3lla.js → 0oos8qpc1ugos.js} +1 -1
  87. package/.next/standalone/.next/static/chunks/{2u8vh73tpw28_.js → 0unrx9g_x0u7j.js} +1 -1
  88. package/.next/standalone/.next/static/chunks/{3tjji451hrgc0.js → 2qjn-leir0x_s.js} +1 -1
  89. package/.next/standalone/.next/static/chunks/{2lwipmv5etc8m.js → 2xbxkaszi1xnw.js} +1 -1
  90. package/.next/standalone/.next/static/chunks/{1j6o9min_6qa9.js → 33umhy-x204qj.js} +2 -2
  91. package/.next/standalone/.next/static/chunks/{0ca7octhod0g2.js → 3nux7kkrc3eps.js} +1 -1
  92. package/.next/standalone/.next/static/chunks/40tpgs4fjct5p.js +1 -0
  93. package/.next/standalone/.next/static/chunks/{31u94yukw9fz-.js → 40uih96wnkowy.js} +1 -1
  94. package/.next/standalone/package.json +9 -9
  95. package/.next/standalone/sdk/python/CHANGELOG.md +32 -9
  96. package/.next/standalone/sdk/python/failproofai_sdk/_events.py +20 -0
  97. package/.next/standalone/sdk/python/failproofai_sdk/_redact.py +233 -0
  98. package/.next/standalone/sdk/python/failproofai_sdk/_writer.py +4 -1
  99. package/.next/standalone/sdk/python/failproofai_sdk/integrations/llama_index.py +5 -5
  100. package/.next/standalone/sdk/python/tests/integrations/test_llama_index.py +2 -2
  101. package/.next/standalone/sdk/python/tests/test_redaction.py +140 -0
  102. package/.next/standalone/sdk/python/tests/test_sdk.py +87 -0
  103. package/.next/standalone/sdk/python/tests/test_server_contract.py +60 -0
  104. package/.next/standalone/sdk/python/tests/test_site_docs.py +2 -2
  105. package/.next/standalone/server.js +1 -1
  106. package/dist/cli.mjs +10 -10
  107. package/dist/worker.mjs +1 -1
  108. package/package.json +9 -9
  109. package/.next/standalone/.next/static/chunks/2pd16tn5qmx13.js +0 -1
  110. /package/.next/standalone/.next/static/{dg8VQ7Ejlo0Qsh_x92oIt → 4_QeL7mSPYsuiSVJmBOTj}/_buildManifest.js +0 -0
  111. /package/.next/standalone/.next/static/{dg8VQ7Ejlo0Qsh_x92oIt → 4_QeL7mSPYsuiSVJmBOTj}/_clientMiddlewareManifest.js +0 -0
  112. /package/.next/standalone/.next/static/{dg8VQ7Ejlo0Qsh_x92oIt → 4_QeL7mSPYsuiSVJmBOTj}/_ssgManifest.js +0 -0
@@ -0,0 +1,233 @@
1
+ """Deterministic credential scrubbing for SDK spool files.
2
+
3
+ This mirrors the daemon's minimal redaction boundary. The SDK applies it
4
+ before bytes reach disk; the daemon applies it again before upload so batches
5
+ written by older SDKs receive the same protection.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+
12
+ _PREFIX_RULES = (
13
+ ("sk-ant-api", 16, "anthropic-key"),
14
+ ("sk-ant-", 16, "anthropic-key"),
15
+ ("sk-proj-", 16, "openai-key"),
16
+ ("sk-", 16, "api-key"),
17
+ ("ghp_", 20, "github-token"),
18
+ ("gho_", 20, "github-token"),
19
+ ("ghu_", 20, "github-token"),
20
+ ("ghs_", 20, "github-token"),
21
+ ("ghr_", 20, "github-token"),
22
+ ("github_pat_", 20, "github-token"),
23
+ ("sb_secret_", 16, "supabase-key"),
24
+ ("sbp_", 20, "supabase-key"),
25
+ ("xoxb-", 16, "slack-token"),
26
+ ("xoxp-", 16, "slack-token"),
27
+ ("AKIA", 16, "aws-access-key-id"),
28
+ ("ASIA", 16, "aws-access-key-id"),
29
+ )
30
+ _STRONG_SECRET_NAMES = ("secret", "password", "passwd", "credential")
31
+ _WEAK_SECRET_NAMES = ("key", "token")
32
+ _MIN_ASSIGNMENT_VALUE = 12
33
+
34
+
35
+ def _is_token_char(char: str) -> bool:
36
+ return char.isascii() and (char.isalnum() or char in "_-")
37
+
38
+
39
+ def _at_boundary(value: str, start: int) -> bool:
40
+ return start == 0 or not _is_token_char(value[start - 1])
41
+
42
+
43
+ def _match_prefix(value: str, start: int):
44
+ if not _at_boundary(value, start):
45
+ return None
46
+ for prefix, minimum, label in _PREFIX_RULES:
47
+ if not value.startswith(prefix, start):
48
+ continue
49
+ end = start + len(prefix)
50
+ while end < len(value) and _is_token_char(value[end]):
51
+ end += 1
52
+ if end - start - len(prefix) >= minimum:
53
+ return end - start, label
54
+ return None
55
+
56
+
57
+ def _match_jwt(value: str, start: int):
58
+ if not _at_boundary(value, start) or not value.startswith("eyJ", start):
59
+ return None
60
+ end = start
61
+ segments = 0
62
+ while segments < 3:
63
+ segment_start = end
64
+ while end < len(value):
65
+ char = value[end]
66
+ if not (char.isascii() and (char.isalnum() or char in "-_=")):
67
+ break
68
+ end += 1
69
+ if end == segment_start:
70
+ break
71
+ segments += 1
72
+ if segments < 3 and end < len(value) and value[end] == ".":
73
+ end += 1
74
+ elif segments < 3:
75
+ break
76
+ length = end - start
77
+ if segments == 3 and length >= 40:
78
+ return length, "jwt"
79
+ return None
80
+
81
+
82
+ def _match_bearer(value: str, start: int):
83
+ if value[start : start + 7].lower() != "bearer ":
84
+ return None
85
+ end = start + 7
86
+ token_bytes = 0
87
+ while end < len(value):
88
+ char = value[end]
89
+ if char.isspace() or char in "\"'":
90
+ break
91
+ token_bytes += len(char.encode("utf-8"))
92
+ end += 1
93
+ if token_bytes >= 8:
94
+ return end - start, "bearer-token"
95
+ return None
96
+
97
+
98
+ def _is_secret_name(name: str) -> bool:
99
+ raw = name.strip("-")
100
+ lowered = raw.lower()
101
+ compound = "_" in raw or "-" in raw or any(char.isupper() for char in raw[1:])
102
+ return any(lowered.endswith(part) for part in _STRONG_SECRET_NAMES) or (
103
+ compound and any(lowered.endswith(part) for part in _WEAK_SECRET_NAMES)
104
+ )
105
+
106
+
107
+ def _is_literal_secret(value: str) -> bool:
108
+ return (
109
+ len(value.encode("utf-8")) >= _MIN_ASSIGNMENT_VALUE
110
+ and not value.startswith(("{", "$", "<", "(", "`", "[redacted:"))
111
+ )
112
+
113
+
114
+ def _match_assignment(value: str, start: int):
115
+ if start == 0:
116
+ return None
117
+ if value[start - 1] == "=" and value[start] in "\"'":
118
+ return None
119
+
120
+ quote = value[start - 1] if value[start - 1] in "\"'" else None
121
+ equals = start - 2 if quote else start - 1
122
+ if equals < 0 or value[equals] != "=":
123
+ return None
124
+
125
+ name_start = equals
126
+ while name_start > 0:
127
+ char = value[name_start - 1]
128
+ if not (char.isascii() and (char.isalnum() or char in "_-")):
129
+ break
130
+ name_start -= 1
131
+ if name_start == equals:
132
+ return None
133
+ if not _is_secret_name(value[name_start:equals]) or value.startswith(
134
+ ("{", "$", "<", "(", "`"), start
135
+ ):
136
+ return None
137
+
138
+ end = start
139
+ value_bytes = 0
140
+ while end < len(value):
141
+ char = value[end]
142
+ if (quote and char == quote) or (
143
+ not quote and (char.isspace() or char in ";&\"'")
144
+ ):
145
+ break
146
+ value_bytes += len(char.encode("utf-8"))
147
+ end += 1
148
+ if value_bytes >= _MIN_ASSIGNMENT_VALUE:
149
+ return end - start, "secret-assignment"
150
+ return None
151
+
152
+
153
+ def scrub_string(value: str) -> tuple[str, int]:
154
+ """Return the minimally redacted string and replacement count."""
155
+ out = []
156
+ cursor = 0
157
+ copied_through = 0
158
+ hits = 0
159
+ while cursor < len(value):
160
+ match = (
161
+ _match_prefix(value, cursor)
162
+ or _match_jwt(value, cursor)
163
+ or _match_bearer(value, cursor)
164
+ or _match_assignment(value, cursor)
165
+ )
166
+ if match is None:
167
+ cursor += 1
168
+ continue
169
+ length, label = match
170
+ out.append(value[copied_through:cursor])
171
+ out.append(f"[redacted:{label}]")
172
+ cursor += length
173
+ copied_through = cursor
174
+ hits += 1
175
+ if not hits:
176
+ return value, 0
177
+ out.append(value[copied_through:])
178
+ return "".join(out), hits
179
+
180
+
181
+ def redaction_enabled(base_dir: Path) -> bool:
182
+ """Read the daemon's redaction switch, defaulting safely to minimal."""
183
+ configured_home = os.environ.get("FAILPROOFAI_HOME")
184
+ if base_dir.name == "custom-agents":
185
+ config_path = base_dir.parent / "config.json"
186
+ elif configured_home:
187
+ config_path = Path(configured_home) / "config.json"
188
+ else:
189
+ config_path = Path.home() / ".failproofai" / "config.json"
190
+ try:
191
+ config = json.loads(config_path.read_text(encoding="utf-8"))
192
+ if not isinstance(config, dict):
193
+ return True
194
+ collector = config.get("collector")
195
+ return not isinstance(collector, dict) or collector.get("redact") != "off"
196
+ except (OSError, TypeError, ValueError):
197
+ return True
198
+
199
+
200
+ def redact_json_line(encoded: str) -> str:
201
+ """Redact credential-shaped keys and string values in a valid JSON event."""
202
+ event = json.loads(encoded)
203
+ hits = 0
204
+
205
+ def scrub(value, field_name=None):
206
+ nonlocal hits
207
+ if isinstance(value, str):
208
+ value, count = scrub_string(value)
209
+ hits += count
210
+ if count == 0 and isinstance(field_name, str) and _is_secret_name(field_name):
211
+ if _is_literal_secret(value):
212
+ hits += 1
213
+ return "[redacted:secret-assignment]"
214
+ return value
215
+ if isinstance(value, list):
216
+ # Elements are more values for the same field, so its name still applies.
217
+ return [scrub(item, field_name) for item in value]
218
+ if isinstance(value, dict):
219
+ result = {}
220
+ for key, item in value.items():
221
+ redacted_key, count = scrub_string(key)
222
+ hits += count
223
+ unique_key = redacted_key
224
+ suffix = 2
225
+ while unique_key in result:
226
+ unique_key = f"{redacted_key}#{suffix}"
227
+ suffix += 1
228
+ result[unique_key] = scrub(item, key)
229
+ return result
230
+ return value
231
+
232
+ redacted = scrub(event)
233
+ return json.dumps(redacted) if hits else encoded
@@ -9,9 +9,9 @@ import threading
9
9
  import weakref
10
10
  from datetime import datetime, timezone
11
11
 
12
+ from failproofai_sdk._redact import redact_json_line, redaction_enabled
12
13
  from failproofai_sdk._resolver import get_base_dir
13
14
 
14
-
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
17
  #: Per-process batch counter, so two batches written inside the same millisecond
@@ -638,11 +638,14 @@ class EventWriter:
638
638
  # whole batch goes back on the queue to be retried.
639
639
  lines = []
640
640
  dropped = 0
641
+ redact = redaction_enabled(get_base_dir())
641
642
  for entry in entries:
642
643
  encoded = _encode_entry(entry)
643
644
  if encoded is None:
644
645
  dropped += 1
645
646
  continue
647
+ if redact:
648
+ encoded = redact_json_line(encoded)
646
649
  lines.append(encoded)
647
650
 
648
651
  if dropped:
@@ -498,11 +498,11 @@ class _State:
498
498
  recorded, so the setting looked like it had worked.
499
499
 
500
500
  That is the switch `docs/start/integrations/llamaindex.mdx` presents as
501
- the control for regulated data, and `collector.redact` explicitly does
502
- not apply to SDK events so there is no second line of defence behind
503
- it. The sibling adapters route every payload through one helper
504
- (LangChain's `_shrink`, Pydantic AI's `capture_content` checks); this is
505
- that helper.
501
+ the control for regulated data. Minimal credential redaction is defence
502
+ in depth, not a replacement for disabling content capture: it catches
503
+ known secret shapes, not arbitrary regulated content. The sibling
504
+ adapters route every payload through one helper (LangChain's `_shrink`,
505
+ Pydantic AI's `capture_content` checks); this is that helper.
506
506
  """
507
507
  if not self.capture_messages:
508
508
  return None
@@ -1524,8 +1524,8 @@ def test_capture_messages_off_records_no_payload_anywhere(
1524
1524
  answers, the arguments and the outputs did not.
1525
1525
 
1526
1526
  `docs/start/integrations/llamaindex.mdx` presents this as the control for
1527
- regulated data, and `collector.redact` explicitly does not apply to SDK
1528
- events, so there was no second line of defence behind it.
1527
+ regulated data. Minimal credential redaction cannot replace it: arbitrary
1528
+ prompts and completions do not necessarily look like credentials.
1529
1529
  """
1530
1530
  llm = StubLLM(script=[("add", {"a": 987654321, "b": 123456789})], final="SECRET-COMPLETION")
1531
1531
  run_agent(calculator(llm), "SECRET-PROMPT: add them")
@@ -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.0"},"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