dsh-device-fleet 0.1.0-dev.3 → 0.1.0-dev.5
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 +2 -2
- package/docs/notes/dev-release.md +10 -5
- package/docs/notes/pairing-session-2026-11-09.md +30 -0
- package/package.json +1 -1
- package/src/host/delegation.js +124 -0
- package/src/host/direct-call.js +13 -0
- package/src/host/execution-slot.js +19 -5
- package/src/host/index.js +1 -0
- package/src/host/tool-policy.js +2 -1
- package/src/host/tools.js +27 -0
- package/src/plugin/index.js +27 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ DeepSeek Harness 的设备联邦插件:把登录同一账号、且都安装本
|
|
|
11
11
|
|
|
12
12
|
## 安装(开发版)
|
|
13
13
|
|
|
14
|
-
已发布到 npm:**`dsh-device-fleet@0.1.0-dev.
|
|
14
|
+
已发布到 npm:**`dsh-device-fleet@0.1.0-dev.3`**(dist-tag `dev`)。
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
17
|
# CLI / web profile
|
|
@@ -21,7 +21,7 @@ dsh plugin --profile web add dsh-device-fleet@dev
|
|
|
21
21
|
dsh plugin --profile desktop add dsh-device-fleet@dev
|
|
22
22
|
|
|
23
23
|
# 离线安装包(拷到另一台设备)
|
|
24
|
-
dsh plugin --profile desktop add C:/path/to/dsh-device-fleet-0.1.0-dev.
|
|
24
|
+
dsh plugin --profile desktop add C:/path/to/dsh-device-fleet-0.1.0-dev.3.tgz
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
两台设备的 **DSH 版本与插件版本必须一致**(ADR-0024);不一致会在握手时直接拒绝。
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# 开发版发布记录
|
|
2
2
|
|
|
3
3
|
- 包名:`dsh-device-fleet`
|
|
4
|
-
- 版本:`0.1.0-dev.
|
|
4
|
+
- 版本:`0.1.0-dev.3`
|
|
5
5
|
- dist-tag:`dev`(同时也可用精确版本安装)
|
|
6
6
|
- registry:<https://registry.npmjs.org/dsh-device-fleet>
|
|
7
|
-
- tarball:<https://registry.npmjs.org/dsh-device-fleet/-/dsh-device-fleet-0.1.0-dev.
|
|
8
|
-
- 本地离线包:`dsh-device-fleet-0.1.0-dev.
|
|
7
|
+
- tarball:<https://registry.npmjs.org/dsh-device-fleet/-/dsh-device-fleet-0.1.0-dev.3.tgz>
|
|
8
|
+
- 本地离线包:`dsh-device-fleet-0.1.0-dev.3.tgz`(仓库根目录,可拷贝安装)
|
|
9
9
|
- 发布时间:2026-11-09(Asia/Shanghai)
|
|
10
10
|
- 内容:网络层 v1/v2 + Host 插件 + 直调 MVP(`fleet_invite/join/list/tools/call/read/fetch/run`);Room/Board/委派/审批 UI 未包含。
|
|
11
11
|
|
|
@@ -21,7 +21,7 @@ dsh plugin --profile desktop add dsh-device-fleet@dev
|
|
|
21
21
|
|
|
22
22
|
## 已知限制
|
|
23
23
|
|
|
24
|
-
- 严格同版本:两台设备必须安装同一 `0.1.0-dev.
|
|
24
|
+
- 严格同版本:两台设备必须安装同一 `0.1.0-dev.3`,DSH 版本也要同一发布线;
|
|
25
25
|
- `fleet_run` 的 `ask` 审批目前 fail-closed(审批收件箱未实现);
|
|
26
26
|
- `fleet_fetch` 仅返回文本;
|
|
27
27
|
- 首次加载会在 `$DSH_HOME/device-fleet/` 自动创建身份与独立 Fleet;加入已有 Fleet 需要先 `fleet_join` 再重启;
|
|
@@ -33,4 +33,9 @@ dsh plugin --profile desktop add dsh-device-fleet@dev
|
|
|
33
33
|
- 第一次 activate 失败:`@deepseek-ai/schemastery` 只有默认导出(`import Schema from ...`),改为默认导入;
|
|
34
34
|
- 第二次 activate 失败:Cordis 直接访问未 inject 的服务会抛错 → 可选服务统一走 `ctx.get(name)`;
|
|
35
35
|
- 最终 `set_plugin include:device-fleet enabled=true` → `application: applied`,8 个 `fleet_*` 工具出现在模型工具表,`fleet_list` 返回本机 device id,`taskui_probe` 仍为 v0.4.2 / tasks=247(与 task-list 无冲突)。
|
|
36
|
-
- 已发布 `0.1.0-dev.
|
|
36
|
+
- 已发布 `0.1.0-dev.3`(包含上述激活修复);`0.1.0-dev.1` 会激活失败,不要使用。
|
|
37
|
+
## 0.1.0-dev.3(本次)
|
|
38
|
+
|
|
39
|
+
- 邀请里新增 `endpoints`(创建者的局域网/热点 IPv4 + direct 端口),加入方重启后自动向这些地址回连,绕开组播发现失败;
|
|
40
|
+
- 新增 `fleet_status`(deviceId/直连端口/发现/连接/roster/hints)与 `fleet_connect(host, port)`(手动直连,成功后会从握手学到对端 deviceId);
|
|
41
|
+
- `fleet_list` 仍是本地视图;配对成功后双方 roster 会自动固定对端公钥。
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# 首次配对实录(2026-11-09)
|
|
2
|
+
|
|
3
|
+
## 环境
|
|
4
|
+
|
|
5
|
+
- 设备 A:本机(Windows,DSH desktop profile,`device-fleet` 0.1.0-dev.3)
|
|
6
|
+
- deviceId:`50fdaf90-f0aa-440e-9fa6-c60ba5051001`,deviceName:`desktop`
|
|
7
|
+
- 直连:`0.0.0.0:60506`(热点网段 `192.168.137.1:60506`)
|
|
8
|
+
- allowedRoots:`E:/Dsh_test`
|
|
9
|
+
- 设备 B:另一台设备(同一热点 `192.168.137.0/24`,plugin 0.1.0-dev.3)
|
|
10
|
+
- deviceId:`c2d42572-e5f0-4d7e-9fff-c28d9a668439`
|
|
11
|
+
- 热点网段内网段:A `192.168.137.1/24`,B 为客户端之一。
|
|
12
|
+
|
|
13
|
+
## 过程
|
|
14
|
+
|
|
15
|
+
1. dev.3 邀请携带 `endpoints`(A 的 `210.73.18.88:60506` 与 `192.168.137.1:60506`)。
|
|
16
|
+
2. B 安装 dev.3 → `fleet_join` 新邀请 → 重启;插件读取 enrollment endpoints,自动回连 A。
|
|
17
|
+
3. 结果:双方握手成功,roster 互固定公钥;A 侧 `fleet_list` 显示 B `online`,`fleet_status.connected` 含 B。
|
|
18
|
+
4. `fleet_tools(device=B)` 成功(说明 RPC/加密通道正常)。
|
|
19
|
+
5. `fleet_read(device=B, path=E:/Dsh_test/hello-from-A.txt)` 返回 `denied: no allowed roots configured on this device` —— 策略按设计拦截,B 侧尚未配置 allowedRoots。
|
|
20
|
+
|
|
21
|
+
## 结论
|
|
22
|
+
|
|
23
|
+
- **网络层 v1 直连 + 配对流程打通**(组播发现在该热点下不可用,邀请 endpoints 自动回连解决了问题)。
|
|
24
|
+
- **Tool Policy 生效**:目标设备未配置允许根时直调被拒。
|
|
25
|
+
- 待配置 B 的 `policy.allowedRoots` / `execution.cwd` 后可双向直调;`fleet_run` 待审批收件箱 UI。
|
|
26
|
+
|
|
27
|
+
## 复现要点
|
|
28
|
+
|
|
29
|
+
- 邀请里的 endpoints 由 `networkInterfaces()` 的非回环 IPv4 + 当前 direct port 组成;direct 端口每次插件重载会变,重载后需重新生成邀请或手动 `fleet_connect`。
|
|
30
|
+
- 同一账号指纹:邀请携带 `accountFingerprint`;B 登录不同账号会在加载时被标记 mismatch 并拒绝连接。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-device-fleet",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.5",
|
|
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",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Cross-device delegation (ADR-0015): the caller asks the target device's
|
|
2
|
+
// agent to do a one-shot task and gets the final assistant output back.
|
|
3
|
+
//
|
|
4
|
+
// On the target device this runs through the official SubagentProvider
|
|
5
|
+
// contract with `delivery: 'caller'`, so the child agent's own model turn and
|
|
6
|
+
// tools run on that device, and the `result` promise resolves with the final
|
|
7
|
+
// content blocks. No Room, no shared history: delegation is a one-shot task.
|
|
8
|
+
const DEFAULT_DELEGATE_TIMEOUT_MS = 180_000
|
|
9
|
+
const DEFAULT_PROVIDER = 'spawn'
|
|
10
|
+
const DEFAULT_LABEL = 'fleet delegation'
|
|
11
|
+
|
|
12
|
+
function textOf(result) {
|
|
13
|
+
const blocks = Array.isArray(result?.output) ? result.output : []
|
|
14
|
+
return blocks
|
|
15
|
+
.map((block) => (block?.type === 'text' ? block.text : `[${String(block?.type ?? 'block')}]`))
|
|
16
|
+
.join('\n')
|
|
17
|
+
.trim()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function withTimeout(promise, timeoutMs, onTimeout) {
|
|
21
|
+
let timer
|
|
22
|
+
return Promise.race([
|
|
23
|
+
promise.finally(() => clearTimeout(timer)),
|
|
24
|
+
new Promise((_, reject) => {
|
|
25
|
+
timer = setTimeout(() => reject(onTimeout()), timeoutMs)
|
|
26
|
+
timer.unref?.()
|
|
27
|
+
}),
|
|
28
|
+
])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class DelegationHost {
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} options
|
|
34
|
+
* @param {object} options.subagents ctx.subagents service
|
|
35
|
+
* @param {object} options.slot ExecutionSlot
|
|
36
|
+
* @param {import('./tool-policy.js').ToolPolicy} options.policy
|
|
37
|
+
* @param {(request: object) => Promise<boolean>} [options.requestApproval]
|
|
38
|
+
* @param {string} [options.deviceId]
|
|
39
|
+
* @param {string} [options.defaultCwd]
|
|
40
|
+
* @param {string} [options.defaultAgentPreset]
|
|
41
|
+
* @param {string} [options.provider]
|
|
42
|
+
* @param {number} [options.timeoutMs]
|
|
43
|
+
*/
|
|
44
|
+
constructor({
|
|
45
|
+
subagents,
|
|
46
|
+
slot,
|
|
47
|
+
policy,
|
|
48
|
+
requestApproval,
|
|
49
|
+
deviceId,
|
|
50
|
+
defaultCwd,
|
|
51
|
+
defaultAgentPreset,
|
|
52
|
+
provider = DEFAULT_PROVIDER,
|
|
53
|
+
timeoutMs = DEFAULT_DELEGATE_TIMEOUT_MS,
|
|
54
|
+
}) {
|
|
55
|
+
this.subagents = subagents
|
|
56
|
+
this.slot = slot
|
|
57
|
+
this.policy = policy
|
|
58
|
+
this.requestApproval = requestApproval
|
|
59
|
+
this.deviceId = deviceId
|
|
60
|
+
this.defaultCwd = defaultCwd
|
|
61
|
+
this.defaultAgentPreset = defaultAgentPreset
|
|
62
|
+
this.provider = provider
|
|
63
|
+
this.timeoutMs = timeoutMs
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async delegate({ fromDeviceId, request = {} }) {
|
|
67
|
+
const task = typeof request.task === 'string' ? request.task.trim() : ''
|
|
68
|
+
if (task === '') throw Object.assign(new Error('delegate needs a non-empty task'), { code: 'bad-request' })
|
|
69
|
+
|
|
70
|
+
const decision = this.policy.decide({ capability: 'delegate', args: {} })
|
|
71
|
+
if (decision.decision === 'deny') {
|
|
72
|
+
throw Object.assign(new Error(decision.reason), { code: 'denied', data: { capability: 'delegate' } })
|
|
73
|
+
}
|
|
74
|
+
if (decision.decision === 'ask') {
|
|
75
|
+
const approved =
|
|
76
|
+
typeof this.requestApproval === 'function'
|
|
77
|
+
? await this.requestApproval({ fromDeviceId, capability: 'delegate', tool: 'delegate', args: { task }, reason: decision.reason })
|
|
78
|
+
: false
|
|
79
|
+
if (approved !== true) throw Object.assign(new Error('local approval was not granted'), { code: 'approval_denied' })
|
|
80
|
+
this.policy.grant(decision.key, { scope: 'once' })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const agent = await this.slot.ensureAgent()
|
|
84
|
+
const controller = new AbortController()
|
|
85
|
+
const startedAt = Date.now()
|
|
86
|
+
let activation
|
|
87
|
+
try {
|
|
88
|
+
activation = await this.subagents.startActivation({
|
|
89
|
+
provider: request.provider ?? this.provider,
|
|
90
|
+
label: typeof request.label === 'string' && request.label !== '' ? request.label : DEFAULT_LABEL,
|
|
91
|
+
request: {
|
|
92
|
+
prompt: [{ type: 'text', text: task }],
|
|
93
|
+
parent: agent,
|
|
94
|
+
...(request.cwd ?? this.defaultCwd ? { cwd: request.cwd ?? this.defaultCwd } : {}),
|
|
95
|
+
...(request.agentPreset ?? this.defaultAgentPreset
|
|
96
|
+
? { agentPreset: request.agentPreset ?? this.defaultAgentPreset }
|
|
97
|
+
: {}),
|
|
98
|
+
},
|
|
99
|
+
signal: controller.signal,
|
|
100
|
+
delivery: 'caller',
|
|
101
|
+
})
|
|
102
|
+
const result = await withTimeout(
|
|
103
|
+
activation.result,
|
|
104
|
+
Number.isFinite(request.timeoutMs) ? request.timeoutMs : this.timeoutMs,
|
|
105
|
+
() => {
|
|
106
|
+
controller.abort()
|
|
107
|
+
return Object.assign(new Error('delegation timed out; the child agent was asked to stop'), { code: 'delegate-timeout' })
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
return {
|
|
111
|
+
text: textOf(result) || '(delegated agent returned no text)',
|
|
112
|
+
stopReason: result?.stopReason ?? null,
|
|
113
|
+
childId: activation.childId,
|
|
114
|
+
durationMs: Date.now() - startedAt,
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
try {
|
|
118
|
+
await activation?.dispose?.()
|
|
119
|
+
} catch {
|
|
120
|
+
/* best effort */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/host/direct-call.js
CHANGED
|
@@ -27,6 +27,7 @@ export class DirectCallHost {
|
|
|
27
27
|
* @param {string} [options.dshVersion]
|
|
28
28
|
* @param {string[]} [options.capabilities]
|
|
29
29
|
* @param {string} [options.shellTool]
|
|
30
|
+
* @param {object} [options.delegation] DelegationHost for `fleet.delegate`
|
|
30
31
|
*/
|
|
31
32
|
constructor({
|
|
32
33
|
slot,
|
|
@@ -40,6 +41,7 @@ export class DirectCallHost {
|
|
|
40
41
|
capabilities = [],
|
|
41
42
|
shellTool = DEFAULT_SHELL_TOOL,
|
|
42
43
|
approvalTimeoutMs = DEFAULT_POLICY_TIMEOUT_MS,
|
|
44
|
+
delegation = null,
|
|
43
45
|
}) {
|
|
44
46
|
this.slot = slot
|
|
45
47
|
this.policy = policy
|
|
@@ -52,6 +54,7 @@ export class DirectCallHost {
|
|
|
52
54
|
this.capabilities = capabilities
|
|
53
55
|
this.shellTool = shellTool
|
|
54
56
|
this.approvalTimeoutMs = approvalTimeoutMs
|
|
57
|
+
this.delegation = delegation
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
info() {
|
|
@@ -75,6 +78,11 @@ export class DirectCallHost {
|
|
|
75
78
|
return { deviceId: this.deviceId, tools: await this.slot.listTools(), policy: this.policy.summarize() }
|
|
76
79
|
case 'fleet.call':
|
|
77
80
|
return this.handleCall({ fromDeviceId: deviceId, request: params ?? {} })
|
|
81
|
+
case 'fleet.delegate':
|
|
82
|
+
if (!this.delegation) {
|
|
83
|
+
throw Object.assign(new Error('delegation is unavailable on this device'), { code: 'unsupported' })
|
|
84
|
+
}
|
|
85
|
+
return this.delegation.delegate({ fromDeviceId: deviceId, request: params ?? {} })
|
|
78
86
|
default:
|
|
79
87
|
throw Object.assign(new Error(`unknown fleet method: ${String(method)}`), { code: 'unknown-method' })
|
|
80
88
|
}
|
|
@@ -178,6 +186,11 @@ export class DirectCallClient {
|
|
|
178
186
|
return this.node.request(deviceId, 'fleet.call', { capability, tool, args }, options)
|
|
179
187
|
}
|
|
180
188
|
|
|
189
|
+
/** Ask the target device's agent to do a one-shot task and return its final text. */
|
|
190
|
+
async delegate(deviceId, request, options = {}) {
|
|
191
|
+
return this.node.request(deviceId, 'fleet.delegate', request, options)
|
|
192
|
+
}
|
|
193
|
+
|
|
181
194
|
/** Format a remote result for a model-facing text tool. */
|
|
182
195
|
render(result) {
|
|
183
196
|
if (result?.isError) return `Error: ${result.text || 'remote tool failed'}`
|
|
@@ -55,11 +55,25 @@ export class ExecutionSlot {
|
|
|
55
55
|
*/
|
|
56
56
|
async executeTool({ name, args = {}, signal }) {
|
|
57
57
|
const agent = await this.ensureAgent()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
// execToolBatch requires a real AbortSignal: without one the pipeline
|
|
59
|
+
// throws before dispatch, which surfaces as "Tool batch execution failed".
|
|
60
|
+
const effectiveSignal = signal ?? new AbortController().signal
|
|
61
|
+
let batch
|
|
62
|
+
try {
|
|
63
|
+
batch = await this.tools.execute({
|
|
64
|
+
calls: [{ id: randomUUID(), name, arguments: JSON.stringify(args ?? {}) }],
|
|
65
|
+
agent,
|
|
66
|
+
signal: effectiveSignal,
|
|
67
|
+
})
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const cause = error?.cause
|
|
70
|
+
const detail = cause?.message ?? error?.message ?? String(error)
|
|
71
|
+
throw Object.assign(new Error(`tool ${name} failed: ${String(detail)}`), {
|
|
72
|
+
code: 'tool-failed',
|
|
73
|
+
cause,
|
|
74
|
+
data: { causeCode: cause?.code ?? null, causeMessage: cause?.message ?? null },
|
|
75
|
+
})
|
|
76
|
+
}
|
|
63
77
|
const first = batch?.results?.[0]?.result
|
|
64
78
|
if (!first) throw Object.assign(new Error(`tool ${name} returned no result`), { code: 'tool-no-result' })
|
|
65
79
|
const text = (first.content ?? [])
|
package/src/host/index.js
CHANGED
package/src/host/tool-policy.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the raw-tool allowlist, and short-lived grants created by approvals.
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
|
|
6
|
-
export const CAPABILITIES = ['read', 'list', 'grep', 'fetch', 'run', 'tool']
|
|
6
|
+
export const CAPABILITIES = ['read', 'list', 'grep', 'fetch', 'run', 'tool', 'delegate']
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_POLICY = Object.freeze({
|
|
9
9
|
allowedRoots: [],
|
|
@@ -14,6 +14,7 @@ export const DEFAULT_POLICY = Object.freeze({
|
|
|
14
14
|
fetch: 'auto',
|
|
15
15
|
run: 'ask',
|
|
16
16
|
tool: 'deny',
|
|
17
|
+
delegate: 'ask',
|
|
17
18
|
},
|
|
18
19
|
allowedTools: [],
|
|
19
20
|
approvalTtlMs: 15 * 60 * 1000,
|
package/src/host/tools.js
CHANGED
|
@@ -109,6 +109,20 @@ export function createFleetHandlers({ node, client = new DirectCallClient({ node
|
|
|
109
109
|
}
|
|
110
110
|
},
|
|
111
111
|
|
|
112
|
+
async fleet_delegate({ device, task, label, cwd }) {
|
|
113
|
+
try {
|
|
114
|
+
const result = await client.delegate(
|
|
115
|
+
device,
|
|
116
|
+
{ task, ...(label ? { label } : {}), ...(cwd ? { cwd } : {}) },
|
|
117
|
+
{ timeoutMs: 200_000 },
|
|
118
|
+
)
|
|
119
|
+
const meta = result?.childId ? `\n[delegated on ${device} · ${String(result.durationMs ?? 0)}ms · child ${String(result.childId)}]` : ''
|
|
120
|
+
return { text: `${String(result?.text ?? '')}${meta}`.trim() }
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return fail(error)
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
|
|
112
126
|
async fleet_read({ device, path, offset, limit }) {
|
|
113
127
|
try {
|
|
114
128
|
const args = { file_path: path }
|
|
@@ -211,6 +225,19 @@ export function createFleetToolDefinitions({ defineTool, handlers }) {
|
|
|
211
225
|
output,
|
|
212
226
|
execute: (args) => handlers.fleet_call(args),
|
|
213
227
|
}),
|
|
228
|
+
defineTool({
|
|
229
|
+
name: 'fleet_delegate',
|
|
230
|
+
description:
|
|
231
|
+
'Delegate a one-shot task to another device\'s agent and wait for its final answer. Use it when the work needs that device\'s model and tools ("let B build/check this"), not just one tool call. The delegated agent runs on the target device; it does not join a Room and its history is not shared. The target device decides whether delegation is allowed or needs local approval.',
|
|
232
|
+
parameters: {
|
|
233
|
+
device: commonDevice,
|
|
234
|
+
task: { type: 'string', required: true, description: 'Self-contained task instructions for the target agent.' },
|
|
235
|
+
label: { type: 'string', description: 'Short label shown in the target device\'s subagent list.' },
|
|
236
|
+
cwd: { type: 'string', description: 'Optional working directory on the target device.' },
|
|
237
|
+
},
|
|
238
|
+
output,
|
|
239
|
+
execute: (args) => handlers.fleet_delegate(args),
|
|
240
|
+
}),
|
|
214
241
|
defineTool({
|
|
215
242
|
name: 'fleet_read',
|
|
216
243
|
description:
|
package/src/plugin/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import Schema from '@deepseek-ai/schemastery'
|
|
|
13
13
|
import { FleetNode } from '../net/fleet-node.js'
|
|
14
14
|
import { accountFingerprint } from '../net/identity.js'
|
|
15
15
|
import { DirectCallClient, DirectCallHost } from '../host/direct-call.js'
|
|
16
|
+
import { DelegationHost } from '../host/delegation.js'
|
|
16
17
|
import { ExecutionSlot, DEFAULT_SHELL_TOOL } from '../host/execution-slot.js'
|
|
17
18
|
import { ToolPolicy } from '../host/tool-policy.js'
|
|
18
19
|
import { createFleetHandlers, createFleetToolDefinitions } from '../host/tools.js'
|
|
@@ -60,9 +61,11 @@ export const Config = Schema.object({
|
|
|
60
61
|
agentPreset: Schema.string(),
|
|
61
62
|
shellTool: Schema.string().default(DEFAULT_SHELL_TOOL),
|
|
62
63
|
approvalTimeoutMs: Schema.number().default(60000),
|
|
64
|
+
delegateTimeoutMs: Schema.number().default(180000),
|
|
65
|
+
subagentProvider: Schema.string().default('spawn'),
|
|
63
66
|
})
|
|
64
|
-
.default({ shellTool: DEFAULT_SHELL_TOOL, approvalTimeoutMs: 60000 })
|
|
65
|
-
.description('Execution Slot
|
|
67
|
+
.default({ shellTool: DEFAULT_SHELL_TOOL, approvalTimeoutMs: 60000, delegateTimeoutMs: 180000, subagentProvider: 'spawn' })
|
|
68
|
+
.description('Execution Slot and delegation settings for direct calls and one-shot delegation.'),
|
|
66
69
|
policy: Schema.object({
|
|
67
70
|
allowedRoots: Schema.array(Schema.string()).default([]),
|
|
68
71
|
capabilities: Schema.any().default({}),
|
|
@@ -153,11 +156,32 @@ export function apply(ctx, config) {
|
|
|
153
156
|
const policy = new ToolPolicy(config.policy)
|
|
154
157
|
const sessionController = service('sessionController')
|
|
155
158
|
const fs = service('fs')
|
|
159
|
+
const subagents = service('subagents')
|
|
156
160
|
if (!sessionController) logger.warn?.('[device-fleet] sessionController unavailable: direct calls to stateful tools will fail')
|
|
157
161
|
const slot = sessionController
|
|
158
162
|
? new ExecutionSlot({ sessionController, tools: ctx.tools, config: config.execution })
|
|
159
163
|
: null
|
|
160
164
|
|
|
165
|
+
const delegation =
|
|
166
|
+
slot && subagents
|
|
167
|
+
? new DelegationHost({
|
|
168
|
+
subagents,
|
|
169
|
+
slot,
|
|
170
|
+
policy,
|
|
171
|
+
deviceId: state.identity.deviceId,
|
|
172
|
+
defaultCwd: config.execution?.cwd,
|
|
173
|
+
defaultAgentPreset: config.execution?.agentPreset,
|
|
174
|
+
provider: config.execution?.subagentProvider ?? 'spawn',
|
|
175
|
+
timeoutMs: config.execution?.delegateTimeoutMs ?? 180_000,
|
|
176
|
+
requestApproval: config.autoApproveAsk
|
|
177
|
+
? async () => true
|
|
178
|
+
: async (request) => {
|
|
179
|
+
logger.info?.(`[device-fleet] delegation approval required from ${request.fromDeviceId} (no approval UI yet)`)
|
|
180
|
+
return false
|
|
181
|
+
},
|
|
182
|
+
})
|
|
183
|
+
: null
|
|
184
|
+
|
|
161
185
|
const host = slot
|
|
162
186
|
? new DirectCallHost({
|
|
163
187
|
slot,
|
|
@@ -170,6 +194,7 @@ export function apply(ctx, config) {
|
|
|
170
194
|
capabilities: CAPABILITIES,
|
|
171
195
|
shellTool: config.execution?.shellTool ?? DEFAULT_SHELL_TOOL,
|
|
172
196
|
approvalTimeoutMs: config.execution?.approvalTimeoutMs ?? 60_000,
|
|
197
|
+
delegation,
|
|
173
198
|
requestApproval: config.autoApproveAsk
|
|
174
199
|
? async () => true
|
|
175
200
|
: async (request) => {
|