@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,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
|
+
}
|