dsh-device-fleet 0.1.0-dev.3 → 0.1.0-dev.31
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 +7 -3
- package/assets/avatars/host.png +0 -0
- package/assets/avatars/user.png +0 -0
- package/assets/avatars/worker.png +0 -0
- package/client.js +1682 -0
- package/cordis.patch.yml +3 -3
- package/docs/notes/approval-inbox-phase.md +31 -0
- package/docs/notes/approval-panel-ui.md +38 -0
- package/docs/notes/dev-release.md +134 -5
- package/docs/notes/fleet-console-ui.md +44 -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/docs/notes/ui-surface-audit.md +93 -0
- package/package.json +11 -1
- package/src/host/approval-inbox.js +163 -0
- package/src/host/approval-web.js +95 -0
- package/src/host/avatar-web.js +179 -0
- package/src/host/config-web.js +84 -0
- package/src/host/delegation.js +127 -0
- package/src/host/direct-call.js +27 -8
- package/src/host/execution-slot.js +27 -5
- package/src/host/index.js +8 -0
- package/src/host/room-service.js +435 -0
- package/src/host/runtime-config.js +192 -0
- package/src/host/store.js +27 -1
- package/src/host/tool-policy.js +13 -10
- package/src/host/tools.js +341 -1
- package/src/host/ui-web.js +139 -0
- package/src/net/fleet-node.js +30 -0
- package/src/plugin/index.js +421 -16
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Read/action HTTP surface for the Fleet console: device overview, room list and
|
|
2
|
+
// detail, room user actions, pairing actions, and approval history.
|
|
3
|
+
const MAX_BODY_BYTES = 128 * 1024
|
|
4
|
+
|
|
5
|
+
function json(res, status, value) {
|
|
6
|
+
res.statusCode = status
|
|
7
|
+
res.setHeader('content-type', 'application/json; charset=utf-8')
|
|
8
|
+
res.setHeader('cache-control', 'no-store')
|
|
9
|
+
res.end(JSON.stringify(value))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function readJsonBody(req) {
|
|
13
|
+
const chunks = []
|
|
14
|
+
let size = 0
|
|
15
|
+
for await (const chunk of req) {
|
|
16
|
+
size += chunk.length
|
|
17
|
+
if (size > MAX_BODY_BYTES) throw Object.assign(new Error('request body is too large'), { code: 'body-too-large' })
|
|
18
|
+
chunks.push(chunk)
|
|
19
|
+
}
|
|
20
|
+
if (chunks.length === 0) return {}
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
23
|
+
} catch {
|
|
24
|
+
throw Object.assign(new Error('request body must be JSON'), { code: 'bad-json' })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function statusFor(error) {
|
|
29
|
+
if (error?.code === 'unknown-room') return 404
|
|
30
|
+
return ['bad-request', 'bad-json', 'body-too-large', 'readonly', 'unknown-member', 'unknown-task'].includes(error?.code)
|
|
31
|
+
? 400
|
|
32
|
+
: 500
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function actionRoute(webServer, path, method, run) {
|
|
36
|
+
return webServer.register({
|
|
37
|
+
kind: 'exact',
|
|
38
|
+
path,
|
|
39
|
+
handler: async (req, res) => {
|
|
40
|
+
if (req.method !== method) return json(res, 405, { error: 'method not allowed' })
|
|
41
|
+
try {
|
|
42
|
+
const body = method === 'POST' ? await readJsonBody(req) : {}
|
|
43
|
+
json(res, 200, (await run(body, req)) ?? { ok: true })
|
|
44
|
+
} catch (error) {
|
|
45
|
+
json(res, statusFor(error), { error: String(error?.message ?? error), code: error?.code ?? 'error' })
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {object} options
|
|
53
|
+
* @param {object} options.webServer ctx.webServer
|
|
54
|
+
* @param {object} options.inbox ApprovalInbox
|
|
55
|
+
* @param {object} options.rooms RoomService
|
|
56
|
+
* @param {() => object} options.getOverview
|
|
57
|
+
* @param {object} [options.actions] UI actions (room send/task/close, pairing)
|
|
58
|
+
*/
|
|
59
|
+
export function registerUiRoutes({ webServer, inbox, rooms, getOverview, actions = null, logger = null }) {
|
|
60
|
+
if (!webServer?.register || !rooms) return () => {}
|
|
61
|
+
const disposers = []
|
|
62
|
+
disposers.push(
|
|
63
|
+
actionRoute(webServer, '/device-fleet/overview', 'GET', async () => ({
|
|
64
|
+
...getOverview(),
|
|
65
|
+
approvals: inbox?.list?.() ?? [],
|
|
66
|
+
approvalHistory: inbox?.history?.() ?? [],
|
|
67
|
+
})),
|
|
68
|
+
)
|
|
69
|
+
disposers.push(
|
|
70
|
+
actionRoute(webServer, '/device-fleet/rooms', 'GET', async () => ({ rooms: rooms.list() })),
|
|
71
|
+
)
|
|
72
|
+
disposers.push(
|
|
73
|
+
actionRoute(webServer, '/device-fleet/room', 'GET', async (_body, req) => {
|
|
74
|
+
let roomId = ''
|
|
75
|
+
try {
|
|
76
|
+
roomId = new URL(req.url ?? '/', 'http://local').searchParams.get('id') ?? ''
|
|
77
|
+
} catch {
|
|
78
|
+
roomId = ''
|
|
79
|
+
}
|
|
80
|
+
if (roomId === '') throw Object.assign(new Error('room id is required'), { code: 'bad-request' })
|
|
81
|
+
return { room: rooms.detail(roomId, { limit: 100 }) }
|
|
82
|
+
}),
|
|
83
|
+
)
|
|
84
|
+
disposers.push(
|
|
85
|
+
actionRoute(webServer, '/device-fleet/room/send', 'POST', async (body) => {
|
|
86
|
+
if (!actions?.sendRoomMessage) throw Object.assign(new Error('room actions are unavailable'), { code: 'unsupported' })
|
|
87
|
+
return { ok: true, result: await actions.sendRoomMessage(body ?? {}) }
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
disposers.push(
|
|
91
|
+
actionRoute(webServer, '/device-fleet/room/task', 'POST', async (body) => {
|
|
92
|
+
if (!actions?.roomTask) throw Object.assign(new Error('room actions are unavailable'), { code: 'unsupported' })
|
|
93
|
+
return { ok: true, result: await actions.roomTask(body ?? {}) }
|
|
94
|
+
}),
|
|
95
|
+
)
|
|
96
|
+
disposers.push(
|
|
97
|
+
actionRoute(webServer, '/device-fleet/room/close', 'POST', async (body) => {
|
|
98
|
+
if (!actions?.closeRoom) throw Object.assign(new Error('room actions are unavailable'), { code: 'unsupported' })
|
|
99
|
+
return { ok: true, result: await actions.closeRoom(body ?? {}) }
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
disposers.push(
|
|
103
|
+
actionRoute(webServer, '/device-fleet/room/member/remove', 'POST', async (body) => {
|
|
104
|
+
if (!actions?.removeMember) throw Object.assign(new Error('room actions are unavailable'), { code: 'unsupported' })
|
|
105
|
+
return { ok: true, result: await actions.removeMember(body ?? {}) }
|
|
106
|
+
}),
|
|
107
|
+
)
|
|
108
|
+
disposers.push(
|
|
109
|
+
actionRoute(webServer, '/device-fleet/room/upgrade', 'POST', async (body) => {
|
|
110
|
+
if (!actions?.upgradeRoom) throw Object.assign(new Error('room actions are unavailable'), { code: 'unsupported' })
|
|
111
|
+
return { ok: true, room: await actions.upgradeRoom(body ?? {}) }
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
114
|
+
disposers.push(
|
|
115
|
+
actionRoute(webServer, '/device-fleet/room/focus', 'POST', async (body) => {
|
|
116
|
+
if (!actions?.setRoomFocus) throw Object.assign(new Error('room actions are unavailable'), { code: 'unsupported' })
|
|
117
|
+
return { ok: true, result: actions.setRoomFocus(body ?? {}) }
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
disposers.push(
|
|
121
|
+
actionRoute(webServer, '/device-fleet/pair/invite', 'POST', async () => {
|
|
122
|
+
if (!actions?.createInvite) throw Object.assign(new Error('pairing actions are unavailable'), { code: 'unsupported' })
|
|
123
|
+
return { ok: true, code: await actions.createInvite() }
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
126
|
+
disposers.push(
|
|
127
|
+
actionRoute(webServer, '/device-fleet/pair/connect', 'POST', async (body) => {
|
|
128
|
+
if (!actions?.connectAddress) throw Object.assign(new Error('pairing actions are unavailable'), { code: 'unsupported' })
|
|
129
|
+
return { ok: true, result: await actions.connectAddress(body ?? {}) }
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
132
|
+
disposers.push(
|
|
133
|
+
actionRoute(webServer, '/device-fleet/approvals/history', 'GET', async () => ({ history: inbox?.history?.() ?? [] })),
|
|
134
|
+
)
|
|
135
|
+
if (logger) logger.debug?.('[device-fleet] UI action routes registered')
|
|
136
|
+
return () => {
|
|
137
|
+
for (const dispose of disposers) dispose()
|
|
138
|
+
}
|
|
139
|
+
}
|
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
|
|
|
@@ -481,6 +489,28 @@ export class FleetNode extends EventEmitter {
|
|
|
481
489
|
return [...this.channels.keys()]
|
|
482
490
|
}
|
|
483
491
|
|
|
492
|
+
/** Apply live network changes without recreating the node or its identity. */
|
|
493
|
+
async reconfigure({ name, direct, discovery, relay } = {}) {
|
|
494
|
+
if (this.closed) return
|
|
495
|
+
if (typeof name === 'string' && name !== '') this.name = name
|
|
496
|
+
const networkChanged = direct !== undefined || discovery !== undefined || relay !== undefined
|
|
497
|
+
if (!networkChanged) return
|
|
498
|
+
const wasStarted = this.started
|
|
499
|
+
await this.close()
|
|
500
|
+
this.closed = false
|
|
501
|
+
this.started = false
|
|
502
|
+
this.hintAttempt = 0
|
|
503
|
+
this.discovered.clear()
|
|
504
|
+
if (direct !== undefined) this.directConfig = direct
|
|
505
|
+
if (discovery !== undefined) this.discoveryConfig = discovery
|
|
506
|
+
if (relay !== undefined) this.relayConfig = relay
|
|
507
|
+
this.directServer = null
|
|
508
|
+
this.discovery = null
|
|
509
|
+
this.relayClient = null
|
|
510
|
+
this.directAddress = null
|
|
511
|
+
if (wasStarted) await this.start()
|
|
512
|
+
}
|
|
513
|
+
|
|
484
514
|
async close() {
|
|
485
515
|
this.closed = true
|
|
486
516
|
clearTimeout(this.hintTimer)
|