@datafog/fogclaw 0.1.5 → 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 +83 -4
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +100 -1
- package/dist/config.js.map +1 -1
- 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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +135 -30
- 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/scanner.d.ts +13 -2
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +76 -2
- package/dist/scanner.js.map +1 -1
- 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 +17 -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/fogclaw.config.example.json +19 -1
- package/openclaw.plugin.json +63 -2
- package/package.json +9 -9
- package/scripts/ci/he-docs-drift.sh +0 -0
- package/scripts/ci/he-docs-lint.sh +0 -0
- package/scripts/ci/he-plans-lint.sh +0 -0
- package/scripts/ci/he-runbooks-lint.sh +0 -0
- package/scripts/ci/he-specs-lint.sh +0 -0
- package/scripts/ci/he-spikes-lint.sh +0 -0
- package/scripts/runbooks/select-runbooks.sh +0 -0
- package/src/config.ts +139 -2
- package/src/extract.ts +98 -0
- package/src/index.ts +194 -36
- package/src/message-sending-handler.ts +87 -0
- package/src/scanner.ts +114 -8
- package/src/tool-result-handler.ts +133 -0
- package/src/types.ts +23 -0
- package/tests/config.test.ts +55 -81
- package/tests/extract.test.ts +185 -0
- package/tests/message-sending-handler.test.ts +244 -0
- package/tests/plugin-smoke.test.ts +139 -3
- package/tests/scanner.test.ts +61 -1
- package/tests/tool-result-handler.test.ts +329 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous tool_result_persist hook handler for FogClaw.
|
|
3
|
+
*
|
|
4
|
+
* Scans tool result text for PII using the regex engine (synchronous),
|
|
5
|
+
* redacts detected entities, and returns the transformed message.
|
|
6
|
+
* GLiNER is not used here because tool_result_persist is synchronous-only.
|
|
7
|
+
*/
|
|
8
|
+
import { redact } from "./redactor.js";
|
|
9
|
+
import { extractText, replaceText } from "./extract.js";
|
|
10
|
+
import { canonicalType, resolveAction } from "./types.js";
|
|
11
|
+
/**
|
|
12
|
+
* Build an allowlist filter from config. Replicates Scanner.filterByPolicy
|
|
13
|
+
* and Scanner.shouldAllowlistEntity logic synchronously.
|
|
14
|
+
*/
|
|
15
|
+
function buildAllowlistFilter(config) {
|
|
16
|
+
const globalValues = new Set(config.allowlist.values.map((v) => v.trim().toLowerCase()));
|
|
17
|
+
const globalPatterns = config.allowlist.patterns
|
|
18
|
+
.filter((p) => p.length > 0)
|
|
19
|
+
.map((p) => new RegExp(p, "i"));
|
|
20
|
+
const entityValues = new Map();
|
|
21
|
+
for (const [entityType, values] of Object.entries(config.allowlist.entities)) {
|
|
22
|
+
const canonical = canonicalType(entityType);
|
|
23
|
+
const set = new Set(values
|
|
24
|
+
.map((v) => v.trim().toLowerCase())
|
|
25
|
+
.filter((v) => v.length > 0));
|
|
26
|
+
entityValues.set(canonical, set);
|
|
27
|
+
}
|
|
28
|
+
// Short-circuit: if no allowlist entries, keep everything
|
|
29
|
+
if (globalValues.size === 0 && globalPatterns.length === 0 && entityValues.size === 0) {
|
|
30
|
+
return () => true;
|
|
31
|
+
}
|
|
32
|
+
// Return true if entity should be KEPT (not allowlisted)
|
|
33
|
+
return (entity) => {
|
|
34
|
+
const normalizedText = entity.text.trim().toLowerCase();
|
|
35
|
+
if (globalValues.has(normalizedText))
|
|
36
|
+
return false;
|
|
37
|
+
if (globalPatterns.some((pattern) => pattern.test(entity.text)))
|
|
38
|
+
return false;
|
|
39
|
+
const perEntity = entityValues.get(entity.label);
|
|
40
|
+
if (perEntity && perEntity.has(normalizedText))
|
|
41
|
+
return false;
|
|
42
|
+
return true;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create a synchronous tool_result_persist hook handler.
|
|
47
|
+
*
|
|
48
|
+
* The returned function must NOT return a Promise — OpenClaw rejects
|
|
49
|
+
* async tool_result_persist handlers.
|
|
50
|
+
*/
|
|
51
|
+
export function createToolResultHandler(config, regexEngine, logger) {
|
|
52
|
+
const shouldKeep = buildAllowlistFilter(config);
|
|
53
|
+
return (event, _ctx) => {
|
|
54
|
+
const text = extractText(event.message);
|
|
55
|
+
if (!text)
|
|
56
|
+
return;
|
|
57
|
+
// Scan with regex engine (synchronous)
|
|
58
|
+
let entities = regexEngine.scan(text);
|
|
59
|
+
if (entities.length === 0)
|
|
60
|
+
return;
|
|
61
|
+
// Apply allowlist filtering
|
|
62
|
+
entities = entities.filter(shouldKeep);
|
|
63
|
+
if (entities.length === 0)
|
|
64
|
+
return;
|
|
65
|
+
// All guardrail modes produce span-level redaction in tool results.
|
|
66
|
+
// Determine which entities are actionable (all of them — block/warn/redact
|
|
67
|
+
// all produce redaction at the tool result level).
|
|
68
|
+
const actionableEntities = entities.filter((entity) => {
|
|
69
|
+
const action = resolveAction(entity, config);
|
|
70
|
+
return action === "redact" || action === "block" || action === "warn";
|
|
71
|
+
});
|
|
72
|
+
if (actionableEntities.length === 0)
|
|
73
|
+
return;
|
|
74
|
+
// Redact
|
|
75
|
+
const result = redact(text, actionableEntities, config.redactStrategy);
|
|
76
|
+
// Replace text in the message
|
|
77
|
+
const modifiedMessage = replaceText(event.message, result.redacted_text);
|
|
78
|
+
// Audit logging
|
|
79
|
+
if (config.auditEnabled && logger) {
|
|
80
|
+
const labels = [...new Set(actionableEntities.map((e) => e.label))];
|
|
81
|
+
logger.info(`[FOGCLAW AUDIT] tool_result_scan ${JSON.stringify({
|
|
82
|
+
totalEntities: actionableEntities.length,
|
|
83
|
+
labels,
|
|
84
|
+
toolName: event.toolName ?? null,
|
|
85
|
+
source: "tool_result",
|
|
86
|
+
})}`);
|
|
87
|
+
}
|
|
88
|
+
return { message: modifiedMessage };
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=tool-result-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-result-handler.js","sourceRoot":"","sources":["../src/tool-result-handler.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ export interface Entity {
|
|
|
8
8
|
}
|
|
9
9
|
export type RedactStrategy = "token" | "mask" | "hash";
|
|
10
10
|
export type GuardrailAction = "redact" | "block" | "warn";
|
|
11
|
+
export interface EntityConfidenceThresholds {
|
|
12
|
+
[entityType: string]: number;
|
|
13
|
+
}
|
|
14
|
+
export interface EntityAllowlist {
|
|
15
|
+
values: string[];
|
|
16
|
+
patterns: string[];
|
|
17
|
+
entities: Record<string, string[]>;
|
|
18
|
+
}
|
|
11
19
|
export interface FogClawConfig {
|
|
12
20
|
enabled: boolean;
|
|
13
21
|
guardrail_mode: GuardrailAction;
|
|
@@ -16,6 +24,9 @@ export interface FogClawConfig {
|
|
|
16
24
|
confidence_threshold: number;
|
|
17
25
|
custom_entities: string[];
|
|
18
26
|
entityActions: Record<string, GuardrailAction>;
|
|
27
|
+
entityConfidenceThresholds: EntityConfidenceThresholds;
|
|
28
|
+
allowlist: EntityAllowlist;
|
|
29
|
+
auditEnabled: boolean;
|
|
19
30
|
}
|
|
20
31
|
export interface ScanResult {
|
|
21
32
|
entities: Entity[];
|
|
@@ -26,6 +37,12 @@ export interface RedactResult {
|
|
|
26
37
|
mapping: Record<string, string>;
|
|
27
38
|
entities: Entity[];
|
|
28
39
|
}
|
|
40
|
+
export interface GuardrailPlan {
|
|
41
|
+
blocked: Entity[];
|
|
42
|
+
warned: Entity[];
|
|
43
|
+
redacted: Entity[];
|
|
44
|
+
}
|
|
29
45
|
export declare const CANONICAL_TYPE_MAP: Record<string, string>;
|
|
30
46
|
export declare function canonicalType(entityType: string): string;
|
|
47
|
+
export declare function resolveAction(entity: Entity, config: FogClawConfig): GuardrailAction;
|
|
31
48
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC5B;AAED,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvD,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,eAAe,CAAC;IAChC,cAAc,EAAE,cAAc,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC5B;AAED,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvD,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,0BAA0B;IACzC,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,eAAe,CAAC;IAChC,cAAc,EAAE,cAAc,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC/C,0BAA0B,EAAE,0BAA0B,CAAC;IACvD,SAAS,EAAE,eAAe,CAAC;IAC3B,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAYrD,CAAC;AAEF,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGxD;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,eAAe,CAEpF"}
|
package/dist/types.js
CHANGED
|
@@ -15,4 +15,7 @@ export function canonicalType(entityType) {
|
|
|
15
15
|
const normalized = entityType.toUpperCase().trim();
|
|
16
16
|
return CANONICAL_TYPE_MAP[normalized] ?? normalized;
|
|
17
17
|
}
|
|
18
|
+
export function resolveAction(entity, config) {
|
|
19
|
+
return config.entityActions[entity.label] ?? config.guardrail_mode;
|
|
20
|
+
}
|
|
18
21
|
//# sourceMappingURL=types.js.map
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAqDA,MAAM,CAAC,MAAM,kBAAkB,GAA2B;IACxD,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,UAAU;IACf,GAAG,EAAE,QAAQ;IACb,GAAG,EAAE,cAAc;IACnB,GAAG,EAAE,UAAU;IACf,GAAG,EAAE,UAAU;IACf,GAAG,EAAE,SAAS;IACd,YAAY,EAAE,OAAO;IACrB,sBAAsB,EAAE,KAAK;IAC7B,kBAAkB,EAAE,aAAa;IACjC,aAAa,EAAE,MAAM;CACtB,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,UAAkB;IAC9C,MAAM,UAAU,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;IACnD,OAAO,kBAAkB,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,MAAqB;IACjE,OAAO,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,cAAc,CAAC;AACrE,CAAC"}
|
package/docs/OBSERVABILITY.md
CHANGED
|
@@ -4,22 +4,29 @@ use_when: "Documenting logging, metrics, tracing, and health check conventions f
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Logging Strategy
|
|
7
|
-
- Prefer structured logs with consistent fields (service, env, request_id/trace_id, user_id when safe).
|
|
8
|
-
- Never log secrets; be deliberate about PII.
|
|
9
|
-
- Log at boundaries and on errors; avoid noisy per-loop logging in hot paths.
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
- Track the golden signals: latency, traffic, errors, saturation.
|
|
13
|
-
- Prefer histograms for latency; keep label cardinality low.
|
|
8
|
+
FogClaw uses `api.logger` provided by OpenClaw during plugin registration. Three log levels are used:
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
10
|
+
- `info` — Audit log entries for PII detections, plugin lifecycle events (registration, config load).
|
|
11
|
+
- `warn` — GLiNER model initialization failures, degraded mode notifications, scan errors that fall back gracefully.
|
|
12
|
+
- `error` — Configuration validation failures, unrecoverable errors.
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
- Health checks are fast and deterministic; readiness reflects dependency availability when needed.
|
|
21
|
-
- Document expected status codes and what "unhealthy" means operationally.
|
|
14
|
+
Never log raw PII values. Audit entries include entity counts and type labels only.
|
|
22
15
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
## Audit Log Format
|
|
17
|
+
|
|
18
|
+
When `auditEnabled: true`, FogClaw emits structured JSON audit entries on each scan that detects entities:
|
|
19
|
+
|
|
20
|
+
[FOGCLAW AUDIT] guardrail_scan {"totalEntities":2,"blocked":1,"warned":0,"redacted":1,"blockedLabels":["SSN"],"warnedLabels":[],"redactedLabels":["EMAIL"],"source":"prompt"}
|
|
21
|
+
|
|
22
|
+
The `source` field distinguishes scan surfaces: `"prompt"` for `before_agent_start`, `"tool_result"` for `tool_result_persist`.
|
|
23
|
+
|
|
24
|
+
## Health Signals
|
|
25
|
+
|
|
26
|
+
- **Plugin registration:** `[fogclaw] Plugin registered` log line at startup confirms the plugin loaded and configured successfully.
|
|
27
|
+
- **GLiNER availability:** Logged at startup. If the ONNX model fails to download or load, FogClaw logs a warning and operates in regex-only mode.
|
|
28
|
+
- **Scan activity:** Audit entries indicate active scanning. Absence of audit entries when PII is known to be present may indicate misconfiguration, a disabled plugin, or a gap in hook coverage.
|
|
29
|
+
|
|
30
|
+
## Metrics and Traces
|
|
31
|
+
|
|
32
|
+
FogClaw does not emit standalone metrics or traces. It operates within OpenClaw's process and relies on the host's observability infrastructure. Audit log entries serve as the primary observability signal.
|
package/docs/SECURITY.md
CHANGED
|
@@ -5,11 +5,13 @@ use_when: "Capturing security expectations for this repo: threat model, auth/aut
|
|
|
5
5
|
|
|
6
6
|
## Threat Model
|
|
7
7
|
|
|
8
|
-
FogClaw processes
|
|
8
|
+
FogClaw processes text from two surfaces: user prompts (`before_agent_start`) and tool results (`tool_result_persist`). Both may contain PII. The main risks are:
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
1. **PII leaking through unscanned paths.** Any text surface that FogClaw does not hook into is a gap. Currently covered: user prompts and tool results. Not yet covered: outbound messages (`message_sending`), historical messages, compacted summaries.
|
|
11
|
+
2. **Redaction logic errors.** If redaction produces malformed output (e.g., offset miscalculation), original PII spans could leak through or be partially visible.
|
|
12
|
+
3. **Accidental PII in logs/errors.** Audit entries, error messages, and crash output must never contain raw PII values.
|
|
13
|
+
4. **Regex false negatives.** The synchronous tool result path uses regex-only detection. Edge-case PII formats (international phone numbers, non-standard SSN formatting) may not match.
|
|
14
|
+
5. **GLiNER unavailability.** If the ONNX model fails to load, the prompt-level scanner degrades to regex-only mode silently. Users may not realize unstructured entities (names, organizations) are not being detected.
|
|
13
15
|
|
|
14
16
|
## Auth Model
|
|
15
17
|
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: 2026-02-17-feat-tool-result-pii-scanning
|
|
3
|
+
status: active
|
|
4
|
+
phase: plan
|
|
5
|
+
plan_mode: lightweight
|
|
6
|
+
detail_level: more
|
|
7
|
+
priority: high
|
|
8
|
+
owner: sidmohan
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Add PII scanning to tool results via tool_result_persist hook
|
|
12
|
+
|
|
13
|
+
This Plan is a living document. Keep `Progress`, `Surprises & Discoveries`, `Decision Log`, `Outcomes & Retrospective`, and `Revision Notes` current as work proceeds.
|
|
14
|
+
|
|
15
|
+
This plan must be maintained in accordance with `docs/PLANS.md`.
|
|
16
|
+
|
|
17
|
+
## Purpose / Big Picture
|
|
18
|
+
|
|
19
|
+
FogClaw currently scans only the user prompt for PII. When an agent reads a file, fetches a web page, or queries an API, the tool result flows into the session transcript unscanned. After this change, FogClaw will intercept every tool result via OpenClaw's `tool_result_persist` hook and redact PII spans (SSN, email, phone, credit card, IP address, date, zip code) before the content is persisted to the session. The agent will see `[SSN_1]` instead of `123-45-6789`.
|
|
20
|
+
|
|
21
|
+
To verify it works: install FogClaw in OpenClaw, ask the agent to read a file that contains a phone number and an SSN, then inspect the session transcript. The raw values should be replaced with redaction tokens.
|
|
22
|
+
|
|
23
|
+
## Progress
|
|
24
|
+
|
|
25
|
+
- [x] (2026-02-17T17:28:00Z) P1 [M1] Create `src/extract.ts` with `extractText` and `replaceText` functions
|
|
26
|
+
- [x] (2026-02-17T17:28:00Z) P2 [M1] Create `tests/extract.test.ts` covering string content, content block arrays, nested structures, empty/null, and non-text types
|
|
27
|
+
- [x] (2026-02-17T17:28:00Z) P3 [M1] All extract tests pass — 27 tests passed
|
|
28
|
+
- [x] (2026-02-17T17:29:00Z) P4 [M2] Create `src/tool-result-handler.ts` with synchronous `createToolResultHandler` factory
|
|
29
|
+
- [x] (2026-02-17T17:29:00Z) P5 [M2] Create `tests/tool-result-handler.test.ts` covering scanning, redaction, audit logging, allowlist, and edge cases
|
|
30
|
+
- [x] (2026-02-17T17:29:00Z) P6 [M2] Register `tool_result_persist` hook in `src/index.ts`
|
|
31
|
+
- [x] (2026-02-17T17:29:00Z) P7 [M2] All tool-result-handler tests pass — 21 tests passed
|
|
32
|
+
- [x] (2026-02-17T17:30:00Z) P8 [M3] Extend `tests/plugin-smoke.test.ts` with `tool_result_persist` hook registration and transformation tests
|
|
33
|
+
- [x] (2026-02-17T17:30:00Z) P9 [M3] Full test suite passes — 149 tests, 8 files, 0 failures
|
|
34
|
+
- [x] (2026-02-17T17:30:00Z) P10 [M3] Commit all changes — 3b7564f
|
|
35
|
+
|
|
36
|
+
## Surprises & Discoveries
|
|
37
|
+
|
|
38
|
+
- Observation: The Scanner class's `regexEngine` field is private, so we instantiated a fresh `RegexEngine` directly in `register()` rather than exposing the Scanner's internal instance.
|
|
39
|
+
Evidence: `const toolResultRegex = new RegexEngine();` in src/index.ts. RegexEngine is stateless (only uses pattern matching), so a separate instance is functionally identical.
|
|
40
|
+
|
|
41
|
+
- Observation: The null byte separator approach for multi-block content works cleanly — regex PII patterns never match across `\0` boundaries.
|
|
42
|
+
Evidence: 27 extract tests pass including multi-block scenarios with mixed text/image blocks.
|
|
43
|
+
|
|
44
|
+
## Decision Log
|
|
45
|
+
|
|
46
|
+
- Decision: Use RegexEngine and redact() directly instead of going through Scanner
|
|
47
|
+
Rationale: Scanner.scan() is declared `async` (returns a Promise) even when GLiNER is disabled, because the method signature is `async scan(...)`. The `tool_result_persist` hook in OpenClaw is synchronous-only — if a handler returns a Promise, OpenClaw logs a warning and ignores the result. RegexEngine.scan() and redact() are both fully synchronous, so we call them directly.
|
|
48
|
+
Date/Author: 2026-02-17, sidmohan
|
|
49
|
+
|
|
50
|
+
- Decision: All guardrail modes (redact, block, warn) produce span-level redaction in tool results
|
|
51
|
+
Rationale: Unlike `before_agent_start` where "block" can only prepend a warning context, `tool_result_persist` actually transforms the message. Span-level redaction is the safest behavior — it removes the PII while preserving surrounding context that the agent needs to reason. Replacing the entire tool result would destroy useful non-PII information.
|
|
52
|
+
Date/Author: 2026-02-17, sidmohan
|
|
53
|
+
|
|
54
|
+
- Decision: Reuse existing FogClaw config (guardrail_mode, entityActions, redactStrategy, allowlist)
|
|
55
|
+
Rationale: Users should have one mental model — "I set SSN to block, and it's blocked everywhere." Adding a separate config section for tool results would create inconsistency and confusion. If a user needs different behavior per-surface, that can be a future initiative.
|
|
56
|
+
Date/Author: 2026-02-17, sidmohan
|
|
57
|
+
|
|
58
|
+
## Outcomes & Retrospective
|
|
59
|
+
|
|
60
|
+
All three milestones completed. FogClaw now scans tool results for PII via `tool_result_persist` hook using the regex engine synchronously. 149 tests pass across 8 test files with zero regressions. New modules: `src/extract.ts` (text extraction/replacement), `src/tool-result-handler.ts` (synchronous handler factory). The implementation adds 52 new tests (27 extract + 21 handler + 4 smoke).
|
|
61
|
+
|
|
62
|
+
## Context and Orientation
|
|
63
|
+
|
|
64
|
+
FogClaw is an OpenClaw plugin that detects and redacts PII in agent conversations. The plugin lives at `/Users/sidmohan/Projects/datafog/fogclaw`.
|
|
65
|
+
|
|
66
|
+
Key files relevant to this plan:
|
|
67
|
+
|
|
68
|
+
- `src/index.ts` — Plugin entry point. Exports a plugin object with `id`, `name`, and `register(api)`. The `register` function loads config, initializes the Scanner, registers the `before_agent_start` hook, and registers three tools (`fogclaw_scan`, `fogclaw_preview`, `fogclaw_redact`). This is where we will add the `tool_result_persist` hook registration.
|
|
69
|
+
|
|
70
|
+
- `src/engines/regex.ts` — The RegexEngine class. Has a `scan(text: string): Entity[]` method that is fully synchronous. Detects 7 PII types: EMAIL, PHONE, SSN, CREDIT_CARD, IP_ADDRESS, DATE, ZIP_CODE. Each match gets confidence 1.0 and source "regex".
|
|
71
|
+
|
|
72
|
+
- `src/redactor.ts` — The `redact(text, entities, strategy)` function. Fully synchronous. Takes text, detected entities, and a strategy ("token", "mask", or "hash"). Returns `{ redacted_text, mapping, entities }`. Sorts entities by position descending and replaces from end to start to avoid offset corruption.
|
|
73
|
+
|
|
74
|
+
- `src/types.ts` — Type definitions including `Entity`, `RedactStrategy`, `GuardrailAction`, `FogClawConfig`, `ScanResult`, `RedactResult`. Also has `canonicalType()` for normalizing entity labels and `CANONICAL_TYPE_MAP`.
|
|
75
|
+
|
|
76
|
+
- `src/config.ts` — `loadConfig(raw)` merges defaults with overrides and validates. The `FogClawConfig` type includes `guardrail_mode`, `entityActions`, `redactStrategy`, `allowlist`, `auditEnabled`, and others.
|
|
77
|
+
|
|
78
|
+
- `src/scanner.ts` — The `Scanner` class that orchestrates regex + GLiNER engines. Its `scan()` method is `async` (cannot be used in synchronous hooks). Includes `filterByPolicy()` which applies allowlist filtering — we will need to replicate or extract this logic for the synchronous path.
|
|
79
|
+
|
|
80
|
+
- `tests/plugin-smoke.test.ts` — Integration tests for the plugin contract. Creates a mock `api` object with `pluginConfig`, `logger`, `on()`, and `registerTool()`. Tests verify hook registration and tool behavior.
|
|
81
|
+
|
|
82
|
+
OpenClaw's `tool_result_persist` hook contract (from OpenClaw's `src/plugins/types.ts`):
|
|
83
|
+
|
|
84
|
+
- **Event type**: `{ toolName?: string, toolCallId?: string, message: AgentMessage, isSynthetic?: boolean }`
|
|
85
|
+
- **Context type**: `{ agentId?: string, sessionKey?: string, toolName?: string, toolCallId?: string }`
|
|
86
|
+
- **Result type**: `{ message?: AgentMessage }` — return a modified message, or void to leave it unchanged
|
|
87
|
+
- **Execution**: Synchronous only. If a handler returns a Promise, OpenClaw warns and ignores the result.
|
|
88
|
+
- **Where it runs**: Inside `SessionManager.appendMessage`, via `session-tool-result-guard-wrapper.ts`. Fires on every tool result before it is written to the session transcript.
|
|
89
|
+
|
|
90
|
+
The `AgentMessage` type varies by provider and tool, but tool results typically contain text content in one of these shapes:
|
|
91
|
+
- A plain string
|
|
92
|
+
- An array of content blocks, each with `{ type: "text", text: string }` or `{ type: "image", ... }`
|
|
93
|
+
- A structured object with a `content` property that is one of the above
|
|
94
|
+
|
|
95
|
+
## Milestones
|
|
96
|
+
|
|
97
|
+
### Milestone 1 — Text extraction and replacement utilities
|
|
98
|
+
|
|
99
|
+
After this milestone, FogClaw will have a utility module that can defensively extract all text from an `AgentMessage` tool result payload (regardless of its internal shape) and replace text spans within it. This is the foundation for scanning — you need to get text out of the message to scan it, and put redacted text back in.
|
|
100
|
+
|
|
101
|
+
The module will be at `src/extract.ts` with two exported functions:
|
|
102
|
+
|
|
103
|
+
- `extractText(message: unknown): string` — walks the message structure and concatenates all text content into a single string, with segment boundaries marked so offsets can be mapped back. Returns empty string for non-text content.
|
|
104
|
+
- `replaceText(message: unknown, redactedText: string): unknown` — takes the original message and a redacted version of the extracted text, and returns a new message object with text content replaced. Preserves the original structure (arrays of content blocks stay as arrays, etc.).
|
|
105
|
+
|
|
106
|
+
Verification: run `pnpm test tests/extract.test.ts` and see all tests pass, covering: plain string messages, content block arrays with mixed text/image blocks, nested content properties, empty/null messages, and messages with no text content.
|
|
107
|
+
|
|
108
|
+
### Milestone 2 — Synchronous tool result handler
|
|
109
|
+
|
|
110
|
+
After this milestone, FogClaw will have a handler factory at `src/tool-result-handler.ts` that produces a synchronous `tool_result_persist` handler, and the handler will be registered in `src/index.ts`.
|
|
111
|
+
|
|
112
|
+
The factory function `createToolResultHandler(config, regexEngine, logger?)` returns a function with the signature `(event, ctx) => { message } | void`. The handler:
|
|
113
|
+
|
|
114
|
+
1. Extracts text from `event.message` using `extractText`
|
|
115
|
+
2. Scans text with `regexEngine.scan(text)` (synchronous)
|
|
116
|
+
3. Filters results through the allowlist (replicating `Scanner.filterByPolicy` logic synchronously)
|
|
117
|
+
4. Determines per-entity action from `config.entityActions` with `config.guardrail_mode` as fallback
|
|
118
|
+
5. Redacts all actionable entities using `redact()` (synchronous)
|
|
119
|
+
6. Replaces text in the message using `replaceText`
|
|
120
|
+
7. Emits an audit log entry if `config.auditEnabled` and entities were found
|
|
121
|
+
8. Returns `{ message: modifiedMessage }` if any redaction occurred, or `void` if no PII found
|
|
122
|
+
|
|
123
|
+
The hook will be registered in `src/index.ts` inside the `register(api)` function, alongside the existing `before_agent_start` hook:
|
|
124
|
+
|
|
125
|
+
api.on("tool_result_persist", handler);
|
|
126
|
+
|
|
127
|
+
Verification: run `pnpm test tests/tool-result-handler.test.ts` and see all tests pass, covering: SSN redaction in tool results, email/phone detection, allowlist exclusion, audit log emission, no-op when no PII found, and various message shapes.
|
|
128
|
+
|
|
129
|
+
### Milestone 3 — Integration smoke test
|
|
130
|
+
|
|
131
|
+
After this milestone, the existing plugin smoke test at `tests/plugin-smoke.test.ts` will be extended to verify that FogClaw registers a `tool_result_persist` hook and that invoking it with a tool result containing PII produces a transformed message.
|
|
132
|
+
|
|
133
|
+
Verification: run `pnpm test` (full suite) and see all tests pass with no regressions.
|
|
134
|
+
|
|
135
|
+
## Plan of Work
|
|
136
|
+
|
|
137
|
+
The work proceeds in three sequential steps. Each builds on the previous.
|
|
138
|
+
|
|
139
|
+
**Step 1: Text extraction module.** Create `src/extract.ts` with `extractText` and `replaceText`. The `extractText` function should handle these `AgentMessage` shapes: (a) the message itself is a string, (b) the message has a `content` property that is a string, (c) the message has a `content` property that is an array of blocks where each text block has `{ type: "text", text: string }`. For arrays, concatenate text blocks with a newline separator and track the offset ranges so `replaceText` can map redacted text back to the correct blocks. Create `tests/extract.test.ts` with tests for each shape plus edge cases (null, undefined, empty string, image-only content blocks, deeply nested content).
|
|
140
|
+
|
|
141
|
+
**Step 2: Tool result handler.** Create `src/tool-result-handler.ts`. Import `RegexEngine` from `src/engines/regex.ts`, `redact` from `src/redactor.ts`, `extractText`/`replaceText` from `src/extract.ts`, and types from `src/types.ts`. The factory function `createToolResultHandler` takes `FogClawConfig`, a `RegexEngine` instance, and an optional logger object. It returns a synchronous handler function. Inside the handler: extract text, scan, filter by allowlist (replicate the allowlist filtering logic from `Scanner.filterByPolicy` in `src/scanner.ts` — the filtering checks `config.allowlist.values`, `config.allowlist.patterns`, and `config.allowlist.entities`), determine actions, redact, replace, audit, return. Then update `src/index.ts` to call `createToolResultHandler` during registration and register the returned handler with `api.on("tool_result_persist", handler)`. Create `tests/tool-result-handler.test.ts`.
|
|
142
|
+
|
|
143
|
+
**Step 3: Smoke test extension.** In `tests/plugin-smoke.test.ts`, add a test that verifies `tool_result_persist` appears in the registered hooks after `register(api)` is called. Add a second test that invokes the hook handler with a mock tool result message containing an SSN, and asserts the returned message has the SSN replaced with a redaction token like `[SSN_1]`.
|
|
144
|
+
|
|
145
|
+
## Concrete Steps
|
|
146
|
+
|
|
147
|
+
All commands run from the FogClaw repo root at `/Users/sidmohan/Projects/datafog/fogclaw`.
|
|
148
|
+
|
|
149
|
+
After creating `src/extract.ts` and `tests/extract.test.ts`:
|
|
150
|
+
|
|
151
|
+
pnpm test tests/extract.test.ts
|
|
152
|
+
|
|
153
|
+
Expected: all extract tests pass (text extraction from various message shapes, replacement, edge cases).
|
|
154
|
+
|
|
155
|
+
After creating `src/tool-result-handler.ts` and `tests/tool-result-handler.test.ts` and updating `src/index.ts`:
|
|
156
|
+
|
|
157
|
+
pnpm test tests/tool-result-handler.test.ts
|
|
158
|
+
|
|
159
|
+
Expected: all handler tests pass (scanning, redaction, audit, allowlist, no-op cases).
|
|
160
|
+
|
|
161
|
+
After extending `tests/plugin-smoke.test.ts`:
|
|
162
|
+
|
|
163
|
+
pnpm test tests/plugin-smoke.test.ts
|
|
164
|
+
|
|
165
|
+
Expected: all smoke tests pass, including new `tool_result_persist` tests.
|
|
166
|
+
|
|
167
|
+
Full suite validation:
|
|
168
|
+
|
|
169
|
+
pnpm test
|
|
170
|
+
|
|
171
|
+
Expected: all tests pass, no regressions in existing `before_agent_start`, scanner, redactor, regex, or config tests.
|
|
172
|
+
|
|
173
|
+
Type check:
|
|
174
|
+
|
|
175
|
+
pnpm lint
|
|
176
|
+
|
|
177
|
+
Expected: no type errors.
|
|
178
|
+
|
|
179
|
+
## Validation and Acceptance
|
|
180
|
+
|
|
181
|
+
The feature is complete when:
|
|
182
|
+
|
|
183
|
+
1. `pnpm test` passes with all existing tests plus new tests for extract, tool-result-handler, and extended smoke tests.
|
|
184
|
+
2. `pnpm lint` passes with no type errors.
|
|
185
|
+
3. A tool result message containing `"Call 555-123-4567 or email john@example.com"` is passed to the `tool_result_persist` handler and the returned message contains `"Call [PHONE_1] or email [EMAIL_1]"` (with token strategy) and the original values do not appear.
|
|
186
|
+
4. A tool result message containing no PII returns `void` (no modification).
|
|
187
|
+
5. An allowlisted value (e.g., `noreply@example.com`) is not redacted even when detected.
|
|
188
|
+
6. When `auditEnabled: true`, the logger receives an audit entry with `source: "tool_result"`, entity count, and labels but no raw PII values.
|
|
189
|
+
|
|
190
|
+
## Idempotence and Recovery
|
|
191
|
+
|
|
192
|
+
All changes are additive — new files (`src/extract.ts`, `src/tool-result-handler.ts`) and new tests. No existing files are modified except `src/index.ts` (adding a hook registration) and `tests/plugin-smoke.test.ts` (adding test cases).
|
|
193
|
+
|
|
194
|
+
If a step fails partway, delete the partially created files and restart from the milestone. No database migrations, no state files, no destructive operations.
|
|
195
|
+
|
|
196
|
+
Running `pnpm test` at any point is safe and idempotent.
|
|
197
|
+
|
|
198
|
+
## Artifacts and Notes
|
|
199
|
+
|
|
200
|
+
Full test suite output:
|
|
201
|
+
|
|
202
|
+
✓ tests/extract.test.ts (27 tests) 4ms
|
|
203
|
+
✓ tests/config.test.ts (6 tests) 4ms
|
|
204
|
+
✓ tests/redactor.test.ts (21 tests) 6ms
|
|
205
|
+
✓ tests/regex.test.ts (39 tests) 11ms
|
|
206
|
+
✓ tests/tool-result-handler.test.ts (21 tests) 10ms
|
|
207
|
+
✓ tests/gliner.test.ts (12 tests) 10ms
|
|
208
|
+
✓ tests/plugin-smoke.test.ts (8 tests) 9ms
|
|
209
|
+
✓ tests/scanner.test.ts (15 tests) 13ms
|
|
210
|
+
|
|
211
|
+
Test Files 8 passed (8)
|
|
212
|
+
Tests 149 passed (149)
|
|
213
|
+
|
|
214
|
+
Type check: `npx tsc --noEmit` — clean, no errors.
|
|
215
|
+
|
|
216
|
+
## Interfaces and Dependencies
|
|
217
|
+
|
|
218
|
+
**New module `src/extract.ts`:**
|
|
219
|
+
|
|
220
|
+
export function extractText(message: unknown): string
|
|
221
|
+
export function replaceText(message: unknown, redactedText: string): unknown
|
|
222
|
+
|
|
223
|
+
**New module `src/tool-result-handler.ts`:**
|
|
224
|
+
|
|
225
|
+
import { RegexEngine } from "./engines/regex.js";
|
|
226
|
+
import { FogClawConfig } from "./types.js";
|
|
227
|
+
|
|
228
|
+
interface Logger {
|
|
229
|
+
info(msg: string): void;
|
|
230
|
+
warn(msg: string): void;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
interface ToolResultPersistEvent {
|
|
234
|
+
toolName?: string;
|
|
235
|
+
toolCallId?: string;
|
|
236
|
+
message: unknown;
|
|
237
|
+
isSynthetic?: boolean;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
interface ToolResultPersistContext {
|
|
241
|
+
agentId?: string;
|
|
242
|
+
sessionKey?: string;
|
|
243
|
+
toolName?: string;
|
|
244
|
+
toolCallId?: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function createToolResultHandler(
|
|
248
|
+
config: FogClawConfig,
|
|
249
|
+
regexEngine: RegexEngine,
|
|
250
|
+
logger?: Logger,
|
|
251
|
+
): (event: ToolResultPersistEvent, ctx: ToolResultPersistContext) =>
|
|
252
|
+
{ message: unknown } | void
|
|
253
|
+
|
|
254
|
+
**Modified `src/index.ts`:**
|
|
255
|
+
|
|
256
|
+
Inside the `register(api)` function, after the existing `before_agent_start` registration, add:
|
|
257
|
+
|
|
258
|
+
const toolResultHandler = createToolResultHandler(config, scanner.regexEngine, api.logger);
|
|
259
|
+
api.on("tool_result_persist", toolResultHandler);
|
|
260
|
+
|
|
261
|
+
This requires exposing `regexEngine` from the Scanner class (currently private). Either make it a public property or instantiate a separate RegexEngine in `register()`.
|
|
262
|
+
|
|
263
|
+
**No new dependencies.** All imports are from existing FogClaw modules or Node built-ins.
|
|
264
|
+
|
|
265
|
+
## Pull Request
|
|
266
|
+
|
|
267
|
+
Populated by `he-github`.
|
|
268
|
+
|
|
269
|
+
- pr:
|
|
270
|
+
- branch:
|
|
271
|
+
- commit:
|
|
272
|
+
- ci:
|
|
273
|
+
|
|
274
|
+
## Review Findings
|
|
275
|
+
|
|
276
|
+
Populated by `he-review`.
|
|
277
|
+
|
|
278
|
+
## Verify/Release Decision
|
|
279
|
+
|
|
280
|
+
Populated by `he-verify-release`.
|
|
281
|
+
|
|
282
|
+
- decision:
|
|
283
|
+
- date:
|
|
284
|
+
- open findings by priority (if any):
|
|
285
|
+
- evidence:
|
|
286
|
+
- rollback:
|
|
287
|
+
- post-release checks:
|
|
288
|
+
- owner:
|
|
289
|
+
|
|
290
|
+
## Revision Notes
|
|
291
|
+
|
|
292
|
+
- 2026-02-17T00:00:00Z: Initialized plan from template. Reason: establish PLANS-compliant execution baseline for tool result PII scanning.
|
|
293
|
+
- 2026-02-17T17:30:00Z: All milestones completed. Updated Progress, Surprises & Discoveries, Outcomes & Retrospective, and Artifacts sections with implementation evidence.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: 2026-02-17-feat-outbound-message-pii-scanning
|
|
3
|
+
status: intake-complete
|
|
4
|
+
date: 2026-02-17T00:00:00Z
|
|
5
|
+
owner: sidmohan
|
|
6
|
+
plan_mode: lightweight
|
|
7
|
+
spike_recommended: no
|
|
8
|
+
priority: high
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# feat: Add PII scanning to outbound messages via message_sending hook
|
|
12
|
+
|
|
13
|
+
## Purpose / Big Picture
|
|
14
|
+
|
|
15
|
+
FogClaw now scans user prompts (`before_agent_start`) and tool results (`tool_result_persist`), but outbound messages — the agent's final responses delivered to Telegram, Slack, Discord, etc. — are not scanned. If PII slips through into the agent's response (hallucinated, echoed, or reassembled from partial data), it reaches external channels unredacted.
|
|
16
|
+
|
|
17
|
+
By hooking into OpenClaw's `message_sending` lifecycle, FogClaw adds a last-chance gate that scans and redacts PII in outbound messages before they are delivered to recipients.
|
|
18
|
+
|
|
19
|
+
Note: `message_sending` is defined in OpenClaw's type system but not yet invoked upstream. This handler will activate automatically when OpenClaw wires the hook into its outbound message flow.
|
|
20
|
+
|
|
21
|
+
## Scope
|
|
22
|
+
|
|
23
|
+
### In Scope
|
|
24
|
+
|
|
25
|
+
- Register a `message_sending` hook handler in FogClaw's plugin registration
|
|
26
|
+
- Scan `event.content` (outbound message text) using the **full Scanner** (regex + GLiNER) since this hook is async-capable
|
|
27
|
+
- Apply existing `guardrail_mode`, `entityActions`, `redactStrategy`, and `allowlist` config
|
|
28
|
+
- Redact PII spans in the outbound message content (all modes produce span-level redaction, never cancel)
|
|
29
|
+
- Return `{ content: redactedText }` when PII is found
|
|
30
|
+
- Emit audit log entries when `auditEnabled: true`
|
|
31
|
+
- Add unit tests for the handler
|
|
32
|
+
- Extend plugin smoke test
|
|
33
|
+
|
|
34
|
+
### Boundaries
|
|
35
|
+
|
|
36
|
+
- **No message cancellation.** FogClaw will never return `cancel: true`. Span-level redaction is always preferred over dropping messages silently.
|
|
37
|
+
- **No new config surface.** Reuse existing FogClaw config.
|
|
38
|
+
- **No changes to OpenClaw upstream.** Handler will activate when OpenClaw wires the hook.
|
|
39
|
+
- **No scanning of `event.metadata`.** Only `event.content` (the text delivered to the recipient).
|
|
40
|
+
|
|
41
|
+
## Non-Goals
|
|
42
|
+
|
|
43
|
+
- Cancelling message delivery
|
|
44
|
+
- Scanning message metadata or recipient addresses
|
|
45
|
+
- Modifying recipient routing
|
|
46
|
+
|
|
47
|
+
## Risks
|
|
48
|
+
|
|
49
|
+
- **Hook not invoked upstream.** The handler exists but won't fire until OpenClaw activates `message_sending`. This is accepted — the code is ready and waiting.
|
|
50
|
+
- **GLiNER latency on outbound path.** Scanner.scan() is async and may add 50-200ms per message. This is acceptable for outbound messages (not a hot-path like tool_result_persist) and provides coverage for person names and organizations.
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
| ID | Priority | Requirement |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| R1 | critical | Register a `message_sending` hook handler that scans outbound message content for PII using the full Scanner (regex + GLiNER) |
|
|
57
|
+
| R2 | critical | Redact detected PII spans using the configured `redactStrategy` |
|
|
58
|
+
| R3 | critical | Return `{ content: redactedText }` when PII is found; return void when clean |
|
|
59
|
+
| R4 | high | Apply existing `entityActions`, `guardrail_mode`, and `allowlist` config; all actions produce span-level redaction |
|
|
60
|
+
| R5 | high | Never return `cancel: true` — always deliver the (redacted) message |
|
|
61
|
+
| R6 | medium | Emit audit log entry with `source: "outbound"` when PII is detected and `auditEnabled: true` |
|
|
62
|
+
| R7 | low | Handler may be async (Scanner.scan() returns a Promise) |
|
|
63
|
+
|
|
64
|
+
## Success Criteria
|
|
65
|
+
|
|
66
|
+
- Unit tests pass for the message sending handler covering PII detection, redaction, allowlist, audit logging, and no-op cases
|
|
67
|
+
- Plugin smoke test verifies `message_sending` hook registration
|
|
68
|
+
- Plugin smoke test verifies PII in outbound content is redacted
|
|
69
|
+
- All existing tests pass (no regression)
|
|
70
|
+
|
|
71
|
+
## Constraints
|
|
72
|
+
|
|
73
|
+
- Must not introduce new dependencies
|
|
74
|
+
- Must not change the existing `FogClawConfig` type
|
|
75
|
+
- Must reuse the existing Scanner instance (not create a new one)
|
|
76
|
+
|
|
77
|
+
## Priority
|
|
78
|
+
|
|
79
|
+
- priority: high
|
|
80
|
+
- rationale: This is the last-chance safety net before PII reaches external messaging channels. Even if upstream scanning catches most PII, outbound scanning prevents hallucinated or reassembled PII from leaking.
|
|
81
|
+
|
|
82
|
+
## Initial Milestone Candidates
|
|
83
|
+
|
|
84
|
+
- M1: Create `src/message-sending-handler.ts` with async handler factory, plus unit tests
|
|
85
|
+
- M2: Register hook in `src/index.ts`, extend plugin smoke test, full suite validation
|
|
86
|
+
|
|
87
|
+
## Handoff
|
|
88
|
+
|
|
89
|
+
After spec approval, proceed directly to implementation (lightweight plan mode — code mirrors the established `tool-result-handler.ts` pattern).
|
|
90
|
+
|
|
91
|
+
## Revision Notes
|
|
92
|
+
|
|
93
|
+
- 2026-02-17T00:00:00Z: Initialized spec. message_sending hook is typed but not invoked in OpenClaw; handler ships as future-ready.
|