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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09flfsx._.js → [root-of-the-server]__0x1aj1_._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_04rbnaq._.js → _0u3xxgc._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_02yoeej._.js → _0zs_4uu._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
- package/.next/standalone/.next/static/chunks/{27cl8h-c2w7cd.js → 0hw6-c206lsma.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3p9_8x5ezce-9.js → 0m9sx3khjrl1q.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0-tyrpwuc3lla.js → 0oos8qpc1ugos.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2u8vh73tpw28_.js → 0unrx9g_x0u7j.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3tjji451hrgc0.js → 2qjn-leir0x_s.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2lwipmv5etc8m.js → 2xbxkaszi1xnw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1j6o9min_6qa9.js → 33umhy-x204qj.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0ca7octhod0g2.js → 3nux7kkrc3eps.js} +1 -1
- package/.next/standalone/.next/static/chunks/40tpgs4fjct5p.js +1 -0
- package/.next/standalone/.next/static/chunks/{31u94yukw9fz-.js → 40uih96wnkowy.js} +1 -1
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/sdk/python/CHANGELOG.md +32 -9
- package/.next/standalone/sdk/python/failproofai_sdk/_events.py +20 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_redact.py +233 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_writer.py +4 -1
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/llama_index.py +5 -5
- package/.next/standalone/sdk/python/tests/integrations/test_llama_index.py +2 -2
- package/.next/standalone/sdk/python/tests/test_redaction.py +140 -0
- package/.next/standalone/sdk/python/tests/test_sdk.py +87 -0
- package/.next/standalone/sdk/python/tests/test_server_contract.py +60 -0
- package/.next/standalone/sdk/python/tests/test_site_docs.py +2 -2
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +10 -10
- package/dist/worker.mjs +1 -1
- package/package.json +9 -9
- package/.next/standalone/.next/static/chunks/2pd16tn5qmx13.js +0 -1
- /package/.next/standalone/.next/static/{dg8VQ7Ejlo0Qsh_x92oIt → 4_QeL7mSPYsuiSVJmBOTj}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{dg8VQ7Ejlo0Qsh_x92oIt → 4_QeL7mSPYsuiSVJmBOTj}/_clientMiddlewareManifest.js +0 -0
- /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
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
(LangChain's `_shrink`,
|
|
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
|
|
1528
|
-
|
|
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
|
|
319
|
-
|
|
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.
|
|
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
|
|