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
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// Room logic layer (ADR-0002/0005/0006/0011/0021), with local participants for
|
|
2
|
+
// simulation inside one DSH. The Host session log carries the authoritative
|
|
3
|
+
// room events; participant agents are real local DSH sessions woken with an
|
|
4
|
+
// addressed message, so addressing/wake/visibility and board handoff can be
|
|
5
|
+
// exercised before the remote transport and the client UI exist.
|
|
6
|
+
import { randomUUID } from 'node:crypto'
|
|
7
|
+
|
|
8
|
+
const MAX_MESSAGE_CHARS = 20_000
|
|
9
|
+
|
|
10
|
+
const DEFAULT_POLICY = Object.freeze({
|
|
11
|
+
invite: 'members',
|
|
12
|
+
join: 'auto',
|
|
13
|
+
maxMembers: 8,
|
|
14
|
+
boardWrite: 'members',
|
|
15
|
+
remove: 'members',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
function hostMemberId(sessionId) {
|
|
19
|
+
return `host:${sessionId}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function localMemberId(sessionId) {
|
|
23
|
+
return `local:${sessionId}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class RoomService {
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} options
|
|
29
|
+
* @param {object} options.sessionController ctx.sessionController
|
|
30
|
+
* @param {(input: object) => object} [options.createUserMessage] dsh-llm helper
|
|
31
|
+
* @param {(now?: number) => number} [options.now]
|
|
32
|
+
*/
|
|
33
|
+
constructor({ sessionController, createUserMessage = null, now = Date.now } = {}) {
|
|
34
|
+
this.sessionController = sessionController
|
|
35
|
+
this.createUserMessage = createUserMessage
|
|
36
|
+
this.now = now
|
|
37
|
+
/** @type {Map<string, any>} */
|
|
38
|
+
this.rooms = new Map()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async upgradeHost({ agent, policy = {} } = {}) {
|
|
42
|
+
if (!agent?.session?.id) throw Object.assign(new Error('room upgrade needs the calling agent session'), { code: 'bad-request' })
|
|
43
|
+
const sessionId = agent.session.id
|
|
44
|
+
const existing = this.rooms.get(sessionId)
|
|
45
|
+
if (existing) return this.summary(existing)
|
|
46
|
+
const memberId = hostMemberId(sessionId)
|
|
47
|
+
const room = {
|
|
48
|
+
roomId: sessionId,
|
|
49
|
+
createdAt: this.now(),
|
|
50
|
+
hostSessionId: sessionId,
|
|
51
|
+
policy: { ...DEFAULT_POLICY, ...policy },
|
|
52
|
+
members: new Map(),
|
|
53
|
+
messages: [],
|
|
54
|
+
tasks: new Map(),
|
|
55
|
+
closed: false,
|
|
56
|
+
}
|
|
57
|
+
room.members.set(memberId, {
|
|
58
|
+
memberId,
|
|
59
|
+
role: 'host',
|
|
60
|
+
kind: 'local',
|
|
61
|
+
sessionId,
|
|
62
|
+
name: agent.session.header?.title ?? 'host',
|
|
63
|
+
agent,
|
|
64
|
+
})
|
|
65
|
+
this.rooms.set(sessionId, room)
|
|
66
|
+
await this.append(room, 'room/created', { roomId: room.roomId, policy: room.policy, hostMemberId: memberId })
|
|
67
|
+
return this.summary(room)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async addLocalParticipant({ roomId, name = 'worker', agentPreset, cwd } = {}) {
|
|
71
|
+
const room = this.requireRoom(roomId)
|
|
72
|
+
if (room.members.size >= room.policy.maxMembers) {
|
|
73
|
+
throw Object.assign(new Error(`room member limit reached (${room.policy.maxMembers})`), { code: 'room-full' })
|
|
74
|
+
}
|
|
75
|
+
if (!this.sessionController?.create) {
|
|
76
|
+
throw Object.assign(new Error('participant sessions are unavailable'), { code: 'unsupported' })
|
|
77
|
+
}
|
|
78
|
+
const created = await this.sessionController.create({
|
|
79
|
+
...(cwd ? { cwd } : {}),
|
|
80
|
+
...(agentPreset ? { agentPreset } : {}),
|
|
81
|
+
})
|
|
82
|
+
const sessionId = created?.sessionId
|
|
83
|
+
if (!sessionId) throw Object.assign(new Error('could not create the participant session'), { code: 'room-participant-failed' })
|
|
84
|
+
const resolved = await this.sessionController.resolveAgent(sessionId)
|
|
85
|
+
if (!resolved?.agent) throw Object.assign(new Error('participant agent could not be resolved'), { code: 'room-participant-failed' })
|
|
86
|
+
const memberId = localMemberId(sessionId)
|
|
87
|
+
const member = { memberId, role: 'participant', kind: 'local', sessionId, name, agent: resolved.agent }
|
|
88
|
+
room.members.set(memberId, member)
|
|
89
|
+
await this.append(room, 'room/member', {
|
|
90
|
+
roomId: room.roomId,
|
|
91
|
+
action: 'added',
|
|
92
|
+
memberId,
|
|
93
|
+
role: 'participant',
|
|
94
|
+
name,
|
|
95
|
+
sessionId,
|
|
96
|
+
})
|
|
97
|
+
return this.memberSummary(member)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async removeMember({ roomId, memberId, reason = 'removed' } = {}) {
|
|
101
|
+
const room = this.requireRoom(roomId)
|
|
102
|
+
const member = room.members.get(memberId)
|
|
103
|
+
if (!member) throw Object.assign(new Error(`unknown member: ${String(memberId)}`), { code: 'unknown-member' })
|
|
104
|
+
if (member.role === 'host') throw Object.assign(new Error('the host cannot be removed; close the room instead'), { code: 'forbidden' })
|
|
105
|
+
room.members.delete(memberId)
|
|
106
|
+
await this.append(room, 'room/member', { roomId: room.roomId, action: 'removed', memberId, reason })
|
|
107
|
+
return { roomId: room.roomId, memberId }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Send one addressed message. The Host appends it to the canonical log and
|
|
112
|
+
* wakes only the addressed local members (never the sender).
|
|
113
|
+
*/
|
|
114
|
+
async send({ roomId, from, to, text } = {}) {
|
|
115
|
+
const room = this.requireRoom(roomId)
|
|
116
|
+
const body = typeof text === 'string' ? text.trim() : ''
|
|
117
|
+
if (body === '') throw Object.assign(new Error('room message needs text'), { code: 'bad-request' })
|
|
118
|
+
if (body.length > MAX_MESSAGE_CHARS) throw Object.assign(new Error('room message is too long'), { code: 'bad-request' })
|
|
119
|
+
const recipients = Array.isArray(to) ? to.filter((id) => typeof id === 'string') : []
|
|
120
|
+
if (recipients.length === 0) throw Object.assign(new Error('room message needs at least one recipient'), { code: 'bad-request' })
|
|
121
|
+
for (const memberId of recipients) {
|
|
122
|
+
if (!room.members.has(memberId)) throw Object.assign(new Error(`unknown member: ${memberId}`), { code: 'unknown-member' })
|
|
123
|
+
}
|
|
124
|
+
const sender = this.resolveSender(room, from)
|
|
125
|
+
const message = {
|
|
126
|
+
id: randomUUID(),
|
|
127
|
+
roomId: room.roomId,
|
|
128
|
+
from: sender,
|
|
129
|
+
to: recipients,
|
|
130
|
+
text: body,
|
|
131
|
+
createdAt: this.now(),
|
|
132
|
+
}
|
|
133
|
+
room.messages.push(message)
|
|
134
|
+
await this.append(room, 'room/message', {
|
|
135
|
+
roomId: room.roomId,
|
|
136
|
+
messageId: message.id,
|
|
137
|
+
from: sender,
|
|
138
|
+
to: recipients,
|
|
139
|
+
text: body,
|
|
140
|
+
})
|
|
141
|
+
const woken = []
|
|
142
|
+
for (const memberId of recipients) {
|
|
143
|
+
if (sender.memberId === memberId) continue
|
|
144
|
+
const member = room.members.get(memberId)
|
|
145
|
+
if (await this.wake(room, member, message)) woken.push(memberId)
|
|
146
|
+
}
|
|
147
|
+
return { messageId: message.id, to: recipients, woken }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
resolveSender(room, from) {
|
|
151
|
+
if (from && typeof from === 'object') {
|
|
152
|
+
if (typeof from.memberId === 'string') {
|
|
153
|
+
const member = room.members.get(from.memberId)
|
|
154
|
+
if (!member) throw Object.assign(new Error(`unknown sender member: ${from.memberId}`), { code: 'unknown-member' })
|
|
155
|
+
return { kind: 'agent', memberId: member.memberId, name: member.name ?? 'agent' }
|
|
156
|
+
}
|
|
157
|
+
if (typeof from.sessionId === 'string') {
|
|
158
|
+
for (const member of room.members.values()) {
|
|
159
|
+
if (member.sessionId === from.sessionId) {
|
|
160
|
+
return { kind: 'agent', memberId: member.memberId, name: member.name ?? 'agent' }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
throw Object.assign(new Error(`sender session is not a room member: ${from.sessionId}`), { code: 'unknown-member' })
|
|
164
|
+
}
|
|
165
|
+
if (from.kind === 'user') return { kind: 'user', name: 'user' }
|
|
166
|
+
}
|
|
167
|
+
return { kind: 'user', name: 'user' }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Wake one local participant by injecting the addressed message. */
|
|
171
|
+
async wake(room, member, message) {
|
|
172
|
+
if (!member?.agent || typeof member.agent.followup !== 'function' || typeof this.createUserMessage !== 'function') return false
|
|
173
|
+
const senderLabel = message.from.kind === 'user' ? 'user' : (message.from.name ?? 'agent')
|
|
174
|
+
const text = [
|
|
175
|
+
`[Fleet Room ${room.roomId}] ${senderLabel} → you:`,
|
|
176
|
+
message.text,
|
|
177
|
+
'',
|
|
178
|
+
`Reply with room_send (roomId="${room.roomId}", to=["${message.from.memberId ?? 'host'}"]) when you have an answer.`,
|
|
179
|
+
].join('\n')
|
|
180
|
+
const userMessage = this.createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } })
|
|
181
|
+
try {
|
|
182
|
+
member.agent.followup(userMessage)
|
|
183
|
+
return true
|
|
184
|
+
} catch {
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async append(room, type, data) {
|
|
190
|
+
const host = room.members.get(hostMemberId(room.hostSessionId))
|
|
191
|
+
const session = host?.agent?.session
|
|
192
|
+
if (session?.append) await session.append(type, data)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --------------------------------------------------------------- room tasks
|
|
196
|
+
|
|
197
|
+
async addTask({ roomId, title, owner, dependsOn = [], description = '' } = {}) {
|
|
198
|
+
const room = this.requireRoom(roomId)
|
|
199
|
+
if (typeof title !== 'string' || title.trim() === '') throw Object.assign(new Error('task needs a title'), { code: 'bad-request' })
|
|
200
|
+
const ownerMember = room.members.get(owner)
|
|
201
|
+
if (!ownerMember) throw Object.assign(new Error(`unknown task owner: ${String(owner)}`), { code: 'unknown-member' })
|
|
202
|
+
const id = randomUUID()
|
|
203
|
+
const task = {
|
|
204
|
+
id,
|
|
205
|
+
roomId: room.roomId,
|
|
206
|
+
title: title.trim(),
|
|
207
|
+
description,
|
|
208
|
+
owner,
|
|
209
|
+
status: 'pending',
|
|
210
|
+
dependsOn: Array.isArray(dependsOn) ? dependsOn : [],
|
|
211
|
+
readyNotified: false,
|
|
212
|
+
createdAt: this.now(),
|
|
213
|
+
updatedAt: this.now(),
|
|
214
|
+
}
|
|
215
|
+
room.tasks.set(id, task)
|
|
216
|
+
await this.append(room, 'room/task', { roomId: room.roomId, action: 'added', task })
|
|
217
|
+
return task
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async updateTask({ roomId, id, status, note } = {}) {
|
|
221
|
+
const room = this.requireRoom(roomId)
|
|
222
|
+
const task = room.tasks.get(id)
|
|
223
|
+
if (!task) throw Object.assign(new Error(`unknown task: ${String(id)}`), { code: 'unknown-task' })
|
|
224
|
+
const allowed = ['pending', 'running', 'done', 'blocked']
|
|
225
|
+
if (status !== undefined && !allowed.includes(status)) {
|
|
226
|
+
throw Object.assign(new Error(`invalid task status: ${String(status)}`), { code: 'bad-request' })
|
|
227
|
+
}
|
|
228
|
+
if (status !== undefined) task.status = status
|
|
229
|
+
if (typeof note === 'string' && note !== '') task.note = note
|
|
230
|
+
task.updatedAt = this.now()
|
|
231
|
+
await this.append(room, 'room/task', { roomId: room.roomId, action: 'updated', task })
|
|
232
|
+
let handedOff = []
|
|
233
|
+
if (status === 'done') handedOff = await this.handOffReady(room)
|
|
234
|
+
return { task, handedOff }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Wake owners of pending tasks whose dependencies all completed. */
|
|
238
|
+
async handOffReady(room) {
|
|
239
|
+
const ready = []
|
|
240
|
+
for (const task of room.tasks.values()) {
|
|
241
|
+
if (task.status !== 'pending' || task.readyNotified) continue
|
|
242
|
+
if (task.dependsOn.length === 0) continue
|
|
243
|
+
const depsDone = task.dependsOn.every((dep) => room.tasks.get(dep)?.status === 'done')
|
|
244
|
+
if (!depsDone) continue
|
|
245
|
+
task.readyNotified = true
|
|
246
|
+
task.updatedAt = this.now()
|
|
247
|
+
const owner = room.members.get(task.owner)
|
|
248
|
+
const message = {
|
|
249
|
+
id: randomUUID(),
|
|
250
|
+
roomId: room.roomId,
|
|
251
|
+
from: { kind: 'user', name: 'room' },
|
|
252
|
+
to: [task.owner],
|
|
253
|
+
text: `任务 "${task.title}" 的前置任务已完成,现在可以开始。`,
|
|
254
|
+
createdAt: this.now(),
|
|
255
|
+
}
|
|
256
|
+
room.messages.push(message)
|
|
257
|
+
await this.append(room, 'room/message', { roomId: room.roomId, messageId: message.id, from: message.from, to: message.to, text: message.text })
|
|
258
|
+
const woken = owner ? await this.wake(room, owner, message) : false
|
|
259
|
+
ready.push({ taskId: task.id, owner: task.owner, woken })
|
|
260
|
+
}
|
|
261
|
+
return ready
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
listTasks(roomId) {
|
|
265
|
+
const room = this.requireRoom(roomId)
|
|
266
|
+
return [...room.tasks.values()].sort((a, b) => a.createdAt - b.createdAt)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ------------------------------------------------------------------- reads
|
|
270
|
+
|
|
271
|
+
messages({ roomId, limit = 50 } = {}) {
|
|
272
|
+
const room = this.requireRoom(roomId)
|
|
273
|
+
const items = room.messages.slice(-Math.max(1, Math.min(limit, 200)))
|
|
274
|
+
return items.map((message) => ({
|
|
275
|
+
...message,
|
|
276
|
+
fromLabel: message.from.kind === 'user' ? 'user' : `${message.from.name ?? 'agent'} (${message.from.memberId ?? '?'})`,
|
|
277
|
+
}))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
summary(room) {
|
|
281
|
+
return {
|
|
282
|
+
roomId: room.roomId,
|
|
283
|
+
createdAt: room.createdAt,
|
|
284
|
+
closed: room.closed,
|
|
285
|
+
policy: { ...room.policy },
|
|
286
|
+
members: [...room.members.values()].map((member) => this.memberSummary(member)),
|
|
287
|
+
messageCount: room.messages.length,
|
|
288
|
+
tasks: this.listTasks(room.roomId).length,
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
memberSummary(member) {
|
|
293
|
+
return {
|
|
294
|
+
memberId: member.memberId,
|
|
295
|
+
role: member.role,
|
|
296
|
+
kind: member.kind,
|
|
297
|
+
sessionId: member.sessionId,
|
|
298
|
+
name: member.name,
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
status(roomId) {
|
|
303
|
+
return this.summary(this.requireRoom(roomId))
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** All open rooms (for cleanup and diagnostics). */
|
|
307
|
+
openRooms() {
|
|
308
|
+
return [...this.rooms.keys()]
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async close({ roomId, reason = 'closed by host' } = {}) {
|
|
312
|
+
const room = this.requireRoom(roomId)
|
|
313
|
+
room.closed = true
|
|
314
|
+
await this.append(room, 'room/closed', { roomId: room.roomId, reason, closedAt: this.now() })
|
|
315
|
+
this.rooms.delete(room.roomId)
|
|
316
|
+
return { roomId: room.roomId, closed: true, reason }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
requireRoom(roomId) {
|
|
320
|
+
const room = this.rooms.get(roomId)
|
|
321
|
+
if (!room) throw Object.assign(new Error(`unknown or closed room: ${String(roomId)}`), { code: 'unknown-room' })
|
|
322
|
+
return room
|
|
323
|
+
}
|
|
324
|
+
}
|
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
|
|
|
@@ -64,6 +65,7 @@ export function loadState({ dshHome, dataDir, name = 'device' } = {}) {
|
|
|
64
65
|
joinedAt: Date.now(),
|
|
65
66
|
accountFingerprint: decoded.accountFingerprint ?? null,
|
|
66
67
|
relay: decoded.relay ?? null,
|
|
68
|
+
endpoints: decoded.endpoints ?? [],
|
|
67
69
|
}
|
|
68
70
|
writeJsonAtomic(path.join(dir, 'fleet.json'), fleet)
|
|
69
71
|
writeJsonAtomic(path.join(dir, 'enrollment.json'), enrollment)
|
|
@@ -74,7 +76,32 @@ export function loadState({ dshHome, dataDir, name = 'device' } = {}) {
|
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
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)
|
|
78
105
|
}
|
|
79
106
|
|
|
80
107
|
/** Queue an invite for the next plugin load (the running node keeps its fleet). */
|
|
@@ -102,8 +129,8 @@ export function saveConfig(state, config) {
|
|
|
102
129
|
}
|
|
103
130
|
|
|
104
131
|
/** Create an invite for this device's fleet. */
|
|
105
|
-
export function createInvite(state, { accountFingerprint = null, relay = null } = {}) {
|
|
106
|
-
return encodeInvite({ fleet: state.fleet, accountFingerprint, relay })
|
|
132
|
+
export function createInvite(state, { accountFingerprint = null, relay = null, endpoints = [] } = {}) {
|
|
133
|
+
return encodeInvite({ fleet: state.fleet, accountFingerprint, relay, endpoints })
|
|
107
134
|
}
|
|
108
135
|
|
|
109
136
|
/** Join a fleet from an invite code (overwrites fleet/enrollment, keeps identity). */
|
|
@@ -114,6 +141,7 @@ export function enrollFromInvite(state, code) {
|
|
|
114
141
|
joinedAt: Date.now(),
|
|
115
142
|
accountFingerprint: decoded.accountFingerprint ?? null,
|
|
116
143
|
relay: decoded.relay ?? null,
|
|
144
|
+
endpoints: decoded.endpoints ?? [],
|
|
117
145
|
}
|
|
118
146
|
saveFleet(state)
|
|
119
147
|
writeJsonAtomic(path.join(state.dataDir, 'enrollment.json'), state.enrollment)
|
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,
|