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
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * OpenClaw (openclaw gateway) session transcript loader + parser.
3
3
  *
4
- * AUDIT-ONLY (Pillar 2). OpenClaw writes one JSONL transcript per session at
5
- * `~/.openclaw/agents/<agentId>/sessions/<sessionId>.jsonl` (sessionId is a
6
- * UUID), alongside a much larger `<sessionId>.trajectory.jsonl` OTel trace we
7
- * IGNORE, and a `sessions.json` index keyed by sessionKey. Verified live
8
- * against openclaw v2026.7.1.
4
+ * Legacy OpenClaw writes one JSONL transcript per session under `sessions/`;
5
+ * 2026.9.2+ stores the same logical records as `event_json` rows in each
6
+ * agent's `agent/openclaw-agent.sqlite`. SQLite is preferred when both exist.
9
7
  *
10
8
  * The transcript is type-discriminated JSONL:
11
9
  * {type:"session", cwd, …} — header (carries cwd)
@@ -18,8 +16,8 @@
18
16
  * `toolResult` by `toolCallId` (mirrors lib/hermes-sessions.ts) and is PURE, so
19
17
  * it is unit-testable with plain line objects.
20
18
  *
21
- * Home override: set `OPENCLAW_HOME` (used by tests / to point at a copied
22
- * gateway config dir).
19
+ * Home override: `OPENCLAW_STATE_DIR`, then `OPENCLAW_HOME` (used by tests /
20
+ * to point at a copied gateway config dir).
23
21
  */
24
22
  import { readFile } from "node:fs/promises";
25
23
  import { readdirSync, statSync } from "node:fs";
@@ -39,13 +37,18 @@ import {
39
37
  type LogSource,
40
38
  } from "./log-entries";
41
39
  import { formatDuration } from "./format-duration";
40
+ import { readOpenClawSqliteTranscript } from "./openclaw-db";
42
41
 
43
42
  /** OpenClaw sessions are stored under UUID filenames. */
44
43
  export const OPENCLAW_SESSION_ID_RE = /^[0-9a-fA-F-]{36}$/;
45
44
 
46
- /** Absolute path to OpenClaw's config home (override with OPENCLAW_HOME). */
45
+ /** Absolute path to OpenClaw's state home. */
47
46
  export function openclawHome(): string {
48
- return process.env.OPENCLAW_HOME || join(homedir(), ".openclaw");
47
+ return (
48
+ process.env.OPENCLAW_STATE_DIR ||
49
+ process.env.OPENCLAW_HOME ||
50
+ join(homedir(), ".openclaw")
51
+ );
49
52
  }
50
53
 
51
54
  // ── Parsing helpers ──
