dsh-aris-panel 0.2.6 → 0.2.8

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
@@ -100,7 +100,7 @@ This overlay is **not** equivalent to the installed bundle: it cannot restore Co
100
100
  ## Known limits
101
101
 
102
102
  - **Tracks a Harness window, declared as a peer dependency.** DeepSeek Harness is a developer preview; this bundle declares `@deepseek-ai/dsh >=0.1.7-rc.2 <0.3.0-0` as an optional peer. The transport test passes against `0.2.0-rc.2`; the `HostConnectionFetch` registry it uses is present and identical in `0.1.7-rc.2`, so the range covers both. The `dsh` peer-dependency gate reads that range: an incompatible runtime skips the whole bundle with a line in the boot log instead of loading a panel whose transport may have moved. On a newer runtime, re-run `node node_modules/dsh-aris-panel/test/transport-smoke.mjs`, then widen the range, or grant the exact-version exemption with `dsh plugin allow-version`.
103
- - **The panel's browser transport is HTTP, not an RPC channel.** dsh's generic channel registry (`connection.rpc.handle`) resolves `webServer` from Connection's own context, which shipped dsh builds do not inject, so an out-of-tree channel registration throws and every call dies as HTTP 405 from the SPA fallback. ARIS mounts exact Fetch routes on the `/api` prefix instead (documented as `HostConnectionFetch.register`), which dsh already serves behind its Host/Origin fence and browser-session cookie. `test/transport-smoke.mjs` pins both halves of that contract against the installed dsh.
103
+ - **The panel's browser transport is HTTP, not an RPC channel.** dsh's generic channel registry (`connection.rpc.handle`) resolves `webServer` from Connection's own context, which dsh `0.1.7-rc.2` and `0.2.0-rc.2` do not inject there, so an out-of-tree channel registration throws and every call dies as HTTP 405 from the SPA fallback. ARIS mounts exact Fetch routes on the `/api` prefix instead (documented as `HostConnectionFetch.register`), which dsh already serves behind its Host/Origin fence and browser-session cookie. `test/transport-smoke.mjs` pins both halves of that contract against the installed dsh; it cannot cover the carrier's own auth path, so verify a live host with `POST /api/aris-run` and `/api/aris-wb` — 400 means mounted, 404 means not.
104
104
  - **No dsh packages are declared as npm dependencies.** In-box packages are host-provided and resolve from the Harness installation through the profile module fallback. The Harness's own rule keeps `@deepseek-ai/dsh-*` out of `dependencies`, and the packages version in lockstep with the CLI, so any range this bundle pinned would fight the version the user already has installed. The peer range above is a compatibility declaration, marked optional so it never pulls a second copy of the runtime.
105
105
  - **`web_fetch` is off.** Stock dsh ships it disabled and this bundle does not enable it, which would mean depending on a provider package. Skills that reach the web use `web_search`, or `bash` with `curl`.
106
106
  - **Reviewer thread continuity is process-local.** A Harness restart, or any MCP reconnect that replaces the Codex child, loses saved `threadId`s. Rounds after that start fresh; `review-stage/REVIEWER_MEMORY.md` remains the durable record either way.
package/README_CN.md CHANGED
@@ -81,7 +81,7 @@ ARIS_REPO=/absolute/path/to/aris NODE_USE_ENV_PROXY=1 \
81
81
  ## 已知限制
82
82
 
83
83
  - **跟随一个 Harness 版本窗口,并以 peerDependency 声明。** DeepSeek Harness 处于技术预览;本 bundle 把 `@deepseek-ai/dsh >=0.1.7-rc.2 <0.3.0-0` 声明为 optional peerDependency。传输层测试在 `0.2.0-rc.2` 上通过;它所用的 `HostConnectionFetch` 注册表在 `0.1.7-rc.2` 里存在且一致,所以该范围覆盖两者。dsh 的 peerDependency 门禁会读这个范围:不兼容的运行时会在启动日志里**整体跳过本 bundle**,而不是加载一个传输层可能已经变了的面板。升到更新版本后:先跑 `node node_modules/dsh-aris-panel/test/transport-smoke.mjs`,再放宽范围,或用 `dsh plugin allow-version` 授予精确版本豁免。
