dsh-device-fleet 0.1.0-dev.1 → 0.1.0-dev.11
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 +42 -1
- package/client.js +228 -0
- package/cordis.patch.yml +3 -1
- package/docs/notes/approval-inbox-phase.md +31 -0
- package/docs/notes/approval-panel-ui.md +38 -0
- package/docs/notes/dev-release.md +67 -0
- package/docs/notes/pairing-session-2026-11-09.md +30 -0
- package/docs/notes/remote-tests-2026-11-09.md +33 -0
- package/docs/notes/room-logic-phase.md +31 -0
- package/package.json +54 -45
- package/src/host/approval-inbox.js +135 -0
- package/src/host/approval-web.js +95 -0
- package/src/host/delegation.js +127 -0
- package/src/host/direct-call.js +27 -8
- package/src/host/execution-slot.js +19 -5
- package/src/host/index.js +4 -0
- package/src/host/room-service.js +324 -0
- package/src/host/store.js +31 -3
- package/src/host/tool-policy.js +2 -1
- package/src/host/tools.js +376 -1
- package/src/net/fleet-node.js +96 -1
- package/src/net/identity.js +12 -1
- package/src/plugin/index.js +121 -17
package/src/plugin/index.js
CHANGED
|
@@ -5,17 +5,22 @@
|
|
|
5
5
|
// land now (a prompt section and a first-use manual + orchestration nudge).
|
|
6
6
|
// Room, Room Board, delegation and the UI land in later steps.
|
|
7
7
|
import { readFileSync } from 'node:fs'
|
|
8
|
+
import { networkInterfaces } from 'node:os'
|
|
8
9
|
import { fileURLToPath } from 'node:url'
|
|
9
10
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
10
11
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
11
|
-
import
|
|
12
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
12
13
|
import { FleetNode } from '../net/fleet-node.js'
|
|
13
14
|
import { accountFingerprint } from '../net/identity.js'
|
|
14
15
|
import { DirectCallClient, DirectCallHost } from '../host/direct-call.js'
|
|
16
|
+
import { DelegationHost } from '../host/delegation.js'
|
|
17
|
+
import { ApprovalInbox } from '../host/approval-inbox.js'
|
|
18
|
+
import { RoomService } from '../host/room-service.js'
|
|
19
|
+
import { registerApprovalRoutes } from '../host/approval-web.js'
|
|
15
20
|
import { ExecutionSlot, DEFAULT_SHELL_TOOL } from '../host/execution-slot.js'
|
|
16
21
|
import { ToolPolicy } from '../host/tool-policy.js'
|
|
17
22
|
import { createFleetHandlers, createFleetToolDefinitions } from '../host/tools.js'
|
|
18
|
-
import { accountMatches, createInvite, loadState, saveRoster, writePendingEnrollment } from '../host/store.js'
|
|
23
|
+
import { accountMatches, createInvite, loadState, savePeerHint, saveRoster, writePendingEnrollment } from '../host/store.js'
|
|
19
24
|
|
|
20
25
|
export const name = 'device-fleet'
|
|
21
26
|
|
|
@@ -59,9 +64,11 @@ export const Config = Schema.object({
|
|
|
59
64
|
agentPreset: Schema.string(),
|
|
60
65
|
shellTool: Schema.string().default(DEFAULT_SHELL_TOOL),
|
|
61
66
|
approvalTimeoutMs: Schema.number().default(60000),
|
|
67
|
+
delegateTimeoutMs: Schema.number().default(180000),
|
|
68
|
+
subagentProvider: Schema.string().default('spawn'),
|
|
62
69
|
})
|
|
63
|
-
.default({ shellTool: DEFAULT_SHELL_TOOL, approvalTimeoutMs: 60000 })
|
|
64
|
-
.description('Execution Slot
|
|
70
|
+
.default({ shellTool: DEFAULT_SHELL_TOOL, approvalTimeoutMs: 60000, delegateTimeoutMs: 180000, subagentProvider: 'spawn' })
|
|
71
|
+
.description('Execution Slot and delegation settings for direct calls and one-shot delegation.'),
|
|
65
72
|
policy: Schema.object({
|
|
66
73
|
allowedRoots: Schema.array(Schema.string()).default([]),
|
|
67
74
|
capabilities: Schema.any().default({}),
|
|
@@ -72,6 +79,13 @@ export const Config = Schema.object({
|
|
|
72
79
|
.description('Direct-call Tool Policy owned by this device (ADR-0004/ADR-0019).'),
|
|
73
80
|
promptSection: Schema.boolean().default(true).description('Render the one-sentence Fleet nudge when the Fleet has members.'),
|
|
74
81
|
autoApproveAsk: Schema.boolean().default(false).description('Testing only: auto-approve every local approval request.'),
|
|
82
|
+
approvals: Schema.object({
|
|
83
|
+
ttlMs: Schema.number().default(60000),
|
|
84
|
+
maxPending: Schema.number().default(50),
|
|
85
|
+
simulation: Schema.boolean().default(false),
|
|
86
|
+
})
|
|
87
|
+
.default({ ttlMs: 60000, maxPending: 50, simulation: false })
|
|
88
|
+
.description('Local approval inbox (ADR-0020). `simulation` exposes fleet_simulate_request for local testing.'),
|
|
75
89
|
})
|
|
76
90
|
|
|
77
91
|
function packageVersion() {
|
|
@@ -115,7 +129,16 @@ function withManual(definitions) {
|
|
|
115
129
|
export function apply(ctx, config) {
|
|
116
130
|
const pluginVersion = packageVersion()
|
|
117
131
|
const dshVersion = process.env.DSH_CLIENT_VERSION ?? 'unknown'
|
|
118
|
-
|
|
132
|
+
// Optional services: Cordis throws on direct property access for services the
|
|
133
|
+
// plugin did not inject, so every optional service goes through get().
|
|
134
|
+
const service = (name) => {
|
|
135
|
+
try {
|
|
136
|
+
return ctx.get?.(name)
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const logger = service('logger') ?? console
|
|
119
142
|
|
|
120
143
|
let state
|
|
121
144
|
try {
|
|
@@ -134,19 +157,74 @@ export function apply(ctx, config) {
|
|
|
134
157
|
discovery: config.discovery ? true : false,
|
|
135
158
|
relay: config.relay ?? state.enrollment?.relay ?? null,
|
|
136
159
|
roster: state.roster,
|
|
160
|
+
peerHints: [...(state.enrollment?.endpoints ?? []), ...(state.peerHints ?? [])],
|
|
137
161
|
pluginVersion,
|
|
138
162
|
dshVersion,
|
|
139
163
|
capabilities: CAPABILITIES,
|
|
140
164
|
})
|
|
141
165
|
|
|
142
166
|
const policy = new ToolPolicy(config.policy)
|
|
143
|
-
const sessionController =
|
|
144
|
-
const fs =
|
|
167
|
+
const sessionController = service('sessionController')
|
|
168
|
+
const fs = service('fs')
|
|
169
|
+
const subagents = service('subagents')
|
|
170
|
+
const inbox = new ApprovalInbox({
|
|
171
|
+
ttlMs: config.approvals?.ttlMs ?? 60_000,
|
|
172
|
+
maxPending: config.approvals?.maxPending ?? 50,
|
|
173
|
+
})
|
|
174
|
+
inbox.on('request', (request) => logger.info?.(`[device-fleet] approval requested by ${request.fromDeviceId}: ${request.capability}/${String(request.tool ?? '')}`))
|
|
175
|
+
const approvalsApi = {
|
|
176
|
+
list: () => inbox.list(),
|
|
177
|
+
approve: (id, options) => inbox.approve(id, options),
|
|
178
|
+
deny: (id, reason) => inbox.deny(id, reason),
|
|
179
|
+
simulate: config.approvals?.simulation
|
|
180
|
+
? async ({ fromDeviceId, capability, tool, reason }) => {
|
|
181
|
+
const promise = inbox.request({
|
|
182
|
+
fromDeviceId: fromDeviceId ?? 'simulated-device',
|
|
183
|
+
capability: capability ?? 'run',
|
|
184
|
+
tool: tool ?? null,
|
|
185
|
+
reason: reason ?? 'simulated approval request',
|
|
186
|
+
args: {},
|
|
187
|
+
})
|
|
188
|
+
const summary = inbox.list().at(-1)
|
|
189
|
+
// Keep the promise from becoming an unhandled rejection when the
|
|
190
|
+
// request expires without an answer; the demo only inspects the inbox.
|
|
191
|
+
promise.catch(() => {})
|
|
192
|
+
return `simulated approval request ${summary?.id ?? '?'} created; use fleet_approvals / fleet_approve / fleet_deny`
|
|
193
|
+
}
|
|
194
|
+
: null,
|
|
195
|
+
}
|
|
196
|
+
const requestApproval = config.autoApproveAsk
|
|
197
|
+
? async () => ({ approved: true, scope: 'once' })
|
|
198
|
+
: async (request) => inbox.request(request)
|
|
199
|
+
|
|
200
|
+
const rooms = new RoomService({ sessionController, createUserMessage })
|
|
201
|
+
|
|
202
|
+
// Client approval panel data plane (dev.9). Optional service: host-only
|
|
203
|
+
// deployments simply do not expose the panel routes.
|
|
204
|
+
const webServer = service('webServer')
|
|
205
|
+
if (webServer) {
|
|
206
|
+
ctx.effect(() => registerApprovalRoutes({ webServer, inbox, logger }))
|
|
207
|
+
}
|
|
145
208
|
if (!sessionController) logger.warn?.('[device-fleet] sessionController unavailable: direct calls to stateful tools will fail')
|
|
146
209
|
const slot = sessionController
|
|
147
210
|
? new ExecutionSlot({ sessionController, tools: ctx.tools, config: config.execution })
|
|
148
211
|
: null
|
|
149
212
|
|
|
213
|
+
const delegation =
|
|
214
|
+
slot && subagents
|
|
215
|
+
? new DelegationHost({
|
|
216
|
+
subagents,
|
|
217
|
+
slot,
|
|
218
|
+
policy,
|
|
219
|
+
deviceId: state.identity.deviceId,
|
|
220
|
+
defaultCwd: config.execution?.cwd,
|
|
221
|
+
defaultAgentPreset: config.execution?.agentPreset,
|
|
222
|
+
provider: config.execution?.subagentProvider ?? 'spawn',
|
|
223
|
+
timeoutMs: config.execution?.delegateTimeoutMs ?? 180_000,
|
|
224
|
+
requestApproval,
|
|
225
|
+
})
|
|
226
|
+
: null
|
|
227
|
+
|
|
150
228
|
const host = slot
|
|
151
229
|
? new DirectCallHost({
|
|
152
230
|
slot,
|
|
@@ -159,12 +237,8 @@ export function apply(ctx, config) {
|
|
|
159
237
|
capabilities: CAPABILITIES,
|
|
160
238
|
shellTool: config.execution?.shellTool ?? DEFAULT_SHELL_TOOL,
|
|
161
239
|
approvalTimeoutMs: config.execution?.approvalTimeoutMs ?? 60_000,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
: async (request) => {
|
|
165
|
-
logger.info?.(`[device-fleet] approval required from ${request.fromDeviceId} for ${request.capability}/${String(request.tool)} (no approval UI yet)`)
|
|
166
|
-
return false
|
|
167
|
-
},
|
|
240
|
+
delegation,
|
|
241
|
+
requestApproval,
|
|
168
242
|
})
|
|
169
243
|
: null
|
|
170
244
|
|
|
@@ -186,7 +260,7 @@ export function apply(ctx, config) {
|
|
|
186
260
|
})
|
|
187
261
|
|
|
188
262
|
// Bind the local account fingerprint when the account service is composed.
|
|
189
|
-
const account =
|
|
263
|
+
const account = service('deepseekAccount')
|
|
190
264
|
if (account && typeof account.getDeviceIdentity === 'function') {
|
|
191
265
|
void account
|
|
192
266
|
.getDeviceIdentity()
|
|
@@ -204,6 +278,18 @@ export function apply(ctx, config) {
|
|
|
204
278
|
}
|
|
205
279
|
|
|
206
280
|
const client = new DirectCallClient({ node })
|
|
281
|
+
const localEndpoints = () => {
|
|
282
|
+
const port = node.directAddress?.port
|
|
283
|
+
if (!Number.isInteger(port) || port <= 0) return []
|
|
284
|
+
const endpoints = []
|
|
285
|
+
for (const entries of Object.values(networkInterfaces())) {
|
|
286
|
+
for (const entry of entries ?? []) {
|
|
287
|
+
if (entry.family !== 'IPv4' || entry.internal) continue
|
|
288
|
+
endpoints.push({ deviceId: state.identity.deviceId, host: entry.address, port })
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return endpoints
|
|
292
|
+
}
|
|
207
293
|
const pairing = {
|
|
208
294
|
async invite() {
|
|
209
295
|
const fingerprint =
|
|
@@ -213,19 +299,33 @@ export function apply(ctx, config) {
|
|
|
213
299
|
return createInvite(state, {
|
|
214
300
|
accountFingerprint: fingerprint,
|
|
215
301
|
relay: config.relay ?? state.enrollment?.relay ?? null,
|
|
302
|
+
endpoints: localEndpoints(),
|
|
216
303
|
})
|
|
217
304
|
},
|
|
218
305
|
async join(code) {
|
|
219
306
|
writePendingEnrollment(state, code)
|
|
220
307
|
},
|
|
221
308
|
}
|
|
222
|
-
const handlers = createFleetHandlers({
|
|
309
|
+
const handlers = createFleetHandlers({
|
|
310
|
+
node,
|
|
311
|
+
client,
|
|
312
|
+
pairing,
|
|
313
|
+
onPeerHint: async (hint) => {
|
|
314
|
+
try {
|
|
315
|
+
savePeerHint(state, hint)
|
|
316
|
+
} catch (error) {
|
|
317
|
+
logger.warn?.(`[device-fleet] cannot persist peer hint: ${String(error?.message ?? error)}`)
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
approvals: approvalsApi,
|
|
321
|
+
rooms,
|
|
322
|
+
})
|
|
223
323
|
const definitions = createFleetToolDefinitions({ defineTool, handlers })
|
|
224
324
|
const wrapped = withManual(definitions.map((definition) => ({ ...definition, execute: safe(definition.execute) })))
|
|
225
325
|
for (const definition of wrapped) ctx.tools.register(definition)
|
|
226
326
|
|
|
227
327
|
// Adoption layer 2: one sentence, only when the Fleet has members.
|
|
228
|
-
const systemPrompt =
|
|
328
|
+
const systemPrompt = service('systemPrompt')
|
|
229
329
|
if (systemPrompt && config.promptSection !== false) {
|
|
230
330
|
const order = systemPrompt.getSectionOrder?.('TOOL_SUBAGENT') ?? PROMPT_SECTION_ORDER
|
|
231
331
|
ctx.effect(() =>
|
|
@@ -268,9 +368,13 @@ export function apply(ctx, config) {
|
|
|
268
368
|
})
|
|
269
369
|
|
|
270
370
|
ctx.effect(() => () => {
|
|
371
|
+
inbox.close()
|
|
372
|
+
for (const roomId of rooms.openRooms()) {
|
|
373
|
+
void rooms.close({ roomId, reason: 'plugin unloaded' }).catch(() => {})
|
|
374
|
+
}
|
|
271
375
|
void node.close()
|
|
272
376
|
})
|
|
273
377
|
|
|
274
|
-
|
|
378
|
+
logger.info?.(`[device-fleet] v${pluginVersion} device ${state.identity.deviceId} (${config.deviceName})`)
|
|
275
379
|
void ready
|
|
276
380
|
}
|