@@ -60,7 +63,11 @@ function extractText(content: unknown): string {
60
63
  if (typeof content === "string") return content;
61
64
  if (Array.isArray(content)) {
62
65
  return content
63
- .map((c) => (isPlainObject(c) && typeof c.text === "string" ? (c.text as string) : ""))
66
+ .map((c) =>
67
+ isPlainObject(c) && typeof c.text === "string"
68
+ ? (c.text as string)
69
+ : "",
70
+ )
64
71
  .filter(Boolean)
65
72
  .join("\n");
66
73
  }
@@ -134,7 +141,10 @@ export function openclawLinesToLogEntries(
134
141
  if (b.type === "text" && typeof b.text === "string") {
135
142
  blocks.push({ type: "text", text: b.text });
136
143
  } else if (b.type === "toolCall") {
137
- const id = typeof b.id === "string" ? b.id : `${String(b.name ?? "tool")}-${blocks.length}`;
144
+ const id =
145
+ typeof b.id === "string"
146
+ ? b.id
147
+ : `${String(b.name ?? "tool")}-${blocks.length}`;
138
148
  const name = typeof b.name === "string" ? b.name : "tool";
139
149
  const input = isPlainObject(b.arguments) ? b.arguments : {};
140
150
  const block: ToolUseBlock = { type: "tool_use", id, name, input };
@@ -151,17 +161,23 @@ export function openclawLinesToLogEntries(
151
161
  entries.push({
152
162
  type: "assistant",
153
163
  ...base,
154
- message: { role: "assistant", content: blocks, model: typeof m.model === "string" ? m.model : undefined },
164
+ message: {
165
+ role: "assistant",
166
+ content: blocks,
167
+ model: typeof m.model === "string" ? m.model : undefined,
168
+ },
155
169
  } satisfies AssistantEntry);
156
170
  continue;
157
171
  }
158
172
 
159
173
  if (role === "toolResult") {
160
- const callId = typeof m.toolCallId === "string" ? m.toolCallId : undefined;
174
+ const callId =
175
+ typeof m.toolCallId === "string" ? m.toolCallId : undefined;
161
176
  const block = callId ? toolUseById.get(callId) : undefined;
162
177
  if (block) {
163
178
  const details = isPlainObject(m.details) ? m.details : undefined;
164
- const startMs = (callId && toolUseStartMs.get(callId)) || date.getTime();
179
+ const startMs =
180
+ (callId && toolUseStartMs.get(callId)) || date.getTime();
165
181
  const durationMs =
166
182
  details && typeof details.durationMs === "number"
167
183
  ? details.durationMs
@@ -238,13 +254,20 @@ export function listOpenClawTranscripts(): OpenClawTranscriptFile[] {
238
254
  }
239
255
  for (const file of files) {
240
256
  // Only `<uuid>.jsonl` — not `<uuid>.trajectory.jsonl` / `.trajectory-path.json`.
241
- if (!file.endsWith(".jsonl") || file.endsWith(".trajectory.jsonl")) continue;
257
+ if (!file.endsWith(".jsonl") || file.endsWith(".trajectory.jsonl"))
258
+ continue;
242
259
  const sessionId = file.slice(0, -".jsonl".length);
243
260
  if (!OPENCLAW_SESSION_ID_RE.test(sessionId)) continue;
244
261
  const transcriptPath = join(sessionsDir, file);
245
262
  try {
246
263
  const st = statSync(transcriptPath);
247
- out.push({ agentId, sessionId, transcriptPath, mtimeMs: st.mtimeMs, sizeBytes: st.size });
264
+ out.push({
265
+ agentId,
266
+ sessionId,
267
+ transcriptPath,
268
+ mtimeMs: st.mtimeMs,
269
+ sizeBytes: st.size,
270
+ });
248
271
  } catch {
249
272
  // skip unreadable
250
273
  }
@@ -276,6 +299,36 @@ export interface OpenClawSessionLogData {
276
299
  export async function getOpenClawSessionLog(
277
300
  sessionId: string,
278
301
  ): Promise<OpenClawSessionLogData | null> {
302
+ if (!OPENCLAW_SESSION_ID_RE.test(sessionId)) return null;
303
+
304
+ // New OpenClaw versions keep live transcripts in SQLite. Prefer that copy
305
+ // when an archived JSONL with the same UUID also exists.
306
+ const sqlite = await readOpenClawSqliteTranscript(
307
+ openclawHome(),
308
+ listOpenClawAgents(),
309
+ sessionId,
310
+ );
311
+ if (sqlite) {
312
+ const entries = openclawLinesToLogEntries(sqlite.rawLines, "session");
313
+ let cwd: string | undefined;
314
+ for (const line of sqlite.rawLines) {
315
+ if (
316
+ line.type === "session" &&
317
+ typeof line.cwd === "string" &&
318
+ line.cwd.length > 0
319
+ ) {
320
+ cwd = line.cwd;
321
+ break;
322
+ }
323
+ }
324
+ return {
325
+ entries,
326
+ rawLines: sqlite.rawLines,
327
+ cwd,
328
+ filePath: sqlite.filePath,
329
+ };
330
+ }
331
+
279
332
  const filePath = findOpenClawTranscript(sessionId);
280
333
  if (!filePath) return null;
281
334
  let content: string;
@@ -289,7 +342,12 @@ export async function getOpenClawSessionLog(
289
342
  // cwd lives on the `type:"session"` header line.
290
343
  let cwd: string | undefined;
291
344
  for (const line of rawLines) {
292
- if (isPlainObject(line) && line.type === "session" && typeof line.cwd === "string" && line.cwd.length > 0) {
345
+ if (
346
+ isPlainObject(line) &&
347
+ line.type === "session" &&
348
+ typeof line.cwd === "string" &&
349
+ line.cwd.length > 0
350
+ ) {
293
351
  cwd = line.cwd;
294
352
  break;
295
353
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.4",
3
+ "version": "1.0.5-beta.1",
4
4
  "description": "Observability and enforcement for AI agent harnesses. 39 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -121,15 +121,15 @@
121
121
  "browserslist": "4.28.8"
122
122
  },
123
123
  "optionalDependencies": {
124
- "@failproofai/failproofaid-linux-x64": "1.0.4",
125
- "@failproofai/failproofaid-linux-arm64": "1.0.4",
126
- "@failproofai/failproofaid-darwin-x64": "1.0.4",
127
- "@failproofai/failproofaid-darwin-arm64": "1.0.4"
124
+ "@failproofai/failproofaid-linux-x64": "1.0.5-beta.1",
125
+ "@failproofai/failproofaid-linux-arm64": "1.0.5-beta.1",
126
+ "@failproofai/failproofaid-darwin-x64": "1.0.5-beta.1",
127
+ "@failproofai/failproofaid-darwin-arm64": "1.0.5-beta.1"
128
128
  },
129
129
  "failproofaidBinaries": {
130
- "linux-x64": "cca1d41ff951784ba084bf2d72cfad0fe8d988f8226e0e7a4538b1fb8d76a5bd",
131
- "linux-arm64": "55e9c1e2864cbf6057fd0c27e4bc573ca8fba2cd35d480a3db3a55d398e7bec0",
132
- "darwin-x64": "2a46ec77d2cac14a036f35e832f92f16895239814a5f10f02121f3c313e70574",
133
- "darwin-arm64": "a0e7ccc6fe24e1017592637192ba8a26c9d05ca839c98bc72a983094c863b39b"
130
+ "linux-x64": "c782c8d7c8522bf8af6f776640cae80fa671affa5b92d8c09e646c8f381d7fd4",
131
+ "linux-arm64": "6e9717ef40872a530501ad17cc6bc9743546968e11acf45f87f35488d1563e28",
132
+ "darwin-x64": "b47adb286e2cf08370ec2e1293ea3ed42891c7d2a8a1bece3da8e3e2b0d72f7a",
133
+ "darwin-arm64": "a5095522dc3f2518fc8177e9e2a3048062540958e3b623b198b401a7d6a2ddd5"
134
134
  }
135
135
  }
@@ -9,19 +9,49 @@ in `failproofai_sdk/_version.py` becomes that release's GitHub Release body. A
9
9
  release whose section is missing or empty is refused before anything is built —
10
10
  see `scripts/changelog-section.py`.
11
11
 
12
- ## 0.0.1b3 — 2026-09-08
12
+ ## 0.0.1b4 — 2026-09-12
13
13
 
14
- Open for the next release. `0.0.1b2` published on 2026-09-08 and the `bump` job
15
- moved the version here automatically; nothing has landed against `0.0.1b3` yet.
14
+ Open for the next release. `0.0.1b3` published on 2026-09-12 and the `bump` job
15
+ moved the version here automatically; nothing has landed against `0.0.1b4` yet.
16
16
  Add entries as changes merge — this section becomes the GitHub Release body when
17
17
  it ships.
18
18
 
19
- ## 0.0.1b2 — 2026-08-25
19
+ ## 0.0.1b3 — 2026-09-08
20
20
 
21
- Open for the next release. `0.0.1b1` published on 2026-08-24 and the `bump` job
22
- moved the version here automatically; nothing has landed against `0.0.1b2` yet.
23
- Add entries as changes merge this section becomes the GitHub Release body when
24
- it ships.
21
+ ### Fixes
22
+
23
+ - Redact common credential shapes before SDK events reach the on-disk spool.
24
+ The daemon already exposes `collector.redact`, but SDK-written batches could
25
+ contain raw API keys, bearer tokens, JWTs, or secret assignments at rest until
26
+ upload. The SDK now applies the same deterministic minimal rules before each
27
+ JSONL write, defaults safely to redaction when configuration is absent or
28
+ malformed, and honors `collector.redact: off` when verbatim capture is
29
+ explicitly required. The daemon repeats the pass before upload as defence in
30
+ depth for batches written by older SDK versions.
31
+ - **`None` on a promoted column is now dropped and warned about, not refused.**
32
+ A promoted key left at `None` in `**fields` reached the wire as an explicit
33
+ JSON `null`, so `_validate_promoted_string` refused it outright. But `None` is
34
+ how a caller says *I have no value*, and the refusal landed inside their emit
35
+ helper — which swallows telemetry errors, because telemetry must not break a
36
+ run. The event vanished with nothing logged.
37
+
38
+ `agent_end(error_type=None)` is the shape **every successful run** produces:
39
+ `error_type` is populated only on a failing outcome. Found against a real
40
+ multi-agent app, where it silently dropped `agent_end` for every session that
41
+ succeeded — leaving each one with a dangling `agent_start`, no outcome, and no
42
+ evaluation, since the server triggers evaluation on `agent_end`.
43
+
44
+ - **The same fix closes the mirror bug on promoted numerics.** `_build` omits
45
+ `None` only from a dataclass's named `specifics`; `extra` is merged verbatim.
46
+ So `duration_ms=None` was dropped when passed as a named parameter and written
47
+ as an explicit `null` when passed through `**fields` — the same value, two
48
+ outcomes, decided by which door it came through.
49
+
50
+ Both paths now agree: for a promoted column, no value means no key. Nothing
51
+ that worked before changes, and no explicit `null` reaches a promoted column
52
+ from either direction.
53
+
54
+ ## 0.0.1b2 — 2026-08-25
25
55
 
26
56
  - Retire the old inbound evaluator boundary and add evaluator authoring plus the
27
57
  outbound-only v2 worker runtime under the lazy `failproofai_sdk.evaluator`
@@ -99,6 +99,11 @@ def _validate_promoted_string(name: str, value) -> None:
99
99
  own call. `_build` copies the base dict verbatim, so a `None` here reached
100
100
  the wire as an explicit JSON `null`, the row was accepted at 200 OK, and the
101
101
  column was empty for some events and not others with nothing logged anywhere.
102
+
103
+ `**fields` no longer reaches this holding a `None`: `_validate_fields` drops
104
+ the key and warns, so an optional column the caller simply does not have
105
+ costs a log line rather than the whole event. The raise below stays as the
106
+ backstop for any direct caller.
102
107
  """
103
108
  if value is None:
104
109
  raise ValueError(
@@ -320,6 +325,21 @@ class EventNamespace:
320
325
  bad = _RESERVED & fields.keys()
321
326
  if bad:
322
327
  raise ValueError(f"Reserved field names cannot be used as custom fields: {sorted(bad)}")
328
+ # `_build` omits None only from a dataclass's named `specifics`; `extra`
329
+ # is merged verbatim, so a promoted key left at None reaches the wire as
330
+ # an explicit JSON null — accepted at 200 OK, stored as NULL, invisible
331
+ # to every filter on that column. For a promoted column "no value" has to
332
+ # mean "no key", so drop it here, the one place holding the caller's own
333
+ # dict. Warned rather than silent: passing None is still a mistake worth
334
+ # hearing about, it just must not cost the event.
335
+ for name in (_PROMOTED_NUMERIC | _PROMOTED_STRING) & fields.keys():
336
+ if fields[name] is None:
337
+ logger.warning(
338
+ "%s was passed as None and has been omitted from the event; "
339
+ "pass a value, or omit the argument entirely to silence this.",
340
+ name,
341
+ )
342
+ del fields[name]
323
343
  for name in _PROMOTED_NUMERIC & fields.keys():
324
344
  _validate_promoted_numeric(name, fields[name])
325
345
  for name in _PROMOTED_STRING & fields.keys():
@@ -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
@@ -1 +1 @@
1
- __version__ = "0.0.1b3"
1
+ __version__ = "0.0.1b4"
@@ -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")