84
- - **面板的浏览器传输走 HTTP,不再走 RPC channel。** dsh 的通用 channel 注册表(`connection.rpc.handle`)是从 Connection 自己的上下文里解析 `webServer` 的,而发行版 dsh 并不注入它,于是树外插件的 channel 注册直接抛错,每次调用都变成 SPA 兜底返回的 HTTP 405。ARIS 改为在 `/api` 前缀上挂精确 Fetch 路由(即文档中的 `HostConnectionFetch.register`),dsh 本来就为 `/api` 提供了 Host/Origin 信任围栏与浏览器会话 Cookie。`test/transport-smoke.mjs` 针对已安装的 dsh 把这两半事实都钉住。
84
+ - **面板的浏览器传输走 HTTP,不再走 RPC channel。** dsh 的通用 channel 注册表(`connection.rpc.handle`)是从 Connection 自己的上下文里解析 `webServer` 的,而 dsh `0.1.7-rc.2` / `0.2.0-rc.2` 并不在那里注入它,于是树外插件的 channel 注册直接抛错,每次调用都变成 SPA 兜底返回的 HTTP 405。ARIS 改为在 `/api` 前缀上挂精确 Fetch 路由(即文档中的 `HostConnectionFetch.register`),dsh 本来就为 `/api` 提供了 Host/Origin 信任围栏与浏览器会话 Cookie。`test/transport-smoke.mjs` 针对已安装的 dsh 钉住这两半契约;它覆盖不到载体自身的鉴权链路,所以请用真实 host 复核:`POST /api/aris-run` 与 `/api/aris-wb` —— 400 = 已挂载,404 = 未挂载。
85
85
  - **不把任何 dsh 包声明为 npm 依赖。** 内置包由宿主提供,通过 profile 的模块回退从 Harness 安装目录解析。Harness 自己的规约就要求 `@deepseek-ai/dsh-*` 不进 `dependencies`;而且这些子包与 CLI **锁步发版**,本 bundle 无论钉哪个范围,都会和用户已装的版本打架。上面的 peer 范围只是兼容性声明,且标为 optional,因此永远不会拉进第二份运行时副本。
86
86
  - **`web_fetch` 关闭。** 原版 dsh 默认关闭它,本 bundle 也不打开——打开就意味着依赖一个 provider 包。需要联网的技能改用 `web_search`,或用 `bash` 跑 `curl`。
87
87
  - **审稿线程连续性是进程内的。** Harness 重启、或任何一次 MCP 重连替换了 Codex 子进程,已保存的 `threadId` 都会失效,之后的轮次从新线程开始;无论如何 `review-stage/REVIEWER_MEMORY.md` 都是持久记录。
package/dsh/client.js CHANGED
@@ -1009,7 +1009,7 @@ function ArisWorkbenchView(props) {
1009
1009
  setError(String(cause))
1010
1010
  setData(undefined)
1011
1011
  }).finally(function () { if (!silent) setBusy(false) })
1012
- }, [connection, sessionId])
1012
+ }, [sessionId])
1013
1013
 
1014
1014
  React.useEffect(function () { load() }, [load])
1015
1015
 
package/dsh/http-rpc.mjs CHANGED
@@ -3,21 +3,27 @@
3
3
  *
4
4
  * dsh exposes two registries on the Connection service:
5
5
  *
