@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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ClawXRouter",
|
|
3
|
+
"name": "ClawXRouter",
|
|
4
|
+
"description": "Edge-cloud collaborative routing plugin that keeps sensitive data local and routes tasks to cost-effective models — protects user privacy by classifying requests into three safety levels and redacting PII before any cloud forwarding",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"privacy": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"enabled": { "type": "boolean" },
|
|
13
|
+
"s2Policy": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"enum": ["proxy", "local"],
|
|
16
|
+
"description": "S2 handling policy: 'proxy' strips PII via local HTTP proxy before forwarding to cloud (default), 'local' routes S2 to local model entirely"
|
|
17
|
+
},
|
|
18
|
+
"proxyPort": {
|
|
19
|
+
"type": "number",
|
|
20
|
+
"description": "Port for the privacy proxy HTTP server (default: 8403)"
|
|
21
|
+
},
|
|
22
|
+
"checkpoints": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"properties": {
|
|
25
|
+
"onUserMessage": { "type": "array", "items": { "type": "string" } },
|
|
26
|
+
"onToolCallProposed": { "type": "array", "items": { "type": "string" } },
|
|
27
|
+
"onToolCallExecuted": { "type": "array", "items": { "type": "string" } }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"rules": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"keywords": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"S2": { "type": "array", "items": { "type": "string" } },
|
|
37
|
+
"S3": { "type": "array", "items": { "type": "string" } }
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"patterns": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"description": "Regex patterns for matching sensitive content",
|
|
43
|
+
"properties": {
|
|
44
|
+
"S2": { "type": "array", "items": { "type": "string" } },
|
|
45
|
+
"S3": { "type": "array", "items": { "type": "string" } }
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"tools": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": {
|
|
51
|
+
"S2": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": {
|
|
54
|
+
"tools": { "type": "array", "items": { "type": "string" } },
|
|
55
|
+
"paths": { "type": "array", "items": { "type": "string" } }
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"S3": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
61
|
+
"tools": { "type": "array", "items": { "type": "string" } },
|
|
62
|
+
"paths": { "type": "array", "items": { "type": "string" } }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"localModel": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"properties": {
|
|
72
|
+
"enabled": { "type": "boolean" },
|
|
73
|
+
"provider": { "type": "string" },
|
|
74
|
+
"model": { "type": "string" },
|
|
75
|
+
"endpoint": { "type": "string" }
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"guardAgent": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"id": { "type": "string" },
|
|
82
|
+
"workspace": { "type": "string" },
|
|
83
|
+
"model": { "type": "string" }
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"session": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"properties": {
|
|
89
|
+
"isolateGuardHistory": { "type": "boolean" },
|
|
90
|
+
"baseDir": { "type": "string" }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openbmb/clawxrouter",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "Edge-cloud collaborative routing plugin that keeps sensitive data local and routes tasks to cost-effective models — protects user privacy by classifying requests into three safety levels and redacting PII before any cloud forwarding",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"src/",
|
|
13
|
+
"prompts/",
|
|
14
|
+
"config.example.json",
|
|
15
|
+
"openclaw.plugin.json"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"openclaw",
|
|
19
|
+
"privacy",
|
|
20
|
+
"router",
|
|
21
|
+
"guard-agent",
|
|
22
|
+
"clawxrouter"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/OpenBMB/ClawXRouter"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@sinclair/typebox": "0.34.48",
|
|
31
|
+
"synckit": "^0.11.12",
|
|
32
|
+
"tsx": "^4.21.0"
|
|
33
|
+
},
|
|
34
|
+
"openclaw": {
|
|
35
|
+
"extensions": [
|
|
36
|
+
"./index.ts"
|
|
37
|
+
],
|
|
38
|
+
"compat": {
|
|
39
|
+
"pluginApi": ">=2026.3.22"
|
|
40
|
+
},
|
|
41
|
+
"build": {
|
|
42
|
+
"openclawVersion": "2026.3.28"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"vitest": "^4.1.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[SYSTEM] You are a strict privacy classifier. Output ONLY a single JSON object — nothing else.
|
|
2
|
+
|
|
3
|
+
Classify based on ACTUAL data present in the message. Do NOT speculate about unknown file contents.
|
|
4
|
+
|
|
5
|
+
S3 = PRIVATE (local only, never cloud):
|
|
6
|
+
- Financial: payslip, salary, tax, bank account, SSN, 工资单, 报销单, 税表
|
|
7
|
+
- Medical: health records, diagnoses, prescriptions, lab results, 病历, 体检报告
|
|
8
|
+
- Credentials: passwords, API keys, secrets, tokens, private keys, .env files, config with credentials
|
|
9
|
+
- The message itself CONTAINS or EXPLICITLY MENTIONS the above data → S3
|
|
10
|
+
"evaluate these passwords" → S3
|
|
11
|
+
"check my payslip" → S3
|
|
12
|
+
"summarize the medical record in patient_records.pdf" → S3
|
|
13
|
+
"read my .env file" → S3
|
|
14
|
+
"the secret code is XYZ" → S3
|
|
15
|
+
|
|
16
|
+
S2 = SENSITIVE (redact PII, then send to cloud):
|
|
17
|
+
- Addresses (ANY physical address, 地址, 住址, street, road, apartment, 路, 街, 小区, 弄, 号)
|
|
18
|
+
- Gate/door codes, pickup codes, delivery tracking numbers
|
|
19
|
+
- Phone numbers, email addresses, real names used as contact PII
|
|
20
|
+
- License plates, SSN/ID mixed with personal context, chat logs with PII
|
|
21
|
+
- File content containing the above PII → S2
|
|
22
|
+
- ANY mention of "address"/"地址" with actual location data → S2
|
|
23
|
+
"1847 Elm St, gate code 4523#" → S2
|
|
24
|
+
"我的地址是北京市朝阳区xxx" → S2
|
|
25
|
+
"张伟 手机13912345678" → S2
|
|
26
|
+
"schedule meeting with john@example.com" → S2
|
|
27
|
+
|
|
28
|
+
S1 = SAFE: No sensitive data or intent.
|
|
29
|
+
"write a poem about spring" → S1
|
|
30
|
+
"how to read Excel with pandas" → S1
|
|
31
|
+
"read summary_source.txt and write a summary" → S1
|
|
32
|
+
"read notes.md and answer a question" → S1
|
|
33
|
+
"create a market research report about observability tools" → S1
|
|
34
|
+
"find upcoming tech conferences" → S1
|
|
35
|
+
"create a Python project structure" → S1
|
|
36
|
+
|
|
37
|
+
Rules:
|
|
38
|
+
- Passwords/credentials → ALWAYS S3 (never S2)
|
|
39
|
+
- Medical data → ALWAYS S3 (never S2)
|
|
40
|
+
- Gate/access/pickup codes → S2 (not S3)
|
|
41
|
+
- If file content is provided and contains PII → at least S2
|
|
42
|
+
- Generic file operations (read/write .txt, .md, .csv with NEUTRAL names) → S1 unless the message itself contains PII
|
|
43
|
+
- Do NOT escalate just because a file MIGHT contain sensitive data — only escalate when evidence exists in the message
|
|
44
|
+
- "Read X file and summarize" with no PII in the request → S1
|
|
45
|
+
- "Analyze quarterly_sales.csv" or "company_expenses.xlsx" with NO actual financial PII in the message → S1
|
|
46
|
+
- Tool calls (read, write, exec, shell) within the agent workspace are NORMAL operations → S1 unless parameters explicitly contain credentials or PII
|
|
47
|
+
- Reading config.json, settings.json, database.yml for task purposes → S1 (classify based on actual content, not filename speculation)
|
|
48
|
+
- When genuinely unsure AND the filename/context suggests sensitivity → pick higher level
|
|
49
|
+
|
|
50
|
+
Output format: {"level":"S1|S2|S3","reason":"brief"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
You are a task complexity classifier for an AI coding agent. Classify each task into exactly one of five tiers based on the nature of the work.
|
|
2
|
+
|
|
3
|
+
## Tiers
|
|
4
|
+
|
|
5
|
+
SIMPLE — Pure text transformation. Takes existing text and produces modified text: summarizing a single document, rewriting or humanizing content, simple Q&A, greetings.
|
|
6
|
+
|
|
7
|
+
MEDIUM — Standard agent work (default). Writing emails, coding scripts, data analysis (CSV/Excel), project scaffolding, image generation, factual lookups, researching events or conferences, competitive/market research and analysis reports, search-and-replace, memory management.
|
|
8
|
+
|
|
9
|
+
COMPLEX — Structured multi-item processing. Systematically processes a collection or extracts precise information: triaging or searching through multiple emails, creating multiple files and directories as a structured tree, extracting facts or structured data from documents and reports.
|
|
10
|
+
|
|
11
|
+
RESEARCH — Creative synthesis. Original long-form writing or multi-source combination: blog posts, articles, multi-step workflows (read → code → document), briefings from multiple source files.
|
|
12
|
+
|
|
13
|
+
REASONING — Deep PDF analysis. Reading, understanding, and explaining PDF documents in simplified terms.
|
|
14
|
+
|
|
15
|
+
## Disambiguation
|
|
16
|
+
|
|
17
|
+
- Summarizing ONE text file → SIMPLE; synthesizing MULTIPLE text/research source files into a briefing → RESEARCH.
|
|
18
|
+
- Data analysis (CSV, Excel, spreadsheets) → MEDIUM, regardless of file count.
|
|
19
|
+
- Scaffolding a project or library → MEDIUM; creating multiple files and directories from a spec → COMPLEX.
|
|
20
|
+
- Explaining or simplifying a PDF (ELI5) → REASONING; extracting structured data points from a document → COMPLEX.
|
|
21
|
+
- Market/competitive analysis or event/conference research → MEDIUM.
|
|
22
|
+
- When unsure, choose MEDIUM.
|
|
23
|
+
|
|
24
|
+
CRITICAL: Output ONLY a raw JSON object. No markdown, no explanation.
|
|
25
|
+
{"tier":"SIMPLE|MEDIUM|COMPLEX|RESEARCH|REASONING"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
export const clawXrouterConfigSchema = Type.Object({
|
|
4
|
+
privacy: Type.Optional(
|
|
5
|
+
Type.Object({
|
|
6
|
+
enabled: Type.Optional(Type.Boolean()),
|
|
7
|
+
s2Policy: Type.Optional(
|
|
8
|
+
Type.Union([Type.Literal("proxy"), Type.Literal("local")]),
|
|
9
|
+
),
|
|
10
|
+
proxyPort: Type.Optional(Type.Number()),
|
|
11
|
+
checkpoints: Type.Optional(
|
|
12
|
+
Type.Object({
|
|
13
|
+
onUserMessage: Type.Optional(
|
|
14
|
+
Type.Array(
|
|
15
|
+
Type.Union([Type.Literal("ruleDetector"), Type.Literal("localModelDetector")]),
|
|
16
|
+
),
|
|
17
|
+
),
|
|
18
|
+
onToolCallProposed: Type.Optional(
|
|
19
|
+
Type.Array(
|
|
20
|
+
Type.Union([Type.Literal("ruleDetector"), Type.Literal("localModelDetector")]),
|
|
21
|
+
),
|
|
22
|
+
),
|
|
23
|
+
onToolCallExecuted: Type.Optional(
|
|
24
|
+
Type.Array(
|
|
25
|
+
Type.Union([Type.Literal("ruleDetector"), Type.Literal("localModelDetector")]),
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
rules: Type.Optional(
|
|
31
|
+
Type.Object({
|
|
32
|
+
keywords: Type.Optional(
|
|
33
|
+
Type.Object({
|
|
34
|
+
S2: Type.Optional(Type.Array(Type.String())),
|
|
35
|
+
S3: Type.Optional(Type.Array(Type.String())),
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
patterns: Type.Optional(
|
|
39
|
+
Type.Object({
|
|
40
|
+
S2: Type.Optional(Type.Array(Type.String())),
|
|
41
|
+
S3: Type.Optional(Type.Array(Type.String())),
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
tools: Type.Optional(
|
|
45
|
+
Type.Object({
|
|
46
|
+
S2: Type.Optional(
|
|
47
|
+
Type.Object({
|
|
48
|
+
tools: Type.Optional(Type.Array(Type.String())),
|
|
49
|
+
paths: Type.Optional(Type.Array(Type.String())),
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
S3: Type.Optional(
|
|
53
|
+
Type.Object({
|
|
54
|
+
tools: Type.Optional(Type.Array(Type.String())),
|
|
55
|
+
paths: Type.Optional(Type.Array(Type.String())),
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
localModel: Type.Optional(
|
|
63
|
+
Type.Object({
|
|
64
|
+
enabled: Type.Optional(Type.Boolean()),
|
|
65
|
+
type: Type.Optional(
|
|
66
|
+
Type.Union([
|
|
67
|
+
Type.Literal("openai-compatible"),
|
|
68
|
+
Type.Literal("ollama-native"),
|
|
69
|
+
Type.Literal("custom"),
|
|
70
|
+
]),
|
|
71
|
+
),
|
|
72
|
+
provider: Type.Optional(Type.String()),
|
|
73
|
+
model: Type.Optional(Type.String()),
|
|
74
|
+
endpoint: Type.Optional(Type.String()),
|
|
75
|
+
apiKey: Type.Optional(Type.String()),
|
|
76
|
+
module: Type.Optional(Type.String()),
|
|
77
|
+
}),
|
|
78
|
+
),
|
|
79
|
+
guardAgent: Type.Optional(
|
|
80
|
+
Type.Object({
|
|
81
|
+
id: Type.Optional(Type.String()),
|
|
82
|
+
workspace: Type.Optional(Type.String()),
|
|
83
|
+
model: Type.Optional(Type.String()),
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
localProviders: Type.Optional(Type.Array(Type.String())),
|
|
87
|
+
toolAllowlist: Type.Optional(Type.Array(Type.String())),
|
|
88
|
+
modelPricing: Type.Optional(
|
|
89
|
+
Type.Record(
|
|
90
|
+
Type.String(),
|
|
91
|
+
Type.Object({
|
|
92
|
+
inputPer1M: Type.Optional(Type.Number()),
|
|
93
|
+
outputPer1M: Type.Optional(Type.Number()),
|
|
94
|
+
}),
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
session: Type.Optional(
|
|
98
|
+
Type.Object({
|
|
99
|
+
isolateGuardHistory: Type.Optional(Type.Boolean()),
|
|
100
|
+
baseDir: Type.Optional(Type.String()),
|
|
101
|
+
injectDualHistory: Type.Optional(Type.Boolean()),
|
|
102
|
+
historyLimit: Type.Optional(Type.Number()),
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
routers: Type.Optional(
|
|
106
|
+
Type.Record(
|
|
107
|
+
Type.String(),
|
|
108
|
+
Type.Object({
|
|
109
|
+
enabled: Type.Optional(Type.Boolean()),
|
|
110
|
+
type: Type.Optional(Type.Union([Type.Literal("builtin"), Type.Literal("custom"), Type.Literal("configurable")])),
|
|
111
|
+
module: Type.Optional(Type.String()),
|
|
112
|
+
weight: Type.Optional(Type.Number()),
|
|
113
|
+
options: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
114
|
+
}),
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
pipeline: Type.Optional(
|
|
118
|
+
Type.Object({
|
|
119
|
+
onUserMessage: Type.Optional(Type.Array(Type.String())),
|
|
120
|
+
onToolCallProposed: Type.Optional(Type.Array(Type.String())),
|
|
121
|
+
onToolCallExecuted: Type.Optional(Type.Array(Type.String())),
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
124
|
+
redaction: Type.Optional(
|
|
125
|
+
Type.Object({
|
|
126
|
+
internalIp: Type.Optional(Type.Boolean()),
|
|
127
|
+
email: Type.Optional(Type.Boolean()),
|
|
128
|
+
envVar: Type.Optional(Type.Boolean()),
|
|
129
|
+
creditCard: Type.Optional(Type.Boolean()),
|
|
130
|
+
chinesePhone: Type.Optional(Type.Boolean()),
|
|
131
|
+
chineseId: Type.Optional(Type.Boolean()),
|
|
132
|
+
chineseAddress: Type.Optional(Type.Boolean()),
|
|
133
|
+
pin: Type.Optional(Type.Boolean()),
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export const defaultPrivacyConfig = {
|
|
141
|
+
enabled: true,
|
|
142
|
+
s2Policy: "proxy" as "proxy" | "local",
|
|
143
|
+
proxyPort: 8403,
|
|
144
|
+
checkpoints: {
|
|
145
|
+
onUserMessage: ["ruleDetector" as const, "localModelDetector" as const],
|
|
146
|
+
onToolCallProposed: ["ruleDetector" as const],
|
|
147
|
+
onToolCallExecuted: ["ruleDetector" as const],
|
|
148
|
+
},
|
|
149
|
+
rules: {
|
|
150
|
+
keywords: {
|
|
151
|
+
S2: [] as string[],
|
|
152
|
+
S3: [] as string[],
|
|
153
|
+
},
|
|
154
|
+
patterns: {
|
|
155
|
+
S2: [] as string[],
|
|
156
|
+
S3: [] as string[],
|
|
157
|
+
},
|
|
158
|
+
tools: {
|
|
159
|
+
S2: { tools: [] as string[], paths: [] as string[] },
|
|
160
|
+
S3: { tools: [] as string[], paths: [] as string[] },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
localModel: {
|
|
164
|
+
enabled: true,
|
|
165
|
+
type: "openai-compatible" as const,
|
|
166
|
+
model: "openbmb/minicpm4.1",
|
|
167
|
+
endpoint: "http://localhost:11434",
|
|
168
|
+
},
|
|
169
|
+
guardAgent: {
|
|
170
|
+
id: "guard",
|
|
171
|
+
workspace: "~/.openclaw/workspace-guard",
|
|
172
|
+
model: "ollama/openbmb/minicpm4.1",
|
|
173
|
+
},
|
|
174
|
+
localProviders: [] as string[],
|
|
175
|
+
toolAllowlist: [] as string[],
|
|
176
|
+
modelPricing: {
|
|
177
|
+
"claude-sonnet-4.6": { inputPer1M: 3, outputPer1M: 15 },
|
|
178
|
+
"claude-3.5-sonnet": { inputPer1M: 3, outputPer1M: 15 },
|
|
179
|
+
"claude-3.5-haiku": { inputPer1M: 0.8, outputPer1M: 4 },
|
|
180
|
+
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10 },
|
|
181
|
+
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6 },
|
|
182
|
+
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4 },
|
|
183
|
+
"gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4 },
|
|
184
|
+
"deepseek-chat": { inputPer1M: 0.27, outputPer1M: 1.1 },
|
|
185
|
+
} as Record<string, { inputPer1M?: number; outputPer1M?: number }>,
|
|
186
|
+
redaction: {
|
|
187
|
+
internalIp: false,
|
|
188
|
+
email: false,
|
|
189
|
+
envVar: false,
|
|
190
|
+
creditCard: false,
|
|
191
|
+
chinesePhone: false,
|
|
192
|
+
chineseId: false,
|
|
193
|
+
chineseAddress: false,
|
|
194
|
+
pin: false,
|
|
195
|
+
},
|
|
196
|
+
session: {
|
|
197
|
+
isolateGuardHistory: true,
|
|
198
|
+
baseDir: "~/.openclaw",
|
|
199
|
+
injectDualHistory: true,
|
|
200
|
+
historyLimit: 20,
|
|
201
|
+
},
|
|
202
|
+
routers: {
|
|
203
|
+
privacy: { enabled: true, type: "builtin" as const },
|
|
204
|
+
} as Record<string, { enabled?: boolean; type?: "builtin" | "custom" | "configurable"; module?: string; weight?: number; options?: Record<string, unknown> }>,
|
|
205
|
+
pipeline: {
|
|
206
|
+
onUserMessage: ["privacy"],
|
|
207
|
+
onToolCallProposed: ["privacy"],
|
|
208
|
+
onToolCallExecuted: ["privacy"],
|
|
209
|
+
},
|
|
210
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const HOME_DIR = process.env.HOME ?? "/tmp";
|
|
5
|
+
|
|
6
|
+
export const CLAWXROUTER_CONFIG_PATH = join(
|
|
7
|
+
HOME_DIR,
|
|
8
|
+
".openclaw",
|
|
9
|
+
"clawxrouter.json",
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export function saveClawXrouterConfig(privacy: Record<string, unknown>): void {
|
|
13
|
+
try {
|
|
14
|
+
const dir = join(HOME_DIR, ".openclaw");
|
|
15
|
+
mkdirSync(dir, { recursive: true });
|
|
16
|
+
let existing: Record<string, unknown> = {};
|
|
17
|
+
try {
|
|
18
|
+
existing = JSON.parse(readFileSync(CLAWXROUTER_CONFIG_PATH, "utf-8")) as Record<string, unknown>;
|
|
19
|
+
} catch { /* file may not exist yet */ }
|
|
20
|
+
const updated = { ...existing, privacy };
|
|
21
|
+
writeFileSync(CLAWXROUTER_CONFIG_PATH, JSON.stringify(updated, null, 2), "utf-8");
|
|
22
|
+
} catch {
|
|
23
|
+
// best-effort persistence
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/detector.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Checkpoint,
|
|
3
|
+
DetectionContext,
|
|
4
|
+
DetectionResult,
|
|
5
|
+
DetectorType,
|
|
6
|
+
PrivacyConfig,
|
|
7
|
+
SensitivityLevel
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
import { maxLevel } from "./types.js";
|
|
10
|
+
import { detectByRules } from "./rules.js";
|
|
11
|
+
import { detectByLocalModel } from "./local-model.js";
|
|
12
|
+
import { defaultPrivacyConfig } from "./config-schema.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Main detection function that coordinates all detectors.
|
|
16
|
+
*
|
|
17
|
+
* Accepts either a raw `pluginConfig` (legacy — will merge with defaults)
|
|
18
|
+
* or a pre-merged `PrivacyConfig` via the `resolvedConfig` option to avoid
|
|
19
|
+
* double-merging when called from routers that already merged config.
|
|
20
|
+
*/
|
|
21
|
+
export async function detectSensitivityLevel(
|
|
22
|
+
context: DetectionContext,
|
|
23
|
+
pluginConfig: Record<string, unknown>,
|
|
24
|
+
resolvedConfig?: PrivacyConfig,
|
|
25
|
+
): Promise<DetectionResult> {
|
|
26
|
+
const privacyConfig = resolvedConfig ?? mergeWithDefaults(
|
|
27
|
+
(pluginConfig?.privacy as PrivacyConfig) ?? {},
|
|
28
|
+
defaultPrivacyConfig
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Check if privacy is enabled (skip when dry-run so dashboards get real classification)
|
|
32
|
+
if (privacyConfig.enabled === false && !context.dryRun) {
|
|
33
|
+
return {
|
|
34
|
+
level: "S1",
|
|
35
|
+
levelNumeric: 1,
|
|
36
|
+
reason: "Privacy detection disabled",
|
|
37
|
+
detectorType: "ruleDetector",
|
|
38
|
+
confidence: 1.0,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Get detectors for this checkpoint
|
|
43
|
+
const detectors = getDetectorsForCheckpoint(context.checkpoint, privacyConfig);
|
|
44
|
+
|
|
45
|
+
if (detectors.length === 0) {
|
|
46
|
+
// No detectors configured for this checkpoint, default to S1
|
|
47
|
+
return {
|
|
48
|
+
level: "S1",
|
|
49
|
+
levelNumeric: 1,
|
|
50
|
+
reason: "No detectors configured",
|
|
51
|
+
detectorType: "ruleDetector",
|
|
52
|
+
confidence: 1.0,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Run all configured detectors
|
|
57
|
+
const results = await runDetectors(detectors, context, privacyConfig);
|
|
58
|
+
|
|
59
|
+
// Merge results (take maximum level)
|
|
60
|
+
return mergeDetectionResults(results);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get configured detectors for a specific checkpoint
|
|
65
|
+
*/
|
|
66
|
+
function getDetectorsForCheckpoint(
|
|
67
|
+
checkpoint: Checkpoint,
|
|
68
|
+
config: PrivacyConfig
|
|
69
|
+
): DetectorType[] {
|
|
70
|
+
const checkpoints = config.checkpoints ?? {};
|
|
71
|
+
|
|
72
|
+
switch (checkpoint) {
|
|
73
|
+
case "onUserMessage":
|
|
74
|
+
return checkpoints.onUserMessage ?? ["ruleDetector", "localModelDetector"];
|
|
75
|
+
case "onToolCallProposed":
|
|
76
|
+
return checkpoints.onToolCallProposed ?? ["ruleDetector"];
|
|
77
|
+
case "onToolCallExecuted":
|
|
78
|
+
return checkpoints.onToolCallExecuted ?? ["ruleDetector"];
|
|
79
|
+
default:
|
|
80
|
+
return ["ruleDetector"];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Run detectors and collect results.
|
|
86
|
+
*
|
|
87
|
+
* Short-circuits on S3: once any detector returns S3 (highest level),
|
|
88
|
+
* remaining detectors are skipped — no further detection can raise the
|
|
89
|
+
* level and running an LLM judge for a message that will stay local is
|
|
90
|
+
* both wasteful and a needless exposure of sensitive content.
|
|
91
|
+
*/
|
|
92
|
+
async function runDetectors(
|
|
93
|
+
detectors: DetectorType[],
|
|
94
|
+
context: DetectionContext,
|
|
95
|
+
config: PrivacyConfig
|
|
96
|
+
): Promise<DetectionResult[]> {
|
|
97
|
+
const results: DetectionResult[] = [];
|
|
98
|
+
|
|
99
|
+
for (const detector of detectors) {
|
|
100
|
+
try {
|
|
101
|
+
let result: DetectionResult;
|
|
102
|
+
|
|
103
|
+
switch (detector) {
|
|
104
|
+
case "ruleDetector":
|
|
105
|
+
result = detectByRules(context, config);
|
|
106
|
+
break;
|
|
107
|
+
case "localModelDetector":
|
|
108
|
+
result = await detectByLocalModel(context, config);
|
|
109
|
+
break;
|
|
110
|
+
default:
|
|
111
|
+
console.warn(`[ClawXrouter] Unknown detector type: ${detector}`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
results.push(result);
|
|
116
|
+
|
|
117
|
+
if (result.level === "S3") break;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error(`[ClawXrouter] Detector ${detector} failed:`, err);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Merge multiple detection results into a single result
|
|
128
|
+
* Takes the highest severity level and combines reasons
|
|
129
|
+
*/
|
|
130
|
+
function mergeDetectionResults(results: DetectionResult[]): DetectionResult {
|
|
131
|
+
if (results.length === 0) {
|
|
132
|
+
return {
|
|
133
|
+
level: "S1",
|
|
134
|
+
levelNumeric: 1,
|
|
135
|
+
reason: "No detection results",
|
|
136
|
+
detectorType: "ruleDetector",
|
|
137
|
+
confidence: 0,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (results.length === 1) {
|
|
142
|
+
return results[0];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Find the highest level
|
|
146
|
+
const levels = results.map((r) => r.level);
|
|
147
|
+
const finalLevel = maxLevel(...levels);
|
|
148
|
+
|
|
149
|
+
// Collect reasons from all detectors that contributed to the decision
|
|
150
|
+
const relevantResults = results.filter((r) => r.level === finalLevel);
|
|
151
|
+
const reasons = relevantResults
|
|
152
|
+
.map((r) => r.reason)
|
|
153
|
+
.filter((r): r is string => Boolean(r));
|
|
154
|
+
|
|
155
|
+
// Calculate average confidence
|
|
156
|
+
const confidences = results.map((r) => r.confidence ?? 0.5);
|
|
157
|
+
const avgConfidence = confidences.reduce((a, b) => a + b, 0) / confidences.length;
|
|
158
|
+
|
|
159
|
+
// Determine primary detector type (the one that found the highest level)
|
|
160
|
+
const primaryDetector = relevantResults[0]?.detectorType ?? "ruleDetector";
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
level: finalLevel,
|
|
164
|
+
levelNumeric: results.find((r) => r.level === finalLevel)?.levelNumeric ?? 1,
|
|
165
|
+
reason: reasons.length > 0 ? reasons.join("; ") : undefined,
|
|
166
|
+
detectorType: primaryDetector,
|
|
167
|
+
confidence: avgConfidence,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Merge user config with defaults
|
|
173
|
+
*/
|
|
174
|
+
function mergeWithDefaults(
|
|
175
|
+
userConfig: PrivacyConfig,
|
|
176
|
+
defaults: PrivacyConfig
|
|
177
|
+
): PrivacyConfig {
|
|
178
|
+
return {
|
|
179
|
+
enabled: userConfig.enabled ?? defaults.enabled,
|
|
180
|
+
checkpoints: {
|
|
181
|
+
onUserMessage: userConfig.checkpoints?.onUserMessage ?? defaults.checkpoints?.onUserMessage,
|
|
182
|
+
onToolCallProposed:
|
|
183
|
+
userConfig.checkpoints?.onToolCallProposed ?? defaults.checkpoints?.onToolCallProposed,
|
|
184
|
+
onToolCallExecuted:
|
|
185
|
+
userConfig.checkpoints?.onToolCallExecuted ?? defaults.checkpoints?.onToolCallExecuted,
|
|
186
|
+
},
|
|
187
|
+
rules: {
|
|
188
|
+
keywords: {
|
|
189
|
+
S2: userConfig.rules?.keywords?.S2 ?? defaults.rules?.keywords?.S2,
|
|
190
|
+
S3: userConfig.rules?.keywords?.S3 ?? defaults.rules?.keywords?.S3,
|
|
191
|
+
},
|
|
192
|
+
patterns: {
|
|
193
|
+
S2: userConfig.rules?.patterns?.S2 ?? defaults.rules?.patterns?.S2,
|
|
194
|
+
S3: userConfig.rules?.patterns?.S3 ?? defaults.rules?.patterns?.S3,
|
|
195
|
+
},
|
|
196
|
+
tools: {
|
|
197
|
+
S2: {
|
|
198
|
+
tools: userConfig.rules?.tools?.S2?.tools ?? defaults.rules?.tools?.S2?.tools,
|
|
199
|
+
paths: userConfig.rules?.tools?.S2?.paths ?? defaults.rules?.tools?.S2?.paths,
|
|
200
|
+
},
|
|
201
|
+
S3: {
|
|
202
|
+
tools: userConfig.rules?.tools?.S3?.tools ?? defaults.rules?.tools?.S3?.tools,
|
|
203
|
+
paths: userConfig.rules?.tools?.S3?.paths ?? defaults.rules?.tools?.S3?.paths,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
localModel: {
|
|
208
|
+
enabled: userConfig.localModel?.enabled ?? defaults.localModel?.enabled,
|
|
209
|
+
type: userConfig.localModel?.type ?? defaults.localModel?.type,
|
|
210
|
+
provider: userConfig.localModel?.provider ?? defaults.localModel?.provider,
|
|
211
|
+
model: userConfig.localModel?.model ?? defaults.localModel?.model,
|
|
212
|
+
endpoint: userConfig.localModel?.endpoint ?? defaults.localModel?.endpoint,
|
|
213
|
+
apiKey: userConfig.localModel?.apiKey ?? defaults.localModel?.apiKey,
|
|
214
|
+
module: userConfig.localModel?.module ?? defaults.localModel?.module,
|
|
215
|
+
},
|
|
216
|
+
guardAgent: {
|
|
217
|
+
id: userConfig.guardAgent?.id ?? defaults.guardAgent?.id,
|
|
218
|
+
workspace: userConfig.guardAgent?.workspace ?? defaults.guardAgent?.workspace,
|
|
219
|
+
model: userConfig.guardAgent?.model ?? defaults.guardAgent?.model,
|
|
220
|
+
},
|
|
221
|
+
session: {
|
|
222
|
+
isolateGuardHistory:
|
|
223
|
+
userConfig.session?.isolateGuardHistory ?? defaults.session?.isolateGuardHistory,
|
|
224
|
+
baseDir: userConfig.session?.baseDir ?? defaults.session?.baseDir,
|
|
225
|
+
injectDualHistory:
|
|
226
|
+
userConfig.session?.injectDualHistory ?? defaults.session?.injectDualHistory,
|
|
227
|
+
historyLimit: userConfig.session?.historyLimit ?? defaults.session?.historyLimit,
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|