dsh-device-fleet 0.1.0-dev.4 → 0.1.0-dev.6
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/package.json +1 -1
- package/src/host/delegation.js +124 -0
- package/src/host/direct-call.js +13 -0
- package/src/host/index.js +1 -0
- package/src/host/store.js +27 -1
- package/src/host/tool-policy.js +2 -1
- package/src/host/tools.js +29 -1
- package/src/net/fleet-node.js +8 -0
- package/src/plugin/index.js +41 -5
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.6",
|
|
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'}`
|
package/src/host/index.js
CHANGED
package/src/host/store.js
CHANGED
|
@@ -50,6 +50,7 @@ export function loadState({ dshHome, dataDir, name = 'device' } = {}) {
|
|
|
50
50
|
|
|
51
51
|
const roster = readJson(path.join(dir, 'roster.json'), [])
|
|
52
52
|
const config = readJson(path.join(dir, 'config.json'), {})
|
|
53
|
+
const peerHints = readJson(path.join(dir, 'peer-hints.json'), [])
|
|
53
54
|
let enrollment = readJson(path.join(dir, 'enrollment.json'), null)
|
|
54
55
|
let pendingEnrollment = readJson(path.join(dir, 'enrollment.pending.json'), null)
|
|
55
56
|
|
|
@@ -75,7 +76,32 @@ export function loadState({ dshHome, dataDir, name = 'device' } = {}) {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
return {
|
|
79
|
+
return {
|
|
80
|
+
dataDir: dir,
|
|
81
|
+
identity,
|
|
82
|
+
fleet,
|
|
83
|
+
roster: Array.isArray(roster) ? roster : [],
|
|
84
|
+
config,
|
|
85
|
+
enrollment,
|
|
86
|
+
pendingEnrollment,
|
|
87
|
+
peerHints: Array.isArray(peerHints) ? peerHints : [],
|
|
88
|
+
createdFleet,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Persist one manually supplied peer address (from fleet_connect) for reconnects. */
|
|
93
|
+
export function savePeerHint(state, hint) {
|
|
94
|
+
if (!hint || typeof hint.host !== 'string' || !Number.isInteger(hint.port) || hint.port <= 0) return
|
|
95
|
+
const entry = {
|
|
96
|
+
host: hint.host,
|
|
97
|
+
port: hint.port,
|
|
98
|
+
...(typeof hint.deviceId === 'string' ? { deviceId: hint.deviceId } : {}),
|
|
99
|
+
}
|
|
100
|
+
const hints = Array.isArray(state.peerHints) ? state.peerHints : []
|
|
101
|
+
const exists = hints.some((item) => item.host === entry.host && item.port === entry.port && item.deviceId === entry.deviceId)
|
|
102
|
+
if (!exists) hints.push(entry)
|
|
103
|
+
state.peerHints = hints
|
|
104
|
+
writeJsonAtomic(path.join(state.dataDir, 'peer-hints.json'), hints)
|
|
79
105
|
}
|
|
80
106
|
|
|
81
107
|
/** Queue an invite for the next plugin load (the running node keeps its fleet). */
|
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
|
@@ -22,7 +22,7 @@ function fail(error) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Pure handlers; wrapped into ToolDefinitions by `createFleetToolDefinitions`. */
|
|
25
|
-
export function createFleetHandlers({ node, client = new DirectCallClient({ node }), pairing = null }) {
|
|
25
|
+
export function createFleetHandlers({ node, client = new DirectCallClient({ node }), pairing = null, onPeerHint = null }) {
|
|
26
26
|
return {
|
|
27
27
|
async fleet_status() {
|
|
28
28
|
if (typeof node.status !== 'function') return { text: 'Error: [unsupported] status is unavailable' }
|
|
@@ -34,6 +34,7 @@ export function createFleetHandlers({ node, client = new DirectCallClient({ node
|
|
|
34
34
|
try {
|
|
35
35
|
node.addPeerHint?.({ host, port })
|
|
36
36
|
const peer = await node.connectAddress(host, port)
|
|
37
|
+
await onPeerHint?.({ host, port })
|
|
37
38
|
return { text: `connected to ${peer.deviceId} (${peer.name ?? 'device'}) at ${host}:${port}` }
|
|
38
39
|
} catch (error) {
|
|
39
40
|
return fail(error)
|
|
@@ -109,6 +110,20 @@ export function createFleetHandlers({ node, client = new DirectCallClient({ node
|
|
|
109
110
|
}
|
|
110
111
|
},
|
|
111
112
|
|
|
113
|
+
async fleet_delegate({ device, task, label, cwd }) {
|
|
114
|
+
try {
|
|
115
|
+
const result = await client.delegate(
|
|
116
|
+
device,
|
|
117
|
+
{ task, ...(label ? { label } : {}), ...(cwd ? { cwd } : {}) },
|
|
118
|
+
{ timeoutMs: 200_000 },
|
|
119
|
+
)
|
|
120
|
+
const meta = result?.childId ? `\n[delegated on ${device} · ${String(result.durationMs ?? 0)}ms · child ${String(result.childId)}]` : ''
|
|
121
|
+
return { text: `${String(result?.text ?? '')}${meta}`.trim() }
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return fail(error)
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
112
127
|
async fleet_read({ device, path, offset, limit }) {
|
|
113
128
|
try {
|
|
114
129
|
const args = { file_path: path }
|
|
@@ -211,6 +226,19 @@ export function createFleetToolDefinitions({ defineTool, handlers }) {
|
|
|
211
226
|
output,
|
|
212
227
|
execute: (args) => handlers.fleet_call(args),
|
|
213
228
|
}),
|
|
229
|
+
defineTool({
|
|
230
|
+
name: 'fleet_delegate',
|
|
231
|
+
description:
|
|
232
|
+
'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.',
|
|
233
|
+
parameters: {
|
|
234
|
+
device: commonDevice,
|
|
235
|
+
task: { type: 'string', required: true, description: 'Self-contained task instructions for the target agent.' },
|
|
236
|
+
label: { type: 'string', description: 'Short label shown in the target device\'s subagent list.' },
|
|
237
|
+
cwd: { type: 'string', description: 'Optional working directory on the target device.' },
|
|
238
|
+
},
|
|
239
|
+
output,
|
|
240
|
+
execute: (args) => handlers.fleet_delegate(args),
|
|
241
|
+
}),
|
|
214
242
|
defineTool({
|
|
215
243
|
name: 'fleet_read',
|
|
216
244
|
description:
|
package/src/net/fleet-node.js
CHANGED
|
@@ -281,6 +281,14 @@ export class FleetNode extends EventEmitter {
|
|
|
281
281
|
if (current === channel) this.channels.delete(channel.peer.deviceId)
|
|
282
282
|
this.rejectPendingForChannel(channel, Object.assign(new Error('peer connection closed'), { code: 'peer-offline' }))
|
|
283
283
|
this.emit('peer-disconnected', { deviceId: channel.peer?.deviceId, via })
|
|
284
|
+
// Recover automatically: the peer may have restarted or moved; retry the
|
|
285
|
+
// known addresses instead of waiting for the next process start.
|
|
286
|
+
if (!this.closed && this.channels.size === 0 && this.peerHints.length > 0) {
|
|
287
|
+
clearTimeout(this.hintTimer)
|
|
288
|
+
this.hintTimer = null
|
|
289
|
+
this.hintAttempt = 0
|
|
290
|
+
this.scheduleHintConnects()
|
|
291
|
+
}
|
|
284
292
|
})
|
|
285
293
|
}
|
|
286
294
|
|
package/src/plugin/index.js
CHANGED
|
@@ -13,10 +13,11 @@ 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'
|
|
19
|
-
import { accountMatches, createInvite, loadState, saveRoster, writePendingEnrollment } from '../host/store.js'
|
|
20
|
+
import { accountMatches, createInvite, loadState, savePeerHint, saveRoster, writePendingEnrollment } from '../host/store.js'
|
|
20
21
|
|
|
21
22
|
export const name = 'device-fleet'
|
|
22
23
|
|
|
@@ -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({}),
|
|
@@ -144,7 +147,7 @@ export function apply(ctx, config) {
|
|
|
144
147
|
discovery: config.discovery ? true : false,
|
|
145
148
|
relay: config.relay ?? state.enrollment?.relay ?? null,
|
|
146
149
|
roster: state.roster,
|
|
147
|
-
peerHints: state.enrollment?.endpoints ?? [],
|
|
150
|
+
peerHints: [...(state.enrollment?.endpoints ?? []), ...(state.peerHints ?? [])],
|
|
148
151
|
pluginVersion,
|
|
149
152
|
dshVersion,
|
|
150
153
|
capabilities: CAPABILITIES,
|
|
@@ -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) => {
|
|
@@ -243,7 +268,18 @@ export function apply(ctx, config) {
|
|
|
243
268
|
writePendingEnrollment(state, code)
|
|
244
269
|
},
|
|
245
270
|
}
|
|
246
|
-
const handlers = createFleetHandlers({
|
|
271
|
+
const handlers = createFleetHandlers({
|
|
272
|
+
node,
|
|
273
|
+
client,
|
|
274
|
+
pairing,
|
|
275
|
+
onPeerHint: async (hint) => {
|
|
276
|
+
try {
|
|
277
|
+
savePeerHint(state, hint)
|
|
278
|
+
} catch (error) {
|
|
279
|
+
logger.warn?.(`[device-fleet] cannot persist peer hint: ${String(error?.message ?? error)}`)
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
})
|
|
247
283
|
const definitions = createFleetToolDefinitions({ defineTool, handlers })
|
|
248
284
|
const wrapped = withManual(definitions.map((definition) => ({ ...definition, execute: safe(definition.execute) })))
|
|
249
285
|
for (const definition of wrapped) ctx.tools.register(definition)
|