6
- * - `connection.rpc` — generic channel registry. Its `handle()` resolves
7
- * `webServer` from the Connection service's own context, so it only works
8
- * when the shipped `dsh-client-connection` declares `webServer` in its
9
- * inject list. Shipped builds declare `credentials` only, so every
10
- * out-of-tree `connection.rpc.handle()` throws
11
- * `cannot get property "webServer" without inject`, the channel never
12
- * mounts, and the browser sees HTTP 405 from the SPA fallback. Repairing
13
- * that means editing dsh's own package, which every dsh upgrade reverts.
6
+ * - `connection.rpc` — generic channel registry. `handle()` calls
7
+ * `owner.webServer.register(route)` where `owner` is the Connection
8
+ * service's own context, not the caller's: Cordis resolves a service's
9
+ * methods against the context that constructed it, so injecting
10
+ * `webServer` on the *consumer* does not help. In dsh `0.1.7-rc.2` and
11
+ * `0.2.0-rc.2` that context injects `credentials` only while `webServer` is
12
+ * provided by a sibling plugin fiber, so the lookup walks past the
13
+ * provider to the root and throws `cannot get property "webServer" without
14
+ * inject`. The channel then never mounts and the browser gets HTTP 405 from
15
+ * the SPA fallback. Making that path work from outside means editing dsh's
16
+ * own package (the workaround this package used until 0.2.6), which every
17
+ * dsh upgrade reverts; a build whose Connection context does inject
18
+ * `webServer` would not need it.
14
19
  *
15
20
  * - `connection.fetch` — exact-path Fetch registry under `/api`
16
21
  * (documented as `HostConnectionFetch.register(ConnectionFetchRoute)`).
17
- * It touches no other service, and dsh serves the whole `/api` prefix
18
- * behind its Host/Origin trust fence plus the browser-session cookie, so
19
- * an ARIS route mounted here carries exactly the authority the RPC channel
20
- * used to have.
22
+ * Registration resolves nothing beyond the Connection service itself, so it
23
+ * mounts under whatever Connection context the host provides. Serving is
24
+ * dsh's own `/api` prefix route, which applies the Host/Origin trust fence
25
+ * and the browser-session cookie before dispatching to any exact fetch
26
+ * route — the same authority the RPC channel used to carry.
21
27
  *
22
28
  * ARIS therefore mounts one JSON endpoint per view on the Fetch registry. The
23
29
  * wire contract is deliberately tiny and mirrored in `dsh/client.js`:
@@ -26,10 +32,13 @@
26
32
  * 200 {"ok": true, "value": unknown}
27
33
  * 200 {"ok": false, "error": {"code": string, "message": string}}
28
34
  * 400 malformed envelope
29
- * 405 non-POST method
30
35
  *
31
36
  * Application failures travel as HTTP 200 with `ok: false`, so the view shows
32
37
  * the handler's own message instead of a transport error.
38
+ *
39
+ * The route also answers 405 to a direct non-POST dispatch, but dsh's carrier
40
+ * only hands over methods the route declares, so a browser request with another
41
+ * method is answered 404 by the carrier before this handler runs.
33
42
  */
34
43
 
35
44
  /** Method set every ARIS endpoint accepts. */
@@ -112,8 +121,9 @@ export function httpRpcRoute(path, handler) {
112
121
  *
113
122
  * The caller supplies the effect and the injected services, because the two
114
123
  * ARIS views need different ones (`sessions` for both, `agents` and
115
- * `workspaceRegistry` for the workbench only). Registering touches no
116
- * `webServer` lookup, so it works on every shipped dsh build.
124
+ * `workspaceRegistry` for the workbench only). Registration resolves no
125
+ * `webServer`, which is what keeps the mount independent of the Connection
126
+ * context's inject list.
117
127
  *
118
128
  * @param ctx - the ARIS plugin context, which must have `connection` injected.
119
129
  * @param path - exact `/api/<segment>` route path the browser POSTs to.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-aris-panel",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "ARIS research-workflow skills + interactive workbench panel for DeepSeek Harness (user fork of dsh-aris: 83 skills, bundled upstream dsh-aris 0.1.1 payload, ppt-master PPTX backend for the talk pipeline, cross-model Codex review, idea/workflow launcher, research status board).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,7 +8,7 @@
8
8
  "node": "^22.19.0 || >=24.0.0"
9
9
  },
10
10
  "scripts": {
11
- "test": "node test/transport-smoke.mjs"
11
+ "test": "node test/transport-smoke.mjs && node test/client-render-smoke.mjs"
12
12
  },
