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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +6 -6
- 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/build-manifest.json +3 -3
- 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/build-manifest.json +3 -3
- 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 +4 -4
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/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 +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/build-manifest.json +3 -3
- 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/build-manifest.json +3 -3
- 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/build-manifest.json +3 -3
- 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/build-manifest.json +3 -3
- 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 +2 -2
- 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/build-manifest.json +3 -3
- 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 +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/build-manifest.json +3 -3
- 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/build-manifest.json +3 -3
- 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/{[externals]__0-9ti7z._.js → [externals]__1_bftcl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0u3wrvq._.js → [externals]__1msfs-h._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0faqz9x._.js +22 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zyc_nm._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p-qi2t._.js → [root-of-the-server]__112b4go._.js} +22 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1l71g3y._.js +22 -0
- 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/{_1-akg6w._.js → _1c3k-8x._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_openclaw-projects_ts_0gc-p24._.js +3 -0
- 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]__00y9ww2._.js +22 -0
- 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]__09flfsx._.js → [root-of-the-server]__05g1k7_._.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]__0sz_5na._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kaotix._.js +22 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u6sn69._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_02yoeej._.js → _0iv28v5._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1if9ixi._.js → _14c6q09._.js} +21 -2
- package/.next/standalone/.next/server/chunks/ssr/{_04rbnaq._.js → _17noq1a._.js} +2 -2
- 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/chunks/ssr/lib_codex-projects_ts_0pqlw37._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_openclaw-projects_ts_0qxq3bp._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- 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/{3tjji451hrgc0.js → 074xcclgow3yh.js} +1 -1
- package/.next/standalone/.next/static/chunks/{31u94yukw9fz-.js → 0qytob94jbzs6.js} +1 -1
- package/.next/standalone/.next/static/chunks/25cn9xpv4r1ow.js +1 -0
- package/.next/standalone/.next/static/chunks/{1j6o9min_6qa9.js → 26tf_lpco5n-v.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0-tyrpwuc3lla.js → 2b014kj28p0vz.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2lwipmv5etc8m.js → 3_fnbc-v5r-9e.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1c6aptmytnu6q.js → 3kltzmx9k_t6x.js} +1 -1
- package/.next/standalone/.next/static/chunks/{27cl8h-c2w7cd.js → 3pmi7qhtmniku.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2u8vh73tpw28_.js → 3q1rji38vuvr4.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0ca7octhod0g2.js → 3qxl_gibq3bas.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3p9_8x5ezce-9.js → 3vjaz4hld9oo8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2zafgs90r_leq.js → 41ejnuj2d9g7_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-3p73obe0xmrxm.js → turbopack-1xgsp63yhrdu5.js} +1 -1
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +7 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -1
- package/.next/standalone/lib/download-session.ts +58 -13
- package/.next/standalone/lib/openclaw-db.ts +271 -0
- package/.next/standalone/lib/openclaw-projects.ts +54 -18
- package/.next/standalone/lib/openclaw-sessions.ts +75 -17
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/sdk/python/CHANGELOG.md +38 -8
- 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/_version.py +1 -1
- 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 +466 -255
- package/dist/worker.mjs +301 -90
- package/lib/download-session.ts +58 -13
- package/lib/openclaw-db.ts +271 -0
- package/lib/openclaw-projects.ts +54 -18
- package/lib/openclaw-sessions.ts +75 -17
- package/openclaw-plugin/index.js +10 -6
- package/openclaw-plugin/instruct-retry-gate.js +89 -0
- package/package.json +9 -9
- package/src/hooks/policy-evaluator.ts +22 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00-s7h8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__190won8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1sc84bb._.js +0 -3
- package/.next/standalone/.next/static/chunks/2pd16tn5qmx13.js +0 -1
- /package/.next/standalone/.next/static/{dg8VQ7Ejlo0Qsh_x92oIt → Qexks15mjEoPt6cN3D2jb}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{dg8VQ7Ejlo0Qsh_x92oIt → Qexks15mjEoPt6cN3D2jb}/_clientMiddlewareManifest.js +0 -0
- /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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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:
|
|
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
|
|
45
|
+
/** Absolute path to OpenClaw's state home. */
|
|
47
46
|
export function openclawHome(): string {
|
|
48
|
-
return
|
|
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) =>
|
|
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 =
|
|
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: {
|
|
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 =
|
|
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 =
|
|
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"))
|
|
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({
|
|
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 (
|
|
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.
|
|
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.
|
|
125
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.
|
|
126
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.
|
|
127
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.
|
|
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": "
|
|
131
|
-
"linux-arm64": "
|
|
132
|
-
"darwin-x64": "
|
|
133
|
-
"darwin-arm64": "
|
|
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.
|
|
12
|
+
## 0.0.1b4 — 2026-09-12
|
|
13
13
|
|
|
14
|
-
Open for the next release. `0.0.
|
|
15
|
-
moved the version here automatically; nothing has landed against `0.0.
|
|
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.
|
|
19
|
+
## 0.0.1b3 — 2026-09-08
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.
|
|
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
|
|
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")
|