@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.
Files changed (238) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/LICENSE +21 -0
  3. package/README.md +372 -2
  4. package/config/edge.example.json +179 -0
  5. package/config/privacy-policy.json +202 -0
  6. package/cordis.desktop-safe.patch.yml +129 -0
  7. package/cordis.patch.yml +382 -0
  8. package/cordis.yml +374 -0
  9. package/lib/attestation.d.ts +2 -0
  10. package/lib/attestation.d.ts.map +1 -0
  11. package/lib/attestation.js +2 -0
  12. package/lib/audit-log.d.ts +2 -0
  13. package/lib/audit-log.d.ts.map +1 -0
  14. package/lib/audit-log.js +2 -0
  15. package/lib/client.js +87 -0
  16. package/lib/cloud-llm-adapter.d.ts +2 -0
  17. package/lib/cloud-llm-adapter.d.ts.map +1 -0
  18. package/lib/cloud-llm-adapter.js +2 -0
  19. package/lib/dsh-adapter/attestation.d.ts +42 -0
  20. package/lib/dsh-adapter/attestation.d.ts.map +1 -0
  21. package/lib/dsh-adapter/attestation.js +40 -0
  22. package/lib/dsh-adapter/audit-log.d.ts +139 -0
  23. package/lib/dsh-adapter/audit-log.d.ts.map +1 -0
  24. package/lib/dsh-adapter/audit-log.js +376 -0
  25. package/lib/dsh-adapter/cloud-egress.d.ts +181 -0
  26. package/lib/dsh-adapter/cloud-egress.d.ts.map +1 -0
  27. package/lib/dsh-adapter/cloud-egress.js +257 -0
  28. package/lib/dsh-adapter/cloud-llm-adapter.d.ts +416 -0
  29. package/lib/dsh-adapter/cloud-llm-adapter.d.ts.map +1 -0
  30. package/lib/dsh-adapter/cloud-llm-adapter.js +1472 -0
  31. package/lib/dsh-adapter/contract.d.ts +80 -0
  32. package/lib/dsh-adapter/contract.d.ts.map +1 -0
  33. package/lib/dsh-adapter/contract.js +194 -0
  34. package/lib/dsh-adapter/desktop-cookie-janitor.d.ts +214 -0
  35. package/lib/dsh-adapter/desktop-cookie-janitor.d.ts.map +1 -0
  36. package/lib/dsh-adapter/desktop-cookie-janitor.js +409 -0
  37. package/lib/dsh-adapter/dsh-edge-agent.d.ts +48 -0
  38. package/lib/dsh-adapter/dsh-edge-agent.d.ts.map +1 -0
  39. package/lib/dsh-adapter/dsh-edge-agent.js +64 -0
  40. package/lib/dsh-adapter/edge-config.d.ts +484 -0
  41. package/lib/dsh-adapter/edge-config.d.ts.map +1 -0
  42. package/lib/dsh-adapter/edge-config.js +898 -0
  43. package/lib/dsh-adapter/edge-context-view.d.ts +184 -0
  44. package/lib/dsh-adapter/edge-context-view.d.ts.map +1 -0
  45. package/lib/dsh-adapter/edge-context-view.js +483 -0
  46. package/lib/dsh-adapter/effective-model.d.ts +85 -0
  47. package/lib/dsh-adapter/effective-model.d.ts.map +1 -0
  48. package/lib/dsh-adapter/effective-model.js +171 -0
  49. package/lib/dsh-adapter/etscli-shell-guard.d.ts +244 -0
  50. package/lib/dsh-adapter/etscli-shell-guard.d.ts.map +1 -0
  51. package/lib/dsh-adapter/etscli-shell-guard.js +420 -0
  52. package/lib/dsh-adapter/etscli-tools.d.ts +112 -0
  53. package/lib/dsh-adapter/etscli-tools.d.ts.map +1 -0
  54. package/lib/dsh-adapter/etscli-tools.js +393 -0
  55. package/lib/dsh-adapter/file-logger.d.ts +294 -0
  56. package/lib/dsh-adapter/file-logger.d.ts.map +1 -0
  57. package/lib/dsh-adapter/file-logger.js +564 -0
  58. package/lib/dsh-adapter/function-call-external.d.ts +357 -0
  59. package/lib/dsh-adapter/function-call-external.d.ts.map +1 -0
  60. package/lib/dsh-adapter/function-call-external.js +1005 -0
  61. package/lib/dsh-adapter/host-egress-guard.d.ts +161 -0
  62. package/lib/dsh-adapter/host-egress-guard.d.ts.map +1 -0
  63. package/lib/dsh-adapter/host-egress-guard.js +213 -0
  64. package/lib/dsh-adapter/host-llm-trace.d.ts +198 -0
  65. package/lib/dsh-adapter/host-llm-trace.d.ts.map +1 -0
  66. package/lib/dsh-adapter/host-llm-trace.js +344 -0
  67. package/lib/dsh-adapter/http-api-routes.d.ts +61 -0
  68. package/lib/dsh-adapter/http-api-routes.d.ts.map +1 -0
  69. package/lib/dsh-adapter/http-api-routes.js +79 -0
  70. package/lib/dsh-adapter/http-api-sessions.d.ts +105 -0
  71. package/lib/dsh-adapter/http-api-sessions.d.ts.map +1 -0
  72. package/lib/dsh-adapter/http-api-sessions.js +173 -0
  73. package/lib/dsh-adapter/http-api.d.ts +221 -0
  74. package/lib/dsh-adapter/http-api.d.ts.map +1 -0
  75. package/lib/dsh-adapter/http-api.js +1811 -0
  76. package/lib/dsh-adapter/index.d.ts +62 -0
  77. package/lib/dsh-adapter/index.d.ts.map +1 -0
  78. package/lib/dsh-adapter/index.js +97 -0
  79. package/lib/dsh-adapter/input-guard-messages.d.ts +56 -0
  80. package/lib/dsh-adapter/input-guard-messages.d.ts.map +1 -0
  81. package/lib/dsh-adapter/input-guard-messages.js +136 -0
  82. package/lib/dsh-adapter/input-guard.d.ts +508 -0
  83. package/lib/dsh-adapter/input-guard.d.ts.map +1 -0
  84. package/lib/dsh-adapter/input-guard.js +2174 -0
  85. package/lib/dsh-adapter/intent-prompts.d.ts +77 -0
  86. package/lib/dsh-adapter/intent-prompts.d.ts.map +1 -0
  87. package/lib/dsh-adapter/intent-prompts.js +97 -0
  88. package/lib/dsh-adapter/intent-router.d.ts +710 -0
  89. package/lib/dsh-adapter/intent-router.d.ts.map +1 -0
  90. package/lib/dsh-adapter/intent-router.js +2802 -0
  91. package/lib/dsh-adapter/intent-signals.d.ts +208 -0
  92. package/lib/dsh-adapter/intent-signals.d.ts.map +1 -0
  93. package/lib/dsh-adapter/intent-signals.js +391 -0
  94. package/lib/dsh-adapter/invariant-install.d.ts +16 -0
  95. package/lib/dsh-adapter/invariant-install.d.ts.map +1 -0
  96. package/lib/dsh-adapter/invariant-install.js +15 -0
  97. package/lib/dsh-adapter/invariant.d.ts +16 -0
  98. package/lib/dsh-adapter/invariant.d.ts.map +1 -0
  99. package/lib/dsh-adapter/invariant.js +22 -0
  100. package/lib/dsh-adapter/llm-adapters.d.ts +19 -0
  101. package/lib/dsh-adapter/llm-adapters.d.ts.map +1 -0
  102. package/lib/dsh-adapter/llm-adapters.js +16 -0
  103. package/lib/dsh-adapter/llm-internal-caller.d.ts +92 -0
  104. package/lib/dsh-adapter/llm-internal-caller.d.ts.map +1 -0
  105. package/lib/dsh-adapter/llm-internal-caller.js +100 -0
  106. package/lib/dsh-adapter/llm-max-turns.d.ts +55 -0
  107. package/lib/dsh-adapter/llm-max-turns.d.ts.map +1 -0
  108. package/lib/dsh-adapter/llm-max-turns.js +127 -0
  109. package/lib/dsh-adapter/llm-trace-store.d.ts +131 -0
  110. package/lib/dsh-adapter/llm-trace-store.d.ts.map +1 -0
  111. package/lib/dsh-adapter/llm-trace-store.js +205 -0
  112. package/lib/dsh-adapter/llm-trace.d.ts +450 -0
  113. package/lib/dsh-adapter/llm-trace.d.ts.map +1 -0
  114. package/lib/dsh-adapter/llm-trace.js +463 -0
  115. package/lib/dsh-adapter/local-llm-adapter.d.ts +141 -0
  116. package/lib/dsh-adapter/local-llm-adapter.d.ts.map +1 -0
  117. package/lib/dsh-adapter/local-llm-adapter.js +787 -0
  118. package/lib/dsh-adapter/local-storage.d.ts +96 -0
  119. package/lib/dsh-adapter/local-storage.d.ts.map +1 -0
  120. package/lib/dsh-adapter/local-storage.js +198 -0
  121. package/lib/dsh-adapter/model-router.d.ts +79 -0
  122. package/lib/dsh-adapter/model-router.d.ts.map +1 -0
  123. package/lib/dsh-adapter/model-router.js +107 -0
  124. package/lib/dsh-adapter/ohttp-gateway.d.ts +49 -0
  125. package/lib/dsh-adapter/ohttp-gateway.d.ts.map +1 -0
  126. package/lib/dsh-adapter/ohttp-gateway.js +32 -0
  127. package/lib/dsh-adapter/openai-sse.d.ts +85 -0
  128. package/lib/dsh-adapter/openai-sse.d.ts.map +1 -0
  129. package/lib/dsh-adapter/openai-sse.js +462 -0
  130. package/lib/dsh-adapter/pii-utils.d.ts +96 -0
  131. package/lib/dsh-adapter/pii-utils.d.ts.map +1 -0
  132. package/lib/dsh-adapter/pii-utils.js +324 -0
  133. package/lib/dsh-adapter/plugin-host.d.ts +38 -0
  134. package/lib/dsh-adapter/plugin-host.d.ts.map +1 -0
  135. package/lib/dsh-adapter/plugin-host.js +38 -0
  136. package/lib/dsh-adapter/plugin.d.ts +46 -0
  137. package/lib/dsh-adapter/plugin.d.ts.map +1 -0
  138. package/lib/dsh-adapter/plugin.js +100 -0
  139. package/lib/dsh-adapter/privacy-policy.d.ts +237 -0
  140. package/lib/dsh-adapter/privacy-policy.d.ts.map +1 -0
  141. package/lib/dsh-adapter/privacy-policy.js +391 -0
  142. package/lib/dsh-adapter/prompt-guard.d.ts +152 -0
  143. package/lib/dsh-adapter/prompt-guard.d.ts.map +1 -0
  144. package/lib/dsh-adapter/prompt-guard.js +872 -0
  145. package/lib/dsh-adapter/realm-skill-bridge.d.ts +365 -0
  146. package/lib/dsh-adapter/realm-skill-bridge.d.ts.map +1 -0
  147. package/lib/dsh-adapter/realm-skill-bridge.js +1180 -0
  148. package/lib/dsh-adapter/sanitizer-pipeline.d.ts +140 -0
  149. package/lib/dsh-adapter/sanitizer-pipeline.d.ts.map +1 -0
  150. package/lib/dsh-adapter/sanitizer-pipeline.js +431 -0
  151. package/lib/dsh-adapter/secure-bridge.d.ts +47 -0
  152. package/lib/dsh-adapter/secure-bridge.d.ts.map +1 -0
  153. package/lib/dsh-adapter/secure-bridge.js +35 -0
  154. package/lib/dsh-adapter/session-attribution.d.ts +70 -0
  155. package/lib/dsh-adapter/session-attribution.d.ts.map +1 -0
  156. package/lib/dsh-adapter/session-attribution.js +76 -0
  157. package/lib/dsh-adapter/sessions.d.ts +86 -0
  158. package/lib/dsh-adapter/sessions.d.ts.map +1 -0
  159. package/lib/dsh-adapter/sessions.js +179 -0
  160. package/lib/dsh-adapter/tcm.d.ts +55 -0
  161. package/lib/dsh-adapter/tcm.d.ts.map +1 -0
  162. package/lib/dsh-adapter/tcm.js +94 -0
  163. package/lib/dsh-adapter/td-bus.d.ts +87 -0
  164. package/lib/dsh-adapter/td-bus.d.ts.map +1 -0
  165. package/lib/dsh-adapter/td-bus.js +196 -0
  166. package/lib/dsh-adapter/tool-runner.d.ts +185 -0
  167. package/lib/dsh-adapter/tool-runner.d.ts.map +1 -0
  168. package/lib/dsh-adapter/tool-runner.js +271 -0
  169. package/lib/dsh-adapter/tools.d.ts +118 -0
  170. package/lib/dsh-adapter/tools.d.ts.map +1 -0
  171. package/lib/dsh-adapter/tools.js +292 -0
  172. package/lib/dsh-adapter/types.d.ts +23 -0
  173. package/lib/dsh-adapter/types.d.ts.map +1 -0
  174. package/lib/dsh-adapter/types.js +16 -0
  175. package/lib/dsh-adapter/voice-input.d.ts +30 -0
  176. package/lib/dsh-adapter/voice-input.d.ts.map +1 -0
  177. package/lib/dsh-adapter/voice-input.js +34 -0
  178. package/lib/dsh-adapter/web-search-tavily-bocha.d.ts +152 -0
  179. package/lib/dsh-adapter/web-search-tavily-bocha.d.ts.map +1 -0
  180. package/lib/dsh-adapter/web-search-tavily-bocha.js +289 -0
  181. package/lib/http-api.d.ts +2 -0
  182. package/lib/http-api.d.ts.map +1 -0
  183. package/lib/http-api.js +2 -0
  184. package/lib/index.d.ts +48 -0
  185. package/lib/index.d.ts.map +1 -0
  186. package/lib/index.js +49 -0
  187. package/lib/intent-router.d.ts +2 -0
  188. package/lib/intent-router.d.ts.map +1 -0
  189. package/lib/intent-router.js +2 -0
  190. package/lib/llm-adapters.d.ts +2 -0
  191. package/lib/llm-adapters.d.ts.map +1 -0
  192. package/lib/llm-adapters.js +2 -0
  193. package/lib/local-llm-adapter.d.ts +2 -0
  194. package/lib/local-llm-adapter.d.ts.map +1 -0
  195. package/lib/local-llm-adapter.js +2 -0
  196. package/lib/local-storage.d.ts +2 -0
  197. package/lib/local-storage.d.ts.map +1 -0
  198. package/lib/local-storage.js +2 -0
  199. package/lib/model-router.d.ts +2 -0
  200. package/lib/model-router.d.ts.map +1 -0
  201. package/lib/model-router.js +2 -0
  202. package/lib/ohttp-gateway.d.ts +2 -0
  203. package/lib/ohttp-gateway.d.ts.map +1 -0
  204. package/lib/ohttp-gateway.js +2 -0
  205. package/lib/plugin-host.d.ts +2 -0
  206. package/lib/plugin-host.d.ts.map +1 -0
  207. package/lib/plugin-host.js +2 -0
  208. package/lib/privacy-policy.d.ts +2 -0
  209. package/lib/privacy-policy.d.ts.map +1 -0
  210. package/lib/privacy-policy.js +2 -0
  211. package/lib/prompt-guard.d.ts +2 -0
  212. package/lib/prompt-guard.d.ts.map +1 -0
  213. package/lib/prompt-guard.js +2 -0
  214. package/lib/sanitizer-pipeline.d.ts +2 -0
  215. package/lib/sanitizer-pipeline.d.ts.map +1 -0
  216. package/lib/sanitizer-pipeline.js +2 -0
  217. package/lib/secure-bridge.d.ts +2 -0
  218. package/lib/secure-bridge.d.ts.map +1 -0
  219. package/lib/secure-bridge.js +2 -0
  220. package/lib/sessions.d.ts +2 -0
  221. package/lib/sessions.d.ts.map +1 -0
  222. package/lib/sessions.js +2 -0
  223. package/lib/tcm.d.ts +2 -0
  224. package/lib/tcm.d.ts.map +1 -0
  225. package/lib/tcm.js +2 -0
  226. package/lib/td-bus.d.ts +2 -0
  227. package/lib/td-bus.d.ts.map +1 -0
  228. package/lib/td-bus.js +2 -0
  229. package/lib/tools.d.ts +2 -0
  230. package/lib/tools.d.ts.map +1 -0
  231. package/lib/tools.js +2 -0
  232. package/lib/voice-input.d.ts +2 -0
  233. package/lib/voice-input.d.ts.map +1 -0
  234. package/lib/voice-input.js +2 -0
  235. package/package.json +341 -3
  236. package/presets/agent-preset-edge.json +50 -0
  237. package/presets/edge.desktop-safe.patch.yml +503 -0
  238. package/presets/edge.patch.yml +557 -0