13
13
  "keywords": [
14
14
  "dsh-plugin",
@@ -0,0 +1,131 @@
1
+ /**
2
+ * ARIS browser-half render smoke test.
3
+ *
4
+ * node test/client-render-smoke.mjs
5
+ *
6
+ * Why this exists: `test/transport-smoke.mjs` proves the host half mounts, but
7
+ * dsh/client.js is only ever executed by a browser, so a client-side error
8
+ * (an undefined identifier left behind by a refactor, say) reaches the user as
9
+ * "the panel does not open" with no failing test. This file executes the real
10
+ * bundle in Node against a minimal React/element renderer and walks every
11
+ * returned component, so ReferenceErrors and typos in component bodies or hook
12
+ * dependency arrays fail here instead.
13
+ *
14
+ * It is deliberately a stub renderer, not React: components are called and
15
+ * their element trees walked, hooks run eagerly, and network calls are answered
16
+ * with empty payloads. That catches crashes, not behaviour.
17
+ */
18
+
19
+ import { pathToFileURL } from 'node:url'
20
+
21
+ let failures = 0
22
+ function check(label, condition, detail) {
23
+ if (condition) {
24
+ console.log(` PASS ${label}`)
25
+ return
26
+ }
27
+ failures += 1
28
+ console.log(` FAIL ${label}${detail === undefined ? '' : ` — ${detail}`}`)
29
+ }
30
+
31
+ /** Collect anything a hook or effect throws instead of letting it escape. */
32
+ const crashes = []
33
+ process.on('unhandledRejection', (reason) => { crashes.push(reason) })
34
+
35
+ console.log('\n1. the bundle loads the way dsh client-modules loads it')
36
+ let registration
37
+ let documentState
38
+ {
39
+ const styleNodes = []
40
+ documentState = { styleNodes }
41
+ globalThis.window = {
42
+ __ModuleLoader__: {
43
+ load(module) { registration = module },
44
+ },
45
+ }
46
+ globalThis.document = {
47
+ createElement() {
48
+ const node = { textContent: '', removed: false, remove() { node.removed = true } }
49
+ styleNodes.push(node)
50
+ return node
51
+ },
52
+ head: { append() {} },
53
+ }
54
+ // The views answer every call with an empty successful payload.
55
+ globalThis.fetch = async () => ({ status: 200, json: async () => ({ ok: true, value: {} }) })
56
+
57
+ await import(new URL('../dsh/client.js', import.meta.url).href)
58
+ check('the bundle registers one module', registration !== undefined && registration.id === 'dsh-aris-panel',
59
+ registration === undefined ? 'window.__ModuleLoader__.load was never called' : String(registration.id))
60
+ }
61
+
62
+ console.log('\n2. every component renders without crashing')
63
+ let rendered
64
+ {
65
+ /** Minimal React: enough for createElement + the hooks this bundle uses. */
66
+ const React = {
67
+ createElement(type, props, ...children) {
68
+ return { type, props: { ...(props ?? {}), children: children.flat() } }
69
+ },
70
+ useState(initial) { return [typeof initial === 'function' ? initial() : initial, () => {}] },
71
+ useEffect(run) { run() },
72
+ useCallback(fn) { return fn },
73
+ useRef(value) { return { current: value } },
74
+ }
75
+ const requireStub = (name) => {
76
+ if (name === 'react') return React
77
+ throw new Error(`client bundle required an unexpected module: ${name}`)
78
+ }
79
+
80
+ const plugin = registration.factory(requireStub)
81
+ check('the factory returns a plugin with an inject list', Array.isArray(plugin.inject), JSON.stringify(plugin.inject))
82
+ check('the plugin declares the slot service', plugin.inject.includes('slots'))
83
+
84
+ let render
85
+ const ctx = {
86
+ connection: {},
87
+ effect(run) { return run() },
88
+ slots: {
89
+ inject(name, registerCall) {
90
+ check('the plugin injects the conversation view slot', name === 'conversation.view', name)
91
+ registerCall()
92
+ },
93
+ register(spec, renderCall) {
94
+ check('the ARIS slot registers itself', spec.id === 'aris' && spec.name === 'conversation.view', JSON.stringify(spec))
95
+ render = renderCall
96
+ },
97
+ },
98
+ }
99
+ plugin.apply(ctx)
100
+ check('the slot exposes a render function', typeof render === 'function')
101
+
102
+ /** Call function components and walk host elements, like React would. */
103
+ let elements = 0
104
+ const walk = (node, depth = 0) => {
105
+ if (node === null || node === undefined || typeof node === 'boolean' || typeof node === 'string' || typeof node === 'number') return
106
+ if (Array.isArray(node)) { for (const child of node) walk(child, depth); return }
107
+ if (depth > 40) throw new Error('render tree did not settle')
108
+ elements += 1
109
+ if (typeof node.type === 'function') { walk(node.type(node.props ?? {}), depth + 1); return }
110
+ walk(node.props?.children, depth + 1)
111
+ }
112
+
113
+ try {
114
+ rendered = render({ sessionId: 'render-smoke', openView: undefined })
115
+ walk(rendered)
116
+ check('the ARIS tab renders a tree', elements > 5, `${elements} elements`)
117
+ } catch (error) {
118
+ check('the ARIS tab renders a tree', false, `${error.name}: ${error.message}`)
119
+ }
120
+ }
121
+
122
+ console.log('\n3. the stylesheet effect is wired')
123
+ check('one style node was created', documentState.styleNodes.length === 1, String(documentState.styleNodes.length))
124
+
125
+ const asyncCrashes = crashes.filter((reason) => reason !== undefined)
126
+ check('no asynchronous crash escaped the views', asyncCrashes.length === 0, asyncCrashes.map(String).join('; '))
127
+ check('the module left no uncaught error', rendered !== undefined)
128
+
129
+ console.log(failures === 0 ? '\nOK — the browser half renders' : `\n${failures} check(s) failed`)
130
+ await new Promise((resolve) => setTimeout(resolve, 20))
131
+ process.exit(failures === 0 && crashes.length === 0 ? 0 : 1)
@@ -9,15 +9,23 @@
9
9
  * It pins the two facts this package's transport choice rests on:
10
10
  *
11
11
  * 1. `connection.rpc.handle()` cannot mount a channel from an out-of-tree
12
- * plugin on a shipped dsh build. `HostConnectionService.register()`
13
- * resolves `webServer` from Connection's own context, and the shipped
14
- * build injects `credentials` only, so the lookup walks past the plugin
15
- * that provides it and throws. The channel then never mounts, and the
16
- * browser gets HTTP 405 from the SPA fallback (the failure this package
17
- * used to work around by editing dsh's own node_modules).
12
+ * plugin against dsh `0.1.7-rc.2` / `0.2.0-rc.2`. `HostConnectionService
13
+ * .register()` resolves `webServer` from Connection's own context, and
14
+ * those builds inject `credentials` only, so the lookup walks past the
15
+ * plugin that provides it and throws. The channel then never mounts, and
16
+ * the browser gets HTTP 405 from the SPA fallback (the failure this
17
+ * package used to work around by editing dsh's own node_modules).
18
18
  * 2. The Connection Fetch registry route this package uses instead mounts
19
19
  * without any `webServer` lookup and round-trips every envelope shape the
20
- * views depend on.
20
+ * views depend on, including both real view registrations.
21
+ *
22
+ * What this test deliberately does NOT cover: the shipped Connection plugin's
23
+ * own `apply()` (so `/api` prefix mounting, the Host/Origin fence, cookie auth
24
+ * and the node↔fetch body bridge are assumed, not exercised), the browser half
25
+ * (`dsh/client.js` is never executed here), and effect disposal/reload. Those
26
+ * need a live host: POST `/api/aris-run` and `/api/aris-wb` with an
27
+ * authenticated browser session after a restart — 400 means the route is
28
+ * mounted and the envelope was rejected, 404 means it never mounted.
21
29
  *
22
30
  * Set DSH_ROOT to test another install.
23
31
  */
@@ -54,6 +62,8 @@ const cordisEntry = requireFromConnection.resolve('@deepseek-ai/cordis')
54
62
  const { Context } = await import(pathToFileURL(cordisEntry).href)
55
63
  const { HostConnectionService, API_PATH } = await import(pathToFileURL(connectionEntry).href)
56
64
  const { httpRpcRoute, registerHttpRpc } = await import(new URL('../dsh/http-rpc.mjs', import.meta.url).href)
65
+ const { registerWorkbench } = await import(new URL('../dsh/workbench.mjs', import.meta.url).href)
66
+ const { registerRunStatus } = await import(new URL('../dsh/run-status.mjs', import.meta.url).href)
57
67
 
58
68
  let failures = 0
59
69
  function check(label, condition, detail) {
@@ -198,6 +208,58 @@ console.log('\n2. the Fetch registry route ARIS mounts instead')
198
208
 
199
209
  const unknownPath = await handler.fetch(new Request('http://dsh.internal/api/not-aris', { method: 'POST' }))
200
210
  check('an unclaimed /api path is HTTP 404', unknownPath.status === 404)
211
+
212
+ console.log('\n3. the real ARIS view registrations, over the real carrier')
213
+ // The host halves inject these; stubs are enough because registration only
214
+ // stores a handler, and the two endpoints below read workspace state lazily.
215
+ await root.plugin({
216
+ name: 'fake-session-services',
217
+ apply(ctx) {
218
+ ctx.provide('sessions', { get: () => undefined })
219
+ ctx.provide('agents', { create: async () => undefined })
220
+ ctx.provide('workspaceRegistry', {})
221
+ ctx.provide('sessionPersistence', {})
222
+ },
223
+ })
224
+ await root.plugin({
225
+ name: 'aris-views',
226
+ apply(ctx) {
227
+ registerWorkbench(ctx)
228
+ registerRunStatus(ctx)
229
+ },
230
+ })
231
+
232
+ const view = (path, body) => handler.fetch(new Request('http://dsh.internal' + path, {
233
+ method: 'POST',
234
+ headers: { 'content-type': 'application/json' },
235
+ body: JSON.stringify(body),
236
+ }))
237
+ // A nested `ctx.inject(...)` fiber activates asynchronously; a 400 (envelope
238
+ // rejected) means the route exists, a 404 means it never mounted.
239
+ const waitsForRoute = async (path) => {
240
+ const deadline = Date.now() + 3000
241
+ while (Date.now() < deadline) {
242
+ const probe = await view(path, {})
243
+ if (probe.status !== 404) return true
244
+ await new Promise(resolve => setTimeout(resolve, 25))
245
+ }
246
+ return false
247
+ }
248
+ check('/api/aris-wb mounts through the real registerWorkbench()', await waitsForRoute('/api/aris-wb'))
249
+ check('/api/aris-run mounts through the real registerRunStatus()', await waitsForRoute('/api/aris-run'))
250
+
251
+ const skills = await (await view('/api/aris-wb', { endpoint: 'skills', payload: {} })).json()
252
+ check('the workbench skills endpoint answers over HTTP',
253
+ skills.ok === true && Array.isArray(skills.value.skills) && skills.value.skills.length > 20,
254
+ JSON.stringify(skills).slice(0, 200))
255
+
256
+ const state = await (await view('/api/aris-run', { endpoint: 'state', payload: { sessionId: 'no-such-session' } })).json()
257
+ check('the run-status endpoint answers over HTTP',
258
+ state.ok === true && Array.isArray(state.value.artifacts), JSON.stringify(state).slice(0, 200))
259
+
260
+ const unknown = await (await view('/api/aris-run', { endpoint: 'nope', payload: {} })).json()
261
+ check('an unknown endpoint reports through the envelope',
262
+ unknown.ok === false && unknown.error.code === 'UNKNOWN_ENDPOINT', JSON.stringify(unknown).slice(0, 200))
201
263
  }
202
264
 
203
265
  console.log(failures === 0 ? '\nOK — transport contract holds' : `\n${failures} check(s) failed`)