dsh-xray 0.5.0 → 0.6.0

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/README.md CHANGED
@@ -27,7 +27,7 @@
27
27
 
28
28
  **dsh-xray does.**
29
29
 
30
- > **Status: 0.4.x — static + runtime imaging with context-cost attribution.** Static commands work even when dsh cannot boot; `deps`/`health`/`cost`/`shadow` and the agent tool need the plugin mounted.
30
+ > **Status: 0.6.x — static + runtime imaging, context-cost attribution, and a web panel.** Static commands work even when dsh cannot boot; `deps`/`health`/`cost`/`shadow` and the agent tool need the plugin mounted.
31
31
 
32
32
  ---
33
33
 
@@ -79,6 +79,9 @@ Per-plugin fiber lifecycle state, startup failures, transition history.
79
79
  ### 🤖 Agent Self-Introspection
80
80
  The `xray_composition` tool lets agents inspect their own capability set.
81
81
 
82
+ ### 🖥️ Web Panel
83
+ Mounted in `dsh web`, the plugin serves a zero-dependency panel at **`/xray`** — summary, health, deps (with the disable-cascade table), cost, and shadow views, live from the running composition. JSON endpoints under `/xray/api/*` serve the same data.
84
+
82
85
  What every request actually carries — prompt sections observed at assembly, blended with tool schemas:
83
86
 
