Skip to content

Commit 6be36b2

Browse files
hanhuihanhuihanhui
andauthored
fix: keep outbound redaction on regex path (#4)
Co-authored-by: hanhui <hanhui@bytedance.com>
1 parent 005202b commit 6be36b2

7 files changed

Lines changed: 84 additions & 11 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/engines/gliner.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import fs from "node:fs/promises";
22
import path from "node:path";
3-
import { env } from "@xenova/transformers";
43

54
import type { Entity } from "../types.js";
65
import { canonicalType } from "../types.js";
@@ -53,7 +52,7 @@ function toAbsolutePath(value: string): string {
5352
}
5453

5554
function getModelCacheDir(): string {
56-
return env.localModelPath ?? path.join(process.cwd(), ".cache");
55+
return process.env.TRANSFORMERS_CACHE ?? path.join(process.cwd(), ".cache");
5756
}
5857

5958
function sanitizeModelReference(modelPath: string): string {

src/engines/regex.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ const PATTERNS: PatternDef[] = [
2626
pattern:
2727
/\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,
2828
},
29+
{
30+
label: "SECRET",
31+
pattern:
32+
/(?<![A-Za-z0-9_])(?:secret|client[_-]?secret|app[_-]?secret|appSecret|password|passwd|pwd|private[_-]?key)\s*[:=]\s*["']?[A-Za-z0-9._~+/=\-]{8,}["']?/gi,
33+
},
34+
{
35+
label: "TOKEN",
36+
pattern:
37+
/(?<![A-Za-z0-9_])(?:token|access[_-]?token|refresh[_-]?token|api[_-]?key|authorization)\s*[:=]\s*["']?(?:Bearer\s+)?[A-Za-z0-9._~+/=\-]{8,}["']?/gi,
38+
},
2939
{
3040
label: "IP_ADDRESS",
3141
pattern:

src/message-sending-handler.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/**
22
* Async message_sending hook handler for FogClaw.
33
*
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.
4+
* Scans outbound message text for PII using the Scanner's regex-only path,
5+
* redacts detected entities, and returns modified content. Never cancels
6+
* message delivery.
77
*
88
* Note: message_sending is defined in OpenClaw but not yet invoked
99
* upstream. This handler activates automatically when wired.
@@ -40,9 +40,8 @@ export interface MessageSendingResult {
4040
/**
4141
* Create an async message_sending hook handler.
4242
*
43-
* Uses the full Scanner (regex + GLiNER) since this hook supports
44-
* async handlers. All guardrail modes produce span-level redaction;
45-
* cancel is never returned.
43+
* Uses regex only to keep final reply delivery fast. All guardrail modes
44+
* produce span-level redaction; cancel is never returned.
4645
*/
4746
export function createMessageSendingHandler(
4847
config: FogClawConfig,
@@ -57,7 +56,7 @@ export function createMessageSendingHandler(
5756
const text = event.content;
5857
if (!text) return;
5958

60-
const result = await scanner.scan(text);
59+
const result = scanner.scanRegexOnly(text);
6160
if (result.entities.length === 0) return;
6261

6362
// All modes produce span-level redaction for outbound messages.

src/scanner.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ export class Scanner {
8181
return { entities: merged, text };
8282
}
8383

84+
scanRegexOnly(text: string): { entities: Entity[]; text: string } {
85+
if (!text) return { entities: [], text };
86+
87+
const entities = deduplicateEntities(this.filterByPolicy(this.regexEngine.scan(text)));
88+
return { entities, text };
89+
}
90+
8491
private filterByConfidence(entities: Entity[]): Entity[] {
8592
return entities.filter((entity) => {
8693
const threshold = this.getThresholdForLabel(entity.label);

tests/message-sending-handler.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,42 @@ describe("createMessageSendingHandler", () => {
7979
expect(result!.content).not.toContain("john@example.com");
8080
});
8181

82+
it("uses regex-only scanning to keep outbound delivery fast", async () => {
83+
const config = makeConfig();
84+
const scanner = new Scanner(config);
85+
const fullScan = vi.spyOn(scanner, "scan").mockRejectedValue(new Error("GLiNER path should not run"));
86+
const handler = createMessageSendingHandler(config, scanner);
87+
88+
const result = await handler(
89+
{ to: "user", content: "Send to john@example.com" },
90+
makeCtx(),
91+
);
92+
93+
expect(result).toBeDefined();
94+
expect(result!.content).toContain("[EMAIL_1]");
95+
expect(fullScan).not.toHaveBeenCalled();
96+
});
97+
98+
it("redacts secrets and tokens in outbound message", async () => {
99+
const config = makeConfig();
100+
const scanner = new Scanner(config);
101+
const handler = createMessageSendingHandler(config, scanner);
102+
103+
const result = await handler(
104+
{
105+
to: "user",
106+
content: "secret=abcDEF123456 token: Bearer tok_1234567890",
107+
},
108+
makeCtx(),
109+
);
110+
111+
expect(result).toBeDefined();
112+
expect(result!.content).toContain("[SECRET_1]");
113+
expect(result!.content).toContain("[TOKEN_1]");
114+
expect(result!.content).not.toContain("abcDEF123456");
115+
expect(result!.content).not.toContain("tok_1234567890");
116+
});
117+
82118
it("returns void when no PII found", async () => {
83119
const config = makeConfig();
84120
const scanner = new Scanner(config);

tests/regex.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,28 @@ describe("CREDIT_CARD", () => {
168168
});
169169
});
170170

171+
// ---------------------------------------------------------------------------
172+
// SECRET / TOKEN
173+
// ---------------------------------------------------------------------------
174+
describe("SECRET", () => {
175+
it("detects assigned secrets", () => {
176+
const entities = assertSpans("client_secret=abcDEF123456 and password: hunter2222");
177+
const secrets = entities.filter((e) => e.label === "SECRET");
178+
expect(secrets.map((e) => e.text)).toEqual(["client_secret=abcDEF123456", "password: hunter2222"]);
179+
});
180+
});
181+
182+
describe("TOKEN", () => {
183+
it("detects assigned tokens and bearer values", () => {
184+
const entities = assertSpans("token: Bearer tok_1234567890 api-key=key_1234567890");
185+
const tokens = entities.filter((e) => e.label === "TOKEN");
186+
expect(tokens.map((e) => e.text)).toEqual([
187+
"token: Bearer tok_1234567890",
188+
"api-key=key_1234567890",
189+
]);
190+
});
191+
});
192+
171193
// ---------------------------------------------------------------------------
172194
// IP_ADDRESS
173195
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)