@openbmb/clawxrouter 1.0.4
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/config.example.json +204 -0
- package/index.ts +398 -0
- package/openclaw.plugin.json +97 -0
- package/package.json +48 -0
- package/prompts/detection-system.md +50 -0
- package/prompts/token-saver-judge.md +25 -0
- package/src/config-schema.ts +210 -0
- package/src/dashboard-config-io.ts +25 -0
- package/src/detector.ts +230 -0
- package/src/guard-agent.ts +86 -0
- package/src/hooks.ts +1428 -0
- package/src/live-config.ts +75 -0
- package/src/llm-desensitize-worker.ts +7 -0
- package/src/llm-detect-worker.ts +7 -0
- package/src/local-model.ts +723 -0
- package/src/memory-isolation.ts +403 -0
- package/src/privacy-proxy.ts +683 -0
- package/src/prompt-loader.ts +101 -0
- package/src/provider.ts +268 -0
- package/src/router-pipeline.ts +380 -0
- package/src/routers/configurable.ts +208 -0
- package/src/routers/privacy.ts +102 -0
- package/src/routers/token-saver.ts +273 -0
- package/src/rules.ts +320 -0
- package/src/session-manager.ts +377 -0
- package/src/session-state.ts +471 -0
- package/src/stats-dashboard.ts +3402 -0
- package/src/sync-desensitize.ts +48 -0
- package/src/sync-detect.ts +49 -0
- package/src/token-stats.ts +358 -0
- package/src/types.ts +269 -0
- package/src/utils.ts +283 -0
- package/src/worker-loader.mjs +25 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
export type SensitivityLevel = "S1" | "S2" | "S3";
|
|
2
|
+
|
|
3
|
+
export type SensitivityLevelNumeric = 1 | 2 | 3;
|
|
4
|
+
|
|
5
|
+
export type DetectorType = "ruleDetector" | "localModelDetector";
|
|
6
|
+
|
|
7
|
+
export type Checkpoint = "onUserMessage" | "onToolCallProposed" | "onToolCallExecuted";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Edge provider API protocol type.
|
|
11
|
+
* - "openai-compatible": POST /v1/chat/completions (Ollama, vLLM, LiteLLM, LocalAI, LMStudio, SGLang โฆ)
|
|
12
|
+
* - "ollama-native": POST /api/chat (Ollama native API, supports streaming natively)
|
|
13
|
+
* - "custom": User-supplied module exporting a callChat function
|
|
14
|
+
*/
|
|
15
|
+
export type EdgeProviderType = "openai-compatible" | "ollama-native" | "custom";
|
|
16
|
+
|
|
17
|
+
export type PrivacyConfig = {
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
/** S2 handling: "proxy" strips PII via local HTTP proxy (default), "local" routes to local model */
|
|
20
|
+
s2Policy?: "proxy" | "local";
|
|
21
|
+
/** Port for the privacy proxy server (default: 8403) */
|
|
22
|
+
proxyPort?: number;
|
|
23
|
+
checkpoints?: {
|
|
24
|
+
onUserMessage?: DetectorType[];
|
|
25
|
+
onToolCallProposed?: DetectorType[];
|
|
26
|
+
onToolCallExecuted?: DetectorType[];
|
|
27
|
+
};
|
|
28
|
+
rules?: {
|
|
29
|
+
keywords?: {
|
|
30
|
+
S2?: string[];
|
|
31
|
+
S3?: string[];
|
|
32
|
+
};
|
|
33
|
+
/** Regex patterns for matching sensitive content (strings are compiled to RegExp) */
|
|
34
|
+
patterns?: {
|
|
35
|
+
S2?: string[];
|
|
36
|
+
S3?: string[];
|
|
37
|
+
};
|
|
38
|
+
tools?: {
|
|
39
|
+
S2?: {
|
|
40
|
+
tools?: string[];
|
|
41
|
+
paths?: string[];
|
|
42
|
+
};
|
|
43
|
+
S3?: {
|
|
44
|
+
tools?: string[];
|
|
45
|
+
paths?: string[];
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
localModel?: {
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
/** API protocol type (default: "openai-compatible") */
|
|
52
|
+
type?: EdgeProviderType;
|
|
53
|
+
/** Provider name for OpenClaw routing (e.g. "ollama", "vllm", "lmstudio") */
|
|
54
|
+
provider?: string;
|
|
55
|
+
model?: string;
|
|
56
|
+
endpoint?: string;
|
|
57
|
+
apiKey?: string;
|
|
58
|
+
/** Path to custom provider module (type="custom" only). Must export callChat(). */
|
|
59
|
+
module?: string;
|
|
60
|
+
};
|
|
61
|
+
guardAgent?: {
|
|
62
|
+
id?: string;
|
|
63
|
+
workspace?: string;
|
|
64
|
+
/** Full model reference in "provider/model" format (e.g. "ollama/llama3.2:3b", "vllm/qwen2.5:7b") */
|
|
65
|
+
model?: string;
|
|
66
|
+
};
|
|
67
|
+
session?: {
|
|
68
|
+
isolateGuardHistory?: boolean;
|
|
69
|
+
/** Base directory for session histories (default: ~/.openclaw) */
|
|
70
|
+
baseDir?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Inject full-track conversation history as context when routing to
|
|
73
|
+
* local models (S3 / S2-local). This replaces the sanitized placeholders
|
|
74
|
+
* ("๐ [Private content]") with actual previous sensitive interactions
|
|
75
|
+
* so the local model has full conversational context.
|
|
76
|
+
* Default: true (when isolateGuardHistory is true)
|
|
77
|
+
*/
|
|
78
|
+
injectDualHistory?: boolean;
|
|
79
|
+
/** Max number of messages to inject from dual-track history (default: 20) */
|
|
80
|
+
historyLimit?: number;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Additional provider names to treat as "local" (safe for S3 routing).
|
|
84
|
+
* Built-in local providers: ollama, llama.cpp, localai, llamafile, lmstudio, vllm, mlx, sglang, tgi.
|
|
85
|
+
* Add custom entries here if you run your own inference backend.
|
|
86
|
+
*/
|
|
87
|
+
localProviders?: string[];
|
|
88
|
+
/**
|
|
89
|
+
* Tool names exempt from privacy pipeline detection and PII redaction.
|
|
90
|
+
* Default: empty (no tools are exempt). Users can opt-in via config.
|
|
91
|
+
*/
|
|
92
|
+
toolAllowlist?: string[];
|
|
93
|
+
/**
|
|
94
|
+
* Per-model pricing for cloud API cost estimation (USD per 1M tokens).
|
|
95
|
+
* Keys are model name strings; lookup tries exact match, then substring match.
|
|
96
|
+
*/
|
|
97
|
+
modelPricing?: Record<string, {
|
|
98
|
+
inputPer1M?: number;
|
|
99
|
+
outputPer1M?: number;
|
|
100
|
+
}>;
|
|
101
|
+
/**
|
|
102
|
+
* Toggle high-false-positive redaction rules individually.
|
|
103
|
+
* All default to false (off) to avoid over-redaction.
|
|
104
|
+
*/
|
|
105
|
+
redaction?: RedactionOptions;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type RedactionOptions = {
|
|
109
|
+
/** Internal IP addresses (10.x, 172.16-31.x, 192.168.x). Default: false */
|
|
110
|
+
internalIp?: boolean;
|
|
111
|
+
/** Email addresses. Default: false */
|
|
112
|
+
email?: boolean;
|
|
113
|
+
/** .env file content (KEY=VALUE lines). Default: false */
|
|
114
|
+
envVar?: boolean;
|
|
115
|
+
/** Credit card number pattern (13-19 digits). Default: false */
|
|
116
|
+
creditCard?: boolean;
|
|
117
|
+
/** Chinese mobile phone number (1[3-9]x 11 digits). Default: false */
|
|
118
|
+
chinesePhone?: boolean;
|
|
119
|
+
/** Chinese ID card number (18 digits / 17+X). Default: false */
|
|
120
|
+
chineseId?: boolean;
|
|
121
|
+
/** Chinese address patterns (็/ๅธ/ๅบ/่ทฏ/ๅท etc.). Default: false */
|
|
122
|
+
chineseAddress?: boolean;
|
|
123
|
+
/** PIN / pin code contextual rule. Default: false */
|
|
124
|
+
pin?: boolean;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type DetectionContext = {
|
|
128
|
+
checkpoint: Checkpoint;
|
|
129
|
+
message?: string;
|
|
130
|
+
toolName?: string;
|
|
131
|
+
toolParams?: Record<string, unknown>;
|
|
132
|
+
toolResult?: unknown;
|
|
133
|
+
sessionKey?: string;
|
|
134
|
+
agentId?: string;
|
|
135
|
+
recentContext?: string[];
|
|
136
|
+
/** When true, routers should skip the `enabled` check (dry-run from dashboard). */
|
|
137
|
+
dryRun?: boolean;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export type DetectionResult = {
|
|
141
|
+
level: SensitivityLevel;
|
|
142
|
+
levelNumeric: SensitivityLevelNumeric;
|
|
143
|
+
reason?: string;
|
|
144
|
+
detectorType: DetectorType;
|
|
145
|
+
confidence?: number;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// โโ Router Pipeline Types โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
149
|
+
|
|
150
|
+
export type RouterAction = "passthrough" | "redirect" | "transform" | "block";
|
|
151
|
+
|
|
152
|
+
export type RouterDecision = {
|
|
153
|
+
level: SensitivityLevel;
|
|
154
|
+
action?: RouterAction;
|
|
155
|
+
target?: {
|
|
156
|
+
provider: string;
|
|
157
|
+
model: string;
|
|
158
|
+
/** Set by pipeline merge when the winning provider (clawxrouter-privacy) differs
|
|
159
|
+
* from the router that originally selected the model.
|
|
160
|
+
* Used by hooks to stash the correct provider endpoint for the proxy. */
|
|
161
|
+
originalProvider?: string;
|
|
162
|
+
};
|
|
163
|
+
/** When action is "transform", the transformed prompt content */
|
|
164
|
+
transformedContent?: string;
|
|
165
|
+
reason?: string;
|
|
166
|
+
confidence?: number;
|
|
167
|
+
routerId?: string;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Interface for pluggable routers.
|
|
172
|
+
* The built-in "privacy" router wraps the existing detector + desensitization logic.
|
|
173
|
+
* Users can implement custom routers (cost optimization, content filtering, etc.)
|
|
174
|
+
* and register them in the pipeline config.
|
|
175
|
+
*/
|
|
176
|
+
export interface ClawXrouterRouter {
|
|
177
|
+
id: string;
|
|
178
|
+
detect(
|
|
179
|
+
context: DetectionContext,
|
|
180
|
+
config: Record<string, unknown>,
|
|
181
|
+
): Promise<RouterDecision>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type RouterRegistration = {
|
|
185
|
+
enabled?: boolean;
|
|
186
|
+
/** "builtin" for privacy/rules, "custom" for user modules, "configurable" for dashboard-created */
|
|
187
|
+
type?: "builtin" | "custom" | "configurable";
|
|
188
|
+
/** Path to custom router module (type="custom" only) */
|
|
189
|
+
module?: string;
|
|
190
|
+
/** Arbitrary config passed to the router's detect() */
|
|
191
|
+
options?: Record<string, unknown>;
|
|
192
|
+
/**
|
|
193
|
+
* Merge weight (0โ100, default 50). Higher weight wins when multiple routers
|
|
194
|
+
* produce non-passthrough decisions at the same sensitivity level.
|
|
195
|
+
* Safety routers (privacy) should use high weights; optimization routers
|
|
196
|
+
* (token-saver) should use lower weights so they only take effect when
|
|
197
|
+
* safety routers pass through.
|
|
198
|
+
*/
|
|
199
|
+
weight?: number;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export type PipelineConfig = {
|
|
203
|
+
onUserMessage?: string[];
|
|
204
|
+
onToolCallProposed?: string[];
|
|
205
|
+
onToolCallExecuted?: string[];
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// โโ Session / History Types โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
209
|
+
|
|
210
|
+
export type SessionPrivacyState = {
|
|
211
|
+
sessionKey: string;
|
|
212
|
+
/** @deprecated Replaced by per-turn currentTurnLevel. Kept for backward compat. */
|
|
213
|
+
isPrivate: boolean;
|
|
214
|
+
highestLevel: SensitivityLevel;
|
|
215
|
+
/** Highest sensitivity level detected in the CURRENT turn (reset each turn). */
|
|
216
|
+
currentTurnLevel: SensitivityLevel;
|
|
217
|
+
detectionHistory: Array<{
|
|
218
|
+
timestamp: number;
|
|
219
|
+
level: SensitivityLevel;
|
|
220
|
+
checkpoint: Checkpoint;
|
|
221
|
+
reason?: string;
|
|
222
|
+
routerId?: string;
|
|
223
|
+
action?: string;
|
|
224
|
+
target?: string;
|
|
225
|
+
loopId?: string;
|
|
226
|
+
}>;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export type LoopMeta = {
|
|
230
|
+
loopId: string;
|
|
231
|
+
sessionKey: string;
|
|
232
|
+
userMessagePreview: string;
|
|
233
|
+
startedAt: number;
|
|
234
|
+
highestLevel: SensitivityLevel;
|
|
235
|
+
routingTier?: string;
|
|
236
|
+
routedModel?: string;
|
|
237
|
+
routerAction?: string;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export function levelToNumeric(level: SensitivityLevel): SensitivityLevelNumeric {
|
|
241
|
+
switch (level) {
|
|
242
|
+
case "S1":
|
|
243
|
+
return 1;
|
|
244
|
+
case "S2":
|
|
245
|
+
return 2;
|
|
246
|
+
case "S3":
|
|
247
|
+
return 3;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function numericToLevel(numeric: SensitivityLevelNumeric): SensitivityLevel {
|
|
252
|
+
switch (numeric) {
|
|
253
|
+
case 1:
|
|
254
|
+
return "S1";
|
|
255
|
+
case 2:
|
|
256
|
+
return "S2";
|
|
257
|
+
case 3:
|
|
258
|
+
return "S3";
|
|
259
|
+
default:
|
|
260
|
+
return "S1";
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function maxLevel(...levels: SensitivityLevel[]): SensitivityLevel {
|
|
265
|
+
if (levels.length === 0) return "S1";
|
|
266
|
+
const numeric = levels.map(levelToNumeric);
|
|
267
|
+
const max = Math.max(...numeric) as SensitivityLevelNumeric;
|
|
268
|
+
return numericToLevel(max);
|
|
269
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize path for comparison (expand ~, resolve relative paths)
|
|
3
|
+
*/
|
|
4
|
+
export function normalizePath(path: string): string {
|
|
5
|
+
if (path.startsWith("~/")) {
|
|
6
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
7
|
+
return path.replace("~", home);
|
|
8
|
+
}
|
|
9
|
+
return path;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if a path matches any of the patterns
|
|
14
|
+
*/
|
|
15
|
+
export function matchesPathPattern(path: string, patterns: string[]): boolean {
|
|
16
|
+
const normalizedPath = normalizePath(path);
|
|
17
|
+
|
|
18
|
+
for (const pattern of patterns) {
|
|
19
|
+
const normalizedPattern = normalizePath(pattern);
|
|
20
|
+
|
|
21
|
+
// Exact match
|
|
22
|
+
if (normalizedPath === normalizedPattern) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Prefix match (directory)
|
|
27
|
+
if (normalizedPath.startsWith(normalizedPattern + "/") ||
|
|
28
|
+
normalizedPath.startsWith(normalizedPattern + "\\")) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Suffix match (file extension)
|
|
33
|
+
if (pattern.startsWith("*") && normalizedPath.endsWith(pattern.slice(1))) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract paths from tool parameters
|
|
43
|
+
*/
|
|
44
|
+
export function extractPathsFromParams(params: Record<string, unknown>): string[] {
|
|
45
|
+
const paths: string[] = [];
|
|
46
|
+
|
|
47
|
+
// Common path parameter names
|
|
48
|
+
const pathKeys = ["path", "file", "filepath", "filename", "dir", "directory", "target", "source"];
|
|
49
|
+
|
|
50
|
+
for (const key of pathKeys) {
|
|
51
|
+
const value = params[key];
|
|
52
|
+
if (typeof value === "string" && value.trim()) {
|
|
53
|
+
paths.push(value.trim());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Extract filesystem paths embedded in command strings
|
|
58
|
+
const commandKeys = ["command", "cmd", "script"];
|
|
59
|
+
for (const key of commandKeys) {
|
|
60
|
+
const value = params[key];
|
|
61
|
+
if (typeof value === "string" && value.trim()) {
|
|
62
|
+
paths.push(...extractPathsFromCommand(value));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Also check nested objects
|
|
67
|
+
for (const value of Object.values(params)) {
|
|
68
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
69
|
+
paths.push(...extractPathsFromParams(value as Record<string, unknown>));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return paths;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract filesystem paths from a shell command string.
|
|
78
|
+
* Matches absolute paths (/...) and home-relative paths (~/).
|
|
79
|
+
*/
|
|
80
|
+
function extractPathsFromCommand(command: string): string[] {
|
|
81
|
+
const pathRegex = /(?:\/[\w.\-]+(?:\/[\w.\-]*)*|~\/[\w.\-]+(?:\/[\w.\-]*)*)/g;
|
|
82
|
+
const matches = command.match(pathRegex);
|
|
83
|
+
return matches ?? [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sanitize sensitive information from text (comprehensive rule-based redaction).
|
|
88
|
+
* Used for S2 desensitization: redact known patterns then forward to cloud.
|
|
89
|
+
*
|
|
90
|
+
* Two-phase approach:
|
|
91
|
+
* Phase 1 โ Pattern-based: well-known formats (SSH keys, API keys, IPs, etc.)
|
|
92
|
+
* Phase 2 โ Context-based: keyword + connecting words + value
|
|
93
|
+
* e.g. "password is in abc123" โ "[REDACTED:PASSWORD]"
|
|
94
|
+
*
|
|
95
|
+
* Some rules are opt-in via `RedactionOptions` to avoid false positives.
|
|
96
|
+
*/
|
|
97
|
+
export function redactSensitiveInfo(text: string, opts?: import("./types.js").RedactionOptions): string {
|
|
98
|
+
let redacted = text;
|
|
99
|
+
|
|
100
|
+
// โโ Phase 1: Pattern-based redaction (always on โ low false-positive) โโโโโ
|
|
101
|
+
|
|
102
|
+
// Redact SSH private key blocks
|
|
103
|
+
redacted = redacted.replace(
|
|
104
|
+
/-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
|
|
105
|
+
"[REDACTED:PRIVATE_KEY]"
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Redact API keys (sk-xxx, key-xxx patterns)
|
|
109
|
+
redacted = redacted.replace(/\b(?:sk|key|token)-[A-Za-z0-9]{16,}\b/g, "[REDACTED:KEY]");
|
|
110
|
+
|
|
111
|
+
// Redact AWS Access Key IDs
|
|
112
|
+
redacted = redacted.replace(/AKIA[0-9A-Z]{16}/g, "[REDACTED:AWS_KEY]");
|
|
113
|
+
|
|
114
|
+
// Redact database connection strings
|
|
115
|
+
redacted = redacted.replace(
|
|
116
|
+
/(?:mysql|postgres|postgresql|mongodb|redis|amqp):\/\/[^\s"']+/gi,
|
|
117
|
+
"[REDACTED:DB_CONNECTION]"
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// โโ Phase 1a: Opt-in pattern rules (off by default to avoid false positives) โโ
|
|
121
|
+
|
|
122
|
+
if (opts?.internalIp) {
|
|
123
|
+
redacted = redacted.replace(
|
|
124
|
+
/\b(?:10|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b/g,
|
|
125
|
+
"[REDACTED:INTERNAL_IP]"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (opts?.email) {
|
|
130
|
+
redacted = redacted.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, "[REDACTED:EMAIL]");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (opts?.envVar) {
|
|
134
|
+
redacted = redacted.replace(
|
|
135
|
+
/^(?:export\s+)?[A-Z_]{2,}=(?:["'])?[^\s"']+(?:["'])?$/gm,
|
|
136
|
+
"[REDACTED:ENV_VAR]"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (opts?.creditCard) {
|
|
141
|
+
redacted = redacted.replace(
|
|
142
|
+
/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{1,7}\b/g,
|
|
143
|
+
"[REDACTED:CARD_NUMBER]"
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// โโ Phase 1b: Chinese PII pattern-based redaction (opt-in) โโโโโโโโโโโโโโโโโ
|
|
148
|
+
|
|
149
|
+
if (opts?.chinesePhone) {
|
|
150
|
+
redacted = redacted.replace(/(?<!\d)1[3-9]\d{9}(?!\d)/g, "[REDACTED:PHONE]");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (opts?.chineseId) {
|
|
154
|
+
redacted = redacted.replace(/(?<!\d)\d{17}[\dXx](?!\d)/g, "[REDACTED:ID]");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Chinese delivery tracking numbers (keyword-gated, low false-positive โ always on)
|
|
158
|
+
redacted = redacted.replace(
|
|
159
|
+
/(?:ๅฟซ้ๅๅท|่ฟๅๅท|ๅไปถ็ )[๏ผ:\s]*[A-Za-z0-9]{6,20}/g,
|
|
160
|
+
"[REDACTED:DELIVERY]"
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Door access codes following keywords (keyword-gated, low false-positive โ always on)
|
|
164
|
+
redacted = redacted.replace(
|
|
165
|
+
/(?:้จ็ฆ็ |้จ็ฆๅฏ็ |้จ้ๅฏ็ |ๅผ้จๅฏ็ )[๏ผ:\s]*[A-Za-z0-9#*]{3,12}/g,
|
|
166
|
+
"[REDACTED:ACCESS_CODE]"
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (opts?.chineseAddress) {
|
|
170
|
+
redacted = redacted.replace(
|
|
171
|
+
/[\u4e00-\u9fa5]{2,}(?:็|ๅธ|ๅบ|ๅฟ|้|่ทฏ|่ก|ๅทท|ๅผ|ๅท|ๆ |ๅนข|ๅฎค|ๆฅผ|ๅๅ
|้จ็)\d*[\u4e00-\u9fa5\d]*/g,
|
|
172
|
+
"[REDACTED:ADDRESS]"
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// โโ Phase 2: Context-based redaction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
177
|
+
// Match: <keyword> <connecting words> <actual value>
|
|
178
|
+
// This catches patterns like "password is abc123", "credit card number is in 12896489bf"
|
|
179
|
+
//
|
|
180
|
+
// Two CONNECT modes:
|
|
181
|
+
// STRICT โ requires a verb (is/are/was) or delimiter (=/:) before the value.
|
|
182
|
+
// Used for broad keywords like "credit card" to avoid false positives.
|
|
183
|
+
// LOOSE โ also accepts a plain space between keyword and value.
|
|
184
|
+
// Used for credential keywords like "password" where the next word is very
|
|
185
|
+
// likely the value.
|
|
186
|
+
|
|
187
|
+
const STRICT_CONNECT = "(?:\\s+(?:is|are|was|were)(?:\\s+(?:in|at|on|of|for))*|\\s*[=:])\\s*";
|
|
188
|
+
const LOOSE_CONNECT = "(?:\\s+(?:is|are|was|were)(?:\\s+(?:in|at|on|of|for))*\\s*|\\s*[=:]\\s*|\\s+)";
|
|
189
|
+
|
|
190
|
+
const contextualRules: Array<{ pattern: RegExp; label: string }> = [
|
|
191
|
+
{
|
|
192
|
+
pattern: new RegExp(`(?:password|passwd|pwd|passcode)${LOOSE_CONNECT}["']?([^\\s"']{2,})["']?`, "gi"),
|
|
193
|
+
label: "PASSWORD",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
pattern: new RegExp(`(?:credit\\s*card|card\\s*(?:number|no\\.?))${STRICT_CONNECT}["']?([^\\s"']{2,})["']?`, "gi"),
|
|
197
|
+
label: "CARD",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
pattern: new RegExp(`(?:api[_\\s]?key|access[_\\s]?key|SECRET_KEY|API_KEY)${LOOSE_CONNECT}["']?([^\\s"']{2,})["']?`, "gi"),
|
|
201
|
+
label: "API_KEY",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
pattern: new RegExp(`(?:secret)${STRICT_CONNECT}["']?([^\\s"']{2,})["']?`, "gi"),
|
|
205
|
+
label: "SECRET",
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
pattern: new RegExp(`(?:(?:auth[_\\s]?)?token|bearer)${LOOSE_CONNECT}["']?([^\\s"']{2,})["']?`, "gi"),
|
|
209
|
+
label: "TOKEN",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
pattern: new RegExp(`(?:credential|cred)s?${LOOSE_CONNECT}["']?([^\\s"']{2,})["']?`, "gi"),
|
|
213
|
+
label: "CREDENTIAL",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
pattern: new RegExp(`(?:ssn|social\\s*security(?:\\s*(?:number|no\\.?))?)${STRICT_CONNECT}["']?([^\\s"']{2,})["']?`, "gi"),
|
|
217
|
+
label: "SSN",
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
if (opts?.pin) {
|
|
222
|
+
contextualRules.push({
|
|
223
|
+
pattern: new RegExp(`(?:pin(?:\\s*(?:code|number))?)${STRICT_CONNECT}["']?([^\\s"']{2,})["']?`, "gi"),
|
|
224
|
+
label: "PIN",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const rule of contextualRules) {
|
|
229
|
+
redacted = redacted.replace(rule.pattern, `[REDACTED:${rule.label}]`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return redacted;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check if a path refers to protected memory/history directories that cloud models should not access.
|
|
237
|
+
*/
|
|
238
|
+
export function isProtectedMemoryPath(filePath: string, baseDir: string = "~/.openclaw"): boolean {
|
|
239
|
+
const normalizedFile = normalizePath(filePath);
|
|
240
|
+
const normalizedBase = normalizePath(baseDir);
|
|
241
|
+
const escapedBase = normalizedBase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
242
|
+
|
|
243
|
+
// Patterns that cloud models must NOT read
|
|
244
|
+
const protectedPaths = [
|
|
245
|
+
`${escapedBase}/agents/[^/]+/sessions/full`,
|
|
246
|
+
`${escapedBase}/[^/]+/MEMORY-FULL\\.md`,
|
|
247
|
+
`${escapedBase}/[^/]+/memory-full`,
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
for (const regexStr of protectedPaths) {
|
|
251
|
+
const regex = new RegExp(`^${regexStr}`);
|
|
252
|
+
if (regex.test(normalizedFile)) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Also check for direct "full" history paths
|
|
258
|
+
if (
|
|
259
|
+
normalizedFile.includes("/sessions/full/") ||
|
|
260
|
+
normalizedFile.includes("/memory-full/") ||
|
|
261
|
+
normalizedFile.endsWith("/MEMORY-FULL.md")
|
|
262
|
+
) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Resolve the default base URL for a provider based on its name and API type.
|
|
271
|
+
*/
|
|
272
|
+
export function resolveDefaultBaseUrl(provider: string, api?: string): string {
|
|
273
|
+
const p = provider.toLowerCase();
|
|
274
|
+
const a = (api ?? "").toLowerCase();
|
|
275
|
+
if (p === "google" || p.includes("gemini") || p.includes("vertex") ||
|
|
276
|
+
a.includes("google") || a.includes("gemini")) {
|
|
277
|
+
return "https://generativelanguage.googleapis.com/v1beta";
|
|
278
|
+
}
|
|
279
|
+
if (p === "anthropic" || a === "anthropic-messages") {
|
|
280
|
+
return "https://api.anthropic.com";
|
|
281
|
+
}
|
|
282
|
+
return "https://api.openai.com/v1";
|
|
283
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ESM resolve hook that maps .js imports to .ts files
|
|
3
|
+
* when the .js file does not exist on disk.
|
|
4
|
+
*
|
|
5
|
+
* Needed because Node.js v25 strips TS types natively but does NOT
|
|
6
|
+
* rewrite ".js" โ ".ts" in import specifiers the way tsx/ts-node do.
|
|
7
|
+
* Worker threads spawned by synckit therefore fail to resolve
|
|
8
|
+
* co-located .ts modules imported via the conventional ".js" extension.
|
|
9
|
+
*/
|
|
10
|
+
import { register } from "node:module";
|
|
11
|
+
|
|
12
|
+
const hooks = [
|
|
13
|
+
"export async function resolve(specifier, context, nextResolve) {",
|
|
14
|
+
" if (specifier.endsWith('.js') && !specifier.startsWith('node:')) {",
|
|
15
|
+
" try {",
|
|
16
|
+
" return await nextResolve(specifier.replace(/\\.js$/, '.ts'), context);",
|
|
17
|
+
" } catch {",
|
|
18
|
+
" // .ts variant not found โ fall through to original specifier",
|
|
19
|
+
" }",
|
|
20
|
+
" }",
|
|
21
|
+
" return nextResolve(specifier, context);",
|
|
22
|
+
"}",
|
|
23
|
+
].join("\n");
|
|
24
|
+
|
|
25
|
+
register("data:text/javascript," + encodeURIComponent(hooks), import.meta.url);
|