@datafog/fogclaw 0.1.6 → 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 (100) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +39 -0
  3. package/dist/backlog-tools.d.ts +57 -0
  4. package/dist/backlog-tools.d.ts.map +1 -0
  5. package/dist/backlog-tools.js +173 -0
  6. package/dist/backlog-tools.js.map +1 -0
  7. package/dist/backlog.d.ts +82 -0
  8. package/dist/backlog.d.ts.map +1 -0
  9. package/dist/backlog.js +169 -0
  10. package/dist/backlog.js.map +1 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +6 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/extract.d.ts +28 -0
  15. package/dist/extract.d.ts.map +1 -0
  16. package/dist/extract.js +91 -0
  17. package/dist/extract.js.map +1 -0
  18. package/dist/index.d.ts +2 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +96 -3
  21. package/dist/index.js.map +1 -1
  22. package/dist/message-sending-handler.d.ts +41 -0
  23. package/dist/message-sending-handler.d.ts.map +1 -0
  24. package/dist/message-sending-handler.js +54 -0
  25. package/dist/message-sending-handler.js.map +1 -0
  26. package/dist/tool-result-handler.d.ts +37 -0
  27. package/dist/tool-result-handler.d.ts.map +1 -0
  28. package/dist/tool-result-handler.js +95 -0
  29. package/dist/tool-result-handler.js.map +1 -0
  30. package/dist/types.d.ts +16 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/types.js +3 -0
  33. package/dist/types.js.map +1 -1
  34. package/openclaw.plugin.json +11 -1
  35. package/package.json +7 -1
  36. package/.github/workflows/harness-docs.yml +0 -30
  37. package/AGENTS.md +0 -28
  38. package/docs/DATA.md +0 -28
  39. package/docs/DESIGN.md +0 -17
  40. package/docs/DOMAIN_DOCS.md +0 -30
  41. package/docs/FRONTEND.md +0 -24
  42. package/docs/OBSERVABILITY.md +0 -25
  43. package/docs/PLANS.md +0 -171
  44. package/docs/PRODUCT_SENSE.md +0 -20
  45. package/docs/RELIABILITY.md +0 -60
  46. package/docs/SECURITY.md +0 -50
  47. package/docs/design-docs/core-beliefs.md +0 -17
  48. package/docs/design-docs/index.md +0 -8
  49. package/docs/generated/README.md +0 -36
  50. package/docs/generated/memory.md +0 -1
  51. package/docs/plans/2026-02-16-fogclaw-design.md +0 -172
  52. package/docs/plans/2026-02-16-fogclaw-implementation.md +0 -1606
  53. package/docs/plans/README.md +0 -15
  54. package/docs/plans/active/2026-02-16-feat-openclaw-official-submission-plan.md +0 -386
  55. package/docs/plans/active/2026-02-17-feat-release-fogclaw-via-datafog-package-plan.md +0 -328
  56. package/docs/plans/active/2026-02-17-feat-submit-fogclaw-to-openclaw-plan.md +0 -244
  57. package/docs/plans/tech-debt-tracker.md +0 -42
  58. package/docs/plugins/fogclaw.md +0 -101
  59. package/docs/runbooks/address-review-findings.md +0 -30
  60. package/docs/runbooks/ci-failures.md +0 -46
  61. package/docs/runbooks/code-review.md +0 -34
  62. package/docs/runbooks/merge-change.md +0 -28
  63. package/docs/runbooks/pull-request.md +0 -45
  64. package/docs/runbooks/record-evidence.md +0 -43
  65. package/docs/runbooks/reproduce-bug.md +0 -42
  66. package/docs/runbooks/respond-to-feedback.md +0 -42
  67. package/docs/runbooks/review-findings.md +0 -31
  68. package/docs/runbooks/submit-openclaw-plugin.md +0 -68
  69. package/docs/runbooks/update-agents-md.md +0 -59
  70. package/docs/runbooks/update-domain-docs.md +0 -42
  71. package/docs/runbooks/validate-current-state.md +0 -41
  72. package/docs/runbooks/verify-release.md +0 -69
  73. package/docs/specs/2026-02-16-feat-openclaw-official-submission-spec.md +0 -115
  74. package/docs/specs/2026-02-17-feat-submit-fogclaw-to-openclaw.md +0 -125
  75. package/docs/specs/README.md +0 -5
  76. package/docs/specs/index.md +0 -8
  77. package/docs/spikes/README.md +0 -8
  78. package/fogclaw.config.example.json +0 -33
  79. package/scripts/ci/he-docs-config.json +0 -123
  80. package/scripts/ci/he-docs-drift.sh +0 -112
  81. package/scripts/ci/he-docs-lint.sh +0 -234
  82. package/scripts/ci/he-plans-lint.sh +0 -354
  83. package/scripts/ci/he-runbooks-lint.sh +0 -445
  84. package/scripts/ci/he-specs-lint.sh +0 -258
  85. package/scripts/ci/he-spikes-lint.sh +0 -249
  86. package/scripts/runbooks/select-runbooks.sh +0 -154
  87. package/src/config.ts +0 -183
  88. package/src/engines/gliner.ts +0 -240
  89. package/src/engines/regex.ts +0 -71
  90. package/src/index.ts +0 -372
  91. package/src/redactor.ts +0 -51
  92. package/src/scanner.ts +0 -196
  93. package/src/types.ts +0 -71
  94. package/tests/config.test.ts +0 -78
  95. package/tests/gliner.test.ts +0 -289
  96. package/tests/plugin-smoke.test.ts +0 -143
  97. package/tests/redactor.test.ts +0 -320
  98. package/tests/regex.test.ts +0 -345
  99. package/tests/scanner.test.ts +0 -348
  100. package/tsconfig.json +0 -20