@@ -0,0 +1,42 @@
1
+ /**
2
+ * attestation — generates and verifies TEE attestation tokens. Used by
3
+ * the cloud to confirm the device it is talking to is running a known
4
+ * TCB (Trusted Computing Base).
5
+ *
6
+ * @module dsh-edge-agent/attestation
7
+ */
8
+ import { Service } from '@deepseek-ai/cordis';
9
+ import Schema from '@deepseek-ai/schemastery';
10
+ export declare const name = "attestation";
11
+ export declare const inject: readonly ["tcm", "auditLog"];
12
+ export interface Config {
13
+ /** Attestation service URL (when omitted, only local generation is supported). */
14
+ verifierUrl?: string;
15
+ /** Local nonce cache TTL. */
16
+ nonceTtlSeconds: number;
17
+ }
18
+ export declare const Config: Schema<Config>;
19
+ export interface AttestationVerifyResult {
20
+ valid: boolean;
21
+ teeType: string;
22
+ platform: string;
23
+ issuedAt: number;
24
+ expiresAt: number;
25
+ policyId: string;
26
+ }
27
+ export interface AttestationService {
28
+ generateToken(nonce: string): Promise<string>;
29
+ verifyToken(token: string): Promise<AttestationVerifyResult>;
30
+ }
31
+ export declare class AttestationServiceImpl extends Service {
32
+ constructor(ctx: any);
33
+ generateToken(nonce: string): Promise<string>;
34
+ verifyToken(token: string): Promise<AttestationVerifyResult>;
35
+ }
36
+ declare module '@deepseek-ai/cordis' {
37
+ interface Context {
38
+ attestation: AttestationServiceImpl;
39
+ }
40
+ }
41
+ export declare function apply(ctx: any, config: Config): Promise<() => Promise<void>>;
42
+ //# sourceMappingURL=attestation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/attestation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAC7C,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAE7C,eAAO,MAAM,IAAI,gBAAgB,CAAA;AAEjC,eAAO,MAAM,MAAM,8BAA+B,CAAA;AAElD,MAAM,WAAW,MAAM;IACrB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6BAA6B;IAC7B,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CAGhC,CAAA;AAEF,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC7C,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAA;CAC7D;AAED,qBAAa,sBAAuB,SAAQ,OAAO;gBACrC,GAAG,EAAE,GAAG;IAGd,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI7C,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC;CAWnE;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,WAAW,EAAE,sBAAsB,CAAA;KACpC;CACF;AAED,wBAAsB,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAIlF"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * attestation — generates and verifies TEE attestation tokens. Used by
3
+ * the cloud to confirm the device it is talking to is running a known
4
+ * TCB (Trusted Computing Base).
5
+ *
6
+ * @module dsh-edge-agent/attestation
7
+ */
8
+ import { Service } from '@deepseek-ai/cordis';
9
+ import Schema from '@deepseek-ai/schemastery';
10
+ export const name = 'attestation';
11
+ export const inject = ['tcm', 'auditLog'];
12
+ export const Config = Schema.object({
13
+ verifierUrl: Schema.string(),
14
+ nonceTtlSeconds: Schema.natural().default(60),
15
+ });
16
+ export class AttestationServiceImpl extends Service {
17
+ constructor(ctx) {
18
+ super(ctx, 'attestation');
19
+ }
20
+ async generateToken(nonce) {
21
+ void nonce;
22
+ return '';
23
+ }
24
+ async verifyToken(token) {
25
+ void token;
26
+ return {
27
+ valid: false,
28
+ teeType: 'simulated',
29
+ platform: 'unknown',
30
+ issuedAt: 0,
31
+ expiresAt: 0,
32
+ policyId: '',
33
+ };
34
+ }
35
+ }
36
+ export async function apply(ctx, config) {
37
+ ctx.plugin(AttestationServiceImpl);
38
+ void config;
39
+ return async () => { };
40
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * audit-log — M3 PLAINTEXT append-only log with an in-memory ring buffer.
3
+ * Each entry is one JSON line:
4
+ * { ts, level, kind, source, payload }
5
+ *
6
+ * Phase 2 增强:
7
+ * - 敏感字段自动脱敏(手机号、身份证、银行卡、邮箱、API Key)
8
+ * - 日志轮转(按大小/时间)
9
+ *
10
+ * M3 NOTE — A3 swaps the on-disk format for HMAC-chained tamper-evident
11
+ * entries; the public surface (log / query / getSecurityReport) is the
12
+ * durable contract.
13
+ *
14
+ * @module dsh-edge-agent/audit-log
15
+ */
16
+ import type { Context } from '@deepseek-ai/cordis';
17
+ import { Service } from '@deepseek-ai/cordis';
18
+ import Schema from '@deepseek-ai/schemastery';
19
+ export declare const name = "audit-log";
20
+ export declare const inject: readonly [];
21
+ export type AuditLevel = 'debug' | 'info' | 'warn' | 'error' | 'security';
22
+ export interface Config {
23
+ /** Drop entries below this level. */
24
+ minLevel: AuditLevel;
25
+ /** Maximum entries kept in memory; older entries still live on disk. */
26
+ ringBufferSize: number;
27
+ /** Flush the ring buffer to disk every N ms. */
28
+ flushIntervalMs: number;
29
+ /** Filename inside `root` for the persistent log. */
30
+ filename: string;
31
+ /** Directory holding the audit log file. Created if missing. */
32
+ root: string;
33
+ /** Enable PII redaction in payload fields. */
34
+ enablePIIRedaction: boolean;
35
+ /** Rotate log when file exceeds this size (bytes). 0 = disabled. */
36
+ maxFileSizeBytes: number;
37
+ /** Rotate log when file age exceeds this (days). 0 = disabled. */
38
+ maxFileAgeDays: number;
39
+ /** Maximum number of rotated files to keep. */
40
+ maxRotatedFiles: number;
41
+ }
42
+ export declare const Config: Schema<Config>;
43
+ export interface AuditEntry {
44
+ ts: number;
45
+ level: AuditLevel;
46
+ kind: string;
47
+ source: string;
48
+ payload: Record<string, unknown>;
49
+ }
50
+ export interface AuditQuery {
51
+ level?: AuditLevel;
52
+ kind?: string;
53
+ source?: string;
54
+ sinceMs?: number;
55
+ untilMs?: number;
56
+ limit?: number;
57
+ }
58
+ export interface SecurityReport {
59
+ totalEntries: number;
60
+ levelCounts: Record<AuditLevel, number>;
61
+ /** Rate observations: {kind: count-per-minute}. */
62
+ ratePerMinute: Record<string, number>;
63
+ recent: AuditEntry[];
64
+ }
65
+ /** Live listener over freshly recorded entries. Fired synchronously after
66
+ * the ring-buffer push (so unflushed entries are visible), before the
67
+ * disk write — the §7.101 seam that lets `edge-context-view` fold panel
68
+ * state without polling `query()` (which re-reads the whole file). */
69
+ export type AuditEntryListener = (entry: AuditEntry) => void;
70
+ export interface AuditLogService {
71
+ log(entry: Omit<AuditEntry, 'ts'> & {
72
+ ts?: number;
73
+ }): Promise<void>;
74
+ query(filter: AuditQuery): Promise<AuditEntry[]>;
75
+ getSecurityReport(): Promise<SecurityReport>;
76
+ /** Manually trigger a disk flush; mostly for tests. */
77
+ flush(): Promise<void>;
78
+ /** Subscribe to live entries; returns an unsubscribe function.
79
+ * Listener faults are caught — they can never break the audit path. */
80
+ onEntry(listener: AuditEntryListener): () => void;
81
+ }
82
+ declare module '@deepseek-ai/cordis' {
83
+ interface Context {
84
+ auditLog: AuditLogServiceImpl;
85
+ }
86
+ }
87
+ export declare class AuditLogServiceImpl extends Service {
88
+ private readonly filePath;
89
+ private readonly minLevel;
90
+ private readonly ringBufferSize;
91
+ private readonly flushIntervalMs;
92
+ private readonly enablePIIRedaction;
93
+ private readonly maxFileSizeBytes;
94
+ private readonly maxFileAgeDays;
95
+ private readonly maxRotatedFiles;
96
+ private ringBuffer;
97
+ /** Index of the first entry in ringBuffer that has NOT yet been flushed. */
98
+ private flushIndex;
99
+ private flushTimer;
100
+ private dirty;
101
+ private flushPromise;
102
+ private readonly entryListeners;
103
+ constructor(ctx: Context, config: Config);
104
+ /** Test/apply helper that returns the audit log's current file path. */
105
+ filePathForTests(): string;
106
+ /** §7.101 live seam: subscribe to freshly recorded entries. The returned
107
+ * function unsubscribes. Delivery is synchronous after the ring push;
108
+ * a throwing listener is isolated (the audit path is privacy-critical,
109
+ * a viewer fault must never affect it — same fail-open discipline as
110
+ * TdBusServiceImpl.on). */
111
+ onEntry(listener: AuditEntryListener): () => void;
112
+ private notifyEntryListeners;
113
+ /** Start the periodic flush; idempotent. Call once from `apply()`. */
114
+ start(): void;
115
+ /** Stop the periodic flush; for tests and the main shutdown hook. */
116
+ stopForTests(): void;
117
+ log(entry: Omit<AuditEntry, 'ts'> & {
118
+ ts?: number;
119
+ }): Promise<void>;
120
+ query(filter: AuditQuery): Promise<AuditEntry[]>;
121
+ getSecurityReport(): Promise<SecurityReport>;
122
+ flush(): Promise<void>;
123
+ /**
124
+ * Check if the current log file needs rotation (by size or age).
125
+ * If yes, rotate: current → .1, .1 → .2, ..., delete oldest beyond maxRotatedFiles.
126
+ */
127
+ private checkAndRotate;
128
+ /** Read every entry from disk; used by query() to span past the ring buffer. */
129
+ private readAllFromDisk;
130
+ private readDiskEntries;
131
+ }
132
+ export declare function apply(ctx: Context, config: Config): Promise<() => Promise<void>>;
133
+ /**
134
+ * Main-aggregator shutdown hook. Stops the periodic flush and drains
135
+ * any in-memory ring buffer to disk. A3 will replace this with a
136
+ * tamper-evident HMAC finalisation.
137
+ */
138
+ export declare function auditLogShutdown(ctx: Context): Promise<() => Promise<void>>;
139
+ //# sourceMappingURL=audit-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-log.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/audit-log.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,376 @@
1
+ /**
2
+ * audit-log — M3 PLAINTEXT append-only log with an in-memory ring buffer.
3
+ * Each entry is one JSON line:
4
+ * { ts, level, kind, source, payload }
5
+ *
6
+ * Phase 2 增强:
7
+ * - 敏感字段自动脱敏(手机号、身份证、银行卡、邮箱、API Key)
8
+ * - 日志轮转(按大小/时间)
9
+ *
10
+ * M3 NOTE — A3 swaps the on-disk format for HMAC-chained tamper-evident
11
+ * entries; the public surface (log / query / getSecurityReport) is the
12
+ * durable contract.
13
+ *
14
+ * @module dsh-edge-agent/audit-log
15
+ */
16
+ import { Service } from '@deepseek-ai/cordis';
17
+ import Schema from '@deepseek-ai/schemastery';
18
+ import { mkdir, readFile, appendFile, stat } from 'node:fs/promises';
19
+ import { existsSync } from 'node:fs';
20
+ import { dirname, join } from 'node:path';
21
+ import { redactObject } from './pii-utils.js';
22
+ import { renameWithRetry } from './local-storage.js';
23
+ import { edgeConfigCandidates, edgeConfigPath, edgeHome, loadEdgeConfig } from './edge-config.js';
24
+ export const name = 'audit-log';
25
+ export const inject = [];
26
+ const LEVEL_ORDER = {
27
+ debug: 10,
28
+ info: 20,
29
+ warn: 30,
30
+ error: 40,
31
+ security: 50,
32
+ };
33
+ /**
34
+ * Calendar-day index in the system's LOCAL timezone (§7.56). Only diffs
35
+ * between two indexes are meaningful. Mirrors file-logger.ts's helper —
36
+ * kept local to this module so audit-log has no import dependency on the
37
+ * logger plugin (both already duplicate the rotation algorithm by design).
38
+ */
39
+ function localDayIndex(d) {
40
+ return Math.floor(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / 86_400_000);
41
+ }
42
+ // §7.102 "one config file": minLevel / flushIntervalMs / root carry NO Schema
43
+ // default — apply() fills them from config/edge.json (audit.*) and the
44
+ // literals below (also mirrored in the constructor `??` fallbacks) are the
45
+ // last resort. A Schema default here would shadow the file, exactly like the
46
+ // old `env ?? default` patch rows shadowed everything on the desktop GUI.
47
+ export const Config = Schema.object({
48
+ minLevel: Schema.union(['debug', 'info', 'warn', 'error', 'security']),
49
+ ringBufferSize: Schema.natural().default(512),
50
+ flushIntervalMs: Schema.natural(),
51
+ filename: Schema.string().default('audit.plain.jsonl'),
52
+ root: Schema.string(),
53
+ enablePIIRedaction: Schema.boolean().default(true),
54
+ maxFileSizeBytes: Schema.natural().default(10 * 1024 * 1024), // 10MB
55
+ maxFileAgeDays: Schema.number().default(7),
56
+ maxRotatedFiles: Schema.natural().default(5),
57
+ });
58
+ export class AuditLogServiceImpl extends Service {
59
+ filePath;
60
+ minLevel;
61
+ ringBufferSize;
62
+ flushIntervalMs;
63
+ enablePIIRedaction;
64
+ maxFileSizeBytes;
65
+ maxFileAgeDays;
66
+ maxRotatedFiles;
67
+ ringBuffer = [];
68
+ /** Index of the first entry in ringBuffer that has NOT yet been flushed. */
69
+ flushIndex = 0;
70
+ flushTimer;
71
+ dirty = false;
72
+ flushPromise = Promise.resolve();
73
+ entryListeners = new Set();
74
+ constructor(ctx, config) {
75
+ super(ctx, 'auditLog');
76
+ // apply() fills every unset field from config/edge.json; these `??`
77
+ // literals only fire for hand-constructed instances (tests, rig).
78
+ this.filePath = join(config.root ?? edgeHome('audit'), config.filename);
79
+ this.minLevel = config.minLevel ?? 'info';
80
+ this.ringBufferSize = config.ringBufferSize;
81
+ this.flushIntervalMs = config.flushIntervalMs ?? 1000;
82
+ this.enablePIIRedaction = config.enablePIIRedaction;
83
+ this.maxFileSizeBytes = config.maxFileSizeBytes;
84
+ this.maxFileAgeDays = config.maxFileAgeDays;
85
+ this.maxRotatedFiles = config.maxRotatedFiles;
86
+ }
87
+ /** Test/apply helper that returns the audit log's current file path. */
88
+ filePathForTests() {
89
+ return this.filePath;
90
+ }
91
+ /** §7.101 live seam: subscribe to freshly recorded entries. The returned
92
+ * function unsubscribes. Delivery is synchronous after the ring push;
93
+ * a throwing listener is isolated (the audit path is privacy-critical,
94
+ * a viewer fault must never affect it — same fail-open discipline as
95
+ * TdBusServiceImpl.on). */
96
+ onEntry(listener) {
97
+ this.entryListeners.add(listener);
98
+ return () => { this.entryListeners.delete(listener); };
99
+ }
100
+ notifyEntryListeners(record) {
101
+ if (this.entryListeners.size === 0)
102
+ return;
103
+ for (const listener of [...this.entryListeners]) {
104
+ try {
105
+ listener(record);
106
+ }
107
+ catch { /* viewer faults never break the audit path */ }
108
+ }
109
+ }
110
+ /** Start the periodic flush; idempotent. Call once from `apply()`. */
111
+ start() {
112
+ if (this.flushTimer !== undefined)
113
+ return;
114
+ this.flushTimer = setInterval(() => {
115
+ void this.flush().catch(() => { });
116
+ }, this.flushIntervalMs);
117
+ // unref so the timer never holds the process open by itself.
118
+ if (typeof this.flushTimer.unref === 'function')
119
+ this.flushTimer.unref();
120
+ }
121
+ /** Stop the periodic flush; for tests and the main shutdown hook. */
122
+ stopForTests() {
123
+ if (this.flushTimer !== undefined) {
124
+ clearInterval(this.flushTimer);
125
+ this.flushTimer = undefined;
126
+ }
127
+ }
128
+ async log(entry) {
129
+ if (LEVEL_ORDER[entry.level] < LEVEL_ORDER[this.minLevel])
130
+ return;
131
+ // Apply PII redaction to payload if enabled
132
+ let payload = entry.payload;
133
+ if (this.enablePIIRedaction) {
134
+ payload = redactObject(entry.payload).redacted;
135
+ }
136
+ const record = {
137
+ ts: entry.ts ?? Date.now(),
138
+ level: entry.level,
139
+ kind: entry.kind,
140
+ source: entry.source,
141
+ payload,
142
+ };
143
+ // Verify lossless JSON up front so a non-serializable payload never
144
+ // wedges the flush path. `session.event` payloads are derived from the
145
+ // session log (already JSON-validated by dsh-session), but defensively
146
+ // double-check; one bad entry would otherwise break the whole flush.
147
+ try {
148
+ JSON.stringify(record);
149
+ }
150
+ catch (err) {
151
+ this.ctx.logger?.error?.(`[audit-log] dropping non-JSON entry kind=${entry.kind} source=${entry.source} err=${err.message}`);
152
+ return;
153
+ }
154
+ this.ringBuffer.push(record);
155
+ if (this.ringBuffer.length > this.ringBufferSize) {
156
+ this.ringBuffer.shift();
157
+ // Keep flushIndex aligned with the buffer: an entry was dropped from
158
+ // the front, so the "first unflushed" boundary shifts back by one.
159
+ this.flushIndex = Math.max(0, this.flushIndex - 1);
160
+ }
161
+ this.dirty = true;
162
+ this.notifyEntryListeners(record);
163
+ }
164
+ async query(filter) {
165
+ const all = await this.readAllFromDisk();
166
+ let out = all;
167
+ if (filter.level)
168
+ out = out.filter((e) => e.level === filter.level);
169
+ if (filter.kind)
170
+ out = out.filter((e) => e.kind === filter.kind);
171
+ if (filter.source)
172
+ out = out.filter((e) => e.source === filter.source);
173
+ if (filter.sinceMs !== undefined)
174
+ out = out.filter((e) => e.ts >= filter.sinceMs);
175
+ if (filter.untilMs !== undefined)
176
+ out = out.filter((e) => e.ts <= filter.untilMs);
177
+ if (filter.limit !== undefined && out.length > filter.limit) {
178
+ out = out.slice(out.length - filter.limit);
179
+ }
180
+ return out;
181
+ }
182
+ async getSecurityReport() {
183
+ const all = await this.readAllFromDisk();
184
+ const levelCounts = {
185
+ debug: 0, info: 0, warn: 0, error: 0, security: 0,
186
+ };
187
+ const kindCounts = new Map();
188
+ for (const entry of all) {
189
+ levelCounts[entry.level]++;
190
+ kindCounts.set(entry.kind, (kindCounts.get(entry.kind) ?? 0) + 1);
191
+ }
192
+ const ratePerMinute = {};
193
+ for (const [kind, count] of kindCounts.entries()) {
194
+ // crude approximation: per-minute = count / (window minutes)
195
+ ratePerMinute[kind] = count;
196
+ }
197
+ return {
198
+ totalEntries: all.length,
199
+ levelCounts,
200
+ ratePerMinute,
201
+ recent: all.slice(-20),
202
+ };
203
+ }
204
+ async flush() {
205
+ if (!this.dirty)
206
+ return;
207
+ // Serialise flushes.
208
+ this.flushPromise = this.flushPromise.then(async () => {
209
+ // Only flush the [flushIndex..length) slice — never re-write entries
210
+ // already on disk.
211
+ const start = this.flushIndex;
212
+ const end = this.ringBuffer.length;
213
+ if (start >= end) {
214
+ this.dirty = false;
215
+ return;
216
+ }
217
+ const toWrite = this.ringBuffer.slice(start, end);
218
+ if (toWrite.length === 0) {
219
+ this.dirty = false;
220
+ return;
221
+ }
222
+ await mkdir(dirname(this.filePath), { recursive: true });
223
+ // Check if rotation is needed before writing
224
+ await this.checkAndRotate();
225
+ try {
226
+ const lines = toWrite.map((e) => JSON.stringify(e)).join('\n') + '\n';
227
+ // 单次 append(O(新增行)),不再整读整写(旧实现每秒最多重写
228
+ // 10MB)。O_APPEND 还让 §7.107 双进程(desktop + dsh-edge-agent)
229
+ // 共写同一 JSONL 时不再互相丢更新 —— 旧的 tmp+rename 整写会用
230
+ // 自己的快照覆盖另一进程刚 flush 进去的行。
231
+ await appendFile(this.filePath, lines, 'utf-8');
232
+ this.flushIndex = end;
233
+ this.dirty = false;
234
+ }
235
+ catch (err) {
236
+ // A single non-serializable entry would otherwise pin the buffer
237
+ // forever and stop every subsequent flush. Drop the batch and report;
238
+ // log() already pre-validates JSON so this is a belt-and-braces guard.
239
+ this.ctx.logger?.error?.(`[audit-log] flush failed, dropping ${toWrite.length} entries: ${err.message}`);
240
+ this.flushIndex = end;
241
+ this.dirty = false;
242
+ }
243
+ });
244
+ return this.flushPromise;
245
+ }
246
+ /**
247
+ * Check if the current log file needs rotation (by size or age).
248
+ * If yes, rotate: current → .1, .1 → .2, ..., delete oldest beyond maxRotatedFiles.
249
+ */
250
+ async checkAndRotate() {
251
+ if (!existsSync(this.filePath))
252
+ return;
253
+ try {
254
+ const stats = await stat(this.filePath);
255
+ // §7.56 (2026-09-15): calendar-day diff (local time), replacing the
256
+ // elapsed-time form `now - mtime`. Same bug as file-logger: an active
257
+ // append-mode log refreshes mtime on every flush, so elapsed age was
258
+ // always ≈ 0 and the configured `maxFileAgeDays: 7` never fired.
259
+ // Day-index diff rotates on the first flush of a new calendar day
260
+ // once the file is ≥ maxFileAgeDays days old by wall clock.
261
+ const dayDiff = localDayIndex(new Date()) - localDayIndex(new Date(stats.mtimeMs));
262
+ const needsRotation = (this.maxFileSizeBytes > 0 && stats.size >= this.maxFileSizeBytes) ||
263
+ (this.maxFileAgeDays > 0 && dayDiff >= this.maxFileAgeDays);
264
+ if (!needsRotation)
265
+ return;
266
+ // Rotate: shift .N → .N+1, then current → .1
267
+ for (let i = this.maxRotatedFiles; i >= 1; i--) {
268
+ const from = i === 1 ? this.filePath : `${this.filePath}.${i - 1}`;
269
+ const to = `${this.filePath}.${i}`;
270
+ if (existsSync(from)) {
271
+ await renameWithRetry(from, to);
272
+ }
273
+ }
274
+ // Delete files beyond maxRotatedFiles
275
+ const oldest = `${this.filePath}.${this.maxRotatedFiles + 1}`;
276
+ if (existsSync(oldest)) {
277
+ await import('node:fs/promises').then((fs) => fs.rm(oldest, { force: true }));
278
+ }
279
+ }
280
+ catch (err) {
281
+ // Rotation failure should not block flushing
282
+ this.ctx.logger?.error?.(`[audit-log] rotation failed: ${err.message}`);
283
+ }
284
+ }
285
+ /** Read every entry from disk; used by query() to span past the ring buffer. */
286
+ async readAllFromDisk() {
287
+ const disk = await this.readDiskEntries();
288
+ // Disk already holds entries [0..flushIndex); only merge the unflushed
289
+ // tail [flushIndex..length) of the ring buffer to avoid duplicates.
290
+ const unflushed = this.ringBuffer.slice(this.flushIndex);
291
+ const seen = new Set();
292
+ const merged = [];
293
+ for (const e of disk) {
294
+ if (!seen.has(e.ts)) {
295
+ seen.add(e.ts);
296
+ merged.push(e);
297
+ }
298
+ }
299
+ for (const e of unflushed) {
300
+ if (!seen.has(e.ts))
301
+ merged.push(e);
302
+ }
303
+ return merged;
304
+ }
305
+ async readDiskEntries() {
306
+ if (!existsSync(this.filePath))
307
+ return [];
308
+ const raw = await readFile(this.filePath, 'utf-8');
309
+ const out = [];
310
+ for (const line of raw.split('\n')) {
311
+ const trimmed = line.trim();
312
+ if (trimmed.length === 0)
313
+ continue;
314
+ try {
315
+ out.push(JSON.parse(trimmed));
316
+ }
317
+ catch {
318
+ continue;
319
+ }
320
+ }
321
+ return out;
322
+ }
323
+ }
324
+ export async function apply(ctx, config) {
325
+ // §7.102: fill unset fields from config/edge.json (audit.*). The cordis
326
+ // patch rows no longer pass minLevel/flush/root — the file is the source.
327
+ const audit = loadEdgeConfig().audit;
328
+ const c = config;
329
+ if (c.minLevel === undefined && audit?.minLevel) {
330
+ c.minLevel = audit.minLevel;
331
+ }
332
+ if (c.flushIntervalMs === undefined && audit?.flushMs !== undefined) {
333
+ c.flushIntervalMs = audit.flushMs;
334
+ }
335
+ if (c.root === undefined) {
336
+ c.root = audit?.root && audit.root.length > 0 ? audit.root : edgeHome('audit');
337
+ }
338
+ // AuditLogServiceImpl extends `Service`, so its constructor calls
339
+ // `ctx.reflect.provide('auditLog', this)` for us. Do not call
340
+ // `ctx.provide` again — that would throw "service already registered".
341
+ const service = new AuditLogServiceImpl(ctx, config);
342
+ await mkdir(dirname(service.filePathForTests()), { recursive: true }).catch(() => { });
343
+ service.start();
344
+ // §7.107 (2026-10-23): the loaded config file is the single most
345
+ // operationally important fact about a running bundle — three candidate
346
+ // paths exist (bundle config/edge.json → ~/.dsh-edge-agent/edge.json →
347
+ // bundle config/edge.example.json) and the operator had no way to see
348
+ // which one won. One boot row settles it.
349
+ await service.log({
350
+ level: 'info',
351
+ kind: 'config.loaded',
352
+ source: 'audit-log',
353
+ payload: {
354
+ path: edgeConfigPath() ?? '(defaults — no config file found)',
355
+ candidates: edgeConfigCandidates(),
356
+ },
357
+ }).catch(() => { });
358
+ return async () => {
359
+ service.stopForTests();
360
+ await service.flush();
361
+ };
362
+ }
363
+ /**
364
+ * Main-aggregator shutdown hook. Stops the periodic flush and drains
365
+ * any in-memory ring buffer to disk. A3 will replace this with a
366
+ * tamper-evident HMAC finalisation.
367
+ */
368
+ export async function auditLogShutdown(ctx) {
369
+ const svc = ctx.auditLog;
370
+ return async () => {
371
+ if (!svc)
372
+ return;
373
+ svc.stopForTests();
374
+ await svc.flush();
375
+ };
376
+ }