84
87
  ```console
package/README.zh.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  [English](./README.md)
12
12
 
13
- > **状态:0.4.x — 静态 + 运行时成像,含上下文成本归因。** 静态命令在 dsh 起不来时照样能用;`deps`/`health`/`cost`/`shadow` 和 agent 工具需要插件已挂载。
13
+ > **状态:0.6.x — 静态 + 运行时成像、上下文成本归因、Web 面板。** 静态命令在 dsh 起不来时照样能用;`deps`/`health`/`cost`/`shadow` 和 agent 工具需要插件已挂载。
14
14
 
15
15
  `dsh --dump-config` 只给你原始组合树,插件面板只给你平铺列表。它们都不回答:这个插件*为什么*在这、停用它会*连带瘫掉什么*、它在*悄悄消耗什么*。dsh-xray 回答这些。
16
16
 
@@ -91,6 +91,10 @@ orphan overrides (silently skipped) (1)
91
91
 
92
92
  挂载进树后,dsh-xray 注册 `xray_composition` 工具(`view: summary | deps | health | cost | shadow`),agent 可以自答"我有哪些能力 / 哪个插件提供 X / 为什么 Y 不可用"。
93
93
 
94
+ ## Web 面板
95
+
96
+ 在 `dsh web` 中挂载后,插件在 **`/xray`** 提供零依赖面板——summary、health、deps(含停用级联表)、cost、shadow 五个视图,数据实时来自运行中的组合树;`/xray/api/*` 提供同源 JSON。
97
+
94
98
  ## 安全立场
95
99
 
96
100
  dsh-xray 只读不执行。patch 文件里的 loader `!!js` 表达式解析为不透明标记、绝不求值;CLI 从不执行插件代码(`audit` 是对源码文本的模式扫描);挂载的插件只写 `$DSH_HOME/xray/` 目录。详见 [SECURITY.md](./SECURITY.md)。
package/lib/index.js CHANGED
@@ -91,6 +91,43 @@ function apply(ctx) {
91
91
  ];
92
92
  }, 'xray-runtime-snapshot');
93
93
 
94
+ // Web panel: mounts when the profile composes a webServer (dsh web).
95
+ // Headless profiles simply never activate this subplugin.
96
+ ctx.plugin({
97
+ name: 'dsh-xray-panel',
98
+ inject: ['webServer'],
99
+ apply: (wctx) => {
100
+ const { mountPanel } = require('./panel.js');
101
+ const freshSnap = () => {
102
+ const snap = snapshotRegistry(ctx);
103
+ snap.transitions = Object.fromEntries(transitions);
104
+ snap.promptAssembly = lastAssembly;
105
+ return snap;
106
+ };
107
+ wctx.effect(
108
+ () =>
109
+ mountPanel(wctx.webServer, {
110
+ summary: () => {
111
+ const snap = freshSnap();
112
+ return {
113
+ plugins: snap.plugins.length,
114
+ unhealthy: health(snap).unhealthy.length,
115
+ services: Object.keys(serviceGraph(snap).services).length,
116
+ toolSchemaTokens: contextCost(snap).totalTokens,
117
+ capturedAt: snap.capturedAt,
118
+ };
119
+ },
120
+ deps: () => serviceGraph(freshSnap()),
121
+ health: () => health(freshSnap()),
122
+ cost: () => contextCost(freshSnap()),
123
+ shadow: () => shadowing(freshSnap()),
124
+ }),
125
+ 'xray-panel-routes',
126
+ );
127
+ logger.info('xray panel mounted at /xray');
128
+ },
129
+ });
130
+
94
131
  // Agent self-introspection tool: only when a tool registry exists
95
132
  // (headless/web both have one; keep it optional so xray mounts anywhere).
96
133
  ctx.plugin({
package/lib/panel.js ADDED
@@ -0,0 +1,213 @@
1
+ // Self-contained web panel: one HTML page + JSON endpoints, mounted on the
2
+ // host webServer. No client-module bundle, no React, no build step — the
3
+ // model layer already computes everything; this only renders it.
4
+
5
+ const PAGE = `<!doctype html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="utf-8">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1">
10
+ <title>dsh-xray</title>
11
+ <style>
12
+ :root { color-scheme: dark; }
13
+ * { box-sizing: border-box; margin: 0; }
14
+ body { font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
15
+ background: #0d1117; color: #e6edf3; padding: 24px; }
16
+ h1 { font-size: 16px; margin-bottom: 4px; }
17
+ .sub { color: #8b949e; margin-bottom: 20px; }
18
+ nav { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
19
+ nav button { background: #21262d; color: #e6edf3; border: 1px solid #30363d;
20
+ border-radius: 6px; padding: 5px 12px; cursor: pointer; font: inherit; }
21
+ nav button.active { background: #1f6feb; border-color: #1f6feb; }
22
+ table { border-collapse: collapse; width: 100%; margin-top: 8px; }
23
+ th, td { text-align: left; padding: 4px 10px; border-bottom: 1px solid #21262d; }
24
+ th { color: #8b949e; font-weight: normal; }
25
+ .num { text-align: right; }
26
+ .bar { background: #1f6feb; height: 10px; border-radius: 2px; display: inline-block; }
27
+ .warn { color: #f85149; }
28
+ .ok { color: #7ee787; }
29
+ .muted { color: #8b949e; }
30
+ #status { margin: 12px 0; color: #8b949e; }
31
+ .tag { background: #21262d; border-radius: 4px; padding: 1px 6px; margin-left: 6px; font-size: 11px; }
32
+ </style>
33
+ </head>
34
+ <body>
35
+ <h1>dsh-xray</h1>
36
+ <div class="sub">composition X-ray — live from this harness</div>
37
+ <nav id="nav"></nav>
38
+ <div id="status"></div>
39
+ <div id="content"></div>
40
+ <script>
41
+ const views = ['summary', 'health', 'deps', 'cost', 'shadow'];
42
+ const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
43
+ const nav = document.getElementById('nav');
44
+ const content = document.getElementById('content');
45
+ const status = document.getElementById('status');
46
+ let active = 'summary';
47
+
48
+ for (const v of views) {
49
+ const b = document.createElement('button');
50
+ b.textContent = v;
51
+ b.onclick = () => { active = v; render(); };
52
+ b.id = 'nav-' + v;
53
+ nav.appendChild(b);
54
+ }
55
+
56
+ function table(headers, rows) {
57
+ return '<table><tr>' + headers.map((h) => '<th>' + esc(h) + '</th>').join('') + '</tr>'
58
+ + rows.join('') + '</table>';
59
+ }
60
+ function bar(share) {
61
+ return '<span class="bar" style="width:' + Math.max(2, share * 2) + 'px"></span>';
62
+ }
63
+
64
+ const renderers = {
65
+ summary(d) {
66
+ return table(['metric', 'value'], [
67
+ '<tr><td>plugins mounted</td><td class="num">' + d.plugins + '</td></tr>',
68
+ '<tr><td>unhealthy</td><td class="num ' + (d.unhealthy ? 'warn' : 'ok') + '">' + d.unhealthy + '</td></tr>',
69
+ '<tr><td>services</td><td class="num">' + d.services + '</td></tr>',
70
+ '<tr><td>context tokens (tools + sections)</td><td class="num">~' + d.toolSchemaTokens + '</td></tr>',
71
+ ]) + '<p class="muted" style="margin-top:12px">captured ' + esc(d.capturedAt) + '</p>';
72
+ },
73
+ health(d) {
74
+ let html = '<p><span class="ok">' + d.healthy.length + ' healthy</span>'
75
+ + (d.waiting.length ? ' · ' + d.waiting.length + ' waiting' : '')
76
+ + (d.unhealthy.length ? ' · <span class="warn">' + d.unhealthy.length + ' unhealthy</span>' : '') + '</p>';
77
+ if (d.unhealthy.length) {
78
+ html += table(['plugin', 'fiber', 'state', 'error'], d.unhealthy.flatMap((p) =>
79
+ p.fibers.map((f) => '<tr><td>' + esc(p.name) + '</td><td class="num">' + f.uid
80
+ + '</td><td class="warn">' + esc(f.state) + '</td><td>' + esc(f.error ?? '') + '</td></tr>')));
81
+ }
82
+ if (d.waiting.length) {
83
+ html += table(['waiting plugin', 'wants'], d.waiting.map((p) =>
84
+ '<tr><td>' + esc(p.name) + '</td><td>' + esc(p.inject.join(', ')) + '</td></tr>'));
85
+ }
86
+ return html;
87
+ },
88
+ deps(d) {
89
+ const services = Object.entries(d.services).map(([name, node]) =>
90
+ '<tr><td>' + esc(name) + '</td><td>' + esc(node.providers.join(', ') || '—')
91
+ + '</td><td>' + esc(node.consumers.join(', ') || '—') + '</td></tr>');
92
+ let html = table(['service', 'provided by', 'consumed by'], services);
93
+ const cascade = Object.entries(d.cascade);
94
+ if (cascade.length) {
95
+ html = '<h3 style="margin:8px 0">disable-cascade</h3>'
96
+ + table(['provider', 'affects'], cascade.map(([p, a]) =>
97
+ '<tr><td>' + esc(p) + '</td><td>' + esc(a.join(', ')) + '</td></tr>'))
98
+ + '<h3 style="margin:16px 0 8px">services</h3>' + html;
99
+ }
100
+ if (d.unsatisfied.length) {
101
+ html = '<p class="warn">' + d.unsatisfied.length + ' unsatisfied inject(s)</p>' + html;
102
+ }
103
+ return html;
104
+ },
105
+ cost(d) {
106
+ let html = '<p>~' + d.totalTokens + ' tokens: ' + d.toolCount + ' tool schema(s) ~' + d.toolTokens
107
+ + ' + ' + d.sectionCount + ' prompt section(s) ~' + d.sectionTokens + '</p>';
108
+ if (d.sections.length) {
109
+ html += '<h3 style="margin:12px 0 4px">prompt sections</h3>'
110
+ + table(['section', 'tokens', 'share', ''], d.sections.map((s) =>
111
+ '<tr><td>' + esc(s.name) + '</td><td class="num">~' + s.tokens + '</td><td class="num">'
112
+ + s.share + '%</td><td>' + bar(s.share) + '</td></tr>'));
113
+ } else {
114
+ html += '<p class="muted">no prompt assembly observed yet — send one agent message first</p>';
115
+ }
116
+ html += '<h3 style="margin:12px 0 4px">tool schemas</h3>'
117
+ + table(['tool', 'tokens', 'share', ''], d.tools.map((t) =>
118
+ '<tr><td>' + esc(t.name) + '</td><td class="num">~' + t.tokens + '</td><td class="num">'
119
+ + t.share + '%</td><td>' + bar(t.share) + '</td></tr>'));
120
+ return html;
121
+ },
122
+ shadow(d) {
123
+ let html = d.services.length
124
+ ? table(['service', 'providers'], d.services.map((s) =>
125
+ '<tr><td>' + esc(s.service) + '</td><td class="warn">' + esc(s.providers.join(' AND ')) + '</td></tr>'))
126
+ : '<p class="ok">no service is provided by more than one plugin</p>';
127
+ if (d.registrars.length) {
128
+ html += '<h3 style="margin:12px 0 4px">registrars</h3>'
129
+ + table(['plugin', 'registrations'], d.registrars.map((r) =>
130
+ '<tr><td>' + esc(r.plugin) + '</td><td class="num">' + r.registrations + '</td></tr>'));
131
+ }
132
+ return html;
133
+ },
134
+ };
135
+
136
+ async function render() {
137
+ for (const v of views) document.getElementById('nav-' + v).className = v === active ? 'active' : '';
138
+ status.textContent = 'loading ' + active + '…';
139
+ try {
140
+ const res = await fetch('/xray/api/' + active);
141
+ if (!res.ok) throw new Error(await res.text());
142
+ const data = await res.json();
143
+ content.innerHTML = renderers[active](data);
144
+ status.textContent = '';
145
+ } catch (err) {
146
+ status.innerHTML = '<span class="warn">' + esc(err.message) + '</span>';
147
+ content.innerHTML = '';
148
+ }
149
+ }
150
+ render();
151
+ setInterval(() => { if (active === 'health' || active === 'summary') render(); }, 5000);
152
+ </script>
153
+ </body>
154
+ </html>`;
155
+
156
+ function sendJson(response, code, value) {
157
+ const body = JSON.stringify(value);
158
+ response.writeHead(code, {
159
+ 'content-type': 'application/json; charset=utf-8',
160
+ 'cache-control': 'no-store',
161
+ });
162
+ response.end(body);
163
+ }
164
+
165
+ /**
166
+ * Mount the panel routes. `views` supplies fresh data per request:
167
+ * { summary, deps, health, cost, shadow } — each a () => object.
168
+ * Returns the disposers webServer.register produced.
169
+ */
170
+ function mountPanel(webServer, views) {
171
+ const disposers = [];
172
+ disposers.push(
173
+ webServer.register({
174
+ kind: 'exact',
175
+ path: '/xray',
176
+ handler: (request, response) => {
177
+ if (request.method !== 'GET') {
178
+ response.writeHead(405, { allow: 'GET' });
179
+ response.end();
180
+ return;
181
+ }
182
+ response.writeHead(200, {
183
+ 'content-type': 'text/html; charset=utf-8',
184
+ 'cache-control': 'no-store',
185
+ });
186
+ response.end(PAGE);
187
+ },
188
+ }),
189
+ );
190
+ for (const [name, compute] of Object.entries(views)) {
191
+ disposers.push(
192
+ webServer.register({
193
+ kind: 'exact',
194
+ path: `/xray/api/${name}`,
195
+ handler: (request, response) => {
196
+ if (request.method !== 'GET') {
197
+ response.writeHead(405, { allow: 'GET' });
198
+ response.end();
199
+ return;
200
+ }
201
+ try {
202
+ sendJson(response, 200, compute());
203
+ } catch (err) {
204
+ sendJson(response, 500, { error: err.message });
205
+ }
206
+ },
207
+ }),
208
+ );
209
+ }
210
+ return disposers;
211
+ }
212
+
213
+ module.exports = { mountPanel, PAGE };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-xray",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "X-ray for your DeepSeek Harness — see what's actually loaded, why, and what it costs you.",
5
5
  "repository": {
6
6
  "type": "git",