@@ -1,240 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { env } from "@xenova/transformers";
4
-
5
- import type { Entity } from "../types.js";
6
- import { canonicalType } from "../types.js";
7
-
8
- const DEFAULT_NER_LABELS = [
9
- "person",
10
- "organization",
11
- "location",
12
- "address",
13
- "date of birth",
14
- "medical record number",
15
- "account number",
16
- "passport number",
17
- ];
18
-
19
- const GLINER_MODEL_FILES = [
20
- "onnx/model_q4f16.onnx",
21
- "onnx/model_q4.onnx",
22
- "onnx/model_bnb4.onnx",
23
- "onnx/model_int8.onnx",
24
- "onnx/model_uint8.onnx",
25
- "onnx/model_quantized.onnx",
26
- "onnx/model_fp16.onnx",
27
- "onnx/model.onnx",
28
- ];
29
-
30
- const MODEL_DOWNLOAD_TIMEOUT_MS = 120_000;
31
-
32
- function isLikelyLocalPath(modelPath: string): boolean {
33
- const trimmed = modelPath.trim();
34
- if (!trimmed) {
35
- return false;
36
- }
37
-
38
- const lower = trimmed.toLowerCase();
39
- const hasExtension = [".onnx", ".ort", ".bin"].some((ext) => lower.endsWith(ext));
40
- if (hasExtension) {
41
- return true;
42
- }
43
-
44
- if (trimmed.startsWith(".") || path.isAbsolute(trimmed)) {
45
- return true;
46
- }
47
-
48
- return false;
49
- }
50
-
51
- function toAbsolutePath(value: string): string {
52
- return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
53
- }
54
-
55
- function getModelCacheDir(): string {
56
- return env.localModelPath ?? path.join(process.cwd(), ".cache");
57
- }
58
-
59
- function sanitizeModelReference(modelPath: string): string {
60
- return modelPath.trim();
61
- }
62
-
63
- async function fileExists(filePath: string): Promise<boolean> {
64
- try {
65
- await fs.access(filePath);
66
- return true;
67
- } catch {
68
- return false;
69
- }
70
- }
71
-
72
- async function downloadModelIfNeeded(modelRepo: string, filename: string): Promise<string> {
73
- const cacheDir = getModelCacheDir();
74
- const localPath = path.join(cacheDir, modelRepo, filename);
75
-
76
- if (await fileExists(localPath)) {
77
- return localPath;
78
- }
79
-
80
- const url = `https://huggingface.co/${modelRepo}/resolve/main/${filename}`;
81
- const headers = new Headers();
82
- const token = process.env.HF_TOKEN ?? process.env.HF_ACCESS_TOKEN;
83
- if (token) {
84
- headers.set("Authorization", `Bearer ${token}`);
85
- }
86
-
87
- const controller = new AbortController();
88
- const timeout = setTimeout(() => controller.abort(), MODEL_DOWNLOAD_TIMEOUT_MS);
89
-
90
- try {
91
- const response = await fetch(url, { headers, signal: controller.signal });
92
- if (!response.ok) {
93
- throw new Error(`Unable to download model artifact: ${response.status}`);
94
- }
95
-
96
- const bytes = new Uint8Array(await response.arrayBuffer());
97
- await fs.mkdir(path.dirname(localPath), { recursive: true });
98
- await fs.writeFile(localPath, bytes);
99
-
100
- return localPath;
101
- } catch (err) {
102
- if (err instanceof Error && err.name === "AbortError") {
103
- throw new Error(`Model download timed out after ${MODEL_DOWNLOAD_TIMEOUT_MS}ms`);
104
- }
105
-
106
- throw err;
107
- } finally {
108
- clearTimeout(timeout);
109
- }
110
- }
111
-
112
- async function resolveModelPath(modelPath: string): Promise<string> {
113
- const sanitized = sanitizeModelReference(modelPath);
114
- if (!sanitized) {
115
- throw new Error("Model path is empty");
116
- }
117
-
118
- if (isLikelyLocalPath(sanitized)) {
119
- const absolutePath = toAbsolutePath(sanitized);
120
- if (!(await fileExists(absolutePath))) {
121
- throw new Error(`Local GLiNER model file not found at: ${absolutePath}`);
122
- }
123
-
124
- return absolutePath;
125
- }
126
-
127
- const candidates = GLINER_MODEL_FILES;
128
- let lastError: Error | undefined;
129
-
130
- for (const filename of candidates) {
131
- const localPath = path.join(getModelCacheDir(), sanitized, filename);
132
- if (await fileExists(localPath)) {
133
- return localPath;
134
- }
135
- }
136
-
137
- for (const filename of candidates) {
138
- try {
139
- return await downloadModelIfNeeded(sanitized, filename);
140
- } catch (err) {
141
- lastError = err instanceof Error ? err : new Error(String(err));
142
- }
143
- }
144
-
145
- throw new Error(
146
- `Failed to resolve GLiNER model "${sanitized}". Tried ${candidates.join(", ")}: ${
147
- lastError?.message ?? "unknown"
148
- }`,
149
- );
150
- }
151
-
152
- export class GlinerEngine {
153
- private model: any = null;
154
- private modelPath: string;
155
- private threshold: number;
156
- private customLabels: string[] = [];
157
- private initialized = false;
158
-
159
- constructor(modelPath: string, threshold: number = 0.5) {
160
- this.modelPath = modelPath;
161
- this.threshold = threshold;
162
- }
163
-
164
- async initialize(): Promise<void> {
165
- if (this.initialized) return;
166
-
167
- try {
168
- const resolvedModelPath = await resolveModelPath(this.modelPath);
169
- const glinerModule = await import("gliner/node").catch(async () => import("gliner"));
170
- const { Gliner } = glinerModule;
171
- this.model = new Gliner({
172
- tokenizerPath: this.modelPath,
173
- onnxSettings: {
174
- modelPath: resolvedModelPath,
175
- executionProvider: "cpu",
176
- },
177
- maxWidth: 12,
178
- modelType: "span-level",
179
- });
180
- await this.model.initialize();
181
- this.initialized = true;
182
- } catch (err) {
183
- throw new Error(
184
- `Failed to initialize GLiNER model "${this.modelPath}": ${err instanceof Error ? err.message : String(err)}`,
185
- );
186
- }
187
- }
188
-
189
- setCustomLabels(labels: string[]): void {
190
- this.customLabels = labels;
191
- }
192
-
193
- async scan(text: string, extraLabels?: string[]): Promise<Entity[]> {
194
- if (!text) return [];
195
- if (!this.model) {
196
- throw new Error("GLiNER engine not initialized. Call initialize() first.");
197
- }
198
-
199
- const labels = [
200
- ...DEFAULT_NER_LABELS,
201
- ...this.customLabels,
202
- ...(extraLabels ?? []),
203
- ];
204
-
205
- // Deduplicate labels
206
- const uniqueLabels = [...new Set(labels)];
207
-
208
- const rawResults = await this.model.inference({
209
- texts: [text],
210
- entities: uniqueLabels,
211
- flatNer: false,
212
- threshold: this.threshold,
213
- });
214
- const flatResults = Array.isArray(rawResults) ? rawResults.flat() : [];
215
-
216
- return flatResults.map(
217
- (
218
- r: {
219
- spanText?: string;
220
- text: string;
221
- label: string;
222
- score: number;
223
- start: number;
224
- end: number;
225
- },
226
- ) => ({
227
- text: r.spanText ?? r.text,
228
- label: canonicalType(r.label),
229
- start: r.start,
230
- end: r.end,
231
- confidence: r.score,
232
- source: "gliner" as const,
233
- }),
234
- );
235
- }
236
-
237
- get isInitialized(): boolean {
238
- return this.initialized;
239
- }
240
- }
@@ -1,71 +0,0 @@
1
- import type { Entity } from "../types.js";
2
-
3
- interface PatternDef {
4
- label: string;
5
- pattern: RegExp;
6
- }
7
-
8
- const PATTERNS: PatternDef[] = [
9
- {
10
- label: "EMAIL",
11
- pattern:
12
- /(?<![A-Za-z0-9._%+\-@])(?![A-Za-z_]{2,20}=)[A-Za-z0-9!#$%&*+\-/=^_`{|}~][A-Za-z0-9!#$%&'*+\-/=?^_`{|}~.]*@(?:\.?[A-Za-z0-9-]+\.)+[A-Za-z]{2,}(?=$|[^A-Za-z])/gi,
13
- },
14
- {
15
- label: "PHONE",
16
- pattern:
17
- /(?<![A-Za-z0-9])(?:(?:(?:\+?1)[-.\s]?)?(?:\(\d{3}\)|\d{3})[-.\s]?\d{3}[-.\s]?\d{4}|\+\d{1,3}[\s\-.]?\d{1,4}(?:[\s\-.]?\d{2,4}){2,3})(?![-A-Za-z0-9])/gi,
18
- },
19
- {
20
- label: "SSN",
21
- pattern:
22
- /(?<!\d)(?:(?!000|666)\d{3}-(?!00)\d{2}-(?!0000)\d{4}|(?!000|666)\d{3}(?!00)\d{2}(?!0000)\d{4})(?!\d)/g,
23
- },
24
- {
25
- label: "CREDIT_CARD",
26
- pattern:
27
- /\b(?:4\d{12}(?:\d{3})?|5[1-5]\d{14}|3[47]\d{13}|(?:(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4})|(?:3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}))\b/g,
28
- },
29
- {
30
- label: "IP_ADDRESS",
31
- pattern:
32
- /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d))\b/g,
33
- },
34
- {
35
- label: "DATE",
36
- pattern:
37
- /\b(?:(?:0?[1-9]|1[0-2])[/-](?:0?[1-9]|[12]\d|3[01])[/-](?:\d{2}|\d{4})|(?:\d{4})-(?:0?[1-9]|1[0-2])-(?:0?[1-9]|[12]\d|3[01])|(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\s+(?:0?[1-9]|[12]\d|3[01]),\s+(?:19|20)\d{2})\b/gi,
38
- },
39
- {
40
- label: "ZIP_CODE",
41
- pattern: /\b\d{5}(?:-\d{4})?\b/g,
42
- },
43
- ];
44
-
45
- export class RegexEngine {
46
- scan(text: string): Entity[] {
47
- const entities: Entity[] = [];
48
-
49
- for (const { label, pattern } of PATTERNS) {
50
- // Reset lastIndex to avoid stale state from previous calls
51
- pattern.lastIndex = 0;
52
-
53
- let match: RegExpExecArray | null;
54
- while ((match = pattern.exec(text)) !== null) {
55
- entities.push({
56
- text: match[0],
57
- label,
58
- start: match.index,
59
- end: match.index + match[0].length,
60
- confidence: 1.0,
61
- source: "regex",
62
- });
63
- }
64
- }
65
-
66
- // Sort by start position for deterministic output
67
- entities.sort((a, b) => a.start - b.start || a.end - b.end);
68
-
69
- return entities;
70
- }
71
- }
package/src/index.ts DELETED
@@ -1,372 +0,0 @@
1
- import { Scanner } from "./scanner.js";
2
- import { redact } from "./redactor.js";
3
- import { loadConfig } from "./config.js";
4
- import type {
5
- Entity,
6
- FogClawConfig,
7
- GuardrailAction,
8
- RedactResult,
9
- RedactStrategy,
10
- ScanResult,
11
- } from "./types.js";
12
-
13
- export { Scanner } from "./scanner.js";
14
- export { redact } from "./redactor.js";
15
- export { loadConfig, DEFAULT_CONFIG } from "./config.js";
16
- export type {
17
- Entity,
18
- FogClawConfig,
19
- ScanResult,
20
- RedactResult,
21
- RedactStrategy,
22
- GuardrailAction,
23
- } from "./types.js";
24
-
25
- function resolveAction(entity: Entity, config: FogClawConfig): GuardrailAction {
26
- return config.entityActions[entity.label] ?? config.guardrail_mode;
27
- }
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
- // --- TOOL: On-demand scan ---
168
- api.registerTool(
169
- {
170
- name: "fogclaw_scan",
171
- id: "fogclaw_scan",
172
- description:
173
- "Scan text for PII and custom entities. Returns detected entities with types, positions, and confidence scores.",
174
- schema: {
175
- type: "object",
176
- properties: {
177
- text: {
178
- type: "string",
179
- description: "Text to scan for entities",
180
- },
181
- custom_labels: {
182
- type: "array",
183
- items: { type: "string" },
184
- description:
185
- "Additional entity labels for zero-shot detection (e.g., ['competitor name', 'project codename'])",
186
- },
187
- },
188
- required: ["text"],
189
- },
190
- handler: async ({
191
- text,
192
- custom_labels,
193
- }: {
194
- text: string;
195
- custom_labels?: string[];
196
- }) => {
197
- const result = await scanner.scan(text, custom_labels);
198
- return {
199
- content: [
200
- {
201
- type: "text",
202
- text: JSON.stringify(
203
- {
204
- entities: result.entities,
205
- count: result.entities.length,
206
- summary:
207
- result.entities.length > 0
208
- ? `Found ${result.entities.length} entities: ${[...new Set(result.entities.map((entity) => entity.label))].join(", ")}`
209
- : "No entities detected",
210
- },
211
- null,
212
- 2,
213
- ),
214
- },
215
- ],
216
- };
217
- },
218
- }
219
- );
220
-
221
- // --- TOOL: Policy preview ---
222
- api.registerTool(
223
- {
224
- name: "fogclaw_preview",
225
- id: "fogclaw_preview",
226
- description:
227
- "Preview which entities will be blocked, warned, or redacted and the redacted message, without changing runtime behavior.",
228
- schema: {
229
- type: "object",
230
- properties: {
231
- text: {
232
- type: "string",
233
- description: "Text to run through FogClaw policy preview",
234
- },
235
- strategy: {
236
- type: "string",
237
- description:
238
- 'Override redaction strategy for the preview: "token" ([EMAIL_1]), "mask" (****), or "hash" ([EMAIL_a1b2c3...]).',
239
- enum: ["token", "mask", "hash"],
240
- },
241
- custom_labels: {
242
- type: "array",
243
- items: { type: "string" },
244
- description: "Additional entity labels for zero-shot detection",
245
- },
246
- },
247
- required: ["text"],
248
- },
249
- handler: async ({
250
- text,
251
- strategy,
252
- custom_labels,
253
- }: {
254
- text: string;
255
- strategy?: "token" | "mask" | "hash";
256
- custom_labels?: string[];
257
- }) => {
258
- const result = await scanner.scan(text, custom_labels);
259
- const plan = buildGuardrailPlan(result.entities, config);
260
- const summary = planToSummary(plan);
261
- const redacted = redact(
262
- text,
263
- plan.redacted,
264
- strategy ?? config.redactStrategy,
265
- );
266
-
267
- return {
268
- content: [
269
- {
270
- type: "text",
271
- text: JSON.stringify(
272
- {
273
- entities: result.entities,
274
- totalEntities: summary.total,
275
- actionPlan: {
276
- blocked: {
277
- count: summary.blocked,
278
- labels: summary.labels.blocked,
279
- },
280
- warned: {
281
- count: summary.warned,
282
- labels: summary.labels.warned,
283
- },
284
- redacted: {
285
- count: summary.redacted,
286
- labels: summary.labels.redacted,
287
- },
288
- },
289
- redactedText: redacted.redacted_text,
290
- redactionStrategy: strategy ?? config.redactStrategy,
291
- mapping: redacted.mapping,
292
- },
293
- null,
294
- 2,
295
- ),
296
- },
297
- ],
298
- };
299
- },
300
- }
301
- );
302
-
303
- // --- TOOL: On-demand redact ---
304
- api.registerTool(
305
- {
306
- name: "fogclaw_redact",
307
- id: "fogclaw_redact",
308
- description:
309
- "Scan and redact PII/custom entities from text. Returns sanitized text with entities replaced.",
310
- schema: {
311
- type: "object",
312
- properties: {
313
- text: {
314
- type: "string",
315
- description: "Text to scan and redact",
316
- },
317
- strategy: {
318
- type: "string",
319
- description:
320
- 'Redaction strategy: "token" ([EMAIL_1]), "mask" (****), or "hash" ([EMAIL_a1b2c3...])',
321
- enum: ["token", "mask", "hash"],
322
- },
323
- custom_labels: {
324
- type: "array",
325
- items: { type: "string" },
326
- description: "Additional entity labels for zero-shot detection",
327
- },
328
- },
329
- required: ["text"],
330
- },
331
- handler: async ({
332
- text,
333
- strategy,
334
- custom_labels,
335
- }: {
336
- text: string;
337
- strategy?: "token" | "mask" | "hash";
338
- custom_labels?: string[];
339
- }) => {
340
- const result = await scanner.scan(text, custom_labels);
341
- const redacted = redact(
342
- text,
343
- result.entities,
344
- strategy ?? config.redactStrategy,
345
- );
346
- return {
347
- content: [
348
- {
349
- type: "text",
350
- text: JSON.stringify(
351
- {
352
- redacted_text: redacted.redacted_text,
353
- entities_found: result.entities.length,
354
- mapping: redacted.mapping,
355
- },
356
- null,
357
- 2,
358
- ),
359
- },
360
- ],
361
- };
362
- },
363
- }
364
- );
365
-
366
- api.logger?.info(
367
- `[fogclaw] Plugin registered — guardrail: ${config.guardrail_mode}, model: ${config.model}, custom entities: ${config.custom_entities.length}, audit: ${config.auditEnabled}`,
368
- );
369
- },
370
- };
371
-
372
- export default fogclaw;