@dsh-edge/dsh-edge-agent 0.0.0-stage → 0.1.1
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 +142 -0
- package/LICENSE +21 -0
- package/README.md +372 -2
- package/config/edge.example.json +179 -0
- package/config/privacy-policy.json +202 -0
- package/cordis.desktop-safe.patch.yml +129 -0
- package/cordis.patch.yml +382 -0
- package/cordis.yml +374 -0
- package/lib/attestation.d.ts +2 -0
- package/lib/attestation.d.ts.map +1 -0
- package/lib/attestation.js +2 -0
- package/lib/audit-log.d.ts +2 -0
- package/lib/audit-log.d.ts.map +1 -0
- package/lib/audit-log.js +2 -0
- package/lib/client.js +87 -0
- package/lib/cloud-llm-adapter.d.ts +2 -0
- package/lib/cloud-llm-adapter.d.ts.map +1 -0
- package/lib/cloud-llm-adapter.js +2 -0
- package/lib/dsh-adapter/attestation.d.ts +42 -0
- package/lib/dsh-adapter/attestation.d.ts.map +1 -0
- package/lib/dsh-adapter/attestation.js +40 -0
- package/lib/dsh-adapter/audit-log.d.ts +139 -0
- package/lib/dsh-adapter/audit-log.d.ts.map +1 -0
- package/lib/dsh-adapter/audit-log.js +376 -0
- package/lib/dsh-adapter/cloud-egress.d.ts +181 -0
- package/lib/dsh-adapter/cloud-egress.d.ts.map +1 -0
- package/lib/dsh-adapter/cloud-egress.js +257 -0
- package/lib/dsh-adapter/cloud-llm-adapter.d.ts +416 -0
- package/lib/dsh-adapter/cloud-llm-adapter.d.ts.map +1 -0
- package/lib/dsh-adapter/cloud-llm-adapter.js +1472 -0
- package/lib/dsh-adapter/contract.d.ts +80 -0
- package/lib/dsh-adapter/contract.d.ts.map +1 -0
- package/lib/dsh-adapter/contract.js +194 -0
- package/lib/dsh-adapter/desktop-cookie-janitor.d.ts +214 -0
- package/lib/dsh-adapter/desktop-cookie-janitor.d.ts.map +1 -0
- package/lib/dsh-adapter/desktop-cookie-janitor.js +409 -0
- package/lib/dsh-adapter/dsh-edge-agent.d.ts +48 -0
- package/lib/dsh-adapter/dsh-edge-agent.d.ts.map +1 -0
- package/lib/dsh-adapter/dsh-edge-agent.js +64 -0
- package/lib/dsh-adapter/edge-config.d.ts +484 -0
- package/lib/dsh-adapter/edge-config.d.ts.map +1 -0
- package/lib/dsh-adapter/edge-config.js +898 -0
- package/lib/dsh-adapter/edge-context-view.d.ts +184 -0
- package/lib/dsh-adapter/edge-context-view.d.ts.map +1 -0
- package/lib/dsh-adapter/edge-context-view.js +483 -0
- package/lib/dsh-adapter/effective-model.d.ts +85 -0
- package/lib/dsh-adapter/effective-model.d.ts.map +1 -0
- package/lib/dsh-adapter/effective-model.js +171 -0
- package/lib/dsh-adapter/etscli-shell-guard.d.ts +244 -0
- package/lib/dsh-adapter/etscli-shell-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/etscli-shell-guard.js +420 -0
- package/lib/dsh-adapter/etscli-tools.d.ts +112 -0
- package/lib/dsh-adapter/etscli-tools.d.ts.map +1 -0
- package/lib/dsh-adapter/etscli-tools.js +393 -0
- package/lib/dsh-adapter/file-logger.d.ts +294 -0
- package/lib/dsh-adapter/file-logger.d.ts.map +1 -0
- package/lib/dsh-adapter/file-logger.js +564 -0
- package/lib/dsh-adapter/function-call-external.d.ts +357 -0
- package/lib/dsh-adapter/function-call-external.d.ts.map +1 -0
- package/lib/dsh-adapter/function-call-external.js +1005 -0
- package/lib/dsh-adapter/host-egress-guard.d.ts +161 -0
- package/lib/dsh-adapter/host-egress-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/host-egress-guard.js +213 -0
- package/lib/dsh-adapter/host-llm-trace.d.ts +198 -0
- package/lib/dsh-adapter/host-llm-trace.d.ts.map +1 -0
- package/lib/dsh-adapter/host-llm-trace.js +344 -0
- package/lib/dsh-adapter/http-api-routes.d.ts +61 -0
- package/lib/dsh-adapter/http-api-routes.d.ts.map +1 -0
- package/lib/dsh-adapter/http-api-routes.js +79 -0
- package/lib/dsh-adapter/http-api-sessions.d.ts +105 -0
- package/lib/dsh-adapter/http-api-sessions.d.ts.map +1 -0
- package/lib/dsh-adapter/http-api-sessions.js +173 -0
- package/lib/dsh-adapter/http-api.d.ts +221 -0
- package/lib/dsh-adapter/http-api.d.ts.map +1 -0
- package/lib/dsh-adapter/http-api.js +1811 -0
- package/lib/dsh-adapter/index.d.ts +62 -0
- package/lib/dsh-adapter/index.d.ts.map +1 -0
- package/lib/dsh-adapter/index.js +97 -0
- package/lib/dsh-adapter/input-guard-messages.d.ts +56 -0
- package/lib/dsh-adapter/input-guard-messages.d.ts.map +1 -0
- package/lib/dsh-adapter/input-guard-messages.js +136 -0
- package/lib/dsh-adapter/input-guard.d.ts +508 -0
- package/lib/dsh-adapter/input-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/input-guard.js +2174 -0
- package/lib/dsh-adapter/intent-prompts.d.ts +77 -0
- package/lib/dsh-adapter/intent-prompts.d.ts.map +1 -0
- package/lib/dsh-adapter/intent-prompts.js +97 -0
- package/lib/dsh-adapter/intent-router.d.ts +710 -0
- package/lib/dsh-adapter/intent-router.d.ts.map +1 -0
- package/lib/dsh-adapter/intent-router.js +2802 -0
- package/lib/dsh-adapter/intent-signals.d.ts +208 -0
- package/lib/dsh-adapter/intent-signals.d.ts.map +1 -0
- package/lib/dsh-adapter/intent-signals.js +391 -0
- package/lib/dsh-adapter/invariant-install.d.ts +16 -0
- package/lib/dsh-adapter/invariant-install.d.ts.map +1 -0
- package/lib/dsh-adapter/invariant-install.js +15 -0
- package/lib/dsh-adapter/invariant.d.ts +16 -0
- package/lib/dsh-adapter/invariant.d.ts.map +1 -0
- package/lib/dsh-adapter/invariant.js +22 -0
- package/lib/dsh-adapter/llm-adapters.d.ts +19 -0
- package/lib/dsh-adapter/llm-adapters.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-adapters.js +16 -0
- package/lib/dsh-adapter/llm-internal-caller.d.ts +92 -0
- package/lib/dsh-adapter/llm-internal-caller.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-internal-caller.js +100 -0
- package/lib/dsh-adapter/llm-max-turns.d.ts +55 -0
- package/lib/dsh-adapter/llm-max-turns.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-max-turns.js +127 -0
- package/lib/dsh-adapter/llm-trace-store.d.ts +131 -0
- package/lib/dsh-adapter/llm-trace-store.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-trace-store.js +205 -0
- package/lib/dsh-adapter/llm-trace.d.ts +450 -0
- package/lib/dsh-adapter/llm-trace.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-trace.js +463 -0
- package/lib/dsh-adapter/local-llm-adapter.d.ts +141 -0
- package/lib/dsh-adapter/local-llm-adapter.d.ts.map +1 -0
- package/lib/dsh-adapter/local-llm-adapter.js +787 -0
- package/lib/dsh-adapter/local-storage.d.ts +96 -0
- package/lib/dsh-adapter/local-storage.d.ts.map +1 -0
- package/lib/dsh-adapter/local-storage.js +198 -0
- package/lib/dsh-adapter/model-router.d.ts +79 -0
- package/lib/dsh-adapter/model-router.d.ts.map +1 -0
- package/lib/dsh-adapter/model-router.js +107 -0
- package/lib/dsh-adapter/ohttp-gateway.d.ts +49 -0
- package/lib/dsh-adapter/ohttp-gateway.d.ts.map +1 -0
- package/lib/dsh-adapter/ohttp-gateway.js +32 -0
- package/lib/dsh-adapter/openai-sse.d.ts +85 -0
- package/lib/dsh-adapter/openai-sse.d.ts.map +1 -0
- package/lib/dsh-adapter/openai-sse.js +462 -0
- package/lib/dsh-adapter/pii-utils.d.ts +96 -0
- package/lib/dsh-adapter/pii-utils.d.ts.map +1 -0
- package/lib/dsh-adapter/pii-utils.js +324 -0
- package/lib/dsh-adapter/plugin-host.d.ts +38 -0
- package/lib/dsh-adapter/plugin-host.d.ts.map +1 -0
- package/lib/dsh-adapter/plugin-host.js +38 -0
- package/lib/dsh-adapter/plugin.d.ts +46 -0
- package/lib/dsh-adapter/plugin.d.ts.map +1 -0
- package/lib/dsh-adapter/plugin.js +100 -0
- package/lib/dsh-adapter/privacy-policy.d.ts +237 -0
- package/lib/dsh-adapter/privacy-policy.d.ts.map +1 -0
- package/lib/dsh-adapter/privacy-policy.js +391 -0
- package/lib/dsh-adapter/prompt-guard.d.ts +152 -0
- package/lib/dsh-adapter/prompt-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/prompt-guard.js +872 -0
- package/lib/dsh-adapter/realm-skill-bridge.d.ts +365 -0
- package/lib/dsh-adapter/realm-skill-bridge.d.ts.map +1 -0
- package/lib/dsh-adapter/realm-skill-bridge.js +1180 -0
- package/lib/dsh-adapter/sanitizer-pipeline.d.ts +140 -0
- package/lib/dsh-adapter/sanitizer-pipeline.d.ts.map +1 -0
- package/lib/dsh-adapter/sanitizer-pipeline.js +431 -0
- package/lib/dsh-adapter/secure-bridge.d.ts +47 -0
- package/lib/dsh-adapter/secure-bridge.d.ts.map +1 -0
- package/lib/dsh-adapter/secure-bridge.js +35 -0
- package/lib/dsh-adapter/session-attribution.d.ts +70 -0
- package/lib/dsh-adapter/session-attribution.d.ts.map +1 -0
- package/lib/dsh-adapter/session-attribution.js +76 -0
- package/lib/dsh-adapter/sessions.d.ts +86 -0
- package/lib/dsh-adapter/sessions.d.ts.map +1 -0
- package/lib/dsh-adapter/sessions.js +179 -0
- package/lib/dsh-adapter/tcm.d.ts +55 -0
- package/lib/dsh-adapter/tcm.d.ts.map +1 -0
- package/lib/dsh-adapter/tcm.js +94 -0
- package/lib/dsh-adapter/td-bus.d.ts +87 -0
- package/lib/dsh-adapter/td-bus.d.ts.map +1 -0
- package/lib/dsh-adapter/td-bus.js +196 -0
- package/lib/dsh-adapter/tool-runner.d.ts +185 -0
- package/lib/dsh-adapter/tool-runner.d.ts.map +1 -0
- package/lib/dsh-adapter/tool-runner.js +271 -0
- package/lib/dsh-adapter/tools.d.ts +118 -0
- package/lib/dsh-adapter/tools.d.ts.map +1 -0
- package/lib/dsh-adapter/tools.js +292 -0
- package/lib/dsh-adapter/types.d.ts +23 -0
- package/lib/dsh-adapter/types.d.ts.map +1 -0
- package/lib/dsh-adapter/types.js +16 -0
- package/lib/dsh-adapter/voice-input.d.ts +30 -0
- package/lib/dsh-adapter/voice-input.d.ts.map +1 -0
- package/lib/dsh-adapter/voice-input.js +34 -0
- package/lib/dsh-adapter/web-search-tavily-bocha.d.ts +152 -0
- package/lib/dsh-adapter/web-search-tavily-bocha.d.ts.map +1 -0
- package/lib/dsh-adapter/web-search-tavily-bocha.js +289 -0
- package/lib/http-api.d.ts +2 -0
- package/lib/http-api.d.ts.map +1 -0
- package/lib/http-api.js +2 -0
- package/lib/index.d.ts +48 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +49 -0
- package/lib/intent-router.d.ts +2 -0
- package/lib/intent-router.d.ts.map +1 -0
- package/lib/intent-router.js +2 -0
- package/lib/llm-adapters.d.ts +2 -0
- package/lib/llm-adapters.d.ts.map +1 -0
- package/lib/llm-adapters.js +2 -0
- package/lib/local-llm-adapter.d.ts +2 -0
- package/lib/local-llm-adapter.d.ts.map +1 -0
- package/lib/local-llm-adapter.js +2 -0
- package/lib/local-storage.d.ts +2 -0
- package/lib/local-storage.d.ts.map +1 -0
- package/lib/local-storage.js +2 -0
- package/lib/model-router.d.ts +2 -0
- package/lib/model-router.d.ts.map +1 -0
- package/lib/model-router.js +2 -0
- package/lib/ohttp-gateway.d.ts +2 -0
- package/lib/ohttp-gateway.d.ts.map +1 -0
- package/lib/ohttp-gateway.js +2 -0
- package/lib/plugin-host.d.ts +2 -0
- package/lib/plugin-host.d.ts.map +1 -0
- package/lib/plugin-host.js +2 -0
- package/lib/privacy-policy.d.ts +2 -0
- package/lib/privacy-policy.d.ts.map +1 -0
- package/lib/privacy-policy.js +2 -0
- package/lib/prompt-guard.d.ts +2 -0
- package/lib/prompt-guard.d.ts.map +1 -0
- package/lib/prompt-guard.js +2 -0
- package/lib/sanitizer-pipeline.d.ts +2 -0
- package/lib/sanitizer-pipeline.d.ts.map +1 -0
- package/lib/sanitizer-pipeline.js +2 -0
- package/lib/secure-bridge.d.ts +2 -0
- package/lib/secure-bridge.d.ts.map +1 -0
- package/lib/secure-bridge.js +2 -0
- package/lib/sessions.d.ts +2 -0
- package/lib/sessions.d.ts.map +1 -0
- package/lib/sessions.js +2 -0
- package/lib/tcm.d.ts +2 -0
- package/lib/tcm.d.ts.map +1 -0
- package/lib/tcm.js +2 -0
- package/lib/td-bus.d.ts +2 -0
- package/lib/td-bus.d.ts.map +1 -0
- package/lib/td-bus.js +2 -0
- package/lib/tools.d.ts +2 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +2 -0
- package/lib/voice-input.d.ts +2 -0
- package/lib/voice-input.d.ts.map +1 -0
- package/lib/voice-input.js +2 -0
- package/package.json +341 -3
- package/presets/agent-preset-edge.json +50 -0
- package/presets/edge.desktop-safe.patch.yml +503 -0
- package/presets/edge.patch.yml +557 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* privacy-policy — engine that loads, validates, and hot-reloads the
|
|
3
|
+
* privacy policy file. The policy controls routing rules, sanitization
|
|
4
|
+
* rules, data classifications, and global constraints.
|
|
5
|
+
*
|
|
6
|
+
* Phase 4 完整实现:
|
|
7
|
+
* - 策略文件加载(JSON)
|
|
8
|
+
* - 路由规则(LOCAL/CLOUD/SANITIZED/REJECT)
|
|
9
|
+
* - 脱敏规则(PII 类型、策略、模板)
|
|
10
|
+
* - 数据分类(public/internal/confidential/secret)
|
|
11
|
+
* - 热更新(文件监控)
|
|
12
|
+
* - 全局约束(云端请求限制等)
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-edge-agent/privacy-policy
|
|
15
|
+
*/
|
|
16
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
17
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
18
|
+
export declare const name = "privacy-policy";
|
|
19
|
+
export declare const inject: readonly ["auditLog"];
|
|
20
|
+
export interface Config {
|
|
21
|
+
policyPath: string;
|
|
22
|
+
hotReload: boolean;
|
|
23
|
+
watchIntervalMs: number;
|
|
24
|
+
}
|
|
25
|
+
export declare const Config: Schema<Config>;
|
|
26
|
+
export interface RoutingRule {
|
|
27
|
+
/** Regex pattern to match against user input. */
|
|
28
|
+
pattern: string;
|
|
29
|
+
/** Target realm for matching requests. */
|
|
30
|
+
realm: 'LOCAL' | 'CLOUD' | 'SANITIZED' | 'REJECT';
|
|
31
|
+
/** Confidence threshold (0-1). */
|
|
32
|
+
confidence: number;
|
|
33
|
+
/** Human-readable rationale. */
|
|
34
|
+
rationale: string;
|
|
35
|
+
}
|
|
36
|
+
export interface SanitizationRule {
|
|
37
|
+
/** Regex pattern to match PII. */
|
|
38
|
+
pattern: string;
|
|
39
|
+
/**
|
|
40
|
+
* 可读的替换示例,仅用于文档与运维展示,**运行时不生效**。
|
|
41
|
+
*
|
|
42
|
+
* 可逆脱敏要求占位符在会话内唯一可还原,因此 `sanitizer-pipeline`
|
|
43
|
+
* 一律自行生成 `[<PIITYPE>_<seq>]` 形式的占位符并忽略本字段。
|
|
44
|
+
* 历史上本字段曾被误认为可配置(改了没有任何效果),现显式标记为
|
|
45
|
+
* 可选 + 非生效(2026-08-30)。不可逆掩码走 `pii-utils.PII_MASKS`。
|
|
46
|
+
*/
|
|
47
|
+
replacement?: string;
|
|
48
|
+
/** Data classification class. */
|
|
49
|
+
dataClass: string;
|
|
50
|
+
/** PII type name (phone, email, etc.). */
|
|
51
|
+
piiType: string;
|
|
52
|
+
/**
|
|
53
|
+
* §7.106 (2026-10-23): keep the first N characters of the match in the
|
|
54
|
+
* OUTBOUND text and mask only the tail. Rationale: when the PII *is* the
|
|
55
|
+
* task payload, full masking cancels the task (measured 14:59: "query the
|
|
56
|
+
* attribution of 17300969670" → cloud received `[PHONE_1]`, the model
|
|
57
|
+
* replied "that is a placeholder", and re-sending the number re-masked it
|
|
58
|
+
* forever). A phone's first 7 digits ARE the number band — they carry the
|
|
59
|
+
* functional signal (province/city + carrier) while the last 4 are the
|
|
60
|
+
* identity. Desanitize restores the tail exactly, so the user's own view
|
|
61
|
+
* still shows the whole number.
|
|
62
|
+
*/
|
|
63
|
+
keepPrefix?: number;
|
|
64
|
+
}
|
|
65
|
+
export interface DataClassification {
|
|
66
|
+
/** Classification class name. */
|
|
67
|
+
class: 'public' | 'internal' | 'confidential' | 'secret';
|
|
68
|
+
/** Default realm for this classification. */
|
|
69
|
+
defaultRealm: 'LOCAL' | 'CLOUD' | 'SANITIZED';
|
|
70
|
+
/** TTL in seconds for cached classifications. */
|
|
71
|
+
ttlSeconds: number;
|
|
72
|
+
}
|
|
73
|
+
export interface GlobalConstraints {
|
|
74
|
+
/** Max cloud requests per minute. */
|
|
75
|
+
maxCloudRequestsPerMinute: number;
|
|
76
|
+
/** Whether to require attestation for cloud requests. */
|
|
77
|
+
requireAttestation: boolean;
|
|
78
|
+
/** Block all requests if TCB is compromised. */
|
|
79
|
+
blockIfTcbCompromised: boolean;
|
|
80
|
+
}
|
|
81
|
+
/** §7.108 (2026-10-24): intent-stage signal tables carried by the policy
|
|
82
|
+
* file (config/privacy-policy.json → stages.intent). All recognition
|
|
83
|
+
* surfaces — routing regex rules, jev intents, LLM categories AND the
|
|
84
|
+
* §7.107 no-web-need gate — merge these, so the operator teaches every
|
|
85
|
+
* tier about a new domain (e.g. 旅游攻略) by editing ONE file. */
|
|
86
|
+
export interface PolicyIntentStages {
|
|
87
|
+
/** APPENDED to the built-in CLOUD keyword list (CLOUD_KEYWORDS_DEFAULT +
|
|
88
|
+
* patch config `stages.intent.cloudKeywords`). A word here means "this
|
|
89
|
+
* may leave the device" — it unlocks both the rules tier and the gate. */
|
|
90
|
+
cloudKeywords?: string[];
|
|
91
|
+
/** APPENDED to the built-in LOCAL entity list (密盒/ets/etscli…). */
|
|
92
|
+
localEntities?: string[];
|
|
93
|
+
/** Non-empty value REPLACES the LOCAL last-resort keyword list (same
|
|
94
|
+
* semantics as the `DSH_EDGE_INTENT_FALLBACK_KEYWORDS` env var). */
|
|
95
|
+
fallbackKeywords?: string[];
|
|
96
|
+
/** §7.124 (2026-10-27): 家庭设备操作正则层的扩展词表。APPEND to the
|
|
97
|
+
* built-in device vocabulary (intent-signals.ts). Hot-reloadable — the
|
|
98
|
+
* intent-router reads this LIVE per classify(). */
|
|
99
|
+
deviceCommands?: {
|
|
100
|
+
devices?: string[];
|
|
101
|
+
actions?: string[];
|
|
102
|
+
};
|
|
103
|
+
/** §7.109 (2026-10-24): semantic unlock threshold. When set, an LLM/jev
|
|
104
|
+
* CLOUD verdict at confidence >= this value passes the §7.107 gate
|
|
105
|
+
* WITHOUT a keyword/regex signal — the operator's declaration of trust
|
|
106
|
+
* in the classifier (semantic-level intent recognition, e.g.
|
|
107
|
+
* "规划黄山两日游怎么安排" needs public info but carries no keyword).
|
|
108
|
+
* Undefined (default) = keyword-only, §7.107 behaviour unchanged. The
|
|
109
|
+
* entity guardrail runs before the gate and still wins; every
|
|
110
|
+
* data-protection layer (sanitizer fail-closed, prompt-guard PII veto,
|
|
111
|
+
* td-bus rate limits) is untouched. */
|
|
112
|
+
semanticCloudMinConfidence?: number;
|
|
113
|
+
}
|
|
114
|
+
export interface PrivacyPolicy {
|
|
115
|
+
version: string;
|
|
116
|
+
/** §7.92.3 (2026-09-23): Intent category specs. Single source of truth
|
|
117
|
+
* shared by:
|
|
118
|
+
* - intent-router.classifyWithLlm() → builds LLM prompt listing each
|
|
119
|
+
* category's `description` (replaces the hardcoded prompt block at
|
|
120
|
+
* intent-router.ts:1046-1048).
|
|
121
|
+
* - intent-router.classifyWithJev() → builds jev-intent → category map
|
|
122
|
+
* by iterating each category's `jevIntents` array (replaces the
|
|
123
|
+
* hardcoded JEV_INTENT_MAP_DEFAULT const).
|
|
124
|
+
* Operators extend by adding entries to either field; unknown jev-intent
|
|
125
|
+
* strings fall through to LLM (defensive). */
|
|
126
|
+
categories?: IntentCategory[];
|
|
127
|
+
/** §7.108 (2026-10-24): operator-configurable intent-stage signal tables
|
|
128
|
+
* (see PolicyIntentStages). Optional — policies predating v1.7.0 simply
|
|
129
|
+
* omit it and the built-in lists apply unchanged. Hot-reloadable. */
|
|
130
|
+
stages?: {
|
|
131
|
+
intent?: PolicyIntentStages;
|
|
132
|
+
};
|
|
133
|
+
routingRules: RoutingRule[];
|
|
134
|
+
sanitizationRules: SanitizationRule[];
|
|
135
|
+
dataClassifications: DataClassification[];
|
|
136
|
+
globalConstraints: GlobalConstraints;
|
|
137
|
+
}
|
|
138
|
+
/** §7.92.3: One row per intent category. Used by both LLM (description)
|
|
139
|
+
* and jev (jevIntents) so they share a single taxonomy. */
|
|
140
|
+
export interface IntentCategory {
|
|
141
|
+
/** Category id (matches Intent.category + RoutingRule.realm). */
|
|
142
|
+
id: 'LOCAL' | 'CLOUD' | 'SANITIZED' | 'REJECT';
|
|
143
|
+
/** Human-readable description used in the LLM classifier prompt
|
|
144
|
+
* (intent-router.classifyWithLlm). Replaces the previous hardcoded
|
|
145
|
+
* "本地文件操作..." / "知识问答..." block. */
|
|
146
|
+
description: string;
|
|
147
|
+
/** Optional example queries — for human readability in the JSON file
|
|
148
|
+
* and future eval suites. NOT used at runtime. */
|
|
149
|
+
examples?: string[];
|
|
150
|
+
/** jev semantic intent labels that should map to this category.
|
|
151
|
+
* intent-router builds a flat `jevIntent → category` lookup at
|
|
152
|
+
* construction time by iterating every category's `jevIntents`. */
|
|
153
|
+
jevIntents: string[];
|
|
154
|
+
}
|
|
155
|
+
export interface PrivacyPolicyService {
|
|
156
|
+
reload(): Promise<void>;
|
|
157
|
+
get(): PrivacyPolicy;
|
|
158
|
+
getRoutingRules(): RoutingRule[];
|
|
159
|
+
getSanitizationRules(): SanitizationRule[];
|
|
160
|
+
getClassifications(): DataClassification[];
|
|
161
|
+
getConstraints(): GlobalConstraints;
|
|
162
|
+
/** §7.92.3: returns the categories array (may be empty if the loaded
|
|
163
|
+
* policy predates v1.6.0). Callers must guard with a built-in
|
|
164
|
+
* fallback when the array is empty. */
|
|
165
|
+
getCategories(): IntentCategory[];
|
|
166
|
+
/** §7.108: the policy file's stages.intent section (undefined for
|
|
167
|
+
* policies predating v1.7.0). intent-router reads this LIVE per
|
|
168
|
+
* classify, so a hot-reload takes effect on the next turn. */
|
|
169
|
+
getIntentStages(): PolicyIntentStages | undefined;
|
|
170
|
+
/** Match input against routing rules; returns first match or null. */
|
|
171
|
+
matchRouting(text: string): RoutingRule | null;
|
|
172
|
+
/** Classify data based on patterns; returns classification class. */
|
|
173
|
+
classifyData(text: string): string;
|
|
174
|
+
on(event: 'policy/loaded', listener: (policy: PrivacyPolicy) => void): void;
|
|
175
|
+
}
|
|
176
|
+
declare module '@deepseek-ai/cordis' {
|
|
177
|
+
interface Context {
|
|
178
|
+
privacyPolicy: PrivacyPolicyServiceImpl;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Policy change listener — variadic on purpose (emit passes per-event
|
|
182
|
+
* payloads); `any[]` keeps call-site variance permissive. */
|
|
183
|
+
export type PolicyListener = (...args: any[]) => unknown;
|
|
184
|
+
export declare class PrivacyPolicyServiceImpl extends Service implements PrivacyPolicyService {
|
|
185
|
+
private config;
|
|
186
|
+
private auditLog;
|
|
187
|
+
private policy;
|
|
188
|
+
private lastMtime;
|
|
189
|
+
private watchTimer;
|
|
190
|
+
private listeners;
|
|
191
|
+
/**
|
|
192
|
+
* HMAC-SHA256 key for policy file signature verification. Read once at
|
|
193
|
+
* construction from `DSH_EDGE_POLICY_HMAC_KEY`. When set, every reload
|
|
194
|
+
* (initial + hot-reload) verifies `<policyPath>.sig` contains
|
|
195
|
+
* `hex(hmac_sha256(policyBody, key))`. Empty/undefined disables the
|
|
196
|
+
* check (dev/CI default) and emits `policy.signing.disabled` audit.
|
|
197
|
+
*/
|
|
198
|
+
private readonly hmacKey;
|
|
199
|
+
constructor(ctx: any, config: Config, auditLog: any);
|
|
200
|
+
/** Start the policy loader and optional hot-reload watcher. */
|
|
201
|
+
start(): Promise<void>;
|
|
202
|
+
/** Stop the hot-reload watcher. */
|
|
203
|
+
stop(): void;
|
|
204
|
+
/** Reload the policy from disk. */
|
|
205
|
+
reload(): Promise<void>;
|
|
206
|
+
/**
|
|
207
|
+
* HMAC-SHA256 verification using `node:crypto.timingSafeEqual`.
|
|
208
|
+
* Returns true iff `sigRaw` (hex) equals HMAC-SHA256(raw, hmacKey).
|
|
209
|
+
* Lenient on whitespace / case in `sigRaw` to keep operator workflow
|
|
210
|
+
* (`sha256sum policy.json | openssl dgst -hmac KEY -hex`) forgiving.
|
|
211
|
+
*/
|
|
212
|
+
private verifySignature;
|
|
213
|
+
/** Latch so the dev/CI "signing disabled" audit fires at most once. */
|
|
214
|
+
private signingDisabledEmitted;
|
|
215
|
+
/** Check if the policy file has been modified and reload if needed. */
|
|
216
|
+
private checkForUpdates;
|
|
217
|
+
get(): PrivacyPolicy;
|
|
218
|
+
getRoutingRules(): RoutingRule[];
|
|
219
|
+
getSanitizationRules(): SanitizationRule[];
|
|
220
|
+
getClassifications(): DataClassification[];
|
|
221
|
+
getConstraints(): GlobalConstraints;
|
|
222
|
+
/** §7.92.3 (2026-09-23): returns the intent categories spec table.
|
|
223
|
+
* Falls back to [] when the loaded policy predates v1.6.0 — callers
|
|
224
|
+
* must guard with their own built-in defaults in that case. */
|
|
225
|
+
getCategories(): IntentCategory[];
|
|
226
|
+
/** §7.108: live view of the policy file's stages.intent section. */
|
|
227
|
+
getIntentStages(): PolicyIntentStages | undefined;
|
|
228
|
+
/** Match input against routing rules; returns first match or null. */
|
|
229
|
+
matchRouting(text: string): RoutingRule | null;
|
|
230
|
+
/** Classify data based on sanitization rules; returns the highest classification. */
|
|
231
|
+
classifyData(text: string): string;
|
|
232
|
+
on(event: string, listener: PolicyListener): void;
|
|
233
|
+
private emit;
|
|
234
|
+
private audit;
|
|
235
|
+
}
|
|
236
|
+
export declare function apply(ctx: any, config: Config): Promise<() => Promise<void>>;
|
|
237
|
+
//# sourceMappingURL=privacy-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"privacy-policy.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/privacy-policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* privacy-policy — engine that loads, validates, and hot-reloads the
|
|
3
|
+
* privacy policy file. The policy controls routing rules, sanitization
|
|
4
|
+
* rules, data classifications, and global constraints.
|
|
5
|
+
*
|
|
6
|
+
* Phase 4 完整实现:
|
|
7
|
+
* - 策略文件加载(JSON)
|
|
8
|
+
* - 路由规则(LOCAL/CLOUD/SANITIZED/REJECT)
|
|
9
|
+
* - 脱敏规则(PII 类型、策略、模板)
|
|
10
|
+
* - 数据分类(public/internal/confidential/secret)
|
|
11
|
+
* - 热更新(文件监控)
|
|
12
|
+
* - 全局约束(云端请求限制等)
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-edge-agent/privacy-policy
|
|
15
|
+
*/
|
|
16
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
17
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
18
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
19
|
+
import { existsSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
22
|
+
import { PII_PATTERNS, PII_ORDER, PII_DATA_CLASS } from './pii-utils.js';
|
|
23
|
+
import { edgeBundleRoot, edgeHome, loadEdgeConfig } from './edge-config.js';
|
|
24
|
+
export const name = 'privacy-policy';
|
|
25
|
+
export const inject = ['auditLog'];
|
|
26
|
+
export const Config = Schema.object({
|
|
27
|
+
// §7.102 "one config file": no defaults / no required — apply() fills
|
|
28
|
+
// policyPath from config/edge.json (privacy.policyPath) or the
|
|
29
|
+
// ~/.dsh-edge-agent default, and hotReload/watch from privacy.*.
|
|
30
|
+
policyPath: Schema.string(),
|
|
31
|
+
hotReload: Schema.boolean(),
|
|
32
|
+
watchIntervalMs: Schema.natural(),
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* Default policy used when no policy file exists or loading fails.
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_POLICY = {
|
|
38
|
+
// §7.92.3 (2026-09-23): bumped from '1.2.0' to '1.6.0' so the policy
|
|
39
|
+
// file's getCategories() returns the new categories[] spec. The hardcoded
|
|
40
|
+
// DEFAULT_POLICY below mirrors the v1.6.0 schema (categories added).
|
|
41
|
+
version: '1.6.0',
|
|
42
|
+
categories: [
|
|
43
|
+
{
|
|
44
|
+
id: 'LOCAL',
|
|
45
|
+
description: '本地文件操作、设备控制、数学计算、系统管理',
|
|
46
|
+
examples: ['帮我读一下文件', '看一下磁盘空间', '计算 1+1'],
|
|
47
|
+
jevIntents: ['设备控制', '文件操作', 'device_control', 'file_ops'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'CLOUD',
|
|
51
|
+
description: '知识问答、聊天、创意写作、需要联网的任务',
|
|
52
|
+
examples: ['今天北京天气', '翻译这段英文', '写一首诗'],
|
|
53
|
+
jevIntents: ['知识问答', '通用咨询', '天气', '翻译', '新闻', '搜索', 'knowledge', 'weather', 'translation', 'news', 'search'],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'SANITIZED',
|
|
57
|
+
description: '包含 PII 需先脱敏后路由(订单查询、账户管理、客服等含个人信息)',
|
|
58
|
+
examples: ['查我的订单状态', '我的账户余额', '我要退款'],
|
|
59
|
+
jevIntents: [
|
|
60
|
+
'订单查询', '订单状态', '物流跟踪', '物流查询', '快递查询',
|
|
61
|
+
'退款申请', '退款查询', '账户管理', '账户余额', '账户信息',
|
|
62
|
+
'客服', '售后服务',
|
|
63
|
+
'order_inquiry', 'refund', 'refund_request', 'logistics', 'account', 'customer_service',
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'REJECT',
|
|
68
|
+
description: '高危 / 违法 / 违规请求(危险系统操作、投诉、法务)',
|
|
69
|
+
examples: ['rm -rf /', '我要投诉', '怎么告别人'],
|
|
70
|
+
jevIntents: ['投诉', '法务咨询', '法律咨询', '举报', 'complaint', 'legal', 'legal_advice'],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
routingRules: [
|
|
74
|
+
// REJECT rules — must come first (first-match-wins)
|
|
75
|
+
// Only business-dangerous operations; attack patterns handled by prompt-guard
|
|
76
|
+
{
|
|
77
|
+
pattern: '(导出|删除|清空|格式化).*(数据库|系统|磁盘|全部数据)',
|
|
78
|
+
realm: 'REJECT',
|
|
79
|
+
confidence: 0.9,
|
|
80
|
+
rationale: 'Dangerous system operation',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
pattern: '(drop\\s+table|rm\\s+-rf|format\\s+[a-z]:|del\\s+/[fs])',
|
|
84
|
+
realm: 'REJECT',
|
|
85
|
+
confidence: 0.95,
|
|
86
|
+
rationale: 'Destructive command pattern',
|
|
87
|
+
},
|
|
88
|
+
// LOCAL rules
|
|
89
|
+
{
|
|
90
|
+
pattern: '(本地|local|文件|file|计算|calculate|读取|写入)',
|
|
91
|
+
realm: 'LOCAL',
|
|
92
|
+
confidence: 0.8,
|
|
93
|
+
rationale: 'Local operations stay on device',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
sanitizationRules: PII_ORDER.map((piiType) => ({
|
|
97
|
+
pattern: PII_PATTERNS[piiType],
|
|
98
|
+
dataClass: PII_DATA_CLASS[piiType],
|
|
99
|
+
piiType,
|
|
100
|
+
// §7.106: band-style PII keeps its coarse prefix in the outbound text
|
|
101
|
+
// (phone: 7-digit number band = attribution signal; id-card: 6-digit
|
|
102
|
+
// region code), only the identifying tail becomes a placeholder.
|
|
103
|
+
// Everything else stays fully masked — a partial prefix is only
|
|
104
|
+
// meaningful for band-structured PII.
|
|
105
|
+
...(piiType === 'phone' ? { keepPrefix: 7 } : piiType === 'id-card' ? { keepPrefix: 6 } : {}),
|
|
106
|
+
})),
|
|
107
|
+
dataClassifications: [
|
|
108
|
+
{ class: 'public', defaultRealm: 'CLOUD', ttlSeconds: 3600 },
|
|
109
|
+
{ class: 'internal', defaultRealm: 'SANITIZED', ttlSeconds: 1800 },
|
|
110
|
+
{ class: 'confidential', defaultRealm: 'LOCAL', ttlSeconds: 900 },
|
|
111
|
+
{ class: 'secret', defaultRealm: 'LOCAL', ttlSeconds: 300 },
|
|
112
|
+
],
|
|
113
|
+
globalConstraints: {
|
|
114
|
+
maxCloudRequestsPerMinute: 20,
|
|
115
|
+
requireAttestation: false,
|
|
116
|
+
blockIfTcbCompromised: true,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
export class PrivacyPolicyServiceImpl extends Service {
|
|
120
|
+
config;
|
|
121
|
+
auditLog;
|
|
122
|
+
policy = DEFAULT_POLICY;
|
|
123
|
+
lastMtime = 0;
|
|
124
|
+
watchTimer;
|
|
125
|
+
listeners = new Map();
|
|
126
|
+
/**
|
|
127
|
+
* HMAC-SHA256 key for policy file signature verification. Read once at
|
|
128
|
+
* construction from `DSH_EDGE_POLICY_HMAC_KEY`. When set, every reload
|
|
129
|
+
* (initial + hot-reload) verifies `<policyPath>.sig` contains
|
|
130
|
+
* `hex(hmac_sha256(policyBody, key))`. Empty/undefined disables the
|
|
131
|
+
* check (dev/CI default) and emits `policy.signing.disabled` audit.
|
|
132
|
+
*/
|
|
133
|
+
hmacKey;
|
|
134
|
+
constructor(ctx, config, auditLog) {
|
|
135
|
+
super(ctx, 'privacyPolicy');
|
|
136
|
+
this.config = config;
|
|
137
|
+
this.auditLog = auditLog;
|
|
138
|
+
this.hmacKey = process.env.DSH_EDGE_POLICY_HMAC_KEY?.trim() || undefined;
|
|
139
|
+
}
|
|
140
|
+
/** Start the policy loader and optional hot-reload watcher. */
|
|
141
|
+
async start() {
|
|
142
|
+
await this.reload();
|
|
143
|
+
if (this.config.hotReload) {
|
|
144
|
+
this.watchTimer = setInterval(() => {
|
|
145
|
+
void this.checkForUpdates();
|
|
146
|
+
}, this.config.watchIntervalMs);
|
|
147
|
+
if (typeof this.watchTimer.unref === 'function')
|
|
148
|
+
this.watchTimer.unref();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Stop the hot-reload watcher. */
|
|
152
|
+
stop() {
|
|
153
|
+
if (this.watchTimer) {
|
|
154
|
+
clearInterval(this.watchTimer);
|
|
155
|
+
this.watchTimer = undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Reload the policy from disk. */
|
|
159
|
+
async reload() {
|
|
160
|
+
const path = this.config.policyPath;
|
|
161
|
+
if (!existsSync(path)) {
|
|
162
|
+
await this.audit('warn', 'policy.missing', { path });
|
|
163
|
+
this.policy = DEFAULT_POLICY;
|
|
164
|
+
this.emit('policy/loaded', this.policy);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const raw = await readFile(path, 'utf-8');
|
|
169
|
+
// Signature verification (fail-closed on tamper). Only runs when
|
|
170
|
+
// DSH_EDGE_POLICY_HMAC_KEY is set. On mismatch: keep the in-memory
|
|
171
|
+
// policy, audit 'security', do NOT update lastMtime so the same
|
|
172
|
+
// tampered mtime does not re-trigger every poll.
|
|
173
|
+
if (this.hmacKey) {
|
|
174
|
+
const sigPath = `${path}.sig`;
|
|
175
|
+
if (!existsSync(sigPath)) {
|
|
176
|
+
await this.audit('security', 'policy.tamper-detected', {
|
|
177
|
+
path, reason: 'signature-file-missing', sigPath,
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
let sigOk = false;
|
|
182
|
+
try {
|
|
183
|
+
const sigRaw = await readFile(sigPath, 'utf-8');
|
|
184
|
+
sigOk = this.verifySignature(raw, sigRaw);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
await this.audit('security', 'policy.tamper-detected', {
|
|
188
|
+
path, reason: 'signature-read-failed', error: err?.message ?? String(err),
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!sigOk) {
|
|
193
|
+
await this.audit('security', 'policy.tamper-detected', {
|
|
194
|
+
path, reason: 'hmac-mismatch',
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// First time we observe the "signing disabled" state — emit once
|
|
201
|
+
// so operators know the dev/CI default is in effect.
|
|
202
|
+
if (!this.signingDisabledEmitted) {
|
|
203
|
+
this.signingDisabledEmitted = true;
|
|
204
|
+
await this.audit('info', 'policy.signing.disabled', {
|
|
205
|
+
hint: 'set DSH_EDGE_POLICY_HMAC_KEY to enable tamper detection',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const parsed = JSON.parse(raw);
|
|
210
|
+
// Basic validation
|
|
211
|
+
if (!parsed.version || !Array.isArray(parsed.routingRules)) {
|
|
212
|
+
throw new Error('Invalid policy structure');
|
|
213
|
+
}
|
|
214
|
+
this.policy = parsed;
|
|
215
|
+
const stats = await stat(path);
|
|
216
|
+
this.lastMtime = stats.mtimeMs;
|
|
217
|
+
await this.audit('info', 'policy.loaded', { path, version: parsed.version });
|
|
218
|
+
this.emit('policy/loaded', this.policy);
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
await this.audit('error', 'policy.load-failed', { path, error: err.message });
|
|
222
|
+
this.policy = DEFAULT_POLICY;
|
|
223
|
+
this.emit('policy/loaded', this.policy);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* HMAC-SHA256 verification using `node:crypto.timingSafeEqual`.
|
|
228
|
+
* Returns true iff `sigRaw` (hex) equals HMAC-SHA256(raw, hmacKey).
|
|
229
|
+
* Lenient on whitespace / case in `sigRaw` to keep operator workflow
|
|
230
|
+
* (`sha256sum policy.json | openssl dgst -hmac KEY -hex`) forgiving.
|
|
231
|
+
*/
|
|
232
|
+
verifySignature(raw, sigRaw) {
|
|
233
|
+
if (!this.hmacKey)
|
|
234
|
+
return true;
|
|
235
|
+
const expected = createHmac('sha256', this.hmacKey).update(raw, 'utf-8').digest();
|
|
236
|
+
const cleaned = sigRaw.trim().replace(/^0x/u, '').replace(/\s+/gu, '').toLowerCase();
|
|
237
|
+
if (!/^[0-9a-f]+$/u.test(cleaned))
|
|
238
|
+
return false;
|
|
239
|
+
if (cleaned.length !== expected.length * 2)
|
|
240
|
+
return false;
|
|
241
|
+
let actual;
|
|
242
|
+
try {
|
|
243
|
+
actual = Buffer.from(cleaned, 'hex');
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
return timingSafeEqual(expected, actual);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** Latch so the dev/CI "signing disabled" audit fires at most once. */
|
|
256
|
+
signingDisabledEmitted = false;
|
|
257
|
+
/** Check if the policy file has been modified and reload if needed. */
|
|
258
|
+
async checkForUpdates() {
|
|
259
|
+
const path = this.config.policyPath;
|
|
260
|
+
if (!existsSync(path))
|
|
261
|
+
return;
|
|
262
|
+
try {
|
|
263
|
+
const stats = await stat(path);
|
|
264
|
+
if (stats.mtimeMs > this.lastMtime) {
|
|
265
|
+
await this.reload();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Ignore stat errors
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
get() {
|
|
273
|
+
return this.policy;
|
|
274
|
+
}
|
|
275
|
+
getRoutingRules() {
|
|
276
|
+
return this.policy.routingRules;
|
|
277
|
+
}
|
|
278
|
+
getSanitizationRules() {
|
|
279
|
+
return this.policy.sanitizationRules;
|
|
280
|
+
}
|
|
281
|
+
getClassifications() {
|
|
282
|
+
return this.policy.dataClassifications;
|
|
283
|
+
}
|
|
284
|
+
getConstraints() {
|
|
285
|
+
return this.policy.globalConstraints;
|
|
286
|
+
}
|
|
287
|
+
/** §7.92.3 (2026-09-23): returns the intent categories spec table.
|
|
288
|
+
* Falls back to [] when the loaded policy predates v1.6.0 — callers
|
|
289
|
+
* must guard with their own built-in defaults in that case. */
|
|
290
|
+
getCategories() {
|
|
291
|
+
return this.policy.categories ?? [];
|
|
292
|
+
}
|
|
293
|
+
/** §7.108: live view of the policy file's stages.intent section. */
|
|
294
|
+
getIntentStages() {
|
|
295
|
+
return this.policy?.stages?.intent;
|
|
296
|
+
}
|
|
297
|
+
/** Match input against routing rules; returns first match or null. */
|
|
298
|
+
matchRouting(text) {
|
|
299
|
+
for (const rule of this.policy.routingRules) {
|
|
300
|
+
try {
|
|
301
|
+
const regex = new RegExp(rule.pattern, 'i');
|
|
302
|
+
if (regex.test(text)) {
|
|
303
|
+
return rule;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// Invalid regex, skip
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
/** Classify data based on sanitization rules; returns the highest classification. */
|
|
313
|
+
classifyData(text) {
|
|
314
|
+
const classOrder = ['public', 'internal', 'confidential', 'secret'];
|
|
315
|
+
let highest = 'public';
|
|
316
|
+
for (const rule of this.policy.sanitizationRules) {
|
|
317
|
+
try {
|
|
318
|
+
const regex = new RegExp(rule.pattern, 'i');
|
|
319
|
+
if (regex.test(text)) {
|
|
320
|
+
const idx = classOrder.indexOf(rule.dataClass);
|
|
321
|
+
const highestIdx = classOrder.indexOf(highest);
|
|
322
|
+
if (idx > highestIdx) {
|
|
323
|
+
highest = rule.dataClass;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// Invalid regex, skip
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return highest;
|
|
332
|
+
}
|
|
333
|
+
on(event, listener) {
|
|
334
|
+
if (!this.listeners.has(event)) {
|
|
335
|
+
this.listeners.set(event, new Set());
|
|
336
|
+
}
|
|
337
|
+
this.listeners.get(event).add(listener);
|
|
338
|
+
}
|
|
339
|
+
emit(event, ...args) {
|
|
340
|
+
const listeners = this.listeners.get(event);
|
|
341
|
+
if (listeners) {
|
|
342
|
+
for (const listener of listeners) {
|
|
343
|
+
try {
|
|
344
|
+
listener(...args);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// Ignore listener errors
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async audit(level, kind, payload) {
|
|
353
|
+
if (!this.auditLog?.log)
|
|
354
|
+
return;
|
|
355
|
+
try {
|
|
356
|
+
await this.auditLog.log({ level, kind, source: 'privacy-policy', payload });
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// Ignore audit errors
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
export async function apply(ctx, config) {
|
|
364
|
+
// §7.102: fill unset fields from config/edge.json (privacy.*).
|
|
365
|
+
const p = loadEdgeConfig().privacy;
|
|
366
|
+
const c = config;
|
|
367
|
+
if (c.policyPath === undefined) {
|
|
368
|
+
// §7.107 (2026-10-23): the operator's copy under ~/.dsh-edge-agent/
|
|
369
|
+
// wins, but a released bundle also ships its own config/privacy-policy
|
|
370
|
+
// .json — without this fallback an npm/desktop install ran on the
|
|
371
|
+
// 3-rule in-code DEFAULT_POLICY (no credential / PII-stays-local
|
|
372
|
+
// routing rules) because nothing ever copied the shipped policy
|
|
373
|
+
// there. The bundle's own copy is the shipped truth; the operator's
|
|
374
|
+
// copy is the override.
|
|
375
|
+
const operatorCopy = edgeHome('privacy-policy.json');
|
|
376
|
+
const shippedCopy = join(edgeBundleRoot(), 'config', 'privacy-policy.json');
|
|
377
|
+
c.policyPath = p?.policyPath && p.policyPath.length > 0
|
|
378
|
+
? p.policyPath
|
|
379
|
+
: existsSync(operatorCopy) ? operatorCopy : shippedCopy;
|
|
380
|
+
}
|
|
381
|
+
if (c.hotReload === undefined && p?.policyHotReload !== undefined)
|
|
382
|
+
c.hotReload = p.policyHotReload;
|
|
383
|
+
if (c.watchIntervalMs === undefined && p?.policyWatchMs !== undefined)
|
|
384
|
+
c.watchIntervalMs = p.policyWatchMs;
|
|
385
|
+
const auditLog = ctx.get('auditLog');
|
|
386
|
+
const service = new PrivacyPolicyServiceImpl(ctx, config, auditLog);
|
|
387
|
+
await service.start();
|
|
388
|
+
return async () => {
|
|
389
|
+
service.stop();
|
|
390
|
+
};
|
|
391
|
+
}
|