@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,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* edge-context-view — §7.101 host half of the desktop "Edge" tab.
|
|
3
|
+
*
|
|
4
|
+
* Folds the edge privacy audit stream into four panel row-lists (intent /
|
|
5
|
+
* route / interception / threat) plus a per-session summary (turns + tokens
|
|
6
|
+
* + lifetime), and serves them to the desktop client over a framework-
|
|
7
|
+
* standard Connection POST route (`/api/dsh-edge/context`).
|
|
8
|
+
*
|
|
9
|
+
* Data-path decision (docs/notes/dsh-context-walkthrough.md, rulings A/B/D):
|
|
10
|
+
* - The panel sources are audit-log records — NOT subscribable plugin
|
|
11
|
+
* event streams. Live folding rides the new `auditLog.onEntry` seam;
|
|
12
|
+
* `query()` re-reads the whole file and is unusable as a poll source.
|
|
13
|
+
* - SessionEvent vocabulary is closed (no custom kinds), so
|
|
14
|
+
* `sessionProjections` cannot carry edge privacy events. The transport
|
|
15
|
+
* is the Connection fetch-route (dsh-context's detail-channel pattern);
|
|
16
|
+
* the client pulls with a cursor while the tab is mounted.
|
|
17
|
+
* - `ctx.edgeContextView.snapshot()` is a plain data fold, not an
|
|
18
|
+
* AsyncIterable: the browser cannot subscribe to cordis services, so a
|
|
19
|
+
* host-side stream would be an untestable dead API. Unit tests hit the
|
|
20
|
+
* fold directly.
|
|
21
|
+
* - Turns/tokens ride the in-process `session/event` listener (same
|
|
22
|
+
* idiom as http-api.ts:567): turn count from `turn/start`, tokens from
|
|
23
|
+
* `assistant/message` `usage` when the adapter reported accounting —
|
|
24
|
+
* absent usage, the summary says so instead of promising numbers.
|
|
25
|
+
*
|
|
26
|
+
* PII discipline: rows are counts/categories/timestamps/levels only at the
|
|
27
|
+
* panel level; before a payload is stored, every string value passes the
|
|
28
|
+
* `pii-utils` 5-class scanner a second time (audit-log already redacts on
|
|
29
|
+
* write — this is belt-and-braces per the plan's hard rule, `piiRedact`).
|
|
30
|
+
*
|
|
31
|
+
* @module dsh-edge-agent/edge-context-view
|
|
32
|
+
*/
|
|
33
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
34
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
35
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
36
|
+
import type { LlmTraceStoreService } from './llm-trace-store.js';
|
|
37
|
+
import type { AuditEntry } from './audit-log.js';
|
|
38
|
+
export declare const name = "edge-context-view";
|
|
39
|
+
/** Hard dep: the fold is fed exclusively through the audit stream.
|
|
40
|
+
* `connection` arms later via deferred ctx.inject (headless profiles
|
|
41
|
+
* never compose it and the plugin must still mount cleanly). */
|
|
42
|
+
export declare const inject: readonly ["auditLog"];
|
|
43
|
+
export interface Config {
|
|
44
|
+
/** Per-panel row cap (ring). Older rows are dropped from the front. */
|
|
45
|
+
bufferSize: number;
|
|
46
|
+
/** Idle sessions are swept from the store after this long. */
|
|
47
|
+
sessionTtlMs: number;
|
|
48
|
+
/** Re-scan every stored payload string against the 5 PII classes. */
|
|
49
|
+
piiRedact: boolean;
|
|
50
|
+
}
|
|
51
|
+
export declare const Config: Schema<Config>;
|
|
52
|
+
/** Panel ids the client mounts. */
|
|
53
|
+
export type EdgePanel = 'intent' | 'route' | 'interception' | 'threat';
|
|
54
|
+
export interface EdgeRow {
|
|
55
|
+
ts: number;
|
|
56
|
+
kind: string;
|
|
57
|
+
panel: EdgePanel;
|
|
58
|
+
/** Audit entry level, carried so the client can tint warn/error rows. */
|
|
59
|
+
level: string;
|
|
60
|
+
payload: Record<string, unknown>;
|
|
61
|
+
/** Service-global monotonic sequence — the cursor is comparable across
|
|
62
|
+
* the session bucket and the global bucket, which is what a pull
|
|
63
|
+
* merges. (A per-bucket counter would let either side rewind it.) */
|
|
64
|
+
seq: number;
|
|
65
|
+
/** §7.106: committed-turn count at record time; -1 = global-bucket row
|
|
66
|
+
* (session-independent: boot probes, rate-limit signals — always
|
|
67
|
+
* rendered). The client keeps session rows whose turn equals the
|
|
68
|
+
* session's current turn — each new turn wipes the panel back to fresh
|
|
69
|
+
* records (user ruling 2026-10-22: 新的对话旧记录删除). */
|
|
70
|
+
turn: number;
|
|
71
|
+
}
|
|
72
|
+
export interface EdgeSessionSummary {
|
|
73
|
+
sessionId: string;
|
|
74
|
+
startedAt?: number;
|
|
75
|
+
lastSeenAt?: number;
|
|
76
|
+
/** Committed turn count (from `turn/start` session events). Doubles as the
|
|
77
|
+
* current-turn tag the client filters rows by (see {@link EdgeRow.turn}). */
|
|
78
|
+
turns: number;
|
|
79
|
+
/** §7.106: wall clock of the newest `turn/start`. Global-bucket rows (turn
|
|
80
|
+
* -1 — host answer steps carry no sessionId into adapter audits) have no
|
|
81
|
+
* turn attribution, so the client renders them only when ts >= this:
|
|
82
|
+
* last-turn rows age out with the window. */
|
|
83
|
+
turnStartTs?: number;
|
|
84
|
+
/** Token accounting from `assistant/message.usage`; undefined until an
|
|
85
|
+
* adapter reported usage (never faked as 0). */
|
|
86
|
+
tokens?: {
|
|
87
|
+
input?: number;
|
|
88
|
+
output?: number;
|
|
89
|
+
cacheRead?: number;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* §7.106 (2026-10-23): the last answer route observed for this session.
|
|
93
|
+
* `request/context` only fires when the route CHANGES (host contract),
|
|
94
|
+
* so a window filtered to the current turn would show "route undecided"
|
|
95
|
+
* for every turn that kept the same model — even after 3.6k tokens had
|
|
96
|
+
* already streamed back. The route outlives the window; the answer node
|
|
97
|
+
* reads this when no in-window row exists.
|
|
98
|
+
*/
|
|
99
|
+
lastRoute?: {
|
|
100
|
+
provider: string;
|
|
101
|
+
model: string;
|
|
102
|
+
ts: number;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export interface EdgeContextSnapshot {
|
|
106
|
+
/** Cursor to pass back on the next pull: rows strictly newer than this. */
|
|
107
|
+
cursor: number;
|
|
108
|
+
rows: EdgeRow[];
|
|
109
|
+
summary: EdgeSessionSummary;
|
|
110
|
+
}
|
|
111
|
+
export interface EdgeContextViewService {
|
|
112
|
+
snapshot(sessionId: string, cursor?: number): EdgeContextSnapshot;
|
|
113
|
+
/** Test/inspection helper. */
|
|
114
|
+
trackedSessionIds(): string[];
|
|
115
|
+
/**
|
|
116
|
+
* §7.133: LLM trace 正文存储 —— 索引行在 audit 里,正文在这里。
|
|
117
|
+
* 面板点开一次调用时按 traceId 取。
|
|
118
|
+
*/
|
|
119
|
+
readonly traceStore: LlmTraceStoreService;
|
|
120
|
+
}
|
|
121
|
+
/** Panel resolution with the threat re-route winning over the generic
|
|
122
|
+
* interception prefix. */
|
|
123
|
+
export declare function panelForKind(kind: string): EdgePanel | undefined;
|
|
124
|
+
/** Best-effort session attribution. Most guard/intent audit sites do not
|
|
125
|
+
* carry a sessionId (they run pre-turn); those rows land in `_global`
|
|
126
|
+
* and are merged into every snapshot — same fallback semantics as the
|
|
127
|
+
* §7.43 realm forensics. Rows with a real sessionId stay session-scoped. */
|
|
128
|
+
export declare const GLOBAL_BUCKET = "_global";
|
|
129
|
+
/** Copy the payload with only scalar-safe fields, re-tokenized. Nested
|
|
130
|
+
* objects/arrays are flattened away EXCEPT small primitives we want
|
|
131
|
+
* (numbers/booleans kept; strings run through the PII scanner). */
|
|
132
|
+
export declare function safePayload(payload: Record<string, unknown>, piiRedact: boolean): Record<string, unknown>;
|
|
133
|
+
export declare class EdgeContextViewServiceImpl extends Service implements EdgeContextViewService {
|
|
134
|
+
private readonly cfg;
|
|
135
|
+
private readonly buckets;
|
|
136
|
+
private readonly summaries;
|
|
137
|
+
/** §7.133: LLM trace 正文存储(内存 ring)。 */
|
|
138
|
+
readonly traceStore: LlmTraceStoreService;
|
|
139
|
+
/** Service-global row sequence (the pull cursor). */
|
|
140
|
+
private rowSeq;
|
|
141
|
+
private sweepTimer;
|
|
142
|
+
constructor(ctx: Context, config: Config);
|
|
143
|
+
/** Wire the live fold. Returns a disposer. */
|
|
144
|
+
start(): () => void;
|
|
145
|
+
/** Fold one audit record into the right panel bucket. Exported for tests
|
|
146
|
+
* through the service; kept public because the session-event listener
|
|
147
|
+
* also synthesizes rows through it. */
|
|
148
|
+
record(entry: AuditEntry): void;
|
|
149
|
+
/** session/event fold (turn count + host answer rows + usage). `usage` is
|
|
150
|
+
* read defensively: adapters report it only when the upstream answered
|
|
151
|
+
* with accounting. §7.106: field names follow dsh-llm's `TokenUsage`
|
|
152
|
+
* (`inputTokens/outputTokens/cacheReadTokens`); the earlier code read
|
|
153
|
+
* `input/output`, which made every real host usage map to nothing while
|
|
154
|
+
* an empty `tokens` object still painted "0 in / 0 out" — tokens is now
|
|
155
|
+
* only created when at least one authoritative number arrived. */
|
|
156
|
+
recordSessionEvent(sessionId: string, type: string, data: unknown): void;
|
|
157
|
+
/** Synthesize a Route-panel row from a session-event observation. Same ring
|
|
158
|
+
* and cursor as audit-fed rows; level `info`, PII-free scalars only. */
|
|
159
|
+
private hostRow;
|
|
160
|
+
snapshot(sessionId: string, cursor?: number): EdgeContextSnapshot;
|
|
161
|
+
trackedSessionIds(): string[];
|
|
162
|
+
private bucketFor;
|
|
163
|
+
private summaryFor;
|
|
164
|
+
private sweep;
|
|
165
|
+
}
|
|
166
|
+
declare module '@deepseek-ai/cordis' {
|
|
167
|
+
interface Context {
|
|
168
|
+
edgeContextView: EdgeContextViewServiceImpl;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** The client-side pull route (browser-relative spelling lives in the
|
|
172
|
+
* client bundle; the host registers the absolute path behind the
|
|
173
|
+
* harness `/api` fence — same discipline as dsh-context apiRoutes). */
|
|
174
|
+
export declare const EDGE_CONTEXT_ROUTE = "/api/dsh-edge/context";
|
|
175
|
+
/**
|
|
176
|
+
* §7.133: 取 LLM trace 正文的路由。
|
|
177
|
+
*
|
|
178
|
+
* 面板点开一次调用 → POST `{ traceId }` → 返回那条 trace 的完整正文。
|
|
179
|
+
* 索引行在 audit 里(几十字节),正文在这里按需取 —— 面板常驻内存不
|
|
180
|
+
* 受 22.5 万字符的 request 正文影响。
|
|
181
|
+
*/
|
|
182
|
+
export declare const EDGE_TRACE_ROUTE = "/api/dsh-edge/trace";
|
|
183
|
+
export declare function apply(ctx: Context, config: Config): Promise<() => void>;
|
|
184
|
+
//# sourceMappingURL=edge-context-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edge-context-view.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/edge-context-view.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* edge-context-view — §7.101 host half of the desktop "Edge" tab.
|
|
3
|
+
*
|
|
4
|
+
* Folds the edge privacy audit stream into four panel row-lists (intent /
|
|
5
|
+
* route / interception / threat) plus a per-session summary (turns + tokens
|
|
6
|
+
* + lifetime), and serves them to the desktop client over a framework-
|
|
7
|
+
* standard Connection POST route (`/api/dsh-edge/context`).
|
|
8
|
+
*
|
|
9
|
+
* Data-path decision (docs/notes/dsh-context-walkthrough.md, rulings A/B/D):
|
|
10
|
+
* - The panel sources are audit-log records — NOT subscribable plugin
|
|
11
|
+
* event streams. Live folding rides the new `auditLog.onEntry` seam;
|
|
12
|
+
* `query()` re-reads the whole file and is unusable as a poll source.
|
|
13
|
+
* - SessionEvent vocabulary is closed (no custom kinds), so
|
|
14
|
+
* `sessionProjections` cannot carry edge privacy events. The transport
|
|
15
|
+
* is the Connection fetch-route (dsh-context's detail-channel pattern);
|
|
16
|
+
* the client pulls with a cursor while the tab is mounted.
|
|
17
|
+
* - `ctx.edgeContextView.snapshot()` is a plain data fold, not an
|
|
18
|
+
* AsyncIterable: the browser cannot subscribe to cordis services, so a
|
|
19
|
+
* host-side stream would be an untestable dead API. Unit tests hit the
|
|
20
|
+
* fold directly.
|
|
21
|
+
* - Turns/tokens ride the in-process `session/event` listener (same
|
|
22
|
+
* idiom as http-api.ts:567): turn count from `turn/start`, tokens from
|
|
23
|
+
* `assistant/message` `usage` when the adapter reported accounting —
|
|
24
|
+
* absent usage, the summary says so instead of promising numbers.
|
|
25
|
+
*
|
|
26
|
+
* PII discipline: rows are counts/categories/timestamps/levels only at the
|
|
27
|
+
* panel level; before a payload is stored, every string value passes the
|
|
28
|
+
* `pii-utils` 5-class scanner a second time (audit-log already redacts on
|
|
29
|
+
* write — this is belt-and-braces per the plan's hard rule, `piiRedact`).
|
|
30
|
+
*
|
|
31
|
+
* @module dsh-edge-agent/edge-context-view
|
|
32
|
+
*/
|
|
33
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
34
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
35
|
+
import { redactObject } from './pii-utils.js';
|
|
36
|
+
import { provideTraceStore } from './llm-trace-store.js';
|
|
37
|
+
import { realSessionId } from './session-attribution.js';
|
|
38
|
+
export const name = 'edge-context-view';
|
|
39
|
+
/** Hard dep: the fold is fed exclusively through the audit stream.
|
|
40
|
+
* `connection` arms later via deferred ctx.inject (headless profiles
|
|
41
|
+
* never compose it and the plugin must still mount cleanly). */
|
|
42
|
+
export const inject = ['auditLog'];
|
|
43
|
+
export const Config = Schema.object({
|
|
44
|
+
bufferSize: Schema.natural().default(64),
|
|
45
|
+
sessionTtlMs: Schema.natural().default(1_800_000),
|
|
46
|
+
piiRedact: Schema.boolean().default(true),
|
|
47
|
+
});
|
|
48
|
+
/** Kinds that should never reach a panel — boot-time plumbing, hook
|
|
49
|
+
* registration, dependency probes. They are useful in audit but
|
|
50
|
+
* useless (and confusing) in a live monitor. */
|
|
51
|
+
const INTERNAL_KINDS = new Set([
|
|
52
|
+
'input-guard.hook.registered',
|
|
53
|
+
'input-guard.dependency.missing',
|
|
54
|
+
'input-guard.boot.probe',
|
|
55
|
+
'input-guard.session-realm.registered',
|
|
56
|
+
]);
|
|
57
|
+
/** Kind → panel routing table, dumped from the live audit call sites
|
|
58
|
+
* (see walkthrough §6). Unlisted kinds are ignored — the store only
|
|
59
|
+
* carries what the panels render. */
|
|
60
|
+
const PANEL_BY_KIND = [
|
|
61
|
+
// §7.175 (2026-11-11): `input-guard.intent.downgraded` 退役(路由改写
|
|
62
|
+
// 机制整体撤掉),取代它的是 `input-guard.intent.local` —— 本轮判定为
|
|
63
|
+
// LOCAL 时记下的**实际用的** provider/model。它是意图裁决的一环 ⇒ 进
|
|
64
|
+
// intent 面板(此前 `intent.downgraded` 也在这一档)。
|
|
65
|
+
{ match: k => k.startsWith('intent.') || k === 'input-guard.intent.local', panel: 'intent' },
|
|
66
|
+
{
|
|
67
|
+
match: k => k === 'http.chat.route'
|
|
68
|
+
|| k === 'http.chat.stream.route'
|
|
69
|
+
|| k.startsWith('llm.cloud.')
|
|
70
|
+
|| k.startsWith('llm.local.')
|
|
71
|
+
// §7.106: host answer routes folded from the session event stream —
|
|
72
|
+
// the desktop GUI answers through the host's own model (never an
|
|
73
|
+
// edge adapter), so without these the Route panel cannot show where
|
|
74
|
+
// the reply actually came from.
|
|
75
|
+
|| k.startsWith('llm.host.')
|
|
76
|
+
// §7.133: LLM 调用的**索引行** —— 面板列出「这一轮调了哪几次 LLM、
|
|
77
|
+
// 各自多大、多久」。正文不在行里(单轮 22.5 万字符),点开时按
|
|
78
|
+
// traceId 去 store 取。
|
|
79
|
+
|| k === 'llm.trace.index',
|
|
80
|
+
panel: 'route',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
// Interception = everything the guards/sanitizers decided or rewrote.
|
|
84
|
+
// intent.*/http.chat.route already matched above, so order matters.
|
|
85
|
+
match: k => (!INTERNAL_KINDS.has(k) && k.startsWith('input-guard.'))
|
|
86
|
+
|| k.startsWith('guard.')
|
|
87
|
+
|| k.startsWith('sanitizer.')
|
|
88
|
+
|| k.startsWith('input.'),
|
|
89
|
+
panel: 'interception',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
// Threat: escalations + rate-limit trips + explicit threat blocks.
|
|
93
|
+
// (`input.rate-limit`/`input.global-rate-limit` hit the interception
|
|
94
|
+
// rule first by prefix; re-route them here — they are td signals.)
|
|
95
|
+
match: k => k.startsWith('td-bus.')
|
|
96
|
+
|| k === 'http.chat.threat-blocked'
|
|
97
|
+
|| k === 'input.rate-limit'
|
|
98
|
+
|| k === 'input.global-rate-limit',
|
|
99
|
+
panel: 'threat',
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
/** Panel resolution with the threat re-route winning over the generic
|
|
103
|
+
* interception prefix. */
|
|
104
|
+
export function panelForKind(kind) {
|
|
105
|
+
if (kind === 'input.rate-limit' || kind === 'input.global-rate-limit')
|
|
106
|
+
return 'threat';
|
|
107
|
+
for (const rule of PANEL_BY_KIND) {
|
|
108
|
+
if (rule.match(kind))
|
|
109
|
+
return rule.panel;
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
/** Best-effort session attribution. Most guard/intent audit sites do not
|
|
114
|
+
* carry a sessionId (they run pre-turn); those rows land in `_global`
|
|
115
|
+
* and are merged into every snapshot — same fallback semantics as the
|
|
116
|
+
* §7.43 realm forensics. Rows with a real sessionId stay session-scoped. */
|
|
117
|
+
export const GLOBAL_BUCKET = '_global';
|
|
118
|
+
function extractSessionId(payload) {
|
|
119
|
+
// §7.147: 哨兵 `'<pre-session>'`(ops log 的「无会话」显示约定,§7.52)
|
|
120
|
+
// **不是**会话 id。它若被当成 id,会造出一个不存在的桶 —— 那几行从此只在
|
|
121
|
+
// 「有人请求那个假 id」时可见,等于从所有真实会话的视图里消失(比归全局
|
|
122
|
+
// 更糟:宁可全局可见,不要无处可见)。所以这里也判成「没有会话」。
|
|
123
|
+
const sid = realSessionId(payload.sessionId ?? payload.session);
|
|
124
|
+
return sid ?? GLOBAL_BUCKET;
|
|
125
|
+
}
|
|
126
|
+
/** §7.121: how many elements of a string array survive into the panel.
|
|
127
|
+
* Rule-name lists are short (injection rules ≤ a few dozen); the cap is
|
|
128
|
+
* a payload-size guard, not a semantic one. */
|
|
129
|
+
const ARRAY_CAP = 32;
|
|
130
|
+
/** Copy the payload with only scalar-safe fields, re-tokenized. Nested
|
|
131
|
+
* objects/arrays are flattened away EXCEPT small primitives we want
|
|
132
|
+
* (numbers/booleans kept; strings run through the PII scanner). */
|
|
133
|
+
export function safePayload(payload, piiRedact) {
|
|
134
|
+
const out = {};
|
|
135
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
136
|
+
if (typeof value === 'string') {
|
|
137
|
+
if (!piiRedact) {
|
|
138
|
+
out[key] = value;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const { redacted } = redactObject({ [key]: value });
|
|
142
|
+
const v = redacted[key];
|
|
143
|
+
out[key] = typeof v === 'string' ? v : value;
|
|
144
|
+
}
|
|
145
|
+
else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
146
|
+
out[key] = value;
|
|
147
|
+
}
|
|
148
|
+
else if (value === null) {
|
|
149
|
+
out[key] = null;
|
|
150
|
+
}
|
|
151
|
+
else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
152
|
+
// §7.106 (2026-10-23): ONE level of scalar-only nested objects is
|
|
153
|
+
// flattened (`from`/`to` of route.overridden carry exactly the rewrite
|
|
154
|
+
// evidence the flow view shows — dropping them rendered "?/? → ?/?").
|
|
155
|
+
// Deeper objects and arrays stay dropped: panels render scalar rows;
|
|
156
|
+
// deep shapes are the detail channel the plan explicitly cut.
|
|
157
|
+
for (const [k, v] of Object.entries(value)) {
|
|
158
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
159
|
+
out[`${key}.${k}`] = v;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (Array.isArray(value)) {
|
|
164
|
+
// §7.121: string arrays carry the **原始命中明细** and were dropped
|
|
165
|
+
// wholesale — which silently erased the very evidence the operator
|
|
166
|
+
// needs ("哪条规则命中了"). `input.injection.warned` carries
|
|
167
|
+
// `patterns: ['role-override-zh']`; `guard.model-detect.unsafe`
|
|
168
|
+
// carries `threats: [...]`; `sanitizer.sanitized` carries the matched
|
|
169
|
+
// PII types. All of them are **rule/type identifiers** — never user
|
|
170
|
+
// content — so they are safe to surface verbatim.
|
|
171
|
+
//
|
|
172
|
+
// Guardrails: only string elements are kept (numbers/objects are
|
|
173
|
+
// dropped, an object element could carry anything); the count is
|
|
174
|
+
// capped; each element is still PII-scanned so a rule name that
|
|
175
|
+
// somehow embeds a phone number cannot leak.
|
|
176
|
+
const parts = [];
|
|
177
|
+
for (const item of value) {
|
|
178
|
+
if (typeof item !== 'string')
|
|
179
|
+
continue;
|
|
180
|
+
parts.push(piiRedact ? redactObject({ [key]: item }).redacted[key] : item);
|
|
181
|
+
if (parts.length >= ARRAY_CAP)
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
if (parts.length > 0)
|
|
185
|
+
out[key] = parts;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
export class EdgeContextViewServiceImpl extends Service {
|
|
191
|
+
cfg;
|
|
192
|
+
buckets = new Map();
|
|
193
|
+
summaries = new Map();
|
|
194
|
+
/** §7.133: LLM trace 正文存储(内存 ring)。 */
|
|
195
|
+
traceStore;
|
|
196
|
+
/** Service-global row sequence (the pull cursor). */
|
|
197
|
+
rowSeq = 0;
|
|
198
|
+
sweepTimer;
|
|
199
|
+
constructor(ctx, config) {
|
|
200
|
+
super(ctx, 'edgeContextView');
|
|
201
|
+
this.cfg = config;
|
|
202
|
+
// §7.133: 取**共享**的 trace store(写方是 adapter / 安检 / 分类器,
|
|
203
|
+
// 它们先注册;这里取同一个实例 —— 模块单例保证读写同库)。
|
|
204
|
+
this.traceStore = provideTraceStore(ctx);
|
|
205
|
+
}
|
|
206
|
+
/** Wire the live fold. Returns a disposer. */
|
|
207
|
+
start() {
|
|
208
|
+
const auditLog = this.ctx.auditLog;
|
|
209
|
+
const off = auditLog?.onEntry?.(entry => this.record(entry)) ?? (() => { });
|
|
210
|
+
this.sweepTimer = setInterval(() => this.sweep(), Math.max(60_000, Math.floor(this.cfg.sessionTtlMs / 2)));
|
|
211
|
+
if (typeof this.sweepTimer.unref === 'function')
|
|
212
|
+
this.sweepTimer.unref();
|
|
213
|
+
return () => {
|
|
214
|
+
off();
|
|
215
|
+
if (this.sweepTimer !== undefined)
|
|
216
|
+
clearInterval(this.sweepTimer);
|
|
217
|
+
this.sweepTimer = undefined;
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/** Fold one audit record into the right panel bucket. Exported for tests
|
|
221
|
+
* through the service; kept public because the session-event listener
|
|
222
|
+
* also synthesizes rows through it. */
|
|
223
|
+
record(entry) {
|
|
224
|
+
const panel = panelForKind(entry.kind);
|
|
225
|
+
if (panel === undefined)
|
|
226
|
+
return;
|
|
227
|
+
const sessionId = extractSessionId(entry.payload);
|
|
228
|
+
const bucket = this.bucketFor(sessionId);
|
|
229
|
+
this.rowSeq += 1;
|
|
230
|
+
const row = {
|
|
231
|
+
ts: entry.ts,
|
|
232
|
+
kind: entry.kind,
|
|
233
|
+
panel,
|
|
234
|
+
level: entry.level,
|
|
235
|
+
seq: this.rowSeq,
|
|
236
|
+
payload: safePayload(entry.payload, this.cfg.piiRedact),
|
|
237
|
+
// Read-only turn tag: rows fold during a turn (pre-step runs after
|
|
238
|
+
// turn/start commits), so the count already reflects this turn.
|
|
239
|
+
// Global-bucket rows (no session — boot probes, rate-limit signals)
|
|
240
|
+
// carry the -1 sentinel: they are session-independent and always
|
|
241
|
+
// render (filtering them would silence the Threat panel forever).
|
|
242
|
+
turn: sessionId === GLOBAL_BUCKET ? -1 : (this.summaries.get(sessionId)?.turns ?? 0),
|
|
243
|
+
};
|
|
244
|
+
bucket.rows.push(row);
|
|
245
|
+
if (bucket.rows.length > this.cfg.bufferSize)
|
|
246
|
+
bucket.rows.splice(0, bucket.rows.length - this.cfg.bufferSize);
|
|
247
|
+
bucket.lastTouched = Date.now();
|
|
248
|
+
}
|
|
249
|
+
/** session/event fold (turn count + host answer rows + usage). `usage` is
|
|
250
|
+
* read defensively: adapters report it only when the upstream answered
|
|
251
|
+
* with accounting. §7.106: field names follow dsh-llm's `TokenUsage`
|
|
252
|
+
* (`inputTokens/outputTokens/cacheReadTokens`); the earlier code read
|
|
253
|
+
* `input/output`, which made every real host usage map to nothing while
|
|
254
|
+
* an empty `tokens` object still painted "0 in / 0 out" — tokens is now
|
|
255
|
+
* only created when at least one authoritative number arrived. */
|
|
256
|
+
recordSessionEvent(sessionId, type, data) {
|
|
257
|
+
const summary = this.summaryFor(sessionId);
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
summary.lastSeenAt = now;
|
|
260
|
+
if (summary.startedAt === undefined)
|
|
261
|
+
summary.startedAt = now;
|
|
262
|
+
if (type === 'turn/start') {
|
|
263
|
+
summary.turns += 1;
|
|
264
|
+
summary.turnStartTs = now;
|
|
265
|
+
}
|
|
266
|
+
else if (type === 'assistant/message') {
|
|
267
|
+
const usage = data?.usage;
|
|
268
|
+
if (usage !== null && typeof usage === 'object') {
|
|
269
|
+
const acc = (key, legacyKey) => {
|
|
270
|
+
const v = usage[key] ?? usage[legacyKey];
|
|
271
|
+
return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined;
|
|
272
|
+
};
|
|
273
|
+
const input = acc('inputTokens', 'input');
|
|
274
|
+
const output = acc('outputTokens', 'output');
|
|
275
|
+
const cacheRead = acc('cacheReadTokens', 'cacheRead');
|
|
276
|
+
if (input !== undefined || output !== undefined || cacheRead !== undefined) {
|
|
277
|
+
summary.tokens = summary.tokens ?? {};
|
|
278
|
+
if (input !== undefined)
|
|
279
|
+
summary.tokens.input = (summary.tokens.input ?? 0) + input;
|
|
280
|
+
if (output !== undefined)
|
|
281
|
+
summary.tokens.output = (summary.tokens.output ?? 0) + output;
|
|
282
|
+
if (cacheRead !== undefined)
|
|
283
|
+
summary.tokens.cacheRead = (summary.tokens.cacheRead ?? 0) + cacheRead;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else if (type === 'request/context') {
|
|
288
|
+
// The host resolved its answer route (dsh records this event when the
|
|
289
|
+
// provider/model changes — one row per distinct route, not per turn).
|
|
290
|
+
const ctxData = (data ?? {});
|
|
291
|
+
if (typeof ctxData.provider === 'string' || typeof ctxData.model === 'string') {
|
|
292
|
+
const provider = typeof ctxData.provider === 'string' ? ctxData.provider : '';
|
|
293
|
+
const model = typeof ctxData.model === 'string' ? ctxData.model : '';
|
|
294
|
+
this.hostRow(sessionId, 'llm.host.route', { provider, model });
|
|
295
|
+
// Route outlives the turn window — the client answer node reads it
|
|
296
|
+
// when the current turn produced no new request/context event.
|
|
297
|
+
if (provider.length > 0)
|
|
298
|
+
summary.lastRoute = { provider, model, ts: now };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/** Synthesize a Route-panel row from a session-event observation. Same ring
|
|
303
|
+
* and cursor as audit-fed rows; level `info`, PII-free scalars only. */
|
|
304
|
+
hostRow(sessionId, kind, fields) {
|
|
305
|
+
this.rowSeq += 1;
|
|
306
|
+
const bucket = this.bucketFor(sessionId);
|
|
307
|
+
bucket.rows.push({
|
|
308
|
+
ts: Date.now(),
|
|
309
|
+
kind,
|
|
310
|
+
panel: 'route',
|
|
311
|
+
level: 'info',
|
|
312
|
+
seq: this.rowSeq,
|
|
313
|
+
payload: { ...fields },
|
|
314
|
+
turn: this.summaries.get(sessionId)?.turns ?? 0,
|
|
315
|
+
});
|
|
316
|
+
if (bucket.rows.length > this.cfg.bufferSize)
|
|
317
|
+
bucket.rows.splice(0, bucket.rows.length - this.cfg.bufferSize);
|
|
318
|
+
bucket.lastTouched = Date.now();
|
|
319
|
+
}
|
|
320
|
+
snapshot(sessionId, cursor = 0) {
|
|
321
|
+
const rows = [];
|
|
322
|
+
// §7.133: the global bucket must not be visited twice. Asking for the
|
|
323
|
+
// `_global` snapshot used to push every global row **twice** (the same
|
|
324
|
+
// bucket object came back from both lookups) — the panel then rendered
|
|
325
|
+
// each event twice. It became visible with the trace rows (§7.133): one
|
|
326
|
+
// LLM call showed as two identical entries.
|
|
327
|
+
const buckets = sessionId === GLOBAL_BUCKET
|
|
328
|
+
? [this.buckets.get(GLOBAL_BUCKET)]
|
|
329
|
+
: [this.buckets.get(sessionId), this.buckets.get(GLOBAL_BUCKET)];
|
|
330
|
+
for (const bucket of buckets) {
|
|
331
|
+
if (!bucket)
|
|
332
|
+
continue;
|
|
333
|
+
for (const row of bucket.rows) {
|
|
334
|
+
if (row.seq > cursor)
|
|
335
|
+
rows.push(row);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
rows.sort((a, b) => a.seq - b.seq);
|
|
339
|
+
const nextCursor = rows.reduce((m, r) => Math.max(m, r.seq), cursor);
|
|
340
|
+
const summary = this.summaryFor(sessionId);
|
|
341
|
+
return { cursor: nextCursor, rows, summary: { ...summary } };
|
|
342
|
+
}
|
|
343
|
+
trackedSessionIds() {
|
|
344
|
+
return [...this.buckets.keys()].filter(k => k !== GLOBAL_BUCKET);
|
|
345
|
+
}
|
|
346
|
+
bucketFor(sessionId) {
|
|
347
|
+
let bucket = this.buckets.get(sessionId);
|
|
348
|
+
if (!bucket) {
|
|
349
|
+
bucket = { rows: [], lastTouched: Date.now() };
|
|
350
|
+
this.buckets.set(sessionId, bucket);
|
|
351
|
+
}
|
|
352
|
+
return bucket;
|
|
353
|
+
}
|
|
354
|
+
summaryFor(sessionId) {
|
|
355
|
+
let summary = this.summaries.get(sessionId);
|
|
356
|
+
if (!summary) {
|
|
357
|
+
summary = { sessionId, turns: 0 };
|
|
358
|
+
this.summaries.set(sessionId, summary);
|
|
359
|
+
}
|
|
360
|
+
return summary;
|
|
361
|
+
}
|
|
362
|
+
sweep() {
|
|
363
|
+
const now = Date.now();
|
|
364
|
+
for (const [id, bucket] of this.buckets) {
|
|
365
|
+
if (id !== GLOBAL_BUCKET && now - bucket.lastTouched > this.cfg.sessionTtlMs) {
|
|
366
|
+
this.buckets.delete(id);
|
|
367
|
+
this.summaries.delete(id);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** The client-side pull route (browser-relative spelling lives in the
|
|
373
|
+
* client bundle; the host registers the absolute path behind the
|
|
374
|
+
* harness `/api` fence — same discipline as dsh-context apiRoutes). */
|
|
375
|
+
export const EDGE_CONTEXT_ROUTE = '/api/dsh-edge/context';
|
|
376
|
+
/**
|
|
377
|
+
* §7.133: 取 LLM trace 正文的路由。
|
|
378
|
+
*
|
|
379
|
+
* 面板点开一次调用 → POST `{ traceId }` → 返回那条 trace 的完整正文。
|
|
380
|
+
* 索引行在 audit 里(几十字节),正文在这里按需取 —— 面板常驻内存不
|
|
381
|
+
* 受 22.5 万字符的 request 正文影响。
|
|
382
|
+
*/
|
|
383
|
+
export const EDGE_TRACE_ROUTE = '/api/dsh-edge/trace';
|
|
384
|
+
export async function apply(ctx, config) {
|
|
385
|
+
// Boot evidence for the desktop ops log (file-logger picks this up via
|
|
386
|
+
// the ctx.logger exporter chain). Proves the patch row activated.
|
|
387
|
+
ctx.logger?.info?.('[edge-context-view] apply() called — Edge tab host half mounting');
|
|
388
|
+
const service = new EdgeContextViewServiceImpl(ctx, config);
|
|
389
|
+
// No `ctx.edgeContextView = ...` assignment: cordis 0.2.1-rc.2 plugin
|
|
390
|
+
// proxy ctx can't accept arbitrary property writes (would throw
|
|
391
|
+
// `cannot set property "x" without provide`). Service is captured by
|
|
392
|
+
// closure in the route handler below.
|
|
393
|
+
const disposeFold = service.start();
|
|
394
|
+
// Deferred arms: `connection` only exists on GUI hosts. Headless profiles
|
|
395
|
+
// (dsh-edge/dsh-tui) never compose it → no route, plugin stays clean.
|
|
396
|
+
ctx.inject(['connection'], (raw) => {
|
|
397
|
+
const c = raw;
|
|
398
|
+
const register = c.connection?.fetch?.register;
|
|
399
|
+
if (typeof register !== 'function')
|
|
400
|
+
return;
|
|
401
|
+
try {
|
|
402
|
+
register.call(c.connection.fetch, {
|
|
403
|
+
path: EDGE_CONTEXT_ROUTE,
|
|
404
|
+
methods: ['POST'],
|
|
405
|
+
requestBody: 'buffered',
|
|
406
|
+
fetch: async (request) => {
|
|
407
|
+
// Re-prove every input at the boundary — the wire is untrusted.
|
|
408
|
+
let body;
|
|
409
|
+
try {
|
|
410
|
+
body = await request.json();
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return new Response('{"error":"invalid-json"}', { status: 400, headers: { 'content-type': 'application/json' } });
|
|
414
|
+
}
|
|
415
|
+
const sessionId = typeof body.sessionId === 'string' && body.sessionId.length > 0 ? body.sessionId : GLOBAL_BUCKET;
|
|
416
|
+
const cursor = typeof body.cursor === 'number' && Number.isFinite(body.cursor) && body.cursor >= 0
|
|
417
|
+
? Math.floor(body.cursor) : 0;
|
|
418
|
+
const snapshot = service.snapshot(sessionId, cursor);
|
|
419
|
+
// Host rows are already PII-scanned at store time; keep the audit
|
|
420
|
+
// trail of who is pulling out of the payload (no new kinds).
|
|
421
|
+
return new Response(JSON.stringify(snapshot), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
catch { /* a rejecting registry only costs the route, not the plugin */ }
|
|
426
|
+
// §7.133: trace 正文路由 —— 面板点开一次调用时按 traceId 取。
|
|
427
|
+
try {
|
|
428
|
+
register.call(c.connection.fetch, {
|
|
429
|
+
path: EDGE_TRACE_ROUTE,
|
|
430
|
+
methods: ['POST'],
|
|
431
|
+
requestBody: 'buffered',
|
|
432
|
+
fetch: async (request) => {
|
|
433
|
+
let body;
|
|
434
|
+
try {
|
|
435
|
+
body = await request.json();
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return new Response('{"error":"invalid-json"}', { status: 400, headers: { 'content-type': 'application/json' } });
|
|
439
|
+
}
|
|
440
|
+
const traceId = typeof body.traceId === 'string' ? body.traceId : '';
|
|
441
|
+
if (traceId.length === 0) {
|
|
442
|
+
return new Response('{"error":"missing-trace-id"}', { status: 400, headers: { 'content-type': 'application/json' } });
|
|
443
|
+
}
|
|
444
|
+
const found = service.traceStore.get(traceId);
|
|
445
|
+
if (found === undefined) {
|
|
446
|
+
// 正文已被 ring 淘汰 / id 不存在 —— 诚实地说「取不到」,不返回空对象
|
|
447
|
+
// 冒充「正文是空的」。
|
|
448
|
+
return new Response('{"error":"trace-not-found"}', { status: 404, headers: { 'content-type': 'application/json' } });
|
|
449
|
+
}
|
|
450
|
+
return new Response(JSON.stringify(found), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
catch { /* a rejecting registry only costs the route, not the plugin */ }
|
|
455
|
+
});
|
|
456
|
+
// In-process session/event fold (turns + tokens). Cordis 0.2.1-rc.2's
|
|
457
|
+
// plugin-level proxy ctx does NOT expose `.on` directly (would throw
|
|
458
|
+
// `cannot get property "on" without inject`); only Service-internal ctx
|
|
459
|
+
// has it. Wrap in a try/catch so a missing fiber-root `.on` only costs
|
|
460
|
+
// the session summary counter, not the whole plugin mount.
|
|
461
|
+
let disposeSession;
|
|
462
|
+
try {
|
|
463
|
+
const ctxOn = ctx.on;
|
|
464
|
+
if (typeof ctxOn === 'function') {
|
|
465
|
+
disposeSession = ctxOn.call(ctx, 'session/event', (session, event) => {
|
|
466
|
+
const sid = typeof session?.id === 'string' && session.id.length > 0 ? session.id : undefined;
|
|
467
|
+
if (sid === undefined)
|
|
468
|
+
return;
|
|
469
|
+
if (event?.type !== 'turn/start' && event?.type !== 'assistant/message' && event?.type !== 'request/context')
|
|
470
|
+
return;
|
|
471
|
+
service.recordSessionEvent(sid, event.type, event.data);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Plugin-level proxy ctx can't subscribe events. Session summary
|
|
477
|
+
// will fall back to audit-log derived data only.
|
|
478
|
+
}
|
|
479
|
+
return () => {
|
|
480
|
+
disposeFold();
|
|
481
|
+
disposeSession?.();
|
|
482
|
+
};
|
|
483
|
+
}
|