@datafog/fogclaw 0.2.0 → 0.3.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 +11 -0
- package/dist/backlog-tools.d.ts +57 -0
- package/dist/backlog-tools.d.ts.map +1 -0
- package/dist/backlog-tools.js +173 -0
- package/dist/backlog-tools.js.map +1 -0
- package/dist/backlog.d.ts +82 -0
- package/dist/backlog.d.ts.map +1 -0
- package/dist/backlog.js +169 -0
- package/dist/backlog.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +87 -2
- package/dist/index.js.map +1 -1
- package/dist/message-sending-handler.d.ts +2 -1
- package/dist/message-sending-handler.d.ts.map +1 -1
- package/dist/message-sending-handler.js +5 -1
- package/dist/message-sending-handler.js.map +1 -1
- package/dist/tool-result-handler.d.ts +2 -1
- package/dist/tool-result-handler.d.ts.map +1 -1
- package/dist/tool-result-handler.js +5 -1
- package/dist/tool-result-handler.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/openclaw.plugin.json +11 -1
- package/package.json +7 -1
- package/.github/workflows/harness-docs.yml +0 -30
- package/AGENTS.md +0 -28
- package/docs/DATA.md +0 -28
- package/docs/DESIGN.md +0 -17
- package/docs/DOMAIN_DOCS.md +0 -30
- package/docs/FRONTEND.md +0 -24
- package/docs/OBSERVABILITY.md +0 -32
- package/docs/PLANS.md +0 -171
- package/docs/PRODUCT_SENSE.md +0 -20
- package/docs/RELIABILITY.md +0 -60
- package/docs/SECURITY.md +0 -52
- package/docs/design-docs/core-beliefs.md +0 -17
- package/docs/design-docs/index.md +0 -8
- package/docs/generated/README.md +0 -36
- package/docs/generated/memory.md +0 -1
- package/docs/plans/2026-02-16-fogclaw-design.md +0 -172
- package/docs/plans/2026-02-16-fogclaw-implementation.md +0 -1606
- package/docs/plans/README.md +0 -15
- package/docs/plans/active/2026-02-16-feat-openclaw-official-submission-plan.md +0 -386
- package/docs/plans/active/2026-02-17-feat-release-fogclaw-via-datafog-package-plan.md +0 -328
- package/docs/plans/active/2026-02-17-feat-submit-fogclaw-to-openclaw-plan.md +0 -244
- package/docs/plans/active/2026-02-17-feat-tool-result-pii-scanning-plan.md +0 -293
- package/docs/plans/tech-debt-tracker.md +0 -42
- package/docs/plugins/fogclaw.md +0 -101
- package/docs/runbooks/address-review-findings.md +0 -30
- package/docs/runbooks/ci-failures.md +0 -46
- package/docs/runbooks/code-review.md +0 -34
- package/docs/runbooks/merge-change.md +0 -28
- package/docs/runbooks/pull-request.md +0 -45
- package/docs/runbooks/record-evidence.md +0 -43
- package/docs/runbooks/reproduce-bug.md +0 -42
- package/docs/runbooks/respond-to-feedback.md +0 -42
- package/docs/runbooks/review-findings.md +0 -31
- package/docs/runbooks/submit-openclaw-plugin.md +0 -68
- package/docs/runbooks/update-agents-md.md +0 -59
- package/docs/runbooks/update-domain-docs.md +0 -42
- package/docs/runbooks/validate-current-state.md +0 -41
- package/docs/runbooks/verify-release.md +0 -69
- package/docs/specs/2026-02-16-feat-openclaw-official-submission-spec.md +0 -115
- package/docs/specs/2026-02-17-feat-outbound-message-pii-scanning-spec.md +0 -93
- package/docs/specs/2026-02-17-feat-submit-fogclaw-to-openclaw.md +0 -125
- package/docs/specs/2026-02-17-feat-tool-result-pii-scanning-spec.md +0 -122
- package/docs/specs/README.md +0 -5
- package/docs/specs/index.md +0 -8
- package/docs/spikes/README.md +0 -8
- package/fogclaw.config.example.json +0 -33
- package/scripts/ci/he-docs-config.json +0 -123
- package/scripts/ci/he-docs-drift.sh +0 -112
- package/scripts/ci/he-docs-lint.sh +0 -234
- package/scripts/ci/he-plans-lint.sh +0 -354
- package/scripts/ci/he-runbooks-lint.sh +0 -445
- package/scripts/ci/he-specs-lint.sh +0 -258
- package/scripts/ci/he-spikes-lint.sh +0 -249
- package/scripts/runbooks/select-runbooks.sh +0 -154
- package/src/config.ts +0 -183
- package/src/engines/gliner.ts +0 -240
- package/src/engines/regex.ts +0 -71
- package/src/extract.ts +0 -98
- package/src/index.ts +0 -381
- package/src/message-sending-handler.ts +0 -87
- package/src/redactor.ts +0 -51
- package/src/scanner.ts +0 -196
- package/src/tool-result-handler.ts +0 -133
- package/src/types.ts +0 -75
- package/tests/config.test.ts +0 -78
- package/tests/extract.test.ts +0 -185
- package/tests/gliner.test.ts +0 -289
- package/tests/message-sending-handler.test.ts +0 -244
- package/tests/plugin-smoke.test.ts +0 -250
- package/tests/redactor.test.ts +0 -320
- package/tests/regex.test.ts +0 -345
- package/tests/scanner.test.ts +0 -348
- package/tests/tool-result-handler.test.ts +0 -329
- package/tsconfig.json +0 -20
package/src/index.ts
DELETED
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
import { Scanner } from "./scanner.js";
|
|
2
|
-
import { redact } from "./redactor.js";
|
|
3
|
-
import { loadConfig } from "./config.js";
|
|
4
|
-
import { RegexEngine } from "./engines/regex.js";
|
|
5
|
-
import { createToolResultHandler } from "./tool-result-handler.js";
|
|
6
|
-
import { createMessageSendingHandler } from "./message-sending-handler.js";
|
|
7
|
-
import { resolveAction } from "./types.js";
|
|
8
|
-
import type {
|
|
9
|
-
Entity,
|
|
10
|
-
FogClawConfig,
|
|
11
|
-
GuardrailAction,
|
|
12
|
-
RedactResult,
|
|
13
|
-
RedactStrategy,
|
|
14
|
-
ScanResult,
|
|
15
|
-
} from "./types.js";
|
|
16
|
-
|
|
17
|
-
export { Scanner } from "./scanner.js";
|
|
18
|
-
export { redact } from "./redactor.js";
|
|
19
|
-
export { loadConfig, DEFAULT_CONFIG } from "./config.js";
|
|
20
|
-
export type {
|
|
21
|
-
Entity,
|
|
22
|
-
FogClawConfig,
|
|
23
|
-
ScanResult,
|
|
24
|
-
RedactResult,
|
|
25
|
-
RedactStrategy,
|
|
26
|
-
GuardrailAction,
|
|
27
|
-
} from "./types.js";
|
|
28
|
-
|
|
29
|
-
function buildGuardrailPlan(entities: Entity[], config: FogClawConfig) {
|
|
30
|
-
const blocked: Entity[] = [];
|
|
31
|
-
const warned: Entity[] = [];
|
|
32
|
-
const redacted: Entity[] = [];
|
|
33
|
-
|
|
34
|
-
for (const entity of entities) {
|
|
35
|
-
const action = resolveAction(entity, config);
|
|
36
|
-
if (action === "block") blocked.push(entity);
|
|
37
|
-
else if (action === "warn") warned.push(entity);
|
|
38
|
-
else redacted.push(entity);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return { blocked, warned, redacted };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function planToSummary(plan: ReturnType<typeof buildGuardrailPlan>): {
|
|
45
|
-
total: number;
|
|
46
|
-
blocked: number;
|
|
47
|
-
warned: number;
|
|
48
|
-
redacted: number;
|
|
49
|
-
labels: {
|
|
50
|
-
blocked: string[];
|
|
51
|
-
warned: string[];
|
|
52
|
-
redacted: string[];
|
|
53
|
-
};
|
|
54
|
-
} {
|
|
55
|
-
return {
|
|
56
|
-
total: plan.blocked.length + plan.warned.length + plan.redacted.length,
|
|
57
|
-
blocked: plan.blocked.length,
|
|
58
|
-
warned: plan.warned.length,
|
|
59
|
-
redacted: plan.redacted.length,
|
|
60
|
-
labels: {
|
|
61
|
-
blocked: [...new Set(plan.blocked.map((entity) => entity.label))],
|
|
62
|
-
warned: [...new Set(plan.warned.map((entity) => entity.label))],
|
|
63
|
-
redacted: [...new Set(plan.redacted.map((entity) => entity.label))],
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function buildGuardrailContext(plan: ReturnType<typeof buildGuardrailPlan>, config: FogClawConfig): string[] {
|
|
69
|
-
const contextParts: string[] = [];
|
|
70
|
-
|
|
71
|
-
if (plan.blocked.length > 0) {
|
|
72
|
-
const types = [...new Set(plan.blocked.map((entity) => entity.label))].join(", ");
|
|
73
|
-
contextParts.push(
|
|
74
|
-
`[FOGCLAW GUARDRAIL — BLOCKED] The user's message contains sensitive information (${types}). ` +
|
|
75
|
-
`Do NOT process or repeat this information. Ask the user to rephrase without sensitive data.`,
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (plan.warned.length > 0) {
|
|
80
|
-
const types = [...new Set(plan.warned.map((entity) => entity.label))].join(", ");
|
|
81
|
-
contextParts.push(
|
|
82
|
-
`[FOGCLAW NOTICE] PII detected in user message: ${types}. Handle with care.`,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (plan.redacted.length > 0) {
|
|
87
|
-
const labels = [...new Set(plan.redacted.map((entity) => entity.label))].join(", ");
|
|
88
|
-
contextParts.push(
|
|
89
|
-
`[FOGCLAW REDACTED] ${plan.redacted.length} entity(ies) prepared for ${config.redactStrategy} redaction (${labels}).`,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return contextParts;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* OpenClaw plugin definition.
|
|
98
|
-
*
|
|
99
|
-
* Registers:
|
|
100
|
-
* - `before_agent_start` hook for automatic PII guardrail
|
|
101
|
-
* - `fogclaw_scan` tool for on-demand entity detection
|
|
102
|
-
* - `fogclaw_preview` tool for dry-run policy simulation
|
|
103
|
-
* - `fogclaw_redact` tool for on-demand redaction
|
|
104
|
-
*/
|
|
105
|
-
const fogclaw = {
|
|
106
|
-
id: "fogclaw",
|
|
107
|
-
name: "FogClaw",
|
|
108
|
-
|
|
109
|
-
register(api: any) {
|
|
110
|
-
const rawConfig = api.pluginConfig ?? api.getConfig?.() ?? {};
|
|
111
|
-
const config = loadConfig(rawConfig);
|
|
112
|
-
|
|
113
|
-
if (!config.enabled) {
|
|
114
|
-
api.logger?.info("[fogclaw] Plugin disabled via config");
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const scanner = new Scanner(config);
|
|
119
|
-
// Initialize GLiNER in the background — regex works immediately,
|
|
120
|
-
// GLiNER becomes available once the model loads.
|
|
121
|
-
scanner.initialize().catch((err: unknown) => {
|
|
122
|
-
api.logger?.warn(`[fogclaw] GLiNER background init failed: ${String(err)}`);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// --- HOOK: Guardrail on incoming messages ---
|
|
126
|
-
api.on("before_agent_start", async (event: any) => {
|
|
127
|
-
const message = event.prompt ?? "";
|
|
128
|
-
if (!message) return;
|
|
129
|
-
|
|
130
|
-
const result: ScanResult = await scanner.scan(message);
|
|
131
|
-
if (result.entities.length === 0) return;
|
|
132
|
-
|
|
133
|
-
const plan = buildGuardrailPlan(result.entities, config);
|
|
134
|
-
const contextParts = buildGuardrailContext(plan, config);
|
|
135
|
-
|
|
136
|
-
if (config.auditEnabled) {
|
|
137
|
-
const summary = planToSummary(plan);
|
|
138
|
-
api.logger?.info(
|
|
139
|
-
`[FOGCLAW AUDIT] guardrail_scan ${JSON.stringify({
|
|
140
|
-
totalEntities: summary.total,
|
|
141
|
-
blocked: summary.blocked,
|
|
142
|
-
warned: summary.warned,
|
|
143
|
-
redacted: summary.redacted,
|
|
144
|
-
blockedLabels: summary.labels.blocked,
|
|
145
|
-
warnedLabels: summary.labels.warned,
|
|
146
|
-
redactedLabels: summary.labels.redacted,
|
|
147
|
-
})}`,
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (plan.redacted.length > 0) {
|
|
152
|
-
const redactedResult: RedactResult = redact(
|
|
153
|
-
message,
|
|
154
|
-
plan.redacted,
|
|
155
|
-
config.redactStrategy,
|
|
156
|
-
);
|
|
157
|
-
contextParts.push(
|
|
158
|
-
`[FOGCLAW REDACTED] The following is the user's message with PII redacted:\n${redactedResult.redacted_text}`,
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (contextParts.length > 0) {
|
|
163
|
-
return { prependContext: contextParts.join("\n\n") };
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// --- HOOK: Scan tool results for PII before persistence ---
|
|
168
|
-
const toolResultRegex = new RegexEngine();
|
|
169
|
-
const toolResultHandler = createToolResultHandler(config, toolResultRegex, api.logger);
|
|
170
|
-
api.on("tool_result_persist", toolResultHandler);
|
|
171
|
-
|
|
172
|
-
// --- HOOK: Scan outbound messages for PII before delivery ---
|
|
173
|
-
const messageSendingHandler = createMessageSendingHandler(config, scanner, api.logger);
|
|
174
|
-
api.on("message_sending", messageSendingHandler);
|
|
175
|
-
|
|
176
|
-
// --- TOOL: On-demand scan ---
|
|
177
|
-
api.registerTool(
|
|
178
|
-
{
|
|
179
|
-
name: "fogclaw_scan",
|
|
180
|
-
id: "fogclaw_scan",
|
|
181
|
-
description:
|
|
182
|
-
"Scan text for PII and custom entities. Returns detected entities with types, positions, and confidence scores.",
|
|
183
|
-
schema: {
|
|
184
|
-
type: "object",
|
|
185
|
-
properties: {
|
|
186
|
-
text: {
|
|
187
|
-
type: "string",
|
|
188
|
-
description: "Text to scan for entities",
|
|
189
|
-
},
|
|
190
|
-
custom_labels: {
|
|
191
|
-
type: "array",
|
|
192
|
-
items: { type: "string" },
|
|
193
|
-
description:
|
|
194
|
-
"Additional entity labels for zero-shot detection (e.g., ['competitor name', 'project codename'])",
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
required: ["text"],
|
|
198
|
-
},
|
|
199
|
-
handler: async ({
|
|
200
|
-
text,
|
|
201
|
-
custom_labels,
|
|
202
|
-
}: {
|
|
203
|
-
text: string;
|
|
204
|
-
custom_labels?: string[];
|
|
205
|
-
}) => {
|
|
206
|
-
const result = await scanner.scan(text, custom_labels);
|
|
207
|
-
return {
|
|
208
|
-
content: [
|
|
209
|
-
{
|
|
210
|
-
type: "text",
|
|
211
|
-
text: JSON.stringify(
|
|
212
|
-
{
|
|
213
|
-
entities: result.entities,
|
|
214
|
-
count: result.entities.length,
|
|
215
|
-
summary:
|
|
216
|
-
result.entities.length > 0
|
|
217
|
-
? `Found ${result.entities.length} entities: ${[...new Set(result.entities.map((entity) => entity.label))].join(", ")}`
|
|
218
|
-
: "No entities detected",
|
|
219
|
-
},
|
|
220
|
-
null,
|
|
221
|
-
2,
|
|
222
|
-
),
|
|
223
|
-
},
|
|
224
|
-
],
|
|
225
|
-
};
|
|
226
|
-
},
|
|
227
|
-
}
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
// --- TOOL: Policy preview ---
|
|
231
|
-
api.registerTool(
|
|
232
|
-
{
|
|
233
|
-
name: "fogclaw_preview",
|
|
234
|
-
id: "fogclaw_preview",
|
|
235
|
-
description:
|
|
236
|
-
"Preview which entities will be blocked, warned, or redacted and the redacted message, without changing runtime behavior.",
|
|
237
|
-
schema: {
|
|
238
|
-
type: "object",
|
|
239
|
-
properties: {
|
|
240
|
-
text: {
|
|
241
|
-
type: "string",
|
|
242
|
-
description: "Text to run through FogClaw policy preview",
|
|
243
|
-
},
|
|
244
|
-
strategy: {
|
|
245
|
-
type: "string",
|
|
246
|
-
description:
|
|
247
|
-
'Override redaction strategy for the preview: "token" ([EMAIL_1]), "mask" (****), or "hash" ([EMAIL_a1b2c3...]).',
|
|
248
|
-
enum: ["token", "mask", "hash"],
|
|
249
|
-
},
|
|
250
|
-
custom_labels: {
|
|
251
|
-
type: "array",
|
|
252
|
-
items: { type: "string" },
|
|
253
|
-
description: "Additional entity labels for zero-shot detection",
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
required: ["text"],
|
|
257
|
-
},
|
|
258
|
-
handler: async ({
|
|
259
|
-
text,
|
|
260
|
-
strategy,
|
|
261
|
-
custom_labels,
|
|
262
|
-
}: {
|
|
263
|
-
text: string;
|
|
264
|
-
strategy?: "token" | "mask" | "hash";
|
|
265
|
-
custom_labels?: string[];
|
|
266
|
-
}) => {
|
|
267
|
-
const result = await scanner.scan(text, custom_labels);
|
|
268
|
-
const plan = buildGuardrailPlan(result.entities, config);
|
|
269
|
-
const summary = planToSummary(plan);
|
|
270
|
-
const redacted = redact(
|
|
271
|
-
text,
|
|
272
|
-
plan.redacted,
|
|
273
|
-
strategy ?? config.redactStrategy,
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
return {
|
|
277
|
-
content: [
|
|
278
|
-
{
|
|
279
|
-
type: "text",
|
|
280
|
-
text: JSON.stringify(
|
|
281
|
-
{
|
|
282
|
-
entities: result.entities,
|
|
283
|
-
totalEntities: summary.total,
|
|
284
|
-
actionPlan: {
|
|
285
|
-
blocked: {
|
|
286
|
-
count: summary.blocked,
|
|
287
|
-
labels: summary.labels.blocked,
|
|
288
|
-
},
|
|
289
|
-
warned: {
|
|
290
|
-
count: summary.warned,
|
|
291
|
-
labels: summary.labels.warned,
|
|
292
|
-
},
|
|
293
|
-
redacted: {
|
|
294
|
-
count: summary.redacted,
|
|
295
|
-
labels: summary.labels.redacted,
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
|
-
redactedText: redacted.redacted_text,
|
|
299
|
-
redactionStrategy: strategy ?? config.redactStrategy,
|
|
300
|
-
mapping: redacted.mapping,
|
|
301
|
-
},
|
|
302
|
-
null,
|
|
303
|
-
2,
|
|
304
|
-
),
|
|
305
|
-
},
|
|
306
|
-
],
|
|
307
|
-
};
|
|
308
|
-
},
|
|
309
|
-
}
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
// --- TOOL: On-demand redact ---
|
|
313
|
-
api.registerTool(
|
|
314
|
-
{
|
|
315
|
-
name: "fogclaw_redact",
|
|
316
|
-
id: "fogclaw_redact",
|
|
317
|
-
description:
|
|
318
|
-
"Scan and redact PII/custom entities from text. Returns sanitized text with entities replaced.",
|
|
319
|
-
schema: {
|
|
320
|
-
type: "object",
|
|
321
|
-
properties: {
|
|
322
|
-
text: {
|
|
323
|
-
type: "string",
|
|
324
|
-
description: "Text to scan and redact",
|
|
325
|
-
},
|
|
326
|
-
strategy: {
|
|
327
|
-
type: "string",
|
|
328
|
-
description:
|
|
329
|
-
'Redaction strategy: "token" ([EMAIL_1]), "mask" (****), or "hash" ([EMAIL_a1b2c3...])',
|
|
330
|
-
enum: ["token", "mask", "hash"],
|
|
331
|
-
},
|
|
332
|
-
custom_labels: {
|
|
333
|
-
type: "array",
|
|
334
|
-
items: { type: "string" },
|
|
335
|
-
description: "Additional entity labels for zero-shot detection",
|
|
336
|
-
},
|
|
337
|
-
},
|
|
338
|
-
required: ["text"],
|
|
339
|
-
},
|
|
340
|
-
handler: async ({
|
|
341
|
-
text,
|
|
342
|
-
strategy,
|
|
343
|
-
custom_labels,
|
|
344
|
-
}: {
|
|
345
|
-
text: string;
|
|
346
|
-
strategy?: "token" | "mask" | "hash";
|
|
347
|
-
custom_labels?: string[];
|
|
348
|
-
}) => {
|
|
349
|
-
const result = await scanner.scan(text, custom_labels);
|
|
350
|
-
const redacted = redact(
|
|
351
|
-
text,
|
|
352
|
-
result.entities,
|
|
353
|
-
strategy ?? config.redactStrategy,
|
|
354
|
-
);
|
|
355
|
-
return {
|
|
356
|
-
content: [
|
|
357
|
-
{
|
|
358
|
-
type: "text",
|
|
359
|
-
text: JSON.stringify(
|
|
360
|
-
{
|
|
361
|
-
redacted_text: redacted.redacted_text,
|
|
362
|
-
entities_found: result.entities.length,
|
|
363
|
-
mapping: redacted.mapping,
|
|
364
|
-
},
|
|
365
|
-
null,
|
|
366
|
-
2,
|
|
367
|
-
),
|
|
368
|
-
},
|
|
369
|
-
],
|
|
370
|
-
};
|
|
371
|
-
},
|
|
372
|
-
}
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
api.logger?.info(
|
|
376
|
-
`[fogclaw] Plugin registered — guardrail: ${config.guardrail_mode}, model: ${config.model}, custom entities: ${config.custom_entities.length}, audit: ${config.auditEnabled}`,
|
|
377
|
-
);
|
|
378
|
-
},
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
export default fogclaw;
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Async message_sending hook handler for FogClaw.
|
|
3
|
-
*
|
|
4
|
-
* Scans outbound message text for PII using the full Scanner
|
|
5
|
-
* (regex + GLiNER), redacts detected entities, and returns
|
|
6
|
-
* modified content. Never cancels message delivery.
|
|
7
|
-
*
|
|
8
|
-
* Note: message_sending is defined in OpenClaw but not yet invoked
|
|
9
|
-
* upstream. This handler activates automatically when wired.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { Scanner } from "./scanner.js";
|
|
13
|
-
import { redact } from "./redactor.js";
|
|
14
|
-
import { resolveAction } from "./types.js";
|
|
15
|
-
import type { Entity, FogClawConfig } from "./types.js";
|
|
16
|
-
|
|
17
|
-
interface Logger {
|
|
18
|
-
info(msg: string): void;
|
|
19
|
-
warn(msg: string): void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface MessageSendingEvent {
|
|
23
|
-
to: string;
|
|
24
|
-
content: string;
|
|
25
|
-
metadata?: Record<string, unknown>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface MessageSendingContext {
|
|
29
|
-
channelId: string;
|
|
30
|
-
accountId?: string;
|
|
31
|
-
conversationId?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface MessageSendingResult {
|
|
35
|
-
content?: string;
|
|
36
|
-
cancel?: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Create an async message_sending hook handler.
|
|
41
|
-
*
|
|
42
|
-
* Uses the full Scanner (regex + GLiNER) since this hook supports
|
|
43
|
-
* async handlers. All guardrail modes produce span-level redaction;
|
|
44
|
-
* cancel is never returned.
|
|
45
|
-
*/
|
|
46
|
-
export function createMessageSendingHandler(
|
|
47
|
-
config: FogClawConfig,
|
|
48
|
-
scanner: Scanner,
|
|
49
|
-
logger?: Logger,
|
|
50
|
-
): (event: MessageSendingEvent, ctx: MessageSendingContext) => Promise<MessageSendingResult | void> {
|
|
51
|
-
return async (
|
|
52
|
-
event: MessageSendingEvent,
|
|
53
|
-
_ctx: MessageSendingContext,
|
|
54
|
-
): Promise<MessageSendingResult | void> => {
|
|
55
|
-
const text = event.content;
|
|
56
|
-
if (!text) return;
|
|
57
|
-
|
|
58
|
-
const result = await scanner.scan(text);
|
|
59
|
-
if (result.entities.length === 0) return;
|
|
60
|
-
|
|
61
|
-
// All modes produce span-level redaction for outbound messages.
|
|
62
|
-
const actionableEntities = result.entities.filter((entity) => {
|
|
63
|
-
const action = resolveAction(entity, config);
|
|
64
|
-
return action === "redact" || action === "block" || action === "warn";
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
if (actionableEntities.length === 0) return;
|
|
68
|
-
|
|
69
|
-
const redacted = redact(text, actionableEntities, config.redactStrategy);
|
|
70
|
-
|
|
71
|
-
// Audit logging
|
|
72
|
-
if (config.auditEnabled && logger) {
|
|
73
|
-
const labels = [...new Set(actionableEntities.map((e) => e.label))];
|
|
74
|
-
logger.info(
|
|
75
|
-
`[FOGCLAW AUDIT] outbound_scan ${JSON.stringify({
|
|
76
|
-
totalEntities: actionableEntities.length,
|
|
77
|
-
labels,
|
|
78
|
-
channelId: _ctx.channelId ?? null,
|
|
79
|
-
source: "outbound",
|
|
80
|
-
})}`,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Never cancel — always deliver the redacted version.
|
|
85
|
-
return { content: redacted.redacted_text };
|
|
86
|
-
};
|
|
87
|
-
}
|
package/src/redactor.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import type { Entity, RedactResult, RedactStrategy } from "./types.js";
|
|
3
|
-
|
|
4
|
-
export function redact(
|
|
5
|
-
text: string,
|
|
6
|
-
entities: Entity[],
|
|
7
|
-
strategy: RedactStrategy = "token",
|
|
8
|
-
): RedactResult {
|
|
9
|
-
if (entities.length === 0) {
|
|
10
|
-
return { redacted_text: text, mapping: {}, entities: [] };
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Sort by start position descending so we replace from end to start
|
|
14
|
-
// without corrupting earlier offsets
|
|
15
|
-
const sorted = [...entities].sort((a, b) => b.start - a.start);
|
|
16
|
-
|
|
17
|
-
const counters: Record<string, number> = {};
|
|
18
|
-
const mapping: Record<string, string> = {};
|
|
19
|
-
let result = text;
|
|
20
|
-
|
|
21
|
-
for (const entity of sorted) {
|
|
22
|
-
const replacement = makeReplacement(entity, strategy, counters);
|
|
23
|
-
mapping[replacement] = entity.text;
|
|
24
|
-
result = result.slice(0, entity.start) + replacement + result.slice(entity.end);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return { redacted_text: result, mapping, entities };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function makeReplacement(
|
|
31
|
-
entity: Entity,
|
|
32
|
-
strategy: RedactStrategy,
|
|
33
|
-
counters: Record<string, number>,
|
|
34
|
-
): string {
|
|
35
|
-
switch (strategy) {
|
|
36
|
-
case "token": {
|
|
37
|
-
counters[entity.label] = (counters[entity.label] ?? 0) + 1;
|
|
38
|
-
return `[${entity.label}_${counters[entity.label]}]`;
|
|
39
|
-
}
|
|
40
|
-
case "mask": {
|
|
41
|
-
return "*".repeat(Math.max(entity.text.length, 1));
|
|
42
|
-
}
|
|
43
|
-
case "hash": {
|
|
44
|
-
const digest = createHash("sha256")
|
|
45
|
-
.update(entity.text)
|
|
46
|
-
.digest("hex")
|
|
47
|
-
.slice(0, 12);
|
|
48
|
-
return `[${entity.label}_${digest}]`;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|