@ours.network/fleet 1.0.5 → 1.1.0-nightly.10
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 +122 -133
- package/dist/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/capabilities.d.ts +0 -1
- package/dist/application/capabilities.js +3 -13
- package/dist/application/fleet-query-service.d.ts +0 -3
- package/dist/application/fleet-query-service.js +14 -25
- package/dist/application/role-creation-service.d.ts +5 -24
- package/dist/application/role-creation-service.js +27 -67
- package/dist/application/role-removal-service.js +6 -3
- package/dist/application/role-repository.d.ts +1 -2
- package/dist/application/role-repository.js +7 -7
- package/dist/application/session-control.d.ts +0 -12
- package/dist/application/session-control.js +0 -42
- package/dist/application/session-mutations.d.ts +5 -5
- package/dist/application/task-room-service.d.ts +1 -1
- package/dist/application/task-room-service.js +154 -16
- package/dist/application/types.d.ts +1 -8
- package/dist/briefing.js +12 -11
- package/dist/build-info.json +5 -5
- package/dist/cli.js +143 -63
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +66 -6
- package/dist/config.js +398 -43
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +2 -2
- package/dist/daemon-recovery.d.ts +93 -0
- package/dist/daemon-recovery.js +328 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +103 -127
- package/dist/doctor.js +18 -11
- package/dist/fleet-command-audit.d.ts +160 -0
- package/dist/fleet-command-audit.js +433 -0
- package/dist/fleet-proxy.d.ts +3 -1
- package/dist/fleet-proxy.js +30 -8
- package/dist/generated-agent-source.d.ts +9 -0
- package/dist/generated-agent-source.js +53 -0
- package/dist/harness/acp-session-transport.d.ts +8 -0
- package/dist/harness/acp-session-transport.js +3 -0
- package/dist/harness/agent-session.d.ts +51 -0
- package/dist/harness/agent-session.js +1 -0
- package/dist/harness/claude-code-session.d.ts +24 -0
- package/dist/harness/claude-code-session.js +36 -0
- package/dist/harness/claude-code.d.ts +2 -1
- package/dist/harness/claude-code.js +41 -81
- package/dist/harness/codex-session.d.ts +24 -0
- package/dist/harness/codex-session.js +36 -0
- package/dist/harness/codex.d.ts +2 -1
- package/dist/harness/codex.js +55 -45
- package/dist/harness/registry.js +4 -0
- package/dist/harness/types.d.ts +6 -38
- package/dist/index.d.ts +4 -6
- package/dist/index.js +2 -4
- package/dist/init-guidance.d.ts +1 -0
- package/dist/init-guidance.js +6 -0
- package/dist/isolation/resources.d.ts +3 -7
- package/dist/isolation/resources.js +3 -7
- package/dist/model-env.d.ts +4 -6
- package/dist/model-env.js +5 -8
- package/dist/monitor.d.ts +1 -24
- package/dist/monitor.js +5 -140
- package/dist/ops.js +4 -2
- package/dist/owner-channel/channel.d.ts +58 -2
- package/dist/owner-channel/channel.js +269 -29
- package/dist/owner-channel/commands.d.ts +2 -0
- package/dist/owner-channel/commands.js +31 -13
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/preset-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -0
- package/dist/reports/artifact.d.ts +10 -0
- package/dist/reports/artifact.js +32 -0
- package/dist/reports/index.d.ts +5 -0
- package/dist/reports/index.js +5 -0
- package/dist/reports/render.d.ts +6 -0
- package/dist/reports/render.js +240 -0
- package/dist/reports/service.d.ts +42 -0
- package/dist/reports/service.js +57 -0
- package/dist/reports/tasks.d.ts +52 -0
- package/dist/reports/tasks.js +177 -0
- package/dist/reports/types.d.ts +315 -0
- package/dist/reports/types.js +6 -0
- package/dist/resolved-plan.d.ts +3 -2
- package/dist/resolved-plan.js +21 -5
- package/dist/rooms-tasks/cli.js +225 -40
- package/dist/rooms-tasks/config.js +24 -13
- package/dist/rooms-tasks/external-worker.d.ts +1 -0
- package/dist/rooms-tasks/external-worker.js +14 -9
- package/dist/rooms-tasks/provision.js +98 -36
- package/dist/rooms-tasks/templates.d.ts +0 -1
- package/dist/rooms-tasks/templates.js +9 -76
- package/dist/rooms-tasks/types.d.ts +10 -16
- package/dist/rooms-tasks/types.js +1 -4
- package/dist/runner.d.ts +13 -16
- package/dist/runner.js +110 -122
- package/dist/sensitive-config.d.ts +2 -0
- package/dist/sensitive-config.js +4 -0
- package/dist/session/acp.d.ts +7 -2
- package/dist/session/acp.js +29 -6
- package/dist/session/activity.d.ts +2 -3
- package/dist/session/activity.js +2 -3
- package/dist/session/arbiter.d.ts +4 -4
- package/dist/session/control.d.ts +17 -4
- package/dist/session/control.js +35 -0
- package/dist/session/types.d.ts +11 -5
- package/dist/spawn.d.ts +8 -33
- package/dist/spawn.js +53 -213
- package/dist/supervisor/none.d.ts +1 -1
- package/dist/supervisor/none.js +2 -2
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/tmux.d.ts +1 -14
- package/dist/tmux.js +1 -51
- package/dist/watchdog/config.d.ts +10 -7
- package/dist/watchdog/config.js +30 -18
- package/dist/watchdog/run.js +12 -36
- package/dist/web/auth.d.ts +1 -1
- package/dist/web/fleet-config-service.d.ts +11 -8
- package/dist/web/fleet-config-service.js +384 -183
- package/dist/web/runtime.js +4 -15
- package/dist/web/server.d.ts +1 -3
- package/dist/web/server.js +3 -14
- package/dist/web/topology-promote.js +26 -19
- package/dist/web/topology.js +2 -2
- package/dist/web/yaml-document-edit.js +2 -0
- package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
- package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
- package/dist/web-app/index.html +2 -2
- package/examples/fleet/agents/Alice.yaml +11 -0
- package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
- package/examples/fleet/brains/claude-default.yaml +5 -0
- package/examples/fleet/roles/coordinator.yaml +5 -0
- package/examples/fleet/roles/developer.yaml +3 -0
- package/examples/fleet.yaml +74 -0
- package/package.json +5 -8
- package/presets/fleet/agents/Agent.yaml +3 -0
- package/presets/fleet/agents/Architect.yaml +3 -0
- package/presets/fleet/agents/Critic.yaml +3 -0
- package/presets/fleet/agents/Developer.yaml +3 -0
- package/presets/fleet/agents/Secretary.yaml +3 -0
- package/presets/fleet/agents/Tester.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -0
- package/presets/fleet/roles/Agent.yaml +6 -0
- package/presets/fleet/roles/Architect.yaml +6 -0
- package/presets/fleet/roles/Critic.yaml +6 -0
- package/presets/fleet/roles/Developer.yaml +6 -0
- package/presets/fleet/roles/Secretary.yaml +6 -0
- package/presets/fleet/roles/Tester.yaml +6 -0
- package/presets/fleet/room_templates/pair.yaml +11 -0
- package/presets/fleet/room_templates/single.yaml +8 -0
- package/presets/fleet/room_templates/team.yaml +12 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +5 -0
- package/dist/session/tmux.d.ts +0 -28
- package/dist/session/tmux.js +0 -80
- package/dist/web/terminal/bridge.d.ts +0 -27
- package/dist/web/terminal/bridge.js +0 -317
- package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
- package/dist/web-app/assets/index-BCBK78hw.js +0 -10
- package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { loadConfig, ConfigError } from '../config.js';
|
|
3
4
|
import { provisionMembers, getBinPath } from './provision.js';
|
|
4
5
|
import { launchFleetWorker } from './external-worker.js';
|
|
@@ -9,6 +10,7 @@ import { createCoworkAdapter, CoworkProtocolError } from './cowork-adapter.js';
|
|
|
9
10
|
import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from './markdown.js';
|
|
10
11
|
import { TaskRoomApplicationError, TaskRoomApplicationService } from '../application/task-room-service.js';
|
|
11
12
|
import { TaskListError } from './task-lists.js';
|
|
13
|
+
import { recordFleetAuditFailure, recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
|
|
12
14
|
class TaskRoomPublicError extends Error {
|
|
13
15
|
code;
|
|
14
16
|
fields;
|
|
@@ -41,7 +43,7 @@ function taskRoomPublicFailure(error) {
|
|
|
41
43
|
case 'template_not_found': return {
|
|
42
44
|
legacy: `template not found: ${f.template ?? 'requested-template'}`, kind: 'not_found',
|
|
43
45
|
detail: `The requested template ${f.template ?? ''}`.trim() + ' was not found.',
|
|
44
|
-
action: 'Run ours-fleet
|
|
46
|
+
action: 'Run ours-fleet init to seed missing file-backed presets, then run ours-fleet template list.',
|
|
45
47
|
};
|
|
46
48
|
case 'template_mismatch': return {
|
|
47
49
|
legacy: `template ${f.requested ?? 'requested-template'} does not match room ${f.room ?? 'requested-room'}'s provisioned template ${f.provisioned ?? 'recorded-template'}`,
|
|
@@ -89,8 +91,29 @@ function die(e) {
|
|
|
89
91
|
const msg = e instanceof TaskRoomPublicError
|
|
90
92
|
? taskRoomPublicFailure(e).legacy : e instanceof Error ? e.message : String(e);
|
|
91
93
|
process.stderr.write(`error: ${msg}\n`);
|
|
94
|
+
recordFleetAuditFailure(classifyTaskRoomFailure(e));
|
|
92
95
|
process.exit(1);
|
|
93
96
|
}
|
|
97
|
+
function classifyTaskRoomFailure(e) {
|
|
98
|
+
if (e instanceof TaskRoomApplicationError)
|
|
99
|
+
e = taskRoomPublicError(e.code, e.fields);
|
|
100
|
+
if (e instanceof TaskRoomPublicError || e instanceof TaskListError || e instanceof ConfigError)
|
|
101
|
+
return { class: 'validation', effect: 'not_started' };
|
|
102
|
+
if (e instanceof TaskStateError) {
|
|
103
|
+
const known = /^task not found: [A-Za-z0-9_-]{1,128}$/u.test(e.message)
|
|
104
|
+
|| e.message === 'invalid task ID: expected the canonical 17-character lowercase ID'
|
|
105
|
+
|| isKnownTaskStateMessage(e.message);
|
|
106
|
+
return known ? { class: 'validation', effect: 'not_started' }
|
|
107
|
+
: { class: 'runtime', effect: 'unknown' };
|
|
108
|
+
}
|
|
109
|
+
if (e instanceof RoomStateError) {
|
|
110
|
+
const known = /^room not found: [A-Za-z0-9_-]{1,128}$/u.test(e.message)
|
|
111
|
+
|| isKnownRoomStateMessage(e.message);
|
|
112
|
+
return known ? { class: 'validation', effect: 'not_started' }
|
|
113
|
+
: { class: 'runtime', effect: 'unknown' };
|
|
114
|
+
}
|
|
115
|
+
return { class: 'runtime', effect: 'unknown' };
|
|
116
|
+
}
|
|
94
117
|
const TASK_STATE_WORD = '(?:backlog|provisioning|active|review|done|cancelled|failed)';
|
|
95
118
|
const SAFE_ID_WORD = '[A-Za-z0-9_-]{1,128}';
|
|
96
119
|
function isKnownTaskStateMessage(message) {
|
|
@@ -127,6 +150,7 @@ function dieTaskRoom(e) {
|
|
|
127
150
|
action: failure.action,
|
|
128
151
|
});
|
|
129
152
|
process.stderr.write(`${output}\n`);
|
|
153
|
+
recordFleetAuditFailure({ class: 'validation', effect: 'not_started' });
|
|
130
154
|
process.exit(1);
|
|
131
155
|
}
|
|
132
156
|
if (e instanceof TaskListError) {
|
|
@@ -142,6 +166,7 @@ function dieTaskRoom(e) {
|
|
|
142
166
|
: 'Use a valid NFC-normalized name of at most 64 Unicode code points.',
|
|
143
167
|
});
|
|
144
168
|
process.stderr.write(`${output}\n`);
|
|
169
|
+
recordFleetAuditFailure({ class: 'validation', effect: 'not_started' });
|
|
145
170
|
process.exit(1);
|
|
146
171
|
}
|
|
147
172
|
const taskMissing = e instanceof TaskStateError
|
|
@@ -167,6 +192,7 @@ function dieTaskRoom(e) {
|
|
|
167
192
|
: 'Retry once; if it repeats, run ours-fleet doctor and inspect the role logs.',
|
|
168
193
|
});
|
|
169
194
|
process.stderr.write(`${output}\n`);
|
|
195
|
+
recordFleetAuditFailure(classifyTaskRoomFailure(e));
|
|
170
196
|
process.exit(1);
|
|
171
197
|
}
|
|
172
198
|
const taskListMarkdown = (tasks, title = 'Tasks') => renderMarkdownList({
|
|
@@ -175,23 +201,107 @@ const taskListMarkdown = (tasks, title = 'Tasks') => renderMarkdownList({
|
|
|
175
201
|
+ ` — List ${markdownCode(t.list_name ?? 'default')}`
|
|
176
202
|
+ (t.blocked ? ` — 🚧 Blocked: ${markdownProse(t.blocked.reason)}` : '')),
|
|
177
203
|
});
|
|
178
|
-
const taskActionMarkdown = (title, task, fields = []) =>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
]
|
|
194
|
-
|
|
204
|
+
const taskActionMarkdown = (title, task, fields = []) => {
|
|
205
|
+
recordFleetAuditResource('task', task.task_id);
|
|
206
|
+
if (task.room_id)
|
|
207
|
+
recordFleetAuditResource('room', task.room_id);
|
|
208
|
+
return renderMarkdownResult({
|
|
209
|
+
icon: '📋', title,
|
|
210
|
+
fields: [
|
|
211
|
+
{ label: 'ID', value: task.task_id, kind: 'code' },
|
|
212
|
+
{ label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
|
|
213
|
+
{ label: 'List', value: task.list_name ?? 'default', kind: 'code' },
|
|
214
|
+
...fields,
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
function safeSelectionSummary(definition, kind) {
|
|
219
|
+
const selected = definition?.[kind];
|
|
220
|
+
if (!selected || typeof selected !== 'object' || Array.isArray(selected))
|
|
221
|
+
return 'unresolved';
|
|
222
|
+
const selection = selected;
|
|
223
|
+
if (selection.kind === 'ref' && typeof selection.id === 'string')
|
|
224
|
+
return `ref:${selection.id} (reference)`;
|
|
225
|
+
if (selection.kind === 'inline' && typeof selection.fingerprint === 'string')
|
|
226
|
+
return `inline:${selection.fingerprint} (inline)`;
|
|
227
|
+
if (typeof selection.ref === 'string')
|
|
228
|
+
return `ref:${selection.ref}`;
|
|
229
|
+
if (!Object.hasOwn(selection, 'inline'))
|
|
230
|
+
return 'unresolved';
|
|
231
|
+
const canonical = (value) => Array.isArray(value)
|
|
232
|
+
? `[${value.map(canonical).join(',')}]`
|
|
233
|
+
: value && typeof value === 'object'
|
|
234
|
+
? `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b))
|
|
235
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${canonical(child)}`).join(',')}}`
|
|
236
|
+
: JSON.stringify(value);
|
|
237
|
+
const fingerprint = createHash('sha256').update(canonical(selection.inline)).digest('hex').slice(0, 16);
|
|
238
|
+
return `inline:sha256:${fingerprint}`;
|
|
239
|
+
}
|
|
240
|
+
function safePermissionsSummary(definition) {
|
|
241
|
+
const permissions = definition?.permissions;
|
|
242
|
+
if (!permissions || typeof permissions !== 'object' || Array.isArray(permissions))
|
|
243
|
+
return undefined;
|
|
244
|
+
const policy = permissions;
|
|
245
|
+
const fields = ['approval', 'filesystem', 'unattended'].flatMap(key => typeof policy[key] === 'string' ? [`${key}=${policy[key]}`] : []);
|
|
246
|
+
return fields.length ? fields.join(',') : undefined;
|
|
247
|
+
}
|
|
248
|
+
function auditTask(operation, task, previousState, newState = task.state, revision) {
|
|
249
|
+
const materialSameState = operation === 'block' || operation === 'unblock' || operation === 'settling';
|
|
250
|
+
if (previousState === newState && !materialSameState)
|
|
251
|
+
return;
|
|
252
|
+
recordFleetAuditResource('task', task.task_id);
|
|
253
|
+
if (task.room_id)
|
|
254
|
+
recordFleetAuditResource('room', task.room_id);
|
|
255
|
+
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
256
|
+
const definitions = new Map(room?.member_seats.map(seat => [seat.role_name, seat.launch?.agent_definition]) ?? []);
|
|
257
|
+
const semanticOperation = operation === 'recover'
|
|
258
|
+
? newState === 'active' ? 'work' : newState === 'done' ? 'done'
|
|
259
|
+
: newState === 'cancelled' ? 'cancel' : undefined
|
|
260
|
+
: operation;
|
|
261
|
+
if (!semanticOperation)
|
|
262
|
+
return;
|
|
263
|
+
recordFleetAuditPresentation({ kind: 'task', operation: semanticOperation, id: task.task_id, title: task.title,
|
|
264
|
+
previousState, newState,
|
|
265
|
+
revision: revision ?? task.blocked?.at ?? task.ended_at ?? task.started_at ?? task.created_at,
|
|
266
|
+
list: task.list_name ?? 'default',
|
|
267
|
+
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
268
|
+
roomId: task.room_id, agents: [...task.member_roles].sort((a, b) => a.name.localeCompare(b.name)).map(member => ({ name: member.name,
|
|
269
|
+
brain: safeSelectionSummary(definitions.get(member.name), 'brain'),
|
|
270
|
+
role: safeSelectionSummary(definitions.get(member.name), 'role') === 'unresolved'
|
|
271
|
+
? member.cowork_role : safeSelectionSummary(definitions.get(member.name), 'role'),
|
|
272
|
+
permissions: safePermissionsSummary(definitions.get(member.name)) })) });
|
|
273
|
+
}
|
|
274
|
+
function auditRoom(operation, room, previousState, newState = room.state) {
|
|
275
|
+
if (previousState === newState)
|
|
276
|
+
return;
|
|
277
|
+
recordFleetAuditResource('room', room.room_id);
|
|
278
|
+
const semanticOperation = operation === 'recover'
|
|
279
|
+
? newState === 'active' ? 'activate' : newState === 'deleted' ? 'delete'
|
|
280
|
+
: newState === 'closing' ? 'close' : undefined
|
|
281
|
+
: operation;
|
|
282
|
+
if (!semanticOperation)
|
|
283
|
+
return;
|
|
284
|
+
recordFleetAuditPresentation({ kind: 'room', operation: semanticOperation, id: room.room_id, previousState, newState,
|
|
285
|
+
revision: room.closed_at ?? room.activated_at ?? room.close?.accepted_at ?? room.created_at,
|
|
286
|
+
name: room.room_name, taskId: room.task_id,
|
|
287
|
+
template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
288
|
+
participants: [...room.member_seats].sort((a, b) => a.role_name.localeCompare(b.role_name)).map(member => ({ name: member.role_name, id: member.identity_cid,
|
|
289
|
+
brain: safeSelectionSummary(member.launch?.agent_definition, 'brain'),
|
|
290
|
+
role: safeSelectionSummary(member.launch?.agent_definition, 'role') === 'unresolved'
|
|
291
|
+
? member.cowork_role : safeSelectionSummary(member.launch?.agent_definition, 'role'),
|
|
292
|
+
permissions: safePermissionsSummary(member.launch?.agent_definition) })) });
|
|
293
|
+
}
|
|
294
|
+
const roomActionMarkdown = (title, room, fields = []) => {
|
|
295
|
+
recordFleetAuditResource('room', room.room_id);
|
|
296
|
+
return renderMarkdownResult({
|
|
297
|
+
icon: '🏠', title,
|
|
298
|
+
fields: [
|
|
299
|
+
{ label: 'ID', value: room.room_id, kind: 'code' },
|
|
300
|
+
{ label: 'Status', value: roomStatus(room.state), kind: 'markdown' },
|
|
301
|
+
...fields,
|
|
302
|
+
],
|
|
303
|
+
});
|
|
304
|
+
};
|
|
195
305
|
const PUBLIC_SETTLE_WAIT_MS = 60_000;
|
|
196
306
|
const PUBLIC_SETTLE_POLL_MS = 100;
|
|
197
307
|
const sleep = (ms) => new Promise(resolve => { setTimeout(resolve, ms); });
|
|
@@ -208,6 +318,10 @@ async function launchTaskSettleWorker(taskId, configPath) {
|
|
|
208
318
|
actor: { kind: 'local_control', surface: 'cli' }, taskId, error: errorText(error),
|
|
209
319
|
recoveryHint: `External settle worker failed to start. Retry task recover ${taskId}.`,
|
|
210
320
|
});
|
|
321
|
+
const task = getTask(taskId);
|
|
322
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
|
|
323
|
+
state: task.state, category: 'settlement_failed',
|
|
324
|
+
eventId: task.terminal_intent?.error_at ?? task.terminal_intent?.accepted_at ?? task.created_at });
|
|
211
325
|
throw error;
|
|
212
326
|
}
|
|
213
327
|
const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
|
|
@@ -220,7 +334,11 @@ async function launchTaskSettleWorker(taskId, configPath) {
|
|
|
220
334
|
}
|
|
221
335
|
await sleep(PUBLIC_SETTLE_POLL_MS);
|
|
222
336
|
}
|
|
223
|
-
|
|
337
|
+
const task = getTask(taskId);
|
|
338
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
|
|
339
|
+
state: task.state, category: 'settlement_pending',
|
|
340
|
+
eventId: task.terminal_intent?.accepted_at ?? task.created_at });
|
|
341
|
+
return { task, timedOut: true };
|
|
224
342
|
}
|
|
225
343
|
async function launchRoomDeleteWorker(roomId, configPath) {
|
|
226
344
|
const previousErrorAt = getRoomRecord(roomId)?.close?.error_at;
|
|
@@ -232,6 +350,11 @@ async function launchRoomDeleteWorker(roomId, configPath) {
|
|
|
232
350
|
actor: { kind: 'local_control', surface: 'cli' }, roomId, error: errorText(error),
|
|
233
351
|
recoveryHint: `External delete worker failed to start. Retry room delete ${roomId} ${roomId}.`,
|
|
234
352
|
});
|
|
353
|
+
const room = getRoomRecord(roomId);
|
|
354
|
+
if (room)
|
|
355
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: roomId,
|
|
356
|
+
state: room.state, category: 'cleanup_failed',
|
|
357
|
+
eventId: room.close?.error_at ?? room.close?.accepted_at ?? room.created_at });
|
|
235
358
|
throw error;
|
|
236
359
|
}
|
|
237
360
|
const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
|
|
@@ -244,7 +367,12 @@ async function launchRoomDeleteWorker(roomId, configPath) {
|
|
|
244
367
|
}
|
|
245
368
|
await sleep(PUBLIC_SETTLE_POLL_MS);
|
|
246
369
|
}
|
|
247
|
-
|
|
370
|
+
const remaining = getRoomRecord(roomId);
|
|
371
|
+
if (remaining)
|
|
372
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: roomId,
|
|
373
|
+
state: remaining.state, category: 'cleanup_pending',
|
|
374
|
+
eventId: remaining.close?.accepted_at ?? remaining.created_at });
|
|
375
|
+
return { deleted: remaining === undefined, timedOut: true };
|
|
248
376
|
}
|
|
249
377
|
function loadCfg(opts) {
|
|
250
378
|
return loadConfig(opts.configuration);
|
|
@@ -346,21 +474,36 @@ async function provisionRoom(cfg, input) {
|
|
|
346
474
|
setSagaError(record.room_id, error instanceof Error ? error.message : String(error), mismatch
|
|
347
475
|
? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
|
|
348
476
|
: 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
|
|
477
|
+
const failed = getRoomRecord(record.room_id);
|
|
478
|
+
if (failed)
|
|
479
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
|
|
480
|
+
id: failed.room_id, state: failed.state, category: 'provision_failed',
|
|
481
|
+
eventId: `${failed.created_at}:${failed.saga.phase}:${failed.saga.step_index}` });
|
|
349
482
|
throw error;
|
|
350
483
|
}
|
|
351
484
|
}
|
|
352
485
|
record = advanceSaga(record.room_id, 'create_members', 3);
|
|
353
486
|
if (input.template && input.template.members.length > 0) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
487
|
+
try {
|
|
488
|
+
record = await provisionMembers({
|
|
489
|
+
cfg,
|
|
490
|
+
cowork,
|
|
491
|
+
roomId: record.room_id,
|
|
492
|
+
taskId: input.taskId,
|
|
493
|
+
template: input.template,
|
|
494
|
+
binPath: getBinPath(),
|
|
495
|
+
brief: input.brief,
|
|
496
|
+
goal: input.goal,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
const failed = getRoomRecord(record.room_id);
|
|
501
|
+
if (failed)
|
|
502
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
|
|
503
|
+
id: failed.room_id, state: failed.state, category: 'provision_failed',
|
|
504
|
+
eventId: `${failed.created_at}:${failed.saga.phase}:${failed.saga.step_index}` });
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
364
507
|
}
|
|
365
508
|
else {
|
|
366
509
|
record = activateRoom(record.room_id);
|
|
@@ -382,12 +525,11 @@ export function registerTemplateCommands(parent, cOpt) {
|
|
|
382
525
|
return;
|
|
383
526
|
}
|
|
384
527
|
if (!templates.length) {
|
|
385
|
-
console.log('No templates configured.');
|
|
528
|
+
console.log('No templates configured. Run ours-fleet init to seed missing file-backed presets.');
|
|
386
529
|
return;
|
|
387
530
|
}
|
|
388
531
|
for (const t of templates) {
|
|
389
|
-
|
|
390
|
-
console.log(`${t.name}@${t.version}${tag} ${t.description}`);
|
|
532
|
+
console.log(`${t.name}@${t.version} ${t.description} [file: ${t.sourceFile ?? 'manifest'}]`);
|
|
391
533
|
}
|
|
392
534
|
}
|
|
393
535
|
catch (e) {
|
|
@@ -405,11 +547,13 @@ export function registerTemplateCommands(parent, cOpt) {
|
|
|
405
547
|
return;
|
|
406
548
|
}
|
|
407
549
|
console.log(`${t.name}@${t.version} ${t.description}`);
|
|
550
|
+
console.log(`Source: ${t.sourceFile ?? 'manifest'}`);
|
|
408
551
|
if (t.contract)
|
|
409
552
|
console.log(`\nContract:\n${t.contract}`);
|
|
410
553
|
console.log(`\nMembers:`);
|
|
411
554
|
for (const m of t.members)
|
|
412
|
-
console.log(` ${m.slot}: ${m.count}× ${m.role} (
|
|
555
|
+
console.log(` ${m.slot}: ${m.count}× ${m.role} (`
|
|
556
|
+
+ `${'ref' in m.agent ? `agent ref: ${m.agent.ref}` : 'inline Agent'})`);
|
|
413
557
|
console.log(`\nContent hash: ${t.content_hash}`);
|
|
414
558
|
}
|
|
415
559
|
catch (e) {
|
|
@@ -487,7 +631,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
487
631
|
.option('--list <name>', 'filter by task list')
|
|
488
632
|
.option('--group-by-list', 'group deterministic results by list (JSON)')
|
|
489
633
|
.option('--json', 'JSON output')
|
|
490
|
-
.action((opts) => {
|
|
634
|
+
.action(async (opts) => {
|
|
491
635
|
try {
|
|
492
636
|
let stateFilter;
|
|
493
637
|
if (opts.state && opts.state !== 'all') {
|
|
@@ -513,9 +657,10 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
513
657
|
taskCmd.command('lists')
|
|
514
658
|
.description('list named task lists')
|
|
515
659
|
.option('--json', 'JSON output')
|
|
516
|
-
.action((opts) => {
|
|
660
|
+
.action(async (opts) => {
|
|
517
661
|
try {
|
|
518
|
-
const
|
|
662
|
+
const service = taskRoomService(opts.configuration);
|
|
663
|
+
const lists = service.listTaskLists();
|
|
519
664
|
if (opts.json) {
|
|
520
665
|
console.log(JSON.stringify({ schema_version: 1, lists }, null, 2));
|
|
521
666
|
return;
|
|
@@ -606,9 +751,10 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
606
751
|
taskCmd.command('show <id>')
|
|
607
752
|
.description('show task details')
|
|
608
753
|
.option('--json', 'JSON output')
|
|
609
|
-
.action((id, opts) => {
|
|
754
|
+
.action(async (id, opts) => {
|
|
610
755
|
try {
|
|
611
|
-
const
|
|
756
|
+
const service = taskRoomService();
|
|
757
|
+
const { task: t, orchestration: room } = service.getTask(id);
|
|
612
758
|
if (opts.json) {
|
|
613
759
|
console.log(JSON.stringify({
|
|
614
760
|
schema_version: 1, task: t, orchestration: room ?? null,
|
|
@@ -676,9 +822,11 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
676
822
|
.option('--json', 'JSON output')
|
|
677
823
|
.action((id, opts) => {
|
|
678
824
|
try {
|
|
825
|
+
const previous = getTask(id).state;
|
|
679
826
|
const t = taskRoomService().blockTask({
|
|
680
827
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id, reason: opts.reason,
|
|
681
828
|
});
|
|
829
|
+
auditTask('block', t, previous);
|
|
682
830
|
if (opts.json) {
|
|
683
831
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
684
832
|
return;
|
|
@@ -696,9 +844,12 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
696
844
|
.option('--json', 'JSON output')
|
|
697
845
|
.action((id, opts) => {
|
|
698
846
|
try {
|
|
847
|
+
const prior = getTask(id);
|
|
848
|
+
const previous = prior.state;
|
|
699
849
|
const t = taskRoomService().unblockTask({
|
|
700
850
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
701
851
|
});
|
|
852
|
+
auditTask('unblock', t, previous, t.state, prior.blocked?.at);
|
|
702
853
|
if (opts.json) {
|
|
703
854
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
704
855
|
return;
|
|
@@ -716,9 +867,11 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
716
867
|
.option('--json', 'JSON output')
|
|
717
868
|
.action((id, opts) => {
|
|
718
869
|
try {
|
|
870
|
+
const previous = getTask(id).state;
|
|
719
871
|
const t = taskRoomService().reviewTask({
|
|
720
872
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
721
873
|
});
|
|
874
|
+
auditTask('review', t, previous);
|
|
722
875
|
if (opts.json) {
|
|
723
876
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
724
877
|
return;
|
|
@@ -738,6 +891,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
738
891
|
.option('--json', 'JSON output')
|
|
739
892
|
.action(async (id, opts) => {
|
|
740
893
|
try {
|
|
894
|
+
const previous = getTask(id).state;
|
|
741
895
|
let summary = opts.summary;
|
|
742
896
|
if (opts.summaryFile)
|
|
743
897
|
summary = readFileSync(opts.summaryFile, 'utf8');
|
|
@@ -745,10 +899,13 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
745
899
|
const plan = await taskRoomService(opts.configuration).completeTask({
|
|
746
900
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
|
|
747
901
|
});
|
|
902
|
+
if (plan.task.terminal_intent?.status === 'pending')
|
|
903
|
+
auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
|
|
748
904
|
const settled = plan.settlementRequired
|
|
749
905
|
? await launchTaskSettleWorker(id, opts.configuration)
|
|
750
906
|
: { task: plan.task, timedOut: false };
|
|
751
907
|
const t = settled.task;
|
|
908
|
+
auditTask('done', t, previous);
|
|
752
909
|
if (opts.json) {
|
|
753
910
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
754
911
|
return;
|
|
@@ -776,13 +933,17 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
776
933
|
try {
|
|
777
934
|
if (id !== confirmId)
|
|
778
935
|
throw taskRoomPublicError('task_confirmation_mismatch');
|
|
936
|
+
const previous = getTask(id).state;
|
|
779
937
|
const plan = await taskRoomService(opts.configuration).cancelTask({
|
|
780
938
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
781
939
|
});
|
|
940
|
+
if (plan.task.terminal_intent?.status === 'pending')
|
|
941
|
+
auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
|
|
782
942
|
const settled = plan.settlementRequired
|
|
783
943
|
? await launchTaskSettleWorker(id, opts.configuration)
|
|
784
944
|
: { task: plan.task, timedOut: false };
|
|
785
945
|
const t = settled.task;
|
|
946
|
+
auditTask('cancel', t, previous);
|
|
786
947
|
if (opts.json) {
|
|
787
948
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
788
949
|
return;
|
|
@@ -810,9 +971,16 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
810
971
|
try {
|
|
811
972
|
if (id !== confirmId)
|
|
812
973
|
throw taskRoomPublicError('task_confirmation_mismatch');
|
|
974
|
+
let prior;
|
|
975
|
+
try {
|
|
976
|
+
prior = getTask(id);
|
|
977
|
+
}
|
|
978
|
+
catch { /* idempotent already-absent delete */ }
|
|
813
979
|
const deleted = taskRoomService().deleteTask({
|
|
814
980
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
815
981
|
});
|
|
982
|
+
if (prior)
|
|
983
|
+
auditTask('delete', prior, prior.state, 'deleted');
|
|
816
984
|
if (opts.json) {
|
|
817
985
|
console.log(JSON.stringify({ schema_version: 1, task_id: id, deleted }, null, 2));
|
|
818
986
|
return;
|
|
@@ -833,6 +1001,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
833
1001
|
.option('--json', 'JSON output')
|
|
834
1002
|
.action(async (id, opts) => {
|
|
835
1003
|
try {
|
|
1004
|
+
const previous = getTask(id).state;
|
|
836
1005
|
const app = taskRoomService(opts.configuration);
|
|
837
1006
|
const actor = { kind: 'local_control', surface: 'cli' };
|
|
838
1007
|
const begin = await app.beginTaskRecovery({ actor, taskId: id });
|
|
@@ -843,6 +1012,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
843
1012
|
})()
|
|
844
1013
|
: begin.result;
|
|
845
1014
|
const t = recovered.task;
|
|
1015
|
+
auditTask('recover', t, previous);
|
|
846
1016
|
const room = recovered.room;
|
|
847
1017
|
const recoveryActions = recovered.issues.map(issue => {
|
|
848
1018
|
if (issue.code === 'terminal_pending')
|
|
@@ -992,6 +1162,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
992
1162
|
.option('--json', 'JSON output')
|
|
993
1163
|
.action(async (id, opts) => {
|
|
994
1164
|
try {
|
|
1165
|
+
const previous = getTask(id).state;
|
|
995
1166
|
let summary = opts.summary;
|
|
996
1167
|
if (opts.summaryFile)
|
|
997
1168
|
summary = readFileSync(opts.summaryFile, 'utf8');
|
|
@@ -999,10 +1170,13 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
999
1170
|
const plan = await taskRoomService(opts.configuration).finishTask({
|
|
1000
1171
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
|
|
1001
1172
|
});
|
|
1173
|
+
if (plan.task.terminal_intent?.status === 'pending')
|
|
1174
|
+
auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
|
|
1002
1175
|
const settled = plan.settlementRequired
|
|
1003
1176
|
? await launchTaskSettleWorker(id, opts.configuration)
|
|
1004
1177
|
: { task: plan.task, timedOut: false };
|
|
1005
1178
|
const t = settled.task;
|
|
1179
|
+
auditTask('finish', t, previous);
|
|
1006
1180
|
if (opts.json) {
|
|
1007
1181
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
1008
1182
|
return;
|
|
@@ -1126,7 +1300,7 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1126
1300
|
.option('--json', 'JSON output')
|
|
1127
1301
|
.action(async (id, opts) => {
|
|
1128
1302
|
try {
|
|
1129
|
-
const { room } = await taskRoomService(opts.configuration).getRoomDetail(id);
|
|
1303
|
+
const { room, orchestration } = await taskRoomService(opts.configuration).getRoomDetail(id);
|
|
1130
1304
|
const url = `http://localhost:4460/room/${id}`;
|
|
1131
1305
|
if (opts.json) {
|
|
1132
1306
|
console.log(JSON.stringify({ schema_version: 1, room_id: id, url, room_name: room.room_name }, null, 2));
|
|
@@ -1186,10 +1360,16 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1186
1360
|
try {
|
|
1187
1361
|
if (id !== confirmId)
|
|
1188
1362
|
throw taskRoomPublicError('room_confirmation_mismatch');
|
|
1363
|
+
const previous = getRoomRecord(id);
|
|
1189
1364
|
await taskRoomService(opts.configuration).requestRoomDeletion({
|
|
1190
1365
|
actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
|
|
1191
1366
|
});
|
|
1367
|
+
const closing = getRoomRecord(id);
|
|
1368
|
+
if (previous && closing && previous.state !== closing.state)
|
|
1369
|
+
auditRoom('close', closing, previous.state, closing.state);
|
|
1192
1370
|
const settled = await launchRoomDeleteWorker(id, opts.configuration);
|
|
1371
|
+
if (closing)
|
|
1372
|
+
auditRoom('delete', closing, closing.state, settled.deleted ? 'deleted' : 'closing');
|
|
1193
1373
|
if (opts.json) {
|
|
1194
1374
|
console.log(JSON.stringify({ schema_version: 1, room_id: id, deleted: settled.deleted }, null, 2));
|
|
1195
1375
|
return;
|
|
@@ -1226,11 +1406,14 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1226
1406
|
.option('--json', 'JSON output')
|
|
1227
1407
|
.action(async (id, opts) => {
|
|
1228
1408
|
try {
|
|
1409
|
+
const previous = getRoomRecord(id);
|
|
1229
1410
|
const recovered = await taskRoomService(opts.configuration).recoverRoom({
|
|
1230
1411
|
actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
|
|
1231
1412
|
});
|
|
1232
1413
|
if (recovered.kind === 'deletion_worker_required') {
|
|
1233
1414
|
const settled = await launchRoomDeleteWorker(id, opts.configuration);
|
|
1415
|
+
if (previous)
|
|
1416
|
+
auditRoom('recover', previous, previous.state, settled.deleted ? 'deleted' : 'closing');
|
|
1234
1417
|
if (opts.json) {
|
|
1235
1418
|
console.log(JSON.stringify({
|
|
1236
1419
|
schema_version: 1, room: null, orchestration: null,
|
|
@@ -1251,6 +1434,8 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1251
1434
|
return;
|
|
1252
1435
|
}
|
|
1253
1436
|
const { room: cowork, orchestration: r, issues: actions } = recovered;
|
|
1437
|
+
if (r)
|
|
1438
|
+
auditRoom('recover', r, previous?.state ?? 'unresolved', r.state);
|
|
1254
1439
|
if (opts.json) {
|
|
1255
1440
|
console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null, recovery_actions: actions }, null, 2));
|
|
1256
1441
|
return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { ROOMS_KEYS as RK, ROOMS_OWNER_KEYS as ROK, ROOMS_COWORK_KEYS as RCK, ROOMS_DEFAULTS_KEYS as RDK, TASKS_KEYS as TK, TEMPLATE_KEYS as TPK, TEMPLATE_MEMBER_KEYS as TMK,
|
|
4
|
-
import {
|
|
3
|
+
import { ROOMS_KEYS as RK, ROOMS_OWNER_KEYS as ROK, ROOMS_COWORK_KEYS as RCK, ROOMS_DEFAULTS_KEYS as RDK, TASKS_KEYS as TK, TEMPLATE_KEYS as TPK, TEMPLATE_MEMBER_KEYS as TMK, } from './types.js';
|
|
4
|
+
import { isSensitiveConfigKey } from '../sensitive-config.js';
|
|
5
5
|
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
6
6
|
const CID_RE = /^[0-9a-fA-F]{64}$/;
|
|
7
7
|
export class RoomsTasksConfigError extends Error {
|
|
@@ -116,9 +116,6 @@ export function validateRoomTemplatesConfig(raw, path) {
|
|
|
116
116
|
if (!isPlainObject(tplRaw))
|
|
117
117
|
throw new RoomsTasksConfigError(path, `room_templates.${name}: must be a mapping`);
|
|
118
118
|
rejectUnknown(tplRaw, [...TPK, 'name'], path, `room_templates.${name}`);
|
|
119
|
-
const isBuiltinOverride = BUILTIN_TEMPLATES.some(b => b.name === name);
|
|
120
|
-
if (isBuiltinOverride && !tplRaw.override_builtin)
|
|
121
|
-
throw new RoomsTasksConfigError(path, `room_templates.${name}: overrides built-in template; set override_builtin: true and bump version`);
|
|
122
119
|
const version = tplRaw.version;
|
|
123
120
|
if (version === undefined || !Number.isInteger(version) || version < 1)
|
|
124
121
|
throw new RoomsTasksConfigError(path, `room_templates.${name}.version: required positive integer`);
|
|
@@ -136,19 +133,33 @@ export function validateRoomTemplatesConfig(raw, path) {
|
|
|
136
133
|
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].role: required string`);
|
|
137
134
|
if (!Number.isInteger(m.count) || m.count < 1)
|
|
138
135
|
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].count: required positive integer`);
|
|
139
|
-
if (!m.
|
|
140
|
-
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
136
|
+
if (!isPlainObject(m.agent))
|
|
137
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: required mapping`);
|
|
138
|
+
const agentKeys = Object.keys(m.agent);
|
|
139
|
+
const isRef = agentKeys.length === 1 && typeof m.agent.ref === 'string';
|
|
140
|
+
const isDefinition = agentKeys.includes('role') && agentKeys.includes('brain');
|
|
141
|
+
if (!isRef && !isDefinition)
|
|
142
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: must be {ref} or canonical {role, brain, ...}`);
|
|
143
|
+
const containsSensitive = (value) => Array.isArray(value)
|
|
144
|
+
? value.some(containsSensitive)
|
|
145
|
+
: Boolean(value && typeof value === 'object' && Object.entries(value)
|
|
146
|
+
.some(([key, child]) => isSensitiveConfigKey(key) || containsSensitive(child)));
|
|
147
|
+
if (isDefinition && containsSensitive(m.agent))
|
|
148
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: inline sensitive configuration cannot be persisted; use references`);
|
|
149
|
+
if (isDefinition) {
|
|
150
|
+
const definition = m.agent;
|
|
151
|
+
const brain = isPlainObject(definition.brain) && isPlainObject(definition.brain.inline)
|
|
152
|
+
? definition.brain.inline : undefined;
|
|
153
|
+
if (definition.env !== undefined || definition.owner_channel !== undefined
|
|
154
|
+
|| definition.auth_proxy !== undefined || brain?.harness_options !== undefined
|
|
155
|
+
|| brain?.session_options !== undefined)
|
|
156
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: secret-capable inline fields cannot be persisted; use references`);
|
|
145
157
|
}
|
|
146
158
|
return {
|
|
147
159
|
slot: m.slot,
|
|
148
160
|
role: m.role,
|
|
149
161
|
count: m.count,
|
|
150
|
-
|
|
151
|
-
overrides: m.overrides,
|
|
162
|
+
agent: structuredClone(m.agent),
|
|
152
163
|
};
|
|
153
164
|
});
|
|
154
165
|
let room;
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
export declare function fleetWorkerEnv(source?: NodeJS.ProcessEnv): Record<string, string>;
|
|
1
2
|
/** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
|
|
2
3
|
export declare function launchFleetWorker(args: string[], operation: string, configPath?: string): Promise<void>;
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
|
|
4
|
-
export function
|
|
5
|
-
const cli = [process.argv[1], ...args, ...(configPath ? ['-c', configPath] : [])];
|
|
3
|
+
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV } from '../fleet-proxy.js';
|
|
4
|
+
export function fleetWorkerEnv(source = process.env) {
|
|
6
5
|
const inheritedKeys = [
|
|
7
6
|
'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_CONFIG',
|
|
8
7
|
'OURS_PORT', 'OURS_STATE_DIR', 'OURS_API_TOKEN', 'OURS_COWORK_CONFIG',
|
|
9
8
|
];
|
|
10
|
-
const
|
|
11
|
-
|
|
9
|
+
const env = Object.fromEntries(inheritedKeys.flatMap(key => source[key] === undefined ? [] : [[key, source[key]]]));
|
|
10
|
+
// Explicit defense: trusted settle/recover/delete/close workers are effects
|
|
11
|
+
// nested inside the outer audited command, never fresh agent attempts.
|
|
12
|
+
delete env[FLEET_PROXY_STATE_DIR_ENV];
|
|
13
|
+
delete env[FLEET_PROXY_CALLER_ENV];
|
|
14
|
+
return env;
|
|
15
|
+
}
|
|
16
|
+
/** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
|
|
17
|
+
export function launchFleetWorker(args, operation, configPath) {
|
|
18
|
+
const cli = [process.argv[1], ...args, ...(configPath ? ['-c', configPath] : [])];
|
|
19
|
+
const env = fleetWorkerEnv();
|
|
20
|
+
const inherited = Object.entries(env).map(([key, value]) => `${key}=${value}`);
|
|
12
21
|
const suffix = randomUUID().slice(0, 8);
|
|
13
22
|
const safeOperation = operation.replace(/[^A-Za-z0-9_.-]/g, '-');
|
|
14
23
|
return new Promise((resolve, reject) => {
|
|
@@ -29,10 +38,6 @@ export function launchFleetWorker(args, operation, configPath) {
|
|
|
29
38
|
], { timeout: 15_000 }, error => error ? reject(error) : resolve());
|
|
30
39
|
return;
|
|
31
40
|
}
|
|
32
|
-
const env = Object.fromEntries(inherited.map(value => {
|
|
33
|
-
const split = value.indexOf('=');
|
|
34
|
-
return [value.slice(0, split), value.slice(split + 1)];
|
|
35
|
-
}));
|
|
36
41
|
const child = spawn(process.execPath, cli, { detached: true, stdio: 'ignore', env });
|
|
37
42
|
child.once('error', reject);
|
|
38
43
|
child.once('spawn', () => { child.unref(); resolve(); });
|