dsh-device-fleet 0.1.0-dev.15 → 0.1.0-dev.17

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/client.js CHANGED
@@ -273,6 +273,7 @@ window.__ModuleLoader__.load({
273
273
  const [rooms, setRooms] = useState([]);
274
274
  const [selectedRoom, setSelectedRoom] = useState(null);
275
275
  const [roomDetail, setRoomDetail] = useState(null);
276
+ const [roomError, setRoomError] = useState('');
276
277
  const [error, setError] = useState('');
277
278
 
278
279
  useEffect(() => {
@@ -307,12 +308,18 @@ window.__ModuleLoader__.load({
307
308
  let disposed = false;
308
309
  const load = async () => {
309
310
  try {
310
- const response = await fetch('/device-fleet/rooms/' + encodeURIComponent(selectedRoom), { headers: { accept: 'application/json' } });
311
+ const response = await fetch('/device-fleet/room?id=' + encodeURIComponent(selectedRoom), { headers: { accept: 'application/json' } });
311
312
  if (!response.ok) throw new Error('HTTP ' + response.status);
312
313
  const data = await response.json();
313
- if (!disposed) setRoomDetail(data.room ?? null);
314
- } catch {
315
- if (!disposed) setRoomDetail(null);
314
+ if (!disposed) {
315
+ setRoomDetail(data.room ?? null);
316
+ setRoomError('');
317
+ }
318
+ } catch (problem) {
319
+ if (!disposed) {
320
+ setRoomDetail(null);
321
+ setRoomError(String(problem.message ?? problem));
322
+ }
316
323
  }
317
324
  };
318
325
  load();
@@ -434,7 +441,7 @@ window.__ModuleLoader__.load({
434
441
  ),
435
442
  ),
436
443
  ),
437
- h('div', { style: { flex: '2 1 420px', minWidth: 320 } }, selectedRoom ? renderRoomDetail(roomDetail) : h('div', { style: { opacity: 0.65, fontSize: 12 } }, '选择一个房间查看成员、时间线与看板。')),
444
+ h('div', { style: { flex: '2 1 420px', minWidth: 320 } }, selectedRoom ? (roomError && !roomDetail ? h('div', { style: { opacity: 0.7, fontSize: 12 } }, '房间加载失败:' + roomError) : renderRoomDetail(roomDetail)) : h('div', { style: { opacity: 0.65, fontSize: 12 } }, '选择一个房间查看成员、时间线与看板。')),
438
445
  );
439
446
  } else {
440
447
  const approvals = overview.approvals ?? [];
@@ -549,9 +556,15 @@ window.__ModuleLoader__.load({
549
556
  }
550
557
 
551
558
  function Row({ label, children, wide = false }) {
559
+ if (wide) {
560
+ return h('div', { style: { ...rowStyle, flexDirection: 'column', alignItems: 'stretch', gap: 6, padding: '8px 0' } },
561
+ h('div', { style: rowLabelStyle }, label),
562
+ h('div', { style: { width: '100%' } }, children),
563
+ )
564
+ }
552
565
  return h('div', { style: rowStyle },
553
566
  h('div', { style: rowLabelStyle }, label),
554
- h('div', { style: wide ? { width: '100%' } : { width: CONTROL_WIDTH } }, children),
567
+ h('div', { style: { width: CONTROL_WIDTH } }, children),
555
568
  )
556
569
  }
557
570
 
@@ -601,7 +614,7 @@ window.__ModuleLoader__.load({
601
614
  ['capGrep', '搜索内容'],
602
615
  ['capFetch', '获取文件'],
603
616
  ['capRun', '执行命令'],
604
- ['capTool', '调用原始工具'],
617
+ ['capTool', '原始工具(遵循 DSH 审批)'],
605
618
  ['capDelegate', '委派任务'],
606
619
  ]
607
620
 
@@ -635,7 +648,6 @@ window.__ModuleLoader__.load({
635
648
  execDelegateTimeoutMs: execution.delegateTimeoutMs ?? 180000,
636
649
  execSubagentProvider: execution.subagentProvider ?? 'spawn',
637
650
  rootsText: (policy.allowedRoots ?? []).join('\n'),
638
- toolsText: (policy.allowedTools ?? []).join('\n'),
639
651
  capRead: policy.capabilities?.read ?? 'auto',
640
652
  capList: policy.capabilities?.list ?? 'auto',
641
653
  capGrep: policy.capabilities?.grep ?? 'auto',
@@ -676,7 +688,6 @@ window.__ModuleLoader__.load({
676
688
  },
677
689
  policy: {
678
690
  allowedRoots: draft.rootsText.split('\n').map((item) => item.trim()).filter(Boolean),
679
- allowedTools: draft.toolsText.split('\n').map((item) => item.trim()).filter(Boolean),
680
691
  capabilities: {
681
692
  read: draft.capRead,
682
693
  list: draft.capList,
@@ -774,7 +785,7 @@ window.__ModuleLoader__.load({
774
785
  padding: '0 14px',
775
786
  borderRadius: 6,
776
787
  border: '1px solid var(--dsw-alias-brand-primary, #2f6fed)',
777
- background: 'var(--dsw-alias-brand-primary, #2f6fed)',
788
+ background: '#2f6fed',
778
789
  color: '#fff',
779
790
  fontSize: 13,
780
791
  cursor: busy ? 'default' : 'pointer',
@@ -835,7 +846,7 @@ window.__ModuleLoader__.load({
835
846
  h(NumberControl, { value: draft.execDelegateTimeoutMs, onChange: (value) => set('execDelegateTimeoutMs', value) }),
836
847
  ),
837
848
  h('div', { style: sectionStyle }, '远端权限'),
838
- h(Row, { label: '可访问根目录', wide: true },
849
+ h(Row, { label: '允许的根目录', wide: true },
839
850
  h('textarea', {
840
851
  value: draft.rootsText,
841
852
  onChange: (event) => set('rootsText', event.target.value),
@@ -843,14 +854,6 @@ window.__ModuleLoader__.load({
843
854
  style: textareaStyle,
844
855
  }),
845
856
  ),
846
- h(Row, { label: '允许的原始工具', wide: true },
847
- h('textarea', {
848
- value: draft.toolsText,
849
- onChange: (event) => set('toolsText', event.target.value),
850
- rows: 3,
851
- style: textareaStyle,
852
- }),
853
- ),
854
857
  ...CAPABILITY_ROWS.map(([key, label]) =>
855
858
  h(Row, { key, label },
856
859
  h(SelectControl, { value: draft[key], options: modeOptions, onChange: (value) => set(key, value) }),
package/cordis.patch.yml CHANGED
@@ -18,9 +18,9 @@
18
18
  grep: auto
19
19
  fetch: auto
20
20
  run: ask
21
- tool: deny
21
+ tool: auto
22
22
  allowedTools: []
23
23
  promptSection: true
24
24
  autoApproveAsk: false
25
-
26
-
25
+
26
+
@@ -39,19 +39,19 @@ dsh plugin --profile desktop add dsh-device-fleet@dev
39
39
  - 邀请里新增 `endpoints`(创建者的局域网/热点 IPv4 + direct 端口),加入方重启后自动向这些地址回连,绕开组播发现失败;
40
40
  - 新增 `fleet_status`(deviceId/直连端口/发现/连接/roster/hints)与 `fleet_connect(host, port)`(手动直连,成功后会从握手学到对端 deviceId);
41
41
  - `fleet_list` 仍是本地视图;配对成功后双方 roster 会自动固定对端公钥。
42
- ## 0.1.0-dev.14(稳定化)
42
+ ## 0.1.0-dev.16(稳定化)
43
43
 
44
44
  - 直连端口可固定(profile `direct.port`),插件重载不再换端口;
45
45
  - 连接断开后自动重试已知地址(不再只在启动时试一次);
46
46
  - `fleet_connect` 手动地址会持久化到 `peer-hints.json`,重启后继续回连;
47
47
  - 新增 `fleet_delegate`(dev.5):目标设备用 `SubagentProvider` + `delivery:'caller'` 跑 agent 并返回最终结果。
48
- ## 0.1.0-dev.14(审批收件箱,逻辑层)
48
+ ## 0.1.0-dev.16(审批收件箱,逻辑层)
49
49
 
50
50
  - 新增 `ApprovalInbox`:ask 档请求排队、超时 fail-closed、approve(once/ttl)/deny、关闭时全部失败关闭;
51
51
  - 直调与委派的 ask 档改为等待本机收件箱结果(不再一律拒绝);
52
52
  - 新工具:`fleet_approvals` / `fleet_approve` / `fleet_deny`;`fleet_simulate_request`(仅 `approvals.simulation: true`)用于本机无第二台设备时演练流程;
53
53
  - 审批面板(Client UI)与通知尚未实现,当前通过工具/后续 UI 应答。
54
- ## 0.1.0-dev.14(Room 逻辑层,本机模拟)
54
+ ## 0.1.0-dev.16(Room 逻辑层,本机模拟)
55
55
 
56
56
  - `RoomService`:当前会话升级为 Room Host;本地模拟参与者(真实 DSH session/agent);
57
57
  - 寻址消息:`room_send` 只唤醒被点名成员,Host 会话写权威 `room/message` 事件;
@@ -59,22 +59,35 @@ dsh plugin --profile desktop add dsh-device-fleet@dev
59
59
  - Room Board:`room_task_add/update/list`,`dependsOn` 串行交接(前置完成时唤醒下一个 owner),无依赖任务并行不受影响;
60
60
  - 工具:`room_create / room_add_local / room_remove_member / room_send / room_messages / room_status / room_task_* / room_close`;
61
61
  - 远程参与者传输与 Client UI 未做(下一步)。
62
- ## 0.1.0-dev.14(审批面板 UI,第一版)
62
+ ## 0.1.0-dev.16(审批面板 UI,第一版)
63
63
 
64
64
  - Host 新增本地 HTTP 数据面:GET `/device-fleet/approvals`、POST `/device-fleet/approvals/decide`(仅本机 web 载体,决策仍走 ApprovalInbox);
65
65
  - Client half:`client.js` 闭包工厂,注册 `shell.overlay` 浮动「Fleet 审批」按钮 + 面板,轮询待审批列表,支持允许一次 / 允许 15 分钟 / 拒绝;
66
66
  - package.json 增加 `dsh.client` 与 `./client` 导出;`files` 增加 client.js;
67
67
  - Client 视觉验证需要刷新页面;当前无浏览器控制,仅验证了 bundle 语法与 Host 路由注册。
68
- ## 0.1.0-dev.14(审批入口不遮挡)
68
+ ## 0.1.0-dev.16(审批入口不遮挡)
69
69
 
70
70
  - 触发按钮从右下角浮动改为 `sidebar.footer.action`(设置旁边),不再遮挡任务列表/内容面板;
71
71
  - 审批 inbox 改为点击后居中的模态对话框(带遮罩,Esc/点遮罩关闭);
72
72
  - 轮询在 Client apply 中启动并由 ctx.effect 清理;待审批数量作为按钮徽标;
73
73
  - 已实测:面板点击「允许」后 Host `fleet_approvals` 清空。
74
- ## 0.1.0-dev.14(Phase B:运行时配置 + 配置页)
74
+ ## 0.1.0-dev.16(Phase B:运行时配置 + 配置页)
75
75
 
76
76
  - `RuntimeConfig`:profile patch 为 base,UI 修改写入 `$DSH_HOME/device-fleet/config-overrides.json`,apply 时合并并持久化;
77
77
  - 保存即生效:policy/execution/approvals/deviceName/promptSection/autoApproveAsk 直接更新运行时对象;direct/discovery/relay 通过 `FleetNode.reconfigure()` 原地重启网络监听(不重启插件/DSH);`dataDir` 只读;
78
78
  - Host:`GET /device-fleet/config`、`POST /device-fleet/config`(校验/清洗后应用);
79
79
  - Client:注册 `plugins.row.config`(key `dsh-device-fleet#device-fleet`)与 `plugins.bundle.config`(key `dsh-device-fleet`),配置页含设备/网络、执行上下文、Tool Policy、审批与提示四组表单;
80
- - 单测 18/18、网络 9/9。
80
+ - 单测 18/18、网络 9/9。
81
+ ## 0.1.0-dev.16(配置页 DSH 化改版)
82
+
83
+ - Host 新增 `GET /device-fleet/options`:本机地址、可用 preset 列表、shell 工具、subagent providers、工作目录候选;
84
+ - 配置保存前会 ensure 工作目录存在(Device Fleet 工作区自动创建);
85
+ - 配置页重做:行式布局(label 左/控件右)、DSH token 配色、分组标题、去掉多余说明文字与 dataDir 展示;
86
+ - 下拉化:直连地址、局域网发现、Relay 模式、工作目录(工作区/默认/DSH 主目录/自定义)、Agent Preset、Shell、委派 Provider、7 项能力权限、开关项;
87
+ - 仍保留文本输入:设备名称、端口、超时、根目录/工具列表、自定义工作目录与 Relay 地址。
88
+ ## 0.1.0-dev.16(房间详情与配置页修复)
89
+
90
+ - 房间详情接口从 prefix 路由改为精确路由 `GET /device-fleet/room?id=...`,修复控制台一直「加载房间…」;客户端加载失败时显示错误文案;
91
+ - 配置页宽行改为「标签在上、控件在下」,修复根目录等标签竖排换行;
92
+ - 保存按钮改为固定品牌蓝底白字,修复按钮看起来空白;
93
+ - 「允许的原始工具」改为「免审批即用的工具」:从 Host options 拉取本机真实工具清单,以复选框选择(不再手输工具名)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-device-fleet",
3
- "version": "0.1.0-dev.15",
3
+ "version": "0.1.0-dev.17",
4
4
  "description": "DeepSeek Harness device fleet: enroll devices, direct-call remote tools, shared Rooms with lazy-woken agents, and delegated subagents across devices.",
5
5
  "type": "module",
6
6
  "main": "src/plugin/index.js",
@@ -13,7 +13,7 @@ export const DEFAULT_POLICY = Object.freeze({
13
13
  grep: 'auto',
14
14
  fetch: 'auto',
15
15
  run: 'ask',
16
- tool: 'deny',
16
+ tool: 'auto',
17
17
  delegate: 'ask',
18
18
  },
19
19
  allowedTools: [],
@@ -77,15 +77,17 @@ export class ToolPolicy {
77
77
  return { decision: 'allow', reason: 'run is allowed by policy', key: grantKey, capability: capabilityName }
78
78
  }
79
79
  if (capabilityName === 'tool') {
80
- if (!this.config.allowedTools.includes(String(tool))) {
81
- return {
82
- decision: 'deny',
83
- reason: `tool ${String(tool)} is not in the allowed tools list`,
84
- key: grantKey,
85
- capability: capabilityName,
86
- }
80
+ // Raw tools follow the normal DSH tool pipeline on this device: the
81
+ // target Session's tool pre-execute policy and approval seam decide
82
+ // whether a call runs, asks, or is rejected. `deny` disables raw tools
83
+ // entirely; `ask` adds the plugin-level approval in front of DSH.
84
+ if (mode === 'deny') {
85
+ return { decision: 'deny', reason: 'raw tool calls are disabled by policy', key: grantKey, capability: capabilityName }
86
+ }
87
+ if (mode === 'ask') {
88
+ return { decision: 'ask', reason: 'raw tool call requires local approval', key: grantKey, capability: capabilityName }
87
89
  }
88
- return { decision: 'allow', reason: 'tool is allowlisted', key: grantKey, capability: capabilityName }
90
+ return { decision: 'allow', reason: 'raw tool call follows the DSH tool pipeline', key: grantKey, capability: capabilityName }
89
91
  }
90
92
  if (mode === 'allow') return { decision: 'allow', reason: 'capability is allowed by policy', key: grantKey, capability: capabilityName }
91
93
  if (mode === 'auto') return { decision: 'allow', reason: 'capability is auto-approved', key: grantKey, capability: capabilityName }
@@ -57,12 +57,16 @@ export function registerUiRoutes({ webServer, inbox, rooms, getOverview, logger
57
57
  )
58
58
  disposers.push(
59
59
  webServer.register({
60
- kind: 'prefix',
61
- path: '/device-fleet/rooms/',
60
+ kind: 'exact',
61
+ path: '/device-fleet/room',
62
62
  handler: async (req, res) => {
63
63
  if (req.method !== 'GET') return json(res, 405, { error: 'method not allowed' })
64
- const path = pathOf(req)
65
- const roomId = decodeURIComponent(path.slice('/device-fleet/rooms/'.length))
64
+ let roomId = ''
65
+ try {
66
+ roomId = new URL(req.url ?? '/', 'http://local').searchParams.get('id') ?? ''
67
+ } catch {
68
+ roomId = ''
69
+ }
66
70
  if (roomId === '') return json(res, 400, { error: 'room id is required' })
67
71
  try {
68
72
  json(res, 200, { room: rooms.detail(roomId, { limit: 100 }) })
@@ -20,7 +20,7 @@ import { RoomService } from '../host/room-service.js'
20
20
  import { registerApprovalRoutes } from '../host/approval-web.js'
21
21
  import { registerUiRoutes } from '../host/ui-web.js'
22
22
  import { registerConfigRoutes } from '../host/config-web.js'
23
- import { RuntimeConfig } from '../host/runtime-config.js'
23
+ import { RuntimeConfig, deepMerge } from '../host/runtime-config.js'
24
24
  import { ExecutionSlot, DEFAULT_SHELL_TOOL } from '../host/execution-slot.js'
25
25
  import { ToolPolicy } from '../host/tool-policy.js'
26
26
  import { createFleetHandlers, createFleetToolDefinitions } from '../host/tools.js'
@@ -158,6 +158,16 @@ export function apply(ctx, profileConfig) {
158
158
  base: profileConfig ?? {},
159
159
  overridesFile: join(state.dataDir, 'config-overrides.json'),
160
160
  })
161
+ // Migration: an old UI save wrote the legacy `tool: deny` + empty allowlist;
162
+ // raw tools now follow the DSH pipeline by default.
163
+ if (
164
+ runtime.overrides?.policy?.capabilities?.tool === 'deny' &&
165
+ (runtime.overrides?.policy?.allowedTools?.length ?? 0) === 0
166
+ ) {
167
+ runtime.overrides = deepMerge(runtime.overrides, { policy: { capabilities: { tool: 'auto' } } })
168
+ runtime.current = deepMerge(runtime.base, runtime.overrides)
169
+ runtime.persist()
170
+ }
161
171
  const config = runtime.current
162
172
 
163
173
  const node = new FleetNode({
@@ -255,6 +265,7 @@ export function apply(ctx, profileConfig) {
255
265
  return ['spawn']
256
266
  }
257
267
  })(),
268
+ tools: [...toolNames].sort(),
258
269
  presets: presets.map((preset) => ({
259
270
  id: preset.id ?? preset.name,
260
271
  label: preset.name ?? preset.title ?? preset.id,
@@ -339,6 +350,33 @@ export function apply(ctx, profileConfig) {
339
350
  })
340
351
  : null
341
352
 
353
+ // Mirror the normal DSH approval behavior for tools executed in the hidden
354
+ // Execution Slot: the tool pipeline asks here when its own pre-execute policy
355
+ // requires approval, and this device's approval inbox answers.
356
+ ctx.effect(() => {
357
+ if (!slot) return () => {}
358
+ const handler = async (request, next) => {
359
+ try {
360
+ if (request?.signal?.aborted) return 'cancelled'
361
+ if (!slot.sessionId || request?.agent?.session?.id !== slot.sessionId) return next()
362
+ const toolName = typeof request?.toolName === 'string' ? request.toolName : 'tool'
363
+ const outcome = await inbox.request({
364
+ fromDeviceId: 'local-tool-pipeline',
365
+ capability: 'tool',
366
+ tool: toolName,
367
+ reason: request?.displayReason ?? request?.reason ?? `tool "${toolName}" requires approval`,
368
+ args: {},
369
+ })
370
+ if (outcome.approved === true) return 'allowed-once'
371
+ if (outcome.reason === 'inbox-closed') return 'cancelled'
372
+ return 'rejected'
373
+ } catch {
374
+ return next()
375
+ }
376
+ }
377
+ return ctx.on('approval/request', handler)
378
+ })
379
+
342
380
  node.onRequest(async (request) => {
343
381
  if (!host) throw Object.assign(new Error('this device cannot serve direct calls (no execution slot)'), { code: 'unsupported' })
344
382
  return host.handleRequest(request)