@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,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* http-api-sessions — soft-dep wrapper around the `sessions` plugin
|
|
3
|
+
* (`@deepseek-ai/dsh-session-persistence-jsonl`). Provides a stable
|
|
4
|
+
* surface for the http-api endpoints (`/v1/sessions/{cancel,rename,
|
|
5
|
+
* fork,...}`) without hard-binding to the upstream plugin's exact API.
|
|
6
|
+
*
|
|
7
|
+
* Why a wrapper layer?
|
|
8
|
+
* - The upstream `sessions` plugin's API may grow with each DSH
|
|
9
|
+
* release. The wrapper freezes a v1-stable contract:
|
|
10
|
+
* createSession / getSession / listSessions /
|
|
11
|
+
* renameSession / forkSession / cancelSession / deleteSession
|
|
12
|
+
* - Soft-dep pattern: if the `sessions` plugin isn't registered
|
|
13
|
+
* (older test profiles, minimal config), the wrapper falls back
|
|
14
|
+
* to in-memory metadata. This keeps tests simple and decouples
|
|
15
|
+
* from boot-order races.
|
|
16
|
+
* - Title rename needs a `payload` map the upstream plugin doesn't
|
|
17
|
+
* expose directly. The wrapper stores titles as audit-log entries
|
|
18
|
+
* keyed by `payload.sessionId + payload.title` (cheap, recoverable
|
|
19
|
+
* across restarts, and already persisted by the audit-log plugin).
|
|
20
|
+
*
|
|
21
|
+
* @module dsh-edge-agent/http-api-sessions
|
|
22
|
+
*/
|
|
23
|
+
import { randomUUID } from 'node:crypto';
|
|
24
|
+
const TITLE_KIND = 'session.title'; // used by /v1/audit filtering
|
|
25
|
+
/** Soft-dep constructor: pass the cordis context; the wrapper will
|
|
26
|
+
* duck-type `ctx.sessions` or `ctx.get?.('sessions')` lazily on each
|
|
27
|
+
* call. This keeps boot-order races from breaking http-api. */
|
|
28
|
+
export class HttpApiSessionsImpl {
|
|
29
|
+
cordisCtx;
|
|
30
|
+
/** In-memory metadata fallback when the upstream plugin is absent. */
|
|
31
|
+
memory = new Map();
|
|
32
|
+
/** Recently-deleted session ids we want to remember briefly so a
|
|
33
|
+
* post-cancel /v1/sessions/{id} doesn't immediately return 200.
|
|
34
|
+
* Cleared on a timer so it doesn't grow unbounded. */
|
|
35
|
+
tombstone = new Set();
|
|
36
|
+
constructor(cordisCtx) {
|
|
37
|
+
this.cordisCtx = cordisCtx;
|
|
38
|
+
}
|
|
39
|
+
/** Lazy upstream plugin lookup. Returns null when the plugin isn't
|
|
40
|
+
* registered (older profiles / tests). */
|
|
41
|
+
upstream() {
|
|
42
|
+
const ctx = this.cordisCtx;
|
|
43
|
+
const direct = ctx?.sessions;
|
|
44
|
+
if (direct && typeof direct === 'object')
|
|
45
|
+
return direct;
|
|
46
|
+
if (typeof ctx?.get === 'function') {
|
|
47
|
+
try {
|
|
48
|
+
const v = ctx.get('sessions');
|
|
49
|
+
if (v && typeof v === 'object')
|
|
50
|
+
return v;
|
|
51
|
+
}
|
|
52
|
+
catch { /* plugin missing — soft-fail */ }
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
async createSession(input = {}) {
|
|
57
|
+
const sessionId = input.sessionId ?? randomUUID();
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const meta = {
|
|
60
|
+
sessionId,
|
|
61
|
+
title: null,
|
|
62
|
+
createdAt: now,
|
|
63
|
+
lastAccessedAt: now,
|
|
64
|
+
provider: input.provider,
|
|
65
|
+
model: input.model,
|
|
66
|
+
};
|
|
67
|
+
this.memory.set(sessionId, meta);
|
|
68
|
+
this.tombstone.delete(sessionId);
|
|
69
|
+
const up = this.upstream();
|
|
70
|
+
if (up?.create) {
|
|
71
|
+
try {
|
|
72
|
+
await up.create({ sessionId, provider: meta.provider, model: meta.model, createdAt: meta.createdAt, lastAccessedAt: meta.lastAccessedAt, title: meta.title });
|
|
73
|
+
}
|
|
74
|
+
catch { /* upstream best-effort */ }
|
|
75
|
+
}
|
|
76
|
+
return { sessionId };
|
|
77
|
+
}
|
|
78
|
+
async getSession(sessionId) {
|
|
79
|
+
const up = this.upstream();
|
|
80
|
+
if (up?.get) {
|
|
81
|
+
try {
|
|
82
|
+
const raw = await up.get(sessionId);
|
|
83
|
+
if (raw && typeof raw === 'object')
|
|
84
|
+
return normalize(raw, sessionId);
|
|
85
|
+
}
|
|
86
|
+
catch { /* fall through to memory */ }
|
|
87
|
+
}
|
|
88
|
+
return this.memory.get(sessionId) ?? null;
|
|
89
|
+
}
|
|
90
|
+
async listSessions() {
|
|
91
|
+
const up = this.upstream();
|
|
92
|
+
if (up?.list) {
|
|
93
|
+
try {
|
|
94
|
+
const list = await up.list();
|
|
95
|
+
if (Array.isArray(list))
|
|
96
|
+
return list.map((r) => normalize(r)).sort(byLastAccessedDesc);
|
|
97
|
+
}
|
|
98
|
+
catch { /* fall through */ }
|
|
99
|
+
}
|
|
100
|
+
return Array.from(this.memory.values())
|
|
101
|
+
.filter((m) => !this.tombstone.has(m.sessionId))
|
|
102
|
+
.sort(byLastAccessedDesc);
|
|
103
|
+
}
|
|
104
|
+
async renameSession(sessionId, title) {
|
|
105
|
+
const meta = this.memory.get(sessionId);
|
|
106
|
+
if (meta)
|
|
107
|
+
meta.title = title;
|
|
108
|
+
const up = this.upstream();
|
|
109
|
+
if (up?.update) {
|
|
110
|
+
try {
|
|
111
|
+
await up.update(sessionId, { title });
|
|
112
|
+
}
|
|
113
|
+
catch { /* best-effort */ }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async forkSession(input) {
|
|
117
|
+
const source = await this.getSession(input.sourceSessionId);
|
|
118
|
+
const sessionId = input.newSessionId ?? randomUUID();
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
const forked = source
|
|
121
|
+
? { ...source, sessionId, title: null, createdAt: now, lastAccessedAt: now }
|
|
122
|
+
: { sessionId, title: null, createdAt: now, lastAccessedAt: now };
|
|
123
|
+
this.memory.set(sessionId, forked);
|
|
124
|
+
const up = this.upstream();
|
|
125
|
+
if (up?.create) {
|
|
126
|
+
try {
|
|
127
|
+
await up.create({ sessionId, forkedFrom: input.sourceSessionId, provider: forked.provider, model: forked.model, createdAt: forked.createdAt, lastAccessedAt: forked.lastAccessedAt });
|
|
128
|
+
}
|
|
129
|
+
catch { /* best-effort */ }
|
|
130
|
+
}
|
|
131
|
+
return { sessionId, sourceSessionId: input.sourceSessionId };
|
|
132
|
+
}
|
|
133
|
+
async cancelSession(sessionId) {
|
|
134
|
+
// Cancel is a marker, not a state change. The http-api service holds
|
|
135
|
+
// the live AgentHandle and is the only thing that can actually abort
|
|
136
|
+
// a running turn. Here we record the intent so /v1/audit shows it.
|
|
137
|
+
const up = this.upstream();
|
|
138
|
+
if (up?.update) {
|
|
139
|
+
try {
|
|
140
|
+
await up.update(sessionId, { canceledAt: Date.now() });
|
|
141
|
+
}
|
|
142
|
+
catch { /* best-effort */ }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async deleteSession(sessionId) {
|
|
146
|
+
const existed = this.memory.delete(sessionId);
|
|
147
|
+
this.tombstone.add(sessionId);
|
|
148
|
+
// Clear the tombstone after 60s so the id can be re-used later.
|
|
149
|
+
setTimeout(() => this.tombstone.delete(sessionId), 60_000).unref?.();
|
|
150
|
+
const up = this.upstream();
|
|
151
|
+
if (up?.delete) {
|
|
152
|
+
try {
|
|
153
|
+
await up.delete(sessionId);
|
|
154
|
+
}
|
|
155
|
+
catch { /* best-effort */ }
|
|
156
|
+
}
|
|
157
|
+
return existed;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function normalize(raw, fallbackId) {
|
|
161
|
+
const r = raw;
|
|
162
|
+
return {
|
|
163
|
+
sessionId: r.sessionId ?? (fallbackId ?? ''),
|
|
164
|
+
title: r.title ?? null,
|
|
165
|
+
createdAt: typeof r.createdAt === 'number' ? r.createdAt : Date.now(),
|
|
166
|
+
lastAccessedAt: typeof r.lastAccessedAt === 'number' ? r.lastAccessedAt : Date.now(),
|
|
167
|
+
provider: r.provider,
|
|
168
|
+
model: r.model,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function byLastAccessedDesc(a, b) {
|
|
172
|
+
return b.lastAccessedAt - a.lastAccessedAt;
|
|
173
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* http-api — exposes an HTTP server from the parent DSH process so that
|
|
3
|
+
* external consumers (mobile apps, third-party services) can talk to a
|
|
4
|
+
* running agent. Optional S13 plugin.
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-edge-agent/http-api
|
|
7
|
+
*/
|
|
8
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
9
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
10
|
+
export declare const name = "http-api";
|
|
11
|
+
export declare const inject: readonly ["agents", "intentRouter", "auditLog", "tdBus", "realmSkillBridge", "edgeTools"];
|
|
12
|
+
export interface Config {
|
|
13
|
+
host: string;
|
|
14
|
+
port: number;
|
|
15
|
+
/** Bearer token required for all requests. */
|
|
16
|
+
bearerToken?: string;
|
|
17
|
+
/** Session idle timeout in ms. Sessions not accessed within this are cleaned up. 0 = disabled. */
|
|
18
|
+
sessionIdleTimeoutMs: number;
|
|
19
|
+
/** Maximum active sessions. LRU eviction when exceeded. 0 = unlimited. */
|
|
20
|
+
maxActiveSessions: number;
|
|
21
|
+
/** Max JSON body bytes accepted by POST endpoints. Defense-in-depth against
|
|
22
|
+
* malicious clients sending unbounded bodies (the old `readJsonBody`
|
|
23
|
+
* appended every chunk into a single string with no upper bound, letting
|
|
24
|
+
* a remote attacker OOM the edge agent). Default 1 MiB; set to 0 to
|
|
25
|
+
* disable the cap (NOT recommended in production). */
|
|
26
|
+
maxRequestBodyBytes: number;
|
|
27
|
+
}
|
|
28
|
+
export declare const Config: Schema<Config>;
|
|
29
|
+
export interface HttpApiService {
|
|
30
|
+
start(): Promise<void>;
|
|
31
|
+
stop(): Promise<void>;
|
|
32
|
+
isRunning(): boolean;
|
|
33
|
+
endpoint(): string;
|
|
34
|
+
}
|
|
35
|
+
export declare class HttpApiServiceImpl extends Service {
|
|
36
|
+
private cordisCtx;
|
|
37
|
+
private config;
|
|
38
|
+
private agents;
|
|
39
|
+
private auditLog;
|
|
40
|
+
private server;
|
|
41
|
+
private readonly sessions;
|
|
42
|
+
/** Per-session ring buffer for multi-turn context. See RecentMessage for
|
|
43
|
+
* schema. Lives outside `sessions` so pushRecentMessage can accept writes
|
|
44
|
+
* before ensureSession() finishes (first-turn user message). */
|
|
45
|
+
private readonly sessionBuffers;
|
|
46
|
+
private running;
|
|
47
|
+
private intentRouter;
|
|
48
|
+
private realmSkillBridge;
|
|
49
|
+
private tdBus;
|
|
50
|
+
private cleanupTimer;
|
|
51
|
+
/** Soft-dep wrapper for the upstream `sessions` plugin. See
|
|
52
|
+
* `./http-api-sessions.ts`. Constructed lazily in `start()` so the
|
|
53
|
+
* upstream plugin has time to register (boot-order race). */
|
|
54
|
+
private sessionsHelper;
|
|
55
|
+
constructor(cordisCtx: any, config: Config, agents: any, auditLog: any, tdBus?: any);
|
|
56
|
+
/** Route table — built once at construction so handlers can reference
|
|
57
|
+
* the service via `this`. See `./http-api-routes.ts` for the matcher. */
|
|
58
|
+
private readonly routes;
|
|
59
|
+
/** Emit an audit-log entry (silent no-op if missing or wrong API). */
|
|
60
|
+
private audit;
|
|
61
|
+
/** One-line access log (visible in /tmp/dsh.log via stdout exporter).
|
|
62
|
+
*
|
|
63
|
+
* §7.123 (2026-10-27): wrapped in try/catch + optional chaining, the same
|
|
64
|
+
* pattern the §7.46.5 chat lines already use. This helper runs on EVERY
|
|
65
|
+
* request, so an unguarded `this.ctx.logger?.info?.(...)` meant a logger
|
|
66
|
+
* that throws (full disk, a formatter bug, a broken exporter) turned
|
|
67
|
+
* every HTTP call into a 500 — the observability path took the data path
|
|
68
|
+
* down with it. A log line must never be able to fail a request. */
|
|
69
|
+
private accessLog;
|
|
70
|
+
/**
|
|
71
|
+
* §7.123 (2026-10-27): logger call that can never throw.
|
|
72
|
+
*
|
|
73
|
+
* A logger is not a trusted dependency: a full disk, a broken exporter or
|
|
74
|
+
* a formatter bug makes `logger.info(...)` throw. On the request path that
|
|
75
|
+
* turned a successful chat into a 500 (see `ensureSession`'s "Creating
|
|
76
|
+
* agent" line, which sat unguarded in the middle of session creation).
|
|
77
|
+
* Observability must never be able to fail the data path.
|
|
78
|
+
*/
|
|
79
|
+
private safeLog;
|
|
80
|
+
/** Append a recent-message entry to the session's ring buffer. Evicts
|
|
81
|
+
* oldest entries when the buffer exceeds MAX_RECENT_MESSAGES_PER_SESSION.
|
|
82
|
+
* Trims text to 200 chars to bound prompt size (intent-router further
|
|
83
|
+
* truncates per-message inside the historyBlock). Always accepts the
|
|
84
|
+
* push — the buffer map is independent of SessionRecord, so it can be
|
|
85
|
+
* written to before ensureSession() finishes (first-turn user message)
|
|
86
|
+
* or after handleDeleteSession() drops the session (orphan cleanup
|
|
87
|
+
* happens on the next sweep). */
|
|
88
|
+
private pushRecentMessage;
|
|
89
|
+
/** Read a snapshot of the session's recent-message ring buffer.
|
|
90
|
+
* Returns [] if the session has no history yet. The caller MUST take
|
|
91
|
+
* this snapshot BEFORE pushing the current user message — the router
|
|
92
|
+
* embeds the snapshot as the "history" block and appends the current
|
|
93
|
+
* query itself. */
|
|
94
|
+
private getPriorRecentMessages;
|
|
95
|
+
/** Drop the buffer for a session. Called from handleDeleteSession,
|
|
96
|
+
* cleanupExpiredSessions, evictLeastRecentSession, and stop().
|
|
97
|
+
*
|
|
98
|
+
* 也负责清除 sanitizer 的会话映射表——那张表里存的是**明文 PII**,
|
|
99
|
+
* 若只删 buffer 不 purge,PII 会一直驻留在堆里直到进程退出
|
|
100
|
+
* (2026-08-30 修复)。软依赖 + best-effort:sanitizer 未注册或
|
|
101
|
+
* purge 失败都不能影响会话删除本身。 */
|
|
102
|
+
private dropSessionBuffer;
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the actual provider + model to use for a given intent verdict.
|
|
105
|
+
* Falls back to the legacy hardcoded pair (`local-llm` /
|
|
106
|
+
* `cloud-llm` plus env-overridable model) when:
|
|
107
|
+
* - `modelRouter` is not registered on the cordis context (boot-order
|
|
108
|
+
* race: http-api boots before model-router in some configs)
|
|
109
|
+
* - `modelRouter.resolve` is unavailable
|
|
110
|
+
* - the lookup throws (defensive: never let router failure break a
|
|
111
|
+
* chat round-trip)
|
|
112
|
+
*
|
|
113
|
+
* Soft-dep pattern (no `inject`) — same as intent-router, because
|
|
114
|
+
* http-api boots ahead of model-router in the canonical plugin order.
|
|
115
|
+
* Extracted as a module-level pure function so unit tests can exercise
|
|
116
|
+
* the resolution matrix without standing up a full HttpApiServiceImpl.
|
|
117
|
+
*/
|
|
118
|
+
private resolveRoute;
|
|
119
|
+
start(): Promise<void>;
|
|
120
|
+
stop(): Promise<void>;
|
|
121
|
+
isRunning(): boolean;
|
|
122
|
+
endpoint(): string;
|
|
123
|
+
private handleRequest;
|
|
124
|
+
/** Dispatch helper — builds the RouteContext (parsing the body only
|
|
125
|
+
* when the route declares `body: 'json'`), invokes the handler, and
|
|
126
|
+
* maps thrown errors to the right HTTP status code (mirrors the
|
|
127
|
+
* original if/else chain's error semantics). */
|
|
128
|
+
private invokeRoute;
|
|
129
|
+
private checkAuth;
|
|
130
|
+
private handleChat;
|
|
131
|
+
private handleChatStream;
|
|
132
|
+
private handleListSessions;
|
|
133
|
+
/** GET /v1/models — returns the dispatcher's provider catalog. Backed
|
|
134
|
+
* by `modelRouter.listDetailed()`; falls back to a hardcoded pair
|
|
135
|
+
* when the router isn't registered (boot-order race; mirrors
|
|
136
|
+
* `resolveRoute()`'s soft-dep pattern at line ~1440).
|
|
137
|
+
*
|
|
138
|
+
* Soft-dep: cordis throws on `ctx.<name>` when the row isn't in
|
|
139
|
+
* `inject`, so wrap the lookup in try/catch and fall back. */
|
|
140
|
+
private handleListModels;
|
|
141
|
+
/** GET /v1/audit?since=&until=&kind=&level=&sessionId=&limit= — query
|
|
142
|
+
* the audit-log. The audit-log API already supports level/kind/sinceMs/
|
|
143
|
+
* untilMs/limit; we additionally filter on `payload.sessionId` post-
|
|
144
|
+
* hoc because sessionId lives in payload (audit entries don't have a
|
|
145
|
+
* first-class sessionId field — they're `(level, kind, source, payload)`
|
|
146
|
+
* records). Limit defaults to 50, hard cap 500, to keep response size
|
|
147
|
+
* bounded — the audit-log JSONL can be very large for long-running
|
|
148
|
+
* deployments.
|
|
149
|
+
*
|
|
150
|
+
* Path leakage: this endpoint NEVER returns the audit file path —
|
|
151
|
+
* only structured entries. Operators wanting to inspect the raw JSONL
|
|
152
|
+
* should use the existing file-system route (the audit-log's config
|
|
153
|
+
* already publishes its root). */
|
|
154
|
+
private handleListAudit;
|
|
155
|
+
private handleGetSession;
|
|
156
|
+
private handleDeleteSession;
|
|
157
|
+
/** POST /v1/sessions/{id}/cancel — best-effort cancel of any in-flight
|
|
158
|
+
* turn on this session. Currently implemented by disposing the live
|
|
159
|
+
* AgentHandle (same as DELETE); the session record is removed from
|
|
160
|
+
* the in-memory map but persisted metadata is retained so /v1/sessions
|
|
161
|
+
* still shows the historical record. Returns 202 Accepted to signal
|
|
162
|
+
* async semantics (the actual abort may race the next chunk). */
|
|
163
|
+
private handleCancelSession;
|
|
164
|
+
/** POST /v1/sessions/{id}/rename — body `{ title }`. Updates the
|
|
165
|
+
* human-readable title in the persistence layer; the in-memory map
|
|
166
|
+
* is intentionally not the source of truth (titles survive restarts). */
|
|
167
|
+
private handleRenameSession;
|
|
168
|
+
/** POST /v1/sessions/{id}/fork — body `{ atMessageIndex? }` (optional
|
|
169
|
+
* index into the recent-message ring buffer to fork from; defaults
|
|
170
|
+
* to "everything"). Creates a fresh session record and copies the
|
|
171
|
+
* source's recent messages (truncated at the requested index) into
|
|
172
|
+
* the new session's ring buffer. The live AgentHandle is NOT copied —
|
|
173
|
+
* the forked session starts fresh; the next followup() on it spins
|
|
174
|
+
* up a new agent. This is the same semantics as the DSH webui's
|
|
175
|
+
* `sessions.fork` RPC. Returns 201 Created with the new sessionId. */
|
|
176
|
+
private handleForkSession;
|
|
177
|
+
/**
|
|
178
|
+
* Resolve the preset id one new session will run under and the `setup` hook
|
|
179
|
+
* that mounts it. Returns `{}` when the deployment has no agent-presets
|
|
180
|
+
* roster (bare dsh CLI without dsh-agent-presets installed) — agents then
|
|
181
|
+
* run on the host composition, which is the pre-preset behaviour.
|
|
182
|
+
*
|
|
183
|
+
* Mirrors dsh-tui's `composePreset()` (lib/types/dsh-adapter/presets.js:35).
|
|
184
|
+
* The original signature is in dsh-tui's private module surface, so we
|
|
185
|
+
* inline a minimal version here: `resolve()` (exact roster match) +
|
|
186
|
+
* `mount(agentCtx, id)` are the two public calls every implementation
|
|
187
|
+
* exposes.
|
|
188
|
+
*/
|
|
189
|
+
private composeAgentPreset;
|
|
190
|
+
private ensureSession;
|
|
191
|
+
/**
|
|
192
|
+
* Clean up sessions that have been idle longer than sessionIdleTimeoutMs.
|
|
193
|
+
* Called periodically by the cleanup timer.
|
|
194
|
+
*/
|
|
195
|
+
private cleanupExpiredSessions;
|
|
196
|
+
/**
|
|
197
|
+
* Evict the least-recently-used session to make room for a new one.
|
|
198
|
+
* Called when maxActiveSessions is reached.
|
|
199
|
+
*/
|
|
200
|
+
private evictLeastRecentSession;
|
|
201
|
+
private readJsonBody;
|
|
202
|
+
private getOpenApiSpec;
|
|
203
|
+
/** Build the route table — public + bearer-auth. Returns an array of
|
|
204
|
+
* RouteDef entries; the dispatcher (handleRequest) walks it to
|
|
205
|
+
* resolve method+path → handler. Built once at construction; the
|
|
206
|
+
* table is therefore stable across the service's lifetime, which
|
|
207
|
+
* lets future optimization (route hot-reload, per-route metrics) hook
|
|
208
|
+
* in at a single construction site. */
|
|
209
|
+
private buildRoutes;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Pure (exported for unit tests) route resolver. See the inline comment
|
|
213
|
+
* in HttpApiServiceImpl#resolveRoute for the soft-dep / fail-open contract.
|
|
214
|
+
*/
|
|
215
|
+
export declare function resolveRoute(ctx: any, isLocal: boolean): {
|
|
216
|
+
provider: string;
|
|
217
|
+
model: string;
|
|
218
|
+
resolved: boolean;
|
|
219
|
+
};
|
|
220
|
+
export declare function apply(ctx: any, config: Config): Promise<() => Promise<void>>;
|
|
221
|
+
//# sourceMappingURL=http-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-api.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/http-api.ts"],"names":[],"mappings":"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"}
|