@dsh-edge/dsh-edge-agent 0.0.0-stage → 0.1.2
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 +158 -0
- package/LICENSE +21 -0
- package/README.md +181 -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 +253 -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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* local-storage — M3 PLAINTEXT key/value store. Each "collection" is an
|
|
3
|
+
* ordered append-only log of `{op, key, value}` JSON lines. Reads project
|
|
4
|
+
* the latest write per key (a compacted view), and writes append a single
|
|
5
|
+
* line. No encryption, no compression, no TCM dependency.
|
|
6
|
+
*
|
|
7
|
+
* Phase 3 增强:存储时对敏感字段自动脱敏(PII redaction),复用 pii-utils。
|
|
8
|
+
*
|
|
9
|
+
* M3 NOTE — A3 replaces this entire file with an encrypted SQLite-backed
|
|
10
|
+
* store. The plugin shape (Service + put/get/delete/list) is the durable
|
|
11
|
+
* contract; the on-disk format is an implementation detail.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-edge-agent/local-storage
|
|
14
|
+
*/
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
17
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
18
|
+
export declare const name = "local-storage";
|
|
19
|
+
export declare const inject: readonly [];
|
|
20
|
+
export interface Config {
|
|
21
|
+
/** Directory holding the storage files. Created if missing. */
|
|
22
|
+
root: string;
|
|
23
|
+
/** Filename inside `root`. Each line is one log entry. */
|
|
24
|
+
filename: string;
|
|
25
|
+
/** Enable PII redaction before writing to disk. */
|
|
26
|
+
enablePIIRedaction: boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare const Config: Schema<Config>;
|
|
29
|
+
export interface KVEntry {
|
|
30
|
+
key: string;
|
|
31
|
+
value: unknown;
|
|
32
|
+
}
|
|
33
|
+
export interface StorageService {
|
|
34
|
+
get<T = unknown>(collection: string, key: string): Promise<T | null>;
|
|
35
|
+
put<T = unknown>(collection: string, key: string, value: T): Promise<void>;
|
|
36
|
+
delete(collection: string, key: string): Promise<void>;
|
|
37
|
+
list(collection: string): Promise<KVEntry[]>;
|
|
38
|
+
/** Drop every entry in one collection; M3-only test/teardown hook. */
|
|
39
|
+
clear(collection: string): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
declare module '@deepseek-ai/cordis' {
|
|
42
|
+
interface Context {
|
|
43
|
+
localStorage: LocalStorageServiceImpl;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* JSONL-backed KV store. One file per `Config.filename`; lines are
|
|
48
|
+
* `{op: 'put'|'delete', collection, key, value, ts}` records.
|
|
49
|
+
*/
|
|
50
|
+
export declare class LocalStorageServiceImpl extends Service {
|
|
51
|
+
private readonly filePath;
|
|
52
|
+
private readonly enablePIIRedaction;
|
|
53
|
+
private cache;
|
|
54
|
+
private writeQueue;
|
|
55
|
+
private loaded;
|
|
56
|
+
constructor(ctx: Context, config: Config);
|
|
57
|
+
/** Initial scan; idempotent. M3 calls this once per process. */
|
|
58
|
+
load(): Promise<void>;
|
|
59
|
+
get<T = unknown>(collection: string, key: string): Promise<T | null>;
|
|
60
|
+
put<T = unknown>(collection: string, key: string, value: T): Promise<void>;
|
|
61
|
+
delete(collection: string, key: string): Promise<void>;
|
|
62
|
+
list(collection: string): Promise<KVEntry[]>;
|
|
63
|
+
clear(collection: string): Promise<void>;
|
|
64
|
+
private collection;
|
|
65
|
+
/** Append one record; serialized through a tail-of-promise chain.
|
|
66
|
+
*
|
|
67
|
+
* 单行 append(O(1)),不再整读整写(旧的 read-merge-write 让每次
|
|
68
|
+
* put/delete 的成本随历史线性增长)。崩溃一致性:append 中途 crash
|
|
69
|
+
* 最多留半行,load() 的逐行 JSON.parse 会跳过它。
|
|
70
|
+
*
|
|
71
|
+
* 队列防中毒:链内吞掉本次失败并让链继续走 —— 一次磁盘错误(满盘、
|
|
72
|
+
* 权限)曾把 writeQueue 变成永久 rejected,之后所有 put/delete 静默
|
|
73
|
+
* 失效直到重启。失败仍如实抛给当次调用方。 */
|
|
74
|
+
private append;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* rename() with a short backoff for EPERM.
|
|
78
|
+
*
|
|
79
|
+
* Windows quirk (observed 2026-10-21 running the suite on the desktop host):
|
|
80
|
+
* right after writeFile closes the handle, the Defender/indexer scan can
|
|
81
|
+
* hold the file open for a few milliseconds — rename() then fails with
|
|
82
|
+
* EPERM even though nothing is logically wrong. The lock clears on its own;
|
|
83
|
+
* POSIX platforms never hit this path (rename is atomic and unblocked).
|
|
84
|
+
* Shared with audit-log / file-logger, whose atomic writes have the same
|
|
85
|
+
* exposure on the Windows desktop deployment.
|
|
86
|
+
*/
|
|
87
|
+
export declare function renameWithRetry(from: string, to: string, attempts?: number, baseDelayMs?: number): Promise<void>;
|
|
88
|
+
export declare function apply(ctx: Context, config: Config): Promise<() => Promise<void>>;
|
|
89
|
+
/**
|
|
90
|
+
* Main-aggregator shutdown hook. Flushes any pending in-memory state
|
|
91
|
+
* (none today; the JSONL layer writes on every put/delete) and disposes
|
|
92
|
+
* the local-storage service. A3 will replace this body with an
|
|
93
|
+
* encryption-key zeroization step.
|
|
94
|
+
*/
|
|
95
|
+
export declare function storageShutdown(ctx: Context): Promise<() => Promise<void>>;
|
|
96
|
+
//# sourceMappingURL=local-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-storage.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/local-storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* local-storage — M3 PLAINTEXT key/value store. Each "collection" is an
|
|
3
|
+
* ordered append-only log of `{op, key, value}` JSON lines. Reads project
|
|
4
|
+
* the latest write per key (a compacted view), and writes append a single
|
|
5
|
+
* line. No encryption, no compression, no TCM dependency.
|
|
6
|
+
*
|
|
7
|
+
* Phase 3 增强:存储时对敏感字段自动脱敏(PII redaction),复用 pii-utils。
|
|
8
|
+
*
|
|
9
|
+
* M3 NOTE — A3 replaces this entire file with an encrypted SQLite-backed
|
|
10
|
+
* store. The plugin shape (Service + put/get/delete/list) is the durable
|
|
11
|
+
* contract; the on-disk format is an implementation detail.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-edge-agent/local-storage
|
|
14
|
+
*/
|
|
15
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
16
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
17
|
+
import { mkdir, readFile, appendFile, rename } from 'node:fs/promises';
|
|
18
|
+
import { existsSync } from 'node:fs';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { redactObject } from './pii-utils.js';
|
|
21
|
+
import { edgeHome, loadEdgeConfig } from './edge-config.js';
|
|
22
|
+
export const name = 'local-storage';
|
|
23
|
+
export const inject = [];
|
|
24
|
+
export const Config = Schema.object({
|
|
25
|
+
// §7.102 "one config file": root has no required/default — apply() fills
|
|
26
|
+
// it from config/edge.json (storage.root) or ~/.dsh-edge-agent/storage.
|
|
27
|
+
root: Schema.string(),
|
|
28
|
+
filename: Schema.string().default('storage.plain.jsonl'),
|
|
29
|
+
enablePIIRedaction: Schema.boolean().default(true),
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* JSONL-backed KV store. One file per `Config.filename`; lines are
|
|
33
|
+
* `{op: 'put'|'delete', collection, key, value, ts}` records.
|
|
34
|
+
*/
|
|
35
|
+
export class LocalStorageServiceImpl extends Service {
|
|
36
|
+
filePath;
|
|
37
|
+
enablePIIRedaction;
|
|
38
|
+
cache = new Map();
|
|
39
|
+
writeQueue = Promise.resolve();
|
|
40
|
+
loaded = false;
|
|
41
|
+
constructor(ctx, config) {
|
|
42
|
+
super(ctx, 'localStorage');
|
|
43
|
+
this.filePath = join(config.root, config.filename);
|
|
44
|
+
this.enablePIIRedaction = config.enablePIIRedaction;
|
|
45
|
+
}
|
|
46
|
+
/** Initial scan; idempotent. M3 calls this once per process. */
|
|
47
|
+
async load() {
|
|
48
|
+
if (this.loaded)
|
|
49
|
+
return;
|
|
50
|
+
if (!existsSync(this.filePath)) {
|
|
51
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
52
|
+
this.loaded = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const raw = await readFile(this.filePath, 'utf-8');
|
|
56
|
+
for (const line of raw.split('\n')) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (trimmed.length === 0)
|
|
59
|
+
continue;
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(trimmed);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const op = parsed.op;
|
|
68
|
+
const collection = parsed.collection;
|
|
69
|
+
const key = parsed.key;
|
|
70
|
+
// clear 记录没有 key — 跳过它会让重启后被 clear 的 collection 复活。
|
|
71
|
+
if (!collection || (!key && op !== 'clear'))
|
|
72
|
+
continue;
|
|
73
|
+
if (op === 'clear') {
|
|
74
|
+
this.cache.set(collection, new Map());
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const bucket = this.collection(collection);
|
|
78
|
+
if (op === 'delete')
|
|
79
|
+
bucket.delete(key);
|
|
80
|
+
else if (op === 'put')
|
|
81
|
+
bucket.set(key, parsed.value);
|
|
82
|
+
}
|
|
83
|
+
this.loaded = true;
|
|
84
|
+
}
|
|
85
|
+
async get(collection, key) {
|
|
86
|
+
await this.load();
|
|
87
|
+
const bucket = this.collection(collection);
|
|
88
|
+
const value = bucket.get(key);
|
|
89
|
+
if (value === undefined)
|
|
90
|
+
return null;
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
async put(collection, key, value) {
|
|
94
|
+
await this.load();
|
|
95
|
+
// Apply PII redaction to value before storing
|
|
96
|
+
const storedValue = this.enablePIIRedaction
|
|
97
|
+
? redactObject(value).redacted
|
|
98
|
+
: value;
|
|
99
|
+
this.collection(collection).set(key, storedValue);
|
|
100
|
+
await this.append({ op: 'put', collection, key, value: storedValue, ts: Date.now() });
|
|
101
|
+
}
|
|
102
|
+
async delete(collection, key) {
|
|
103
|
+
await this.load();
|
|
104
|
+
const bucket = this.collection(collection);
|
|
105
|
+
if (!bucket.delete(key))
|
|
106
|
+
return;
|
|
107
|
+
await this.append({ op: 'delete', collection, key, ts: Date.now() });
|
|
108
|
+
}
|
|
109
|
+
async list(collection) {
|
|
110
|
+
await this.load();
|
|
111
|
+
const bucket = this.collection(collection);
|
|
112
|
+
return [...bucket.entries()].map(([key, value]) => ({ key, value }));
|
|
113
|
+
}
|
|
114
|
+
async clear(collection) {
|
|
115
|
+
await this.load();
|
|
116
|
+
this.cache.set(collection, new Map());
|
|
117
|
+
await this.append({ op: 'clear', collection, ts: Date.now() });
|
|
118
|
+
}
|
|
119
|
+
collection(name) {
|
|
120
|
+
let bucket = this.cache.get(name);
|
|
121
|
+
if (!bucket) {
|
|
122
|
+
bucket = new Map();
|
|
123
|
+
this.cache.set(name, bucket);
|
|
124
|
+
}
|
|
125
|
+
return bucket;
|
|
126
|
+
}
|
|
127
|
+
/** Append one record; serialized through a tail-of-promise chain.
|
|
128
|
+
*
|
|
129
|
+
* 单行 append(O(1)),不再整读整写(旧的 read-merge-write 让每次
|
|
130
|
+
* put/delete 的成本随历史线性增长)。崩溃一致性:append 中途 crash
|
|
131
|
+
* 最多留半行,load() 的逐行 JSON.parse 会跳过它。
|
|
132
|
+
*
|
|
133
|
+
* 队列防中毒:链内吞掉本次失败并让链继续走 —— 一次磁盘错误(满盘、
|
|
134
|
+
* 权限)曾把 writeQueue 变成永久 rejected,之后所有 put/delete 静默
|
|
135
|
+
* 失效直到重启。失败仍如实抛给当次调用方。 */
|
|
136
|
+
append(record) {
|
|
137
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
138
|
+
const run = async () => {
|
|
139
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
140
|
+
await appendFile(this.filePath, line, 'utf-8');
|
|
141
|
+
};
|
|
142
|
+
const result = this.writeQueue.then(run, run);
|
|
143
|
+
this.writeQueue = result.catch(() => { });
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* rename() with a short backoff for EPERM.
|
|
149
|
+
*
|
|
150
|
+
* Windows quirk (observed 2026-10-21 running the suite on the desktop host):
|
|
151
|
+
* right after writeFile closes the handle, the Defender/indexer scan can
|
|
152
|
+
* hold the file open for a few milliseconds — rename() then fails with
|
|
153
|
+
* EPERM even though nothing is logically wrong. The lock clears on its own;
|
|
154
|
+
* POSIX platforms never hit this path (rename is atomic and unblocked).
|
|
155
|
+
* Shared with audit-log / file-logger, whose atomic writes have the same
|
|
156
|
+
* exposure on the Windows desktop deployment.
|
|
157
|
+
*/
|
|
158
|
+
export async function renameWithRetry(from, to, attempts = 5, baseDelayMs = 20) {
|
|
159
|
+
for (let i = 0;; i++) {
|
|
160
|
+
try {
|
|
161
|
+
await rename(from, to);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
const code = e?.code;
|
|
166
|
+
if (i >= attempts - 1 || code !== 'EPERM' && code !== 'EBUSY' && code !== 'ENOTEMPTY')
|
|
167
|
+
throw e;
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, baseDelayMs * (i + 1)));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export async function apply(ctx, config) {
|
|
173
|
+
// §7.102: root comes from config/edge.json (storage.root) when the row
|
|
174
|
+
// omits it — the patch no longer carries `!!js HOME` path expressions.
|
|
175
|
+
const c = config;
|
|
176
|
+
if (c.root === undefined) {
|
|
177
|
+
const fileRoot = loadEdgeConfig().storage?.root;
|
|
178
|
+
c.root = fileRoot && fileRoot.length > 0 ? fileRoot : edgeHome('storage');
|
|
179
|
+
}
|
|
180
|
+
// LocalStorageServiceImpl extends `Service`, so its constructor calls
|
|
181
|
+
// `ctx.reflect.provide('localStorage', this)` for us. Do not call
|
|
182
|
+
// `ctx.provide` again — that would throw "service already registered".
|
|
183
|
+
const service = new LocalStorageServiceImpl(ctx, config);
|
|
184
|
+
await service.load();
|
|
185
|
+
return async () => { };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Main-aggregator shutdown hook. Flushes any pending in-memory state
|
|
189
|
+
* (none today; the JSONL layer writes on every put/delete) and disposes
|
|
190
|
+
* the local-storage service. A3 will replace this body with an
|
|
191
|
+
* encryption-key zeroization step.
|
|
192
|
+
*/
|
|
193
|
+
export async function storageShutdown(ctx) {
|
|
194
|
+
const svc = ctx.localStorage;
|
|
195
|
+
return async () => {
|
|
196
|
+
svc?.dispose?.();
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-router — M1 + M4 router. Each user request picks a model by
|
|
3
|
+
* name; the router classifies the name into a realm and forwards to
|
|
4
|
+
* `ctx.llm.stream` so the registered adapter for that provider route
|
|
5
|
+
* actually runs. The router never creates an isolated realm and never
|
|
6
|
+
* replaces `ctx.llm` — adapters registered via
|
|
7
|
+
* `ctx.llm.registerAdapter(providers, adapter)` are the only LLM
|
|
8
|
+
* surface, as documented in § 11.2 (3) of dsh-edge-agent-design-v6.md.
|
|
9
|
+
*
|
|
10
|
+
* M4 adds:
|
|
11
|
+
* - `listAvailable()` returns the union of `Config.localModels` and
|
|
12
|
+
* `Config.cloudModels` so callers can enumerate dispatch keys.
|
|
13
|
+
* - `resolve()` accepts either a provider route id (e.g. `local-llm`)
|
|
14
|
+
* or a free-form model id (e.g. `deepseek-chat`) and maps it to the
|
|
15
|
+
* correct route. The prefix rule still decides realm; a free-form
|
|
16
|
+
* model id without a prefix falls back to CLOUD.
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-edge-agent/model-router
|
|
19
|
+
*/
|
|
20
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
21
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
22
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
23
|
+
export declare const name = "model-router";
|
|
24
|
+
export declare const inject: readonly ["llm"];
|
|
25
|
+
export type Realm = 'LOCAL' | 'CLOUD' | 'SANITIZED';
|
|
26
|
+
export interface Config {
|
|
27
|
+
/** Default route id used when a request omits one. */
|
|
28
|
+
defaultModel: string;
|
|
29
|
+
/** Catalog of local provider routes the router can surface. */
|
|
30
|
+
localModels: string[];
|
|
31
|
+
/** Catalog of cloud provider routes the router can surface. */
|
|
32
|
+
cloudModels: string[];
|
|
33
|
+
}
|
|
34
|
+
export declare const Config: Schema<Config>;
|
|
35
|
+
export interface ResolvedRoute {
|
|
36
|
+
realm: Realm;
|
|
37
|
+
provider: string;
|
|
38
|
+
model: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ModelRouterService {
|
|
41
|
+
resolve(model: string): ResolvedRoute;
|
|
42
|
+
listAvailable(): string[];
|
|
43
|
+
/** Detailed enumeration — used by the http-api `/v1/models` endpoint
|
|
44
|
+
* so external clients can discover the dispatcher surface without
|
|
45
|
+
* hard-coding route ids. Returns one entry per registered adapter
|
|
46
|
+
* with realm + availability metadata. */
|
|
47
|
+
listDetailed(): ModelInfo[];
|
|
48
|
+
isAvailable(provider: string): boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface ModelInfo {
|
|
51
|
+
/** Provider route id (e.g. `local-llm`). */
|
|
52
|
+
provider: string;
|
|
53
|
+
/** Realm the provider belongs to. */
|
|
54
|
+
realm: Realm;
|
|
55
|
+
/** §7.100: config membership AND (when the llm registry is readable) a
|
|
56
|
+
* live registered route. Falls back to config membership only when the
|
|
57
|
+
* registry can't be queried. */
|
|
58
|
+
available: boolean;
|
|
59
|
+
}
|
|
60
|
+
export declare class ModelRouterServiceImpl extends Service {
|
|
61
|
+
private readonly localModels;
|
|
62
|
+
private readonly cloudModels;
|
|
63
|
+
constructor(ctx: Context, localModels: readonly string[], cloudModels: readonly string[]);
|
|
64
|
+
resolve(model: string): ResolvedRoute;
|
|
65
|
+
listAvailable(): string[];
|
|
66
|
+
listDetailed(): ModelInfo[];
|
|
67
|
+
isAvailable(provider: string): boolean;
|
|
68
|
+
/** Live registered route ids, or undefined when the registry is unreadable. */
|
|
69
|
+
private liveProviders;
|
|
70
|
+
}
|
|
71
|
+
/** Pure route classifier. Exported so unit tests can pin it without a Cordis ctx. */
|
|
72
|
+
export declare function resolveRoute(model: string, localModels?: readonly string[], cloudModels?: readonly string[]): ResolvedRoute;
|
|
73
|
+
declare module '@deepseek-ai/cordis' {
|
|
74
|
+
interface Context {
|
|
75
|
+
modelRouter: ModelRouterServiceImpl;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export declare function apply(ctx: Context, config: Config): Promise<() => Promise<void>>;
|
|
79
|
+
//# sourceMappingURL=model-router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-router.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/model-router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAC7C,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAE7C,eAAO,MAAM,IAAI,iBAAiB,CAAA;AAElC,eAAO,MAAM,MAAM,kBAAmB,CAAA;AAEtC,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,WAAW,CAAA;AAEnD,MAAM,WAAW,MAAM;IACrB,sDAAsD;IACtD,YAAY,EAAE,MAAM,CAAA;IACpB,+DAA+D;IAC/D,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,+DAA+D;IAC/D,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB;AAED,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CAMhC,CAAA;AAEF,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,KAAK,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAAA;IACrC,aAAa,IAAI,MAAM,EAAE,CAAA;IACzB;;;8CAG0C;IAC1C,YAAY,IAAI,SAAS,EAAE,CAAA;IAC3B,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;CACvC;AAED,MAAM,WAAW,SAAS;IACxB,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,KAAK,EAAE,KAAK,CAAA;IACZ;;qCAEiC;IACjC,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,qBAAa,sBAAuB,SAAQ,OAAO;IACjD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;IAC/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;gBAEnC,GAAG,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE;IAMxF,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa;IAIrC,aAAa,IAAI,MAAM,EAAE;IAIzB,YAAY,IAAI,SAAS,EAAE;IAW3B,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAatC,+EAA+E;IAC/E,OAAO,CAAC,aAAa;CAgBtB;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,WAAW,GAAE,SAAS,MAAM,EAAkB,EAC9C,WAAW,GAAE,SAAS,MAAM,EAAkB,GAC7C,aAAa,CAgBf;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,WAAW,EAAE,sBAAsB,CAAA;KACpC;CACF;AAED,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAMtF"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-router — M1 + M4 router. Each user request picks a model by
|
|
3
|
+
* name; the router classifies the name into a realm and forwards to
|
|
4
|
+
* `ctx.llm.stream` so the registered adapter for that provider route
|
|
5
|
+
* actually runs. The router never creates an isolated realm and never
|
|
6
|
+
* replaces `ctx.llm` — adapters registered via
|
|
7
|
+
* `ctx.llm.registerAdapter(providers, adapter)` are the only LLM
|
|
8
|
+
* surface, as documented in § 11.2 (3) of dsh-edge-agent-design-v6.md.
|
|
9
|
+
*
|
|
10
|
+
* M4 adds:
|
|
11
|
+
* - `listAvailable()` returns the union of `Config.localModels` and
|
|
12
|
+
* `Config.cloudModels` so callers can enumerate dispatch keys.
|
|
13
|
+
* - `resolve()` accepts either a provider route id (e.g. `local-llm`)
|
|
14
|
+
* or a free-form model id (e.g. `deepseek-chat`) and maps it to the
|
|
15
|
+
* correct route. The prefix rule still decides realm; a free-form
|
|
16
|
+
* model id without a prefix falls back to CLOUD.
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-edge-agent/model-router
|
|
19
|
+
*/
|
|
20
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
21
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
22
|
+
export const name = 'model-router';
|
|
23
|
+
export const inject = ['llm'];
|
|
24
|
+
export const Config = Schema.object({
|
|
25
|
+
defaultModel: Schema.string().default('cloud-llm'),
|
|
26
|
+
localModels: Schema.array(Schema.string()).default([
|
|
27
|
+
'local-llm',
|
|
28
|
+
]),
|
|
29
|
+
cloudModels: Schema.array(Schema.string()).default(['cloud-llm']),
|
|
30
|
+
});
|
|
31
|
+
export class ModelRouterServiceImpl extends Service {
|
|
32
|
+
localModels;
|
|
33
|
+
cloudModels;
|
|
34
|
+
constructor(ctx, localModels, cloudModels) {
|
|
35
|
+
super(ctx, 'modelRouter');
|
|
36
|
+
this.localModels = localModels;
|
|
37
|
+
this.cloudModels = cloudModels;
|
|
38
|
+
}
|
|
39
|
+
resolve(model) {
|
|
40
|
+
return resolveRoute(model, this.localModels, this.cloudModels);
|
|
41
|
+
}
|
|
42
|
+
listAvailable() {
|
|
43
|
+
return [...this.localModels, ...this.cloudModels];
|
|
44
|
+
}
|
|
45
|
+
listDetailed() {
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const m of this.localModels) {
|
|
48
|
+
out.push({ provider: m, realm: 'LOCAL', available: this.isAvailable(m) });
|
|
49
|
+
}
|
|
50
|
+
for (const m of this.cloudModels) {
|
|
51
|
+
out.push({ provider: m, realm: 'CLOUD', available: this.isAvailable(m) });
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
isAvailable(provider) {
|
|
56
|
+
const inConfig = this.localModels.includes(provider) || this.cloudModels.includes(provider);
|
|
57
|
+
if (!inConfig)
|
|
58
|
+
return false;
|
|
59
|
+
// §7.100: registry truth when reachable — a configured-but-unregistered
|
|
60
|
+
// route (adapter skipped by DUPLICATE_ADAPTER, minimal graph, or a route
|
|
61
|
+
// the host never activated) must report available:false so /v1/models and
|
|
62
|
+
// future health surfaces stop lying. When the llm registry isn't
|
|
63
|
+
// reachable (old upstream, test fakes), fall back to §7.4 config
|
|
64
|
+
// membership so nothing regresses.
|
|
65
|
+
const live = this.liveProviders();
|
|
66
|
+
return live ? live.has(provider) : true;
|
|
67
|
+
}
|
|
68
|
+
/** Live registered route ids, or undefined when the registry is unreadable. */
|
|
69
|
+
liveProviders() {
|
|
70
|
+
try {
|
|
71
|
+
const anyCtx = this.ctx;
|
|
72
|
+
const llm = anyCtx.llm ?? (typeof anyCtx.get === 'function' ? anyCtx.get('llm') : undefined);
|
|
73
|
+
if (llm && typeof llm.listProviders === 'function') {
|
|
74
|
+
return new Set(llm.listProviders().map((p) => p.id));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// ctx accessor without inject (Cordis throws) or partial fakes — config truth.
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Pure route classifier. Exported so unit tests can pin it without a Cordis ctx. */
|
|
84
|
+
export function resolveRoute(model, localModels = ['local-llm'], cloudModels = ['cloud-llm']) {
|
|
85
|
+
const normalized = model.length > 0 ? model : 'cloud-llm';
|
|
86
|
+
if (localModels.includes(normalized)) {
|
|
87
|
+
return { realm: 'LOCAL', provider: normalized, model: normalized };
|
|
88
|
+
}
|
|
89
|
+
if (cloudModels.includes(normalized)) {
|
|
90
|
+
return { realm: 'CLOUD', provider: normalized, model: normalized };
|
|
91
|
+
}
|
|
92
|
+
if (normalized.startsWith('local-')) {
|
|
93
|
+
return { realm: 'LOCAL', provider: normalized, model: normalized };
|
|
94
|
+
}
|
|
95
|
+
if (normalized.startsWith('cloud-')) {
|
|
96
|
+
return { realm: 'CLOUD', provider: normalized, model: normalized };
|
|
97
|
+
}
|
|
98
|
+
// Unknown prefix: treat as CLOUD (provider/model a la DeepSeek chat-completions).
|
|
99
|
+
return { realm: 'CLOUD', provider: normalized, model: normalized };
|
|
100
|
+
}
|
|
101
|
+
export async function apply(ctx, config) {
|
|
102
|
+
// ModelRouterServiceImpl extends `Service`, so its constructor calls
|
|
103
|
+
// `ctx.reflect.provide('modelRouter', this)` for us. Do not call
|
|
104
|
+
// `ctx.provide` again — that would throw "service already registered".
|
|
105
|
+
void new ModelRouterServiceImpl(ctx, config.localModels, config.cloudModels);
|
|
106
|
+
return async () => { };
|
|
107
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ohttp-gateway — RFC 9458 Oblivious HTTP gateway implementation. Wraps
|
|
3
|
+
* requests in HPKE ciphertext so the relay cannot link user identity
|
|
4
|
+
* to request content.
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-edge-agent/ohttp-gateway
|
|
7
|
+
*/
|
|
8
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
9
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
10
|
+
export declare const name = "ohttp-gateway";
|
|
11
|
+
export declare const inject: readonly ["tcm", "auditLog"];
|
|
12
|
+
export interface Config {
|
|
13
|
+
relayUrl: string;
|
|
14
|
+
targetUrl: string;
|
|
15
|
+
/** Public key for the OHTTP gateway (HPKE). */
|
|
16
|
+
publicKey: string;
|
|
17
|
+
keyId: string;
|
|
18
|
+
/** When true, fall back to plain TLS if the relay is unreachable. */
|
|
19
|
+
allowFallbackTls: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const Config: Schema<Config>;
|
|
22
|
+
export interface OhttpRequest {
|
|
23
|
+
url: string;
|
|
24
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
25
|
+
headers: Record<string, string>;
|
|
26
|
+
body?: Uint8Array;
|
|
27
|
+
}
|
|
28
|
+
export interface OhttpService {
|
|
29
|
+
send(req: OhttpRequest): Promise<{
|
|
30
|
+
status: number;
|
|
31
|
+
headers: Record<string, string>;
|
|
32
|
+
body: Uint8Array;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export declare class OhttpServiceImpl extends Service {
|
|
36
|
+
constructor(ctx: any);
|
|
37
|
+
send(req: OhttpRequest): Promise<{
|
|
38
|
+
status: number;
|
|
39
|
+
headers: {};
|
|
40
|
+
body: Uint8Array<ArrayBuffer>;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
declare module '@deepseek-ai/cordis' {
|
|
44
|
+
interface Context {
|
|
45
|
+
ohttp: OhttpServiceImpl;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export declare function apply(ctx: any, config: Config): Promise<() => Promise<void>>;
|
|
49
|
+
//# sourceMappingURL=ohttp-gateway.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ohttp-gateway.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/ohttp-gateway.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAC7C,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAE7C,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,eAAO,MAAM,MAAM,8BAA+B,CAAA;AAElD,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,qEAAqE;IACrE,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CAMhC,CAAA;AAEF,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAA;IACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,IAAI,CAAC,EAAE,UAAU,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,IAAI,EAAE,UAAU,CAAA;KAAE,CAAC,CAAA;CACxG;AAED,qBAAa,gBAAiB,SAAQ,OAAO;gBAC/B,GAAG,EAAE,GAAG;IAGd,IAAI,CAAC,GAAG,EAAE,YAAY;;;;;CAI7B;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,KAAK,EAAE,gBAAgB,CAAA;KACxB;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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ohttp-gateway — RFC 9458 Oblivious HTTP gateway implementation. Wraps
|
|
3
|
+
* requests in HPKE ciphertext so the relay cannot link user identity
|
|
4
|
+
* to request content.
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-edge-agent/ohttp-gateway
|
|
7
|
+
*/
|
|
8
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
9
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
10
|
+
export const name = 'ohttp-gateway';
|
|
11
|
+
export const inject = ['tcm', 'auditLog'];
|
|
12
|
+
export const Config = Schema.object({
|
|
13
|
+
relayUrl: Schema.string().required(),
|
|
14
|
+
targetUrl: Schema.string().required(),
|
|
15
|
+
publicKey: Schema.string().required(),
|
|
16
|
+
keyId: Schema.string().required(),
|
|
17
|
+
allowFallbackTls: Schema.boolean().default(false),
|
|
18
|
+
});
|
|
19
|
+
export class OhttpServiceImpl extends Service {
|
|
20
|
+
constructor(ctx) {
|
|
21
|
+
super(ctx, 'ohttp');
|
|
22
|
+
}
|
|
23
|
+
async send(req) {
|
|
24
|
+
void req;
|
|
25
|
+
return { status: 200, headers: {}, body: new Uint8Array() };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function apply(ctx, config) {
|
|
29
|
+
ctx.plugin(OhttpServiceImpl);
|
|
30
|
+
void config;
|
|
31
|
+
return async () => { };
|
|
32
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openai-sse — shared helpers for talking to OpenAI-compatible
|
|
3
|
+
* `/chat/completions` endpoints. Both `cloud-llm-adapter` (DeepSeek)
|
|
4
|
+
* and `local-llm-adapter` (`mode: 'real'`) drive their wire envelope
|
|
5
|
+
* and SSE parse through this module so a wire-protocol change lands
|
|
6
|
+
* in one place.
|
|
7
|
+
*
|
|
8
|
+
* Wire shape:
|
|
9
|
+
* POST {baseUrl}/chat/completions
|
|
10
|
+
* { model, messages, stream: true, tools?, temperature?, max_tokens? }
|
|
11
|
+
* response: `text/event-stream` of `data: {...}` frames ending in
|
|
12
|
+
* `data: [DONE]`. The adapter translates frames into the DSH
|
|
13
|
+
* `StreamChunk` union (block-start, text-delta, reasoning-delta,
|
|
14
|
+
* tool-call-delta, block-end, usage, finish).
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-edge-agent/openai-sse
|
|
17
|
+
*/
|
|
18
|
+
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, Message as DshMessage, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
19
|
+
/** Adapter contract surface shared by every OpenAI-compatible endpoint. */
|
|
20
|
+
export interface OpenAiCompatibleAdapter {
|
|
21
|
+
readonly providers: readonly string[];
|
|
22
|
+
readonly baseUrl: string;
|
|
23
|
+
readonly defaultModel: string;
|
|
24
|
+
readonly temperature: number | undefined;
|
|
25
|
+
providerInfo(provider: string): LlmProviderInfo;
|
|
26
|
+
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
27
|
+
resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
|
28
|
+
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
29
|
+
}
|
|
30
|
+
/** Build the wire body for `POST /chat/completions`. */
|
|
31
|
+
export declare function buildChatCompletionsBody(options: GenerateOptions, defaultModel: string, temperature: number | undefined, enableThinking?: boolean, alsoReasoningEffort?: boolean): Record<string, unknown>;
|
|
32
|
+
/** Pull the text out of a content-block array into one concatenated string. */
|
|
33
|
+
export declare function extractText(blocks: readonly {
|
|
34
|
+
type: string;
|
|
35
|
+
text?: string;
|
|
36
|
+
}[]): string;
|
|
37
|
+
/** Read a `Response` body as an SSE stream and yield finished frames. */
|
|
38
|
+
export declare function iterateSseFrames(response: Response, onError: (message: string) => never): AsyncIterable<ParsedEvent>;
|
|
39
|
+
/**
|
|
40
|
+
* Translate a `text-delta` or `tool-call-delta` SSE event into a DSH
|
|
41
|
+
* `StreamChunk`. Returns `null` for `finish` events — the caller
|
|
42
|
+
* tracks the finish reason separately and emits the terminal `finish`
|
|
43
|
+
* through `terminalChunks` once the SSE stream closes.
|
|
44
|
+
*/
|
|
45
|
+
export declare function sseEventToChunk(event: ParsedEvent, toolCalls: Map<number, {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
arguments: string;
|
|
49
|
+
}>): {
|
|
50
|
+
chunk: StreamChunk;
|
|
51
|
+
sawText: boolean;
|
|
52
|
+
sawToolCall: boolean;
|
|
53
|
+
} | null;
|
|
54
|
+
/** Extract the finish reason out of a `finish` event. */
|
|
55
|
+
export declare function sseFinishReason(event: ParsedEvent): 'stop' | 'length' | 'tool-calls' | undefined;
|
|
56
|
+
/** Emit the terminal chunks: text `block-end`, sorted tool-call `block-end`s, then `finish`. */
|
|
57
|
+
export declare function terminalChunks(sawText: boolean, assembledText: string, toolCalls: Map<number, {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
arguments: string;
|
|
61
|
+
}>, finishKind: 'stop' | 'length' | 'tool-calls' | undefined, sawToolCall: boolean): Iterable<StreamChunk>;
|
|
62
|
+
/** SSE parser primitives. Kept package-private so other modules can't lean on the shape. */
|
|
63
|
+
export type ParsedEvent = {
|
|
64
|
+
kind: 'text-delta';
|
|
65
|
+
text: string;
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'tool-call-delta';
|
|
68
|
+
index: number;
|
|
69
|
+
id?: string;
|
|
70
|
+
name?: string;
|
|
71
|
+
argumentsDelta: string;
|
|
72
|
+
} | {
|
|
73
|
+
kind: 'finish';
|
|
74
|
+
finishReason: 'stop' | 'length' | 'tool-calls';
|
|
75
|
+
};
|
|
76
|
+
export declare function splitSseEvents(buffer: string): {
|
|
77
|
+
events: string[];
|
|
78
|
+
remainder: string;
|
|
79
|
+
};
|
|
80
|
+
export declare function parseSseEvent(raw: string): ParsedEvent | undefined;
|
|
81
|
+
/** Default attribution headers every request should carry. */
|
|
82
|
+
export declare function defaultAttributionHeaders(): Record<string, string>;
|
|
83
|
+
/** Type re-exports for downstream consumers. */
|
|
84
|
+
export type { DshMessage, GenerateOptions, StreamChunk };
|
|
85
|
+
//# sourceMappingURL=openai-sse.d.ts.map
|