@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.
Files changed (103) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/backlog-tools.d.ts +57 -0
  3. package/dist/backlog-tools.d.ts.map +1 -0
  4. package/dist/backlog-tools.js +173 -0
  5. package/dist/backlog-tools.js.map +1 -0
  6. package/dist/backlog.d.ts +82 -0
  7. package/dist/backlog.d.ts.map +1 -0
  8. package/dist/backlog.js +169 -0
  9. package/dist/backlog.js.map +1 -0
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +6 -0
  12. package/dist/config.js.map +1 -1
  13. package/dist/index.d.ts +2 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +87 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/message-sending-handler.d.ts +2 -1
  18. package/dist/message-sending-handler.d.ts.map +1 -1
  19. package/dist/message-sending-handler.js +5 -1
  20. package/dist/message-sending-handler.js.map +1 -1
  21. package/dist/tool-result-handler.d.ts +2 -1
  22. package/dist/tool-result-handler.d.ts.map +1 -1
  23. package/dist/tool-result-handler.js +5 -1
  24. package/dist/tool-result-handler.js.map +1 -1
  25. package/dist/types.d.ts +15 -0
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/types.js.map +1 -1
  28. package/openclaw.plugin.json +11 -1
  29. package/package.json +7 -1
  30. package/.github/workflows/harness-docs.yml +0 -30
  31. package/AGENTS.md +0 -28
  32. package/docs/DATA.md +0 -28
  33. package/docs/DESIGN.md +0 -17
  34. package/docs/DOMAIN_DOCS.md +0 -30
  35. package/docs/FRONTEND.md +0 -24
  36. package/docs/OBSERVABILITY.md +0 -32
  37. package/docs/PLANS.md +0 -171
  38. package/docs/PRODUCT_SENSE.md +0 -20
  39. package/docs/RELIABILITY.md +0 -60
  40. package/docs/SECURITY.md +0 -52
  41. package/docs/design-docs/core-beliefs.md +0 -17
  42. package/docs/design-docs/index.md +0 -8
  43. package/docs/generated/README.md +0 -36
  44. package/docs/generated/memory.md +0 -1
  45. package/docs/plans/2026-02-16-fogclaw-design.md +0 -172
  46. package/docs/plans/2026-02-16-fogclaw-implementation.md +0 -1606
  47. package/docs/plans/README.md +0 -15
  48. package/docs/plans/active/2026-02-16-feat-openclaw-official-submission-plan.md +0 -386
  49. package/docs/plans/active/2026-02-17-feat-release-fogclaw-via-datafog-package-plan.md +0 -328
  50. package/docs/plans/active/2026-02-17-feat-submit-fogclaw-to-openclaw-plan.md +0 -244
  51. package/docs/plans/active/2026-02-17-feat-tool-result-pii-scanning-plan.md +0 -293
  52. package/docs/plans/tech-debt-tracker.md +0 -42
  53. package/docs/plugins/fogclaw.md +0 -101
  54. package/docs/runbooks/address-review-findings.md +0 -30
  55. package/docs/runbooks/ci-failures.md +0 -46
  56. package/docs/runbooks/code-review.md +0 -34
  57. package/docs/runbooks/merge-change.md +0 -28
  58. package/docs/runbooks/pull-request.md +0 -45
  59. package/docs/runbooks/record-evidence.md +0 -43
  60. package/docs/runbooks/reproduce-bug.md +0 -42
  61. package/docs/runbooks/respond-to-feedback.md +0 -42
  62. package/docs/runbooks/review-findings.md +0 -31
  63. package/docs/runbooks/submit-openclaw-plugin.md +0 -68
  64. package/docs/runbooks/update-agents-md.md +0 -59
  65. package/docs/runbooks/update-domain-docs.md +0 -42
  66. package/docs/runbooks/validate-current-state.md +0 -41
  67. package/docs/runbooks/verify-release.md +0 -69
  68. package/docs/specs/2026-02-16-feat-openclaw-official-submission-spec.md +0 -115
  69. package/docs/specs/2026-02-17-feat-outbound-message-pii-scanning-spec.md +0 -93
  70. package/docs/specs/2026-02-17-feat-submit-fogclaw-to-openclaw.md +0 -125
  71. package/docs/specs/2026-02-17-feat-tool-result-pii-scanning-spec.md +0 -122
  72. package/docs/specs/README.md +0 -5
  73. package/docs/specs/index.md +0 -8
  74. package/docs/spikes/README.md +0 -8
  75. package/fogclaw.config.example.json +0 -33
  76. package/scripts/ci/he-docs-config.json +0 -123
  77. package/scripts/ci/he-docs-drift.sh +0 -112
  78. package/scripts/ci/he-docs-lint.sh +0 -234
  79. package/scripts/ci/he-plans-lint.sh +0 -354
  80. package/scripts/ci/he-runbooks-lint.sh +0 -445
  81. package/scripts/ci/he-specs-lint.sh +0 -258
  82. package/scripts/ci/he-spikes-lint.sh +0 -249
  83. package/scripts/runbooks/select-runbooks.sh +0 -154
  84. package/src/config.ts +0 -183
  85. package/src/engines/gliner.ts +0 -240
  86. package/src/engines/regex.ts +0 -71
  87. package/src/extract.ts +0 -98
  88. package/src/index.ts +0 -381
  89. package/src/message-sending-handler.ts +0 -87
  90. package/src/redactor.ts +0 -51
  91. package/src/scanner.ts +0 -196
  92. package/src/tool-result-handler.ts +0 -133
  93. package/src/types.ts +0 -75
  94. package/tests/config.test.ts +0 -78
  95. package/tests/extract.test.ts +0 -185
  96. package/tests/gliner.test.ts +0 -289
  97. package/tests/message-sending-handler.test.ts +0 -244
  98. package/tests/plugin-smoke.test.ts +0 -250
  99. package/tests/redactor.test.ts +0 -320
  100. package/tests/regex.test.ts +0 -345
  101. package/tests/scanner.test.ts +0 -348
  102. package/tests/tool-result-handler.test.ts +0 -329
  103. 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
- }