@datafog/fogclaw 0.1.6 → 0.2.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/CHANGELOG.md +31 -0
- package/README.md +39 -0
- package/dist/extract.d.ts +28 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +91 -0
- package/dist/extract.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/message-sending-handler.d.ts +40 -0
- package/dist/message-sending-handler.d.ts.map +1 -0
- package/dist/message-sending-handler.js +50 -0
- package/dist/message-sending-handler.js.map +1 -0
- package/dist/tool-result-handler.d.ts +36 -0
- package/dist/tool-result-handler.d.ts.map +1 -0
- package/dist/tool-result-handler.js +91 -0
- package/dist/tool-result-handler.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -1
- package/docs/OBSERVABILITY.md +22 -15
- package/docs/SECURITY.md +6 -4
- package/docs/plans/active/2026-02-17-feat-tool-result-pii-scanning-plan.md +293 -0
- package/docs/specs/2026-02-17-feat-outbound-message-pii-scanning-spec.md +93 -0
- package/docs/specs/2026-02-17-feat-tool-result-pii-scanning-spec.md +122 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/extract.ts +98 -0
- package/src/index.ts +13 -4
- package/src/message-sending-handler.ts +87 -0
- package/src/tool-result-handler.ts +133 -0
- package/src/types.ts +4 -0
- package/tests/extract.test.ts +185 -0
- package/tests/message-sending-handler.test.ts +244 -0
- package/tests/plugin-smoke.test.ts +109 -2
- package/tests/tool-result-handler.test.ts +329 -0
|
@@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach, beforeAll, afterAll } from "vites
|
|
|
3
3
|
import plugin from "../src/index.js";
|
|
4
4
|
|
|
5
5
|
function createApi() {
|
|
6
|
-
const hooks: Array<{ event: string; handler: (event: any) =>
|
|
6
|
+
const hooks: Array<{ event: string; handler: (event: any) => any }> = [];
|
|
7
7
|
const tools: any[] = [];
|
|
8
8
|
|
|
9
9
|
return {
|
|
@@ -18,7 +18,7 @@ function createApi() {
|
|
|
18
18
|
warn: vi.fn(),
|
|
19
19
|
error: vi.fn(),
|
|
20
20
|
},
|
|
21
|
-
on: vi.fn((event: string, handler: (event: any) =>
|
|
21
|
+
on: vi.fn((event: string, handler: (event: any) => any) => {
|
|
22
22
|
hooks.push({ event, handler });
|
|
23
23
|
}),
|
|
24
24
|
registerTool: vi.fn((tool: any) => {
|
|
@@ -48,6 +48,8 @@ describe("FogClaw OpenClaw plugin contract (integration path)", () => {
|
|
|
48
48
|
|
|
49
49
|
expect(typeof plugin.register).toBe("function");
|
|
50
50
|
expect(api.on).toHaveBeenCalledWith("before_agent_start", expect.any(Function));
|
|
51
|
+
expect(api.on).toHaveBeenCalledWith("tool_result_persist", expect.any(Function));
|
|
52
|
+
expect(api.on).toHaveBeenCalledWith("message_sending", expect.any(Function));
|
|
51
53
|
expect(api.registerTool).toHaveBeenCalledTimes(3);
|
|
52
54
|
|
|
53
55
|
const scanTool = api.tools.find((tool: any) => tool.id === "fogclaw_scan");
|
|
@@ -140,4 +142,109 @@ describe("FogClaw OpenClaw plugin contract (integration path)", () => {
|
|
|
140
142
|
expect(parsed.count).toBe(parsed.entities.length);
|
|
141
143
|
expect(parsed.entities).toEqual(expect.any(Array));
|
|
142
144
|
});
|
|
145
|
+
|
|
146
|
+
it("registers tool_result_persist hook", () => {
|
|
147
|
+
const api = createApi();
|
|
148
|
+
plugin.register(api);
|
|
149
|
+
|
|
150
|
+
const hook = api.hooks.find((entry: any) => entry.event === "tool_result_persist");
|
|
151
|
+
expect(hook).toBeDefined();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("tool_result_persist hook redacts PII in tool result messages", () => {
|
|
155
|
+
const api = createApi();
|
|
156
|
+
plugin.register(api);
|
|
157
|
+
|
|
158
|
+
const hook = api.hooks.find((entry: any) => entry.event === "tool_result_persist");
|
|
159
|
+
expect(hook).toBeDefined();
|
|
160
|
+
|
|
161
|
+
// Simulate a tool result containing an SSN
|
|
162
|
+
const result = hook!.handler({
|
|
163
|
+
toolName: "file_read",
|
|
164
|
+
message: "The patient SSN is 123-45-6789 and phone is 555-123-4567.",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Handler is synchronous — result should not be a Promise
|
|
168
|
+
expect(result).toBeDefined();
|
|
169
|
+
expect(result?.message).toBeDefined();
|
|
170
|
+
const text = result.message as string;
|
|
171
|
+
expect(text).toContain("[SSN_1]");
|
|
172
|
+
expect(text).toContain("[PHONE_1]");
|
|
173
|
+
expect(text).not.toContain("123-45-6789");
|
|
174
|
+
expect(text).not.toContain("555-123-4567");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("tool_result_persist hook returns void for clean tool results", () => {
|
|
178
|
+
const api = createApi();
|
|
179
|
+
plugin.register(api);
|
|
180
|
+
|
|
181
|
+
const hook = api.hooks.find((entry: any) => entry.event === "tool_result_persist");
|
|
182
|
+
const result = hook!.handler({
|
|
183
|
+
toolName: "file_read",
|
|
184
|
+
message: "This file contains no sensitive information.",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(result).toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("tool_result_persist hook emits audit log with source tool_result", () => {
|
|
191
|
+
const api = createApi();
|
|
192
|
+
plugin.register(api);
|
|
193
|
+
|
|
194
|
+
const hook = api.hooks.find((entry: any) => entry.event === "tool_result_persist");
|
|
195
|
+
hook!.handler({
|
|
196
|
+
toolName: "web_fetch",
|
|
197
|
+
message: "Contact john@example.com for details.",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// auditEnabled is true in createApi config
|
|
201
|
+
const auditCalls = api.logger.info.mock.calls.filter(
|
|
202
|
+
(call: any[]) => typeof call[0] === "string" && call[0].includes("tool_result_scan"),
|
|
203
|
+
);
|
|
204
|
+
expect(auditCalls.length).toBe(1);
|
|
205
|
+
expect(auditCalls[0][0]).toContain('"source":"tool_result"');
|
|
206
|
+
expect(auditCalls[0][0]).toContain('"toolName":"web_fetch"');
|
|
207
|
+
expect(auditCalls[0][0]).not.toContain("john@example.com");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("registers message_sending hook", () => {
|
|
211
|
+
const api = createApi();
|
|
212
|
+
plugin.register(api);
|
|
213
|
+
|
|
214
|
+
const hook = api.hooks.find((entry: any) => entry.event === "message_sending");
|
|
215
|
+
expect(hook).toBeDefined();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("message_sending hook redacts PII in outbound messages", async () => {
|
|
219
|
+
const api = createApi();
|
|
220
|
+
plugin.register(api);
|
|
221
|
+
|
|
222
|
+
const hook = api.hooks.find((entry: any) => entry.event === "message_sending");
|
|
223
|
+
expect(hook).toBeDefined();
|
|
224
|
+
|
|
225
|
+
const result = await hook!.handler({
|
|
226
|
+
to: "user123",
|
|
227
|
+
content: "Your SSN is 123-45-6789 and email is john@example.com.",
|
|
228
|
+
}, { channelId: "telegram" });
|
|
229
|
+
|
|
230
|
+
expect(result).toBeDefined();
|
|
231
|
+
expect(result.content).toContain("[SSN_1]");
|
|
232
|
+
expect(result.content).toContain("[EMAIL_1]");
|
|
233
|
+
expect(result.content).not.toContain("123-45-6789");
|
|
234
|
+
expect(result.content).not.toContain("john@example.com");
|
|
235
|
+
expect(result.cancel).toBeUndefined();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("message_sending hook returns void for clean messages", async () => {
|
|
239
|
+
const api = createApi();
|
|
240
|
+
plugin.register(api);
|
|
241
|
+
|
|
242
|
+
const hook = api.hooks.find((entry: any) => entry.event === "message_sending");
|
|
243
|
+
const result = await hook!.handler({
|
|
244
|
+
to: "user123",
|
|
245
|
+
content: "Hello, how can I help?",
|
|
246
|
+
}, { channelId: "slack" });
|
|
247
|
+
|
|
248
|
+
expect(result).toBeUndefined();
|
|
249
|
+
});
|
|
143
250
|
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { createToolResultHandler } from "../src/tool-result-handler.js";
|
|
3
|
+
import { RegexEngine } from "../src/engines/regex.js";
|
|
4
|
+
import type { FogClawConfig } from "../src/types.js";
|
|
5
|
+
|
|
6
|
+
function makeConfig(overrides: Partial<FogClawConfig> = {}): FogClawConfig {
|
|
7
|
+
return {
|
|
8
|
+
enabled: true,
|
|
9
|
+
guardrail_mode: "redact",
|
|
10
|
+
redactStrategy: "token",
|
|
11
|
+
model: "test",
|
|
12
|
+
confidence_threshold: 0.5,
|
|
13
|
+
custom_entities: [],
|
|
14
|
+
entityActions: {},
|
|
15
|
+
entityConfidenceThresholds: {},
|
|
16
|
+
allowlist: { values: [], patterns: [], entities: {} },
|
|
17
|
+
auditEnabled: false,
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeLogger() {
|
|
23
|
+
return {
|
|
24
|
+
info: vi.fn(),
|
|
25
|
+
warn: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("createToolResultHandler", () => {
|
|
30
|
+
const regexEngine = new RegexEngine();
|
|
31
|
+
|
|
32
|
+
it("returns a synchronous function (not async)", () => {
|
|
33
|
+
const handler = createToolResultHandler(makeConfig(), regexEngine);
|
|
34
|
+
expect(typeof handler).toBe("function");
|
|
35
|
+
|
|
36
|
+
// Verify it does not return a Promise
|
|
37
|
+
const result = handler(
|
|
38
|
+
{ message: "no pii here" },
|
|
39
|
+
{},
|
|
40
|
+
);
|
|
41
|
+
// undefined is expected for no-PII — not a Promise
|
|
42
|
+
expect(result).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("redacts SSN in a plain string message", () => {
|
|
46
|
+
const handler = createToolResultHandler(makeConfig(), regexEngine);
|
|
47
|
+
const result = handler(
|
|
48
|
+
{ message: "SSN is 123-45-6789" },
|
|
49
|
+
{},
|
|
50
|
+
);
|
|
51
|
+
expect(result).toBeDefined();
|
|
52
|
+
expect(result!.message).toBe("SSN is [SSN_1]");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("redacts email in a content-string message", () => {
|
|
56
|
+
const handler = createToolResultHandler(makeConfig(), regexEngine);
|
|
57
|
+
const result = handler(
|
|
58
|
+
{ message: { role: "toolResult", content: "Contact john@example.com" } },
|
|
59
|
+
{},
|
|
60
|
+
);
|
|
61
|
+
expect(result).toBeDefined();
|
|
62
|
+
const msg = result!.message as Record<string, unknown>;
|
|
63
|
+
expect(msg.content).toBe("Contact [EMAIL_1]");
|
|
64
|
+
expect(msg.role).toBe("toolResult");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("redacts phone number in content block array", () => {
|
|
68
|
+
const handler = createToolResultHandler(makeConfig(), regexEngine);
|
|
69
|
+
const result = handler(
|
|
70
|
+
{
|
|
71
|
+
message: {
|
|
72
|
+
content: [{ type: "text", text: "Call 555-123-4567 please" }],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{},
|
|
76
|
+
);
|
|
77
|
+
expect(result).toBeDefined();
|
|
78
|
+
const msg = result!.message as Record<string, unknown>;
|
|
79
|
+
const content = msg.content as Array<Record<string, unknown>>;
|
|
80
|
+
expect(content[0].text).toBe("Call [PHONE_1] please");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("redacts multiple PII types in one message", () => {
|
|
84
|
+
const handler = createToolResultHandler(makeConfig(), regexEngine);
|
|
85
|
+
const result = handler(
|
|
86
|
+
{ message: "Call 555-123-4567 or email john@example.com" },
|
|
87
|
+
{},
|
|
88
|
+
);
|
|
89
|
+
expect(result).toBeDefined();
|
|
90
|
+
const text = result!.message as string;
|
|
91
|
+
expect(text).toContain("[PHONE_1]");
|
|
92
|
+
expect(text).toContain("[EMAIL_1]");
|
|
93
|
+
expect(text).not.toContain("555-123-4567");
|
|
94
|
+
expect(text).not.toContain("john@example.com");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns void when no PII is found", () => {
|
|
98
|
+
const handler = createToolResultHandler(makeConfig(), regexEngine);
|
|
99
|
+
const result = handler(
|
|
100
|
+
{ message: "This is clean text with no sensitive data." },
|
|
101
|
+
{},
|
|
102
|
+
);
|
|
103
|
+
expect(result).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns void for empty string message", () => {
|
|
107
|
+
const handler = createToolResultHandler(makeConfig(), regexEngine);
|
|
108
|
+
expect(handler({ message: "" }, {})).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns void for null message", () => {
|
|
112
|
+
const handler = createToolResultHandler(makeConfig(), regexEngine);
|
|
113
|
+
expect(handler({ message: null }, {})).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("returns void for message with no extractable text", () => {
|
|
117
|
+
const handler = createToolResultHandler(makeConfig(), regexEngine);
|
|
118
|
+
expect(
|
|
119
|
+
handler(
|
|
120
|
+
{
|
|
121
|
+
message: {
|
|
122
|
+
content: [{ type: "image", source: { data: "base64" } }],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{},
|
|
126
|
+
),
|
|
127
|
+
).toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("respects allowlist — global values", () => {
|
|
131
|
+
const config = makeConfig({
|
|
132
|
+
allowlist: {
|
|
133
|
+
values: ["noreply@example.com"],
|
|
134
|
+
patterns: [],
|
|
135
|
+
entities: {},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const handler = createToolResultHandler(config, regexEngine);
|
|
139
|
+
const result = handler(
|
|
140
|
+
{ message: "Contact noreply@example.com for help" },
|
|
141
|
+
{},
|
|
142
|
+
);
|
|
143
|
+
// The allowlisted email should not be redacted
|
|
144
|
+
expect(result).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("respects allowlist — global patterns", () => {
|
|
148
|
+
const config = makeConfig({
|
|
149
|
+
allowlist: {
|
|
150
|
+
values: [],
|
|
151
|
+
patterns: ["^internal-"],
|
|
152
|
+
entities: {},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const handler = createToolResultHandler(config, regexEngine);
|
|
156
|
+
const result = handler(
|
|
157
|
+
{ message: "Contact internal-noreply@company.com" },
|
|
158
|
+
{},
|
|
159
|
+
);
|
|
160
|
+
expect(result).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("respects allowlist — per-entity values", () => {
|
|
164
|
+
const config = makeConfig({
|
|
165
|
+
allowlist: {
|
|
166
|
+
values: [],
|
|
167
|
+
patterns: [],
|
|
168
|
+
entities: { EMAIL: ["public@example.com"] },
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
const handler = createToolResultHandler(config, regexEngine);
|
|
172
|
+
const result = handler(
|
|
173
|
+
{ message: "Email public@example.com or secret@example.com" },
|
|
174
|
+
{},
|
|
175
|
+
);
|
|
176
|
+
expect(result).toBeDefined();
|
|
177
|
+
const text = result!.message as string;
|
|
178
|
+
// public@example.com should be preserved, secret@example.com should be redacted
|
|
179
|
+
expect(text).toContain("public@example.com");
|
|
180
|
+
expect(text).not.toContain("secret@example.com");
|
|
181
|
+
expect(text).toContain("[EMAIL_1]");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("uses mask redaction strategy", () => {
|
|
185
|
+
const config = makeConfig({ redactStrategy: "mask" });
|
|
186
|
+
const handler = createToolResultHandler(config, regexEngine);
|
|
187
|
+
const result = handler(
|
|
188
|
+
{ message: "SSN is 123-45-6789" },
|
|
189
|
+
{},
|
|
190
|
+
);
|
|
191
|
+
expect(result).toBeDefined();
|
|
192
|
+
const text = result!.message as string;
|
|
193
|
+
expect(text).toContain("***********");
|
|
194
|
+
expect(text).not.toContain("123-45-6789");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("uses hash redaction strategy", () => {
|
|
198
|
+
const config = makeConfig({ redactStrategy: "hash" });
|
|
199
|
+
const handler = createToolResultHandler(config, regexEngine);
|
|
200
|
+
const result = handler(
|
|
201
|
+
{ message: "SSN is 123-45-6789" },
|
|
202
|
+
{},
|
|
203
|
+
);
|
|
204
|
+
expect(result).toBeDefined();
|
|
205
|
+
const text = result!.message as string;
|
|
206
|
+
expect(text).toMatch(/\[SSN_[a-f0-9]{12}\]/);
|
|
207
|
+
expect(text).not.toContain("123-45-6789");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("applies entityActions config — all modes produce redaction", () => {
|
|
211
|
+
const config = makeConfig({
|
|
212
|
+
entityActions: { SSN: "block", EMAIL: "warn" },
|
|
213
|
+
});
|
|
214
|
+
const handler = createToolResultHandler(config, regexEngine);
|
|
215
|
+
const result = handler(
|
|
216
|
+
{ message: "SSN 123-45-6789, email john@example.com" },
|
|
217
|
+
{},
|
|
218
|
+
);
|
|
219
|
+
expect(result).toBeDefined();
|
|
220
|
+
const text = result!.message as string;
|
|
221
|
+
// Both block and warn modes produce span-level redaction in tool results
|
|
222
|
+
expect(text).toContain("[SSN_1]");
|
|
223
|
+
expect(text).toContain("[EMAIL_1]");
|
|
224
|
+
expect(text).not.toContain("123-45-6789");
|
|
225
|
+
expect(text).not.toContain("john@example.com");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("preserves non-text content blocks", () => {
|
|
229
|
+
const handler = createToolResultHandler(makeConfig(), regexEngine);
|
|
230
|
+
const result = handler(
|
|
231
|
+
{
|
|
232
|
+
message: {
|
|
233
|
+
content: [
|
|
234
|
+
{ type: "text", text: "SSN is 123-45-6789" },
|
|
235
|
+
{ type: "image", source: { data: "imagedata" } },
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{},
|
|
240
|
+
);
|
|
241
|
+
expect(result).toBeDefined();
|
|
242
|
+
const msg = result!.message as Record<string, unknown>;
|
|
243
|
+
const content = msg.content as Array<Record<string, unknown>>;
|
|
244
|
+
expect(content[0].text).toBe("SSN is [SSN_1]");
|
|
245
|
+
expect((content[1] as any).type).toBe("image");
|
|
246
|
+
expect((content[1] as any).source.data).toBe("imagedata");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("audit logging", () => {
|
|
250
|
+
it("emits audit log when auditEnabled and PII found", () => {
|
|
251
|
+
const config = makeConfig({ auditEnabled: true });
|
|
252
|
+
const logger = makeLogger();
|
|
253
|
+
const handler = createToolResultHandler(config, regexEngine, logger);
|
|
254
|
+
|
|
255
|
+
handler(
|
|
256
|
+
{ message: "SSN 123-45-6789", toolName: "file_read" },
|
|
257
|
+
{},
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
expect(logger.info).toHaveBeenCalledOnce();
|
|
261
|
+
const logCall = logger.info.mock.calls[0][0] as string;
|
|
262
|
+
expect(logCall).toContain("[FOGCLAW AUDIT]");
|
|
263
|
+
expect(logCall).toContain("tool_result_scan");
|
|
264
|
+
expect(logCall).toContain('"source":"tool_result"');
|
|
265
|
+
expect(logCall).toContain('"toolName":"file_read"');
|
|
266
|
+
expect(logCall).toContain('"SSN"');
|
|
267
|
+
// Must not contain raw PII
|
|
268
|
+
expect(logCall).not.toContain("123-45-6789");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("does not emit audit log when auditEnabled is false", () => {
|
|
272
|
+
const config = makeConfig({ auditEnabled: false });
|
|
273
|
+
const logger = makeLogger();
|
|
274
|
+
const handler = createToolResultHandler(config, regexEngine, logger);
|
|
275
|
+
|
|
276
|
+
handler({ message: "SSN 123-45-6789" }, {});
|
|
277
|
+
|
|
278
|
+
expect(logger.info).not.toHaveBeenCalled();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("does not emit audit log when no PII found", () => {
|
|
282
|
+
const config = makeConfig({ auditEnabled: true });
|
|
283
|
+
const logger = makeLogger();
|
|
284
|
+
const handler = createToolResultHandler(config, regexEngine, logger);
|
|
285
|
+
|
|
286
|
+
handler({ message: "clean text" }, {});
|
|
287
|
+
|
|
288
|
+
expect(logger.info).not.toHaveBeenCalled();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("includes entity count and labels in audit log", () => {
|
|
292
|
+
const config = makeConfig({ auditEnabled: true });
|
|
293
|
+
const logger = makeLogger();
|
|
294
|
+
const handler = createToolResultHandler(config, regexEngine, logger);
|
|
295
|
+
|
|
296
|
+
handler(
|
|
297
|
+
{ message: "Call 555-123-4567, email john@example.com" },
|
|
298
|
+
{},
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const logCall = logger.info.mock.calls[0][0] as string;
|
|
302
|
+
const parsed = JSON.parse(logCall.replace("[FOGCLAW AUDIT] tool_result_scan ", ""));
|
|
303
|
+
expect(parsed.totalEntities).toBe(2);
|
|
304
|
+
expect(parsed.labels).toContain("PHONE");
|
|
305
|
+
expect(parsed.labels).toContain("EMAIL");
|
|
306
|
+
expect(parsed.source).toBe("tool_result");
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("handles multiple text blocks with PII in different blocks", () => {
|
|
311
|
+
const handler = createToolResultHandler(makeConfig(), regexEngine);
|
|
312
|
+
const result = handler(
|
|
313
|
+
{
|
|
314
|
+
message: {
|
|
315
|
+
content: [
|
|
316
|
+
{ type: "text", text: "First block clean" },
|
|
317
|
+
{ type: "text", text: "Second block SSN 123-45-6789" },
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
{},
|
|
322
|
+
);
|
|
323
|
+
expect(result).toBeDefined();
|
|
324
|
+
const msg = result!.message as Record<string, unknown>;
|
|
325
|
+
const content = msg.content as Array<Record<string, unknown>>;
|
|
326
|
+
expect(content[0].text).toBe("First block clean");
|
|
327
|
+
expect(content[1].text).toBe("Second block SSN [SSN_1]");
|
|
328
|
+
});
|
|
329
|
+
});
|