@ours.network/fleet 0.19.0-nightly.2 → 0.19.0-nightly.3
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/dist/build-info.json +4 -4
- package/dist/config.d.ts +6 -0
- package/dist/config.js +12 -1
- package/dist/rooms-tasks/cli.js +193 -68
- package/dist/rooms-tasks/cowork-adapter.d.ts +21 -24
- package/dist/rooms-tasks/cowork-adapter.js +257 -33
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +1 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.19.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "0.19.0-nightly.3",
|
|
3
|
+
"buildId": "c969df19c5f2",
|
|
4
|
+
"commit": "1cfdb94ec5bc493c83c72f62dd70c669c78b9e68",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-08-
|
|
6
|
+
"builtAt": "2026-08-23T10:19:42.625Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"monitor.interrupt.after_tool"
|
|
9
9
|
]
|
package/dist/config.d.ts
CHANGED
|
@@ -170,6 +170,12 @@ export interface FleetConfig {
|
|
|
170
170
|
tasks?: import('./rooms-tasks/types.js').TasksConfig;
|
|
171
171
|
/** SHA-256 fingerprint of the resolved owner invite (never the invite itself). */
|
|
172
172
|
ownerInviteFingerprint?: string;
|
|
173
|
+
/**
|
|
174
|
+
* Resolved owner invite for immediate private-IPC use only. This property is
|
|
175
|
+
* deliberately non-enumerable so config dumps and JSON output cannot expose
|
|
176
|
+
* the bearer credential.
|
|
177
|
+
*/
|
|
178
|
+
ownerInvite?: string;
|
|
173
179
|
}
|
|
174
180
|
export declare class ConfigError extends Error {
|
|
175
181
|
}
|
package/dist/config.js
CHANGED
|
@@ -276,6 +276,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
276
276
|
// top-level (room_templates merge by name, last writer wins).
|
|
277
277
|
let rooms;
|
|
278
278
|
let ownerInviteFingerprint;
|
|
279
|
+
let ownerInvite;
|
|
279
280
|
let roomTemplates;
|
|
280
281
|
let tasks;
|
|
281
282
|
for (const { file, doc } of docs) {
|
|
@@ -284,6 +285,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
284
285
|
throw new ConfigError(`rooms: defined in multiple files; last: ${file}`);
|
|
285
286
|
const validated = validateRoomsConfig(deepSub(doc.rooms, vars), vars, file);
|
|
286
287
|
ownerInviteFingerprint = validated._invite?.fingerprint;
|
|
288
|
+
ownerInvite = validated._invite?.value;
|
|
287
289
|
const { _invite: _, ...clean } = validated;
|
|
288
290
|
rooms = clean;
|
|
289
291
|
}
|
|
@@ -297,11 +299,20 @@ export function loadConfig(configPath, options = {}) {
|
|
|
297
299
|
tasks = validateTasksConfig(deepSub(doc.tasks, vars), file);
|
|
298
300
|
}
|
|
299
301
|
}
|
|
300
|
-
|
|
302
|
+
const result = {
|
|
301
303
|
roles, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
302
304
|
loops: resolvedLoops.loops,
|
|
303
305
|
rooms, roomTemplates, tasks, ownerInviteFingerprint,
|
|
304
306
|
};
|
|
307
|
+
if (ownerInvite !== undefined) {
|
|
308
|
+
Object.defineProperty(result, 'ownerInvite', {
|
|
309
|
+
value: ownerInvite,
|
|
310
|
+
enumerable: false,
|
|
311
|
+
writable: false,
|
|
312
|
+
configurable: false,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return result;
|
|
305
316
|
}
|
|
306
317
|
/**
|
|
307
318
|
* Canonical form of a 64-hex container ID for authorization decisions. Hex
|
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { loadConfig } from '../config.js';
|
|
2
|
+
import { loadConfig, ConfigError } from '../config.js';
|
|
3
3
|
import { resolveTemplate, listTemplates, snapshotTemplate, hashTemplate, } from './templates.js';
|
|
4
|
-
import { createTask, getTask, listTasks, startTask, blockTask, unblockTask, reviewTask, completeTask, cancelTask, } from './task-state.js';
|
|
5
|
-
import { createRoomRecord, getRoomRecord, listRoomRecords, closeRoom as closeRoomRecord, } from './room-state.js';
|
|
4
|
+
import { createTask, getTask, listTasks, startTask, blockTask, unblockTask, reviewTask, completeTask, cancelTask, updateTaskRoom, } from './task-state.js';
|
|
5
|
+
import { createRoomRecord, getRoomRecord, listRoomRecords, closeRoom as closeRoomRecord, advanceSaga, setOwnerSeat, setSagaError, } from './room-state.js';
|
|
6
|
+
import { createCoworkAdapter, CoworkProtocolError, CoworkUnavailableError } from './cowork-adapter.js';
|
|
6
7
|
function die(e) {
|
|
7
8
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8
9
|
process.stderr.write(`error: ${msg}\n`);
|
|
@@ -14,6 +15,64 @@ function loadCfg(opts) {
|
|
|
14
15
|
function allTemplates(cfg) {
|
|
15
16
|
return cfg.roomTemplates ?? {};
|
|
16
17
|
}
|
|
18
|
+
function coworkFor(cfg) {
|
|
19
|
+
if (!cfg.rooms || cfg.rooms.provider !== 'cowork')
|
|
20
|
+
throw new ConfigError('rooms: configure provider: cowork before creating or querying rooms');
|
|
21
|
+
return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
22
|
+
}
|
|
23
|
+
function resolveRoomTemplate(cfg, name) {
|
|
24
|
+
if (!name)
|
|
25
|
+
return undefined;
|
|
26
|
+
const template = resolveTemplate(name, allTemplates(cfg));
|
|
27
|
+
if (!template)
|
|
28
|
+
throw new Error(`template not found: ${name}`);
|
|
29
|
+
return snapshotTemplate(template);
|
|
30
|
+
}
|
|
31
|
+
async function provisionRoom(cfg, input) {
|
|
32
|
+
const rooms = cfg.rooms;
|
|
33
|
+
if (!rooms)
|
|
34
|
+
throw new ConfigError('rooms: configuration is required');
|
|
35
|
+
const attachOwner = rooms.defaults?.attach_owner !== false;
|
|
36
|
+
if (attachOwner && !cfg.ownerInvite)
|
|
37
|
+
throw new ConfigError('rooms.owner: public_invite or public_invite_file is required when attach_owner is enabled');
|
|
38
|
+
const cowork = coworkFor(cfg);
|
|
39
|
+
const goal = input.goal?.trim() || input.name;
|
|
40
|
+
const briefing = input.brief?.trim() || input.template?.contract?.trim() || goal;
|
|
41
|
+
const created = await cowork.createRoom({
|
|
42
|
+
room_name: input.name,
|
|
43
|
+
goal,
|
|
44
|
+
briefing,
|
|
45
|
+
quiet_membership: input.template?.room?.quiet_membership,
|
|
46
|
+
anonymous: input.template?.room?.anonymous,
|
|
47
|
+
});
|
|
48
|
+
let record = createRoomRecord({
|
|
49
|
+
room_id: created.room_id,
|
|
50
|
+
room_name: input.name,
|
|
51
|
+
room_identity_cid: created.identity_cid,
|
|
52
|
+
task_id: input.taskId,
|
|
53
|
+
template_snapshot: input.template,
|
|
54
|
+
});
|
|
55
|
+
input.onCreated?.(record);
|
|
56
|
+
record = advanceSaga(record.room_id, 'create_room', 1);
|
|
57
|
+
if (attachOwner) {
|
|
58
|
+
try {
|
|
59
|
+
record = advanceSaga(record.room_id, 'attach_owner', 2);
|
|
60
|
+
const accepted = await cowork.acceptInvite(record.room_id, cfg.ownerInvite, {
|
|
61
|
+
role: rooms.owner.role,
|
|
62
|
+
expected_cid: rooms.owner.expected_cid,
|
|
63
|
+
});
|
|
64
|
+
record = setOwnerSeat(record.room_id, accepted.seat_cid, cfg.ownerInviteFingerprint ?? '');
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
|
|
68
|
+
setSagaError(record.room_id, error instanceof Error ? error.message : String(error), mismatch
|
|
69
|
+
? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
|
|
70
|
+
: 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return advanceSaga(record.room_id, 'create_members', 3);
|
|
75
|
+
}
|
|
17
76
|
export function registerTemplateCommands(parent, cOpt) {
|
|
18
77
|
const templateCmd = parent.command('template').description('room template operations');
|
|
19
78
|
cOpt(templateCmd.command('list'))
|
|
@@ -114,7 +173,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
114
173
|
.option('--no-room', 'create task without a room')
|
|
115
174
|
.option('--idempotency-key <key>', 'idempotency key')
|
|
116
175
|
.option('--json', 'JSON output')
|
|
117
|
-
.action((opts) => {
|
|
176
|
+
.action(async (opts) => {
|
|
118
177
|
try {
|
|
119
178
|
const cfg = loadCfg(opts);
|
|
120
179
|
let templateRef;
|
|
@@ -138,7 +197,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
138
197
|
brief = readFileSync(opts.briefFile, 'utf8');
|
|
139
198
|
}
|
|
140
199
|
const origin = { type: 'cli' };
|
|
141
|
-
|
|
200
|
+
let record = createTask({
|
|
142
201
|
title: opts.title,
|
|
143
202
|
brief,
|
|
144
203
|
brief_file: opts.briefFile,
|
|
@@ -147,6 +206,28 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
147
206
|
idempotency_key: opts.idempotencyKey,
|
|
148
207
|
start: !opts.backlog,
|
|
149
208
|
});
|
|
209
|
+
if (!opts.backlog && opts.room !== false && templateRef && !record.room_id) {
|
|
210
|
+
const template = resolveRoomTemplate(cfg, templateRef.name);
|
|
211
|
+
try {
|
|
212
|
+
await provisionRoom(cfg, {
|
|
213
|
+
name: opts.title,
|
|
214
|
+
goal: opts.title,
|
|
215
|
+
brief,
|
|
216
|
+
template,
|
|
217
|
+
taskId: record.task_id,
|
|
218
|
+
onCreated: room => {
|
|
219
|
+
record = updateTaskRoom(record.task_id, room.room_id, room.room_identity_cid);
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
record = getTask(record.task_id);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (error instanceof CoworkUnavailableError) {
|
|
226
|
+
blockTask(record.task_id, 'Cowork management socket is unavailable');
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
150
231
|
if (opts.json) {
|
|
151
232
|
console.log(JSON.stringify({ schema_version: 1, task: record }, null, 2));
|
|
152
233
|
return;
|
|
@@ -224,12 +305,29 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
224
305
|
die(e);
|
|
225
306
|
}
|
|
226
307
|
});
|
|
227
|
-
taskCmd.command('start <id>')
|
|
308
|
+
cOpt(taskCmd.command('start <id>'))
|
|
228
309
|
.description('start a backlog task')
|
|
229
310
|
.option('--json', 'JSON output')
|
|
230
|
-
.action((id, opts) => {
|
|
311
|
+
.action(async (id, opts) => {
|
|
231
312
|
try {
|
|
232
|
-
const
|
|
313
|
+
const cfg = loadCfg(opts);
|
|
314
|
+
let t = startTask(id);
|
|
315
|
+
if (t.template && !t.room_id) {
|
|
316
|
+
const template = resolveRoomTemplate(cfg, t.template.name);
|
|
317
|
+
if (!template || template.content_hash !== t.template.content_hash)
|
|
318
|
+
throw new Error(`task template snapshot no longer matches ${t.template.name}@${t.template.version}`);
|
|
319
|
+
await provisionRoom(cfg, {
|
|
320
|
+
name: t.title,
|
|
321
|
+
goal: t.title,
|
|
322
|
+
brief: t.brief,
|
|
323
|
+
template,
|
|
324
|
+
taskId: t.task_id,
|
|
325
|
+
onCreated: room => {
|
|
326
|
+
t = updateTaskRoom(t.task_id, room.room_id, room.room_identity_cid);
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
t = getTask(t.task_id);
|
|
330
|
+
}
|
|
233
331
|
if (opts.json) {
|
|
234
332
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
235
333
|
return;
|
|
@@ -382,13 +480,12 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
382
480
|
.option('--brief <text>', 'room briefing')
|
|
383
481
|
.option('--brief-file <path>', 'room briefing from file')
|
|
384
482
|
.option('--json', 'JSON output')
|
|
385
|
-
.action((opts) => {
|
|
483
|
+
.action(async (opts) => {
|
|
386
484
|
try {
|
|
387
485
|
const cfg = loadCfg(opts);
|
|
388
486
|
let brief = opts.brief;
|
|
389
487
|
if (opts.briefFile)
|
|
390
488
|
brief = readFileSync(opts.briefFile, 'utf8');
|
|
391
|
-
const roomId = `room-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
392
489
|
let templateSnapshot;
|
|
393
490
|
if (opts.template) {
|
|
394
491
|
const t = resolveTemplate(opts.template, allTemplates(cfg));
|
|
@@ -396,10 +493,11 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
396
493
|
die(new Error(`template not found: ${opts.template}`));
|
|
397
494
|
templateSnapshot = snapshotTemplate(t);
|
|
398
495
|
}
|
|
399
|
-
const record =
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
496
|
+
const record = await provisionRoom(cfg, {
|
|
497
|
+
name: opts.name,
|
|
498
|
+
goal: opts.goal,
|
|
499
|
+
brief,
|
|
500
|
+
template: templateSnapshot,
|
|
403
501
|
});
|
|
404
502
|
if (opts.json) {
|
|
405
503
|
console.log(JSON.stringify({ schema_version: 1, room: record }, null, 2));
|
|
@@ -413,17 +511,22 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
413
511
|
die(e);
|
|
414
512
|
}
|
|
415
513
|
});
|
|
416
|
-
roomCmd.command('list')
|
|
514
|
+
cOpt(roomCmd.command('list'))
|
|
417
515
|
.description('list rooms')
|
|
418
516
|
.option('--state <state>', 'filter by state (active|provisioning|closing|closed|all)')
|
|
419
517
|
.option('--json', 'JSON output')
|
|
420
|
-
.action((opts) => {
|
|
518
|
+
.action(async (opts) => {
|
|
421
519
|
try {
|
|
520
|
+
const cfg = loadCfg(opts);
|
|
422
521
|
let stateFilter;
|
|
423
522
|
if (opts.state && opts.state !== 'all') {
|
|
424
523
|
stateFilter = opts.state;
|
|
425
524
|
}
|
|
426
|
-
const
|
|
525
|
+
const local = new Map(listRoomRecords().map(room => [room.room_id, room]));
|
|
526
|
+
const coworkRooms = await coworkFor(cfg).listRooms();
|
|
527
|
+
const rooms = coworkRooms
|
|
528
|
+
.filter(room => !stateFilter || room.state === stateFilter)
|
|
529
|
+
.map(room => ({ ...room, orchestration: local.get(room.room_id) ?? null }));
|
|
427
530
|
if (opts.json) {
|
|
428
531
|
console.log(JSON.stringify({ schema_version: 1, rooms }, null, 2));
|
|
429
532
|
return;
|
|
@@ -433,122 +536,144 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
433
536
|
return;
|
|
434
537
|
}
|
|
435
538
|
for (const r of rooms)
|
|
436
|
-
console.log(`${r.room_id} ${r.state} ${r.room_name}${r.task_id ? ` (task: ${r.task_id})` : ''}`);
|
|
539
|
+
console.log(`${r.room_id} ${r.state} ${r.room_name}${r.orchestration?.task_id ? ` (task: ${r.orchestration.task_id})` : ''}`);
|
|
437
540
|
}
|
|
438
541
|
catch (e) {
|
|
439
542
|
die(e);
|
|
440
543
|
}
|
|
441
544
|
});
|
|
442
|
-
roomCmd.command('show <id>')
|
|
545
|
+
cOpt(roomCmd.command('show <id>'))
|
|
443
546
|
.description('show room details')
|
|
444
547
|
.option('--json', 'JSON output')
|
|
445
|
-
.action((id, opts) => {
|
|
548
|
+
.action(async (id, opts) => {
|
|
446
549
|
try {
|
|
447
|
-
const
|
|
448
|
-
|
|
550
|
+
const cfg = loadCfg(opts);
|
|
551
|
+
const cowork = await coworkFor(cfg).getRoom(id);
|
|
552
|
+
if (!cowork)
|
|
449
553
|
die(new Error(`room not found: ${id}`));
|
|
554
|
+
const r = getRoomRecord(id);
|
|
450
555
|
if (opts.json) {
|
|
451
|
-
console.log(JSON.stringify({ schema_version: 1, room: r }, null, 2));
|
|
556
|
+
console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null }, null, 2));
|
|
452
557
|
return;
|
|
453
558
|
}
|
|
454
|
-
console.log(`Room: ${
|
|
455
|
-
console.log(`Name: ${
|
|
456
|
-
console.log(`State: ${
|
|
457
|
-
|
|
559
|
+
console.log(`Room: ${cowork.room_id}`);
|
|
560
|
+
console.log(`Name: ${cowork.room_name}`);
|
|
561
|
+
console.log(`State: ${cowork.state}`);
|
|
562
|
+
console.log(`Identity CID: ${cowork.identity_cid}`);
|
|
563
|
+
if (r?.task_id)
|
|
458
564
|
console.log(`Task: ${r.task_id}`);
|
|
459
|
-
if (r
|
|
460
|
-
console.log(`
|
|
461
|
-
|
|
462
|
-
if (r.provisioning_detail)
|
|
565
|
+
if (r)
|
|
566
|
+
console.log(`Saga: ${r.saga.phase} (step ${r.saga.step_index})`);
|
|
567
|
+
if (r?.provisioning_detail)
|
|
463
568
|
console.log(`Detail: ${r.provisioning_detail}`);
|
|
464
|
-
if (r
|
|
569
|
+
if (r?.saga.error)
|
|
465
570
|
console.log(`Error: ${r.saga.error}`);
|
|
466
|
-
if (
|
|
571
|
+
if (cowork.seats.length) {
|
|
467
572
|
console.log('Members:');
|
|
468
|
-
for (const s of
|
|
469
|
-
console.log(` ${s.
|
|
573
|
+
for (const s of cowork.seats)
|
|
574
|
+
console.log(` ${s.identity_cid} (${s.role}) ${s.seat_state}`);
|
|
470
575
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
console.log(`Activated: ${r.activated_at}`);
|
|
474
|
-
if (r.closed_at)
|
|
475
|
-
console.log(`Closed: ${r.closed_at}`);
|
|
576
|
+
if (r)
|
|
577
|
+
console.log(`Tracked by Fleet since: ${r.created_at}`);
|
|
476
578
|
}
|
|
477
579
|
catch (e) {
|
|
478
580
|
die(e);
|
|
479
581
|
}
|
|
480
582
|
});
|
|
481
|
-
roomCmd.command('members <id>')
|
|
583
|
+
cOpt(roomCmd.command('members <id>'))
|
|
482
584
|
.description('show room members')
|
|
483
585
|
.option('--json', 'JSON output')
|
|
484
|
-
.action((id, opts) => {
|
|
586
|
+
.action(async (id, opts) => {
|
|
485
587
|
try {
|
|
588
|
+
const cfg = loadCfg(opts);
|
|
486
589
|
const r = getRoomRecord(id);
|
|
487
|
-
|
|
488
|
-
die(new Error(`room not found: ${id}`));
|
|
590
|
+
const members = await coworkFor(cfg).getSeats(id);
|
|
489
591
|
if (opts.json) {
|
|
490
592
|
console.log(JSON.stringify({
|
|
491
593
|
schema_version: 1,
|
|
492
|
-
room_id:
|
|
493
|
-
members
|
|
494
|
-
owner_seat_cid: r
|
|
594
|
+
room_id: id,
|
|
595
|
+
members,
|
|
596
|
+
owner_seat_cid: r?.owner_seat_cid ?? null,
|
|
495
597
|
}, null, 2));
|
|
496
598
|
return;
|
|
497
599
|
}
|
|
498
|
-
console.log(`Room ${r
|
|
499
|
-
if (r
|
|
600
|
+
console.log(`Room ${id}${r ? ` — ${r.room_name}` : ''}`);
|
|
601
|
+
if (r?.owner_seat_cid)
|
|
500
602
|
console.log(`Owner: ${r.owner_seat_cid}`);
|
|
501
|
-
if (!
|
|
603
|
+
if (!members.length) {
|
|
502
604
|
console.log('No members.');
|
|
503
605
|
return;
|
|
504
606
|
}
|
|
505
|
-
for (const s of
|
|
506
|
-
console.log(` ${s.
|
|
607
|
+
for (const s of members)
|
|
608
|
+
console.log(` ${s.identity_cid} (${s.role}) ${s.seat_state}`);
|
|
507
609
|
}
|
|
508
610
|
catch (e) {
|
|
509
611
|
die(e);
|
|
510
612
|
}
|
|
511
613
|
});
|
|
512
|
-
roomCmd.command('close <id> <confirm-id>')
|
|
614
|
+
cOpt(roomCmd.command('close <id> <confirm-id>'))
|
|
513
615
|
.description('close a room (requires ID twice for confirmation)')
|
|
514
616
|
.option('--json', 'JSON output')
|
|
515
|
-
.action((id, confirmId, opts) => {
|
|
617
|
+
.action(async (id, confirmId, opts) => {
|
|
516
618
|
try {
|
|
517
619
|
if (id !== confirmId)
|
|
518
620
|
die(new Error('confirmation ID must match room ID'));
|
|
519
|
-
const
|
|
621
|
+
const cfg = loadCfg(opts);
|
|
622
|
+
await coworkFor(cfg).closeRoom(id);
|
|
623
|
+
const existing = getRoomRecord(id);
|
|
624
|
+
const r = existing ? closeRoomRecord(id) : undefined;
|
|
520
625
|
if (opts.json) {
|
|
521
|
-
console.log(JSON.stringify({ schema_version: 1,
|
|
626
|
+
console.log(JSON.stringify({ schema_version: 1, room_id: id, state: 'closed', orchestration: r ?? null }, null, 2));
|
|
522
627
|
return;
|
|
523
628
|
}
|
|
524
|
-
console.log(`Room ${
|
|
629
|
+
console.log(`Room ${id} · closed`);
|
|
525
630
|
}
|
|
526
631
|
catch (e) {
|
|
527
632
|
die(e);
|
|
528
633
|
}
|
|
529
634
|
});
|
|
530
|
-
roomCmd.command('recover <id>')
|
|
635
|
+
cOpt(roomCmd.command('recover <id>'))
|
|
531
636
|
.description('attempt to recover a stuck room')
|
|
532
637
|
.option('--json', 'JSON output')
|
|
533
|
-
.action((id, opts) => {
|
|
638
|
+
.action(async (id, opts) => {
|
|
534
639
|
try {
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
640
|
+
const cfg = loadCfg(opts);
|
|
641
|
+
const adapter = coworkFor(cfg);
|
|
642
|
+
const cowork = await adapter.recoverRoom(id);
|
|
643
|
+
let r = getRoomRecord(id);
|
|
644
|
+
if (r && !r.owner_seat_cid
|
|
645
|
+
&& (r.provisioning_detail === 'waiting_owner_invite'
|
|
646
|
+
|| r.provisioning_detail === 'owner_cid_mismatch')) {
|
|
647
|
+
const expected = cfg.rooms.owner.expected_cid.toLowerCase();
|
|
648
|
+
const existing = (await adapter.getSeats(id))
|
|
649
|
+
.find(seat => seat.identity_cid.toLowerCase() === expected && seat.seat_state !== 'removed');
|
|
650
|
+
if (!existing && !cfg.ownerInvite)
|
|
651
|
+
throw new ConfigError('rooms.owner: configure public_invite or public_invite_file before recovery');
|
|
652
|
+
let acceptedCid = existing?.identity_cid;
|
|
653
|
+
if (!acceptedCid) {
|
|
654
|
+
const accepted = await adapter.acceptInvite(id, cfg.ownerInvite, {
|
|
655
|
+
role: cfg.rooms.owner.role,
|
|
656
|
+
expected_cid: cfg.rooms.owner.expected_cid,
|
|
657
|
+
});
|
|
658
|
+
acceptedCid = accepted.seat_cid;
|
|
659
|
+
}
|
|
660
|
+
setOwnerSeat(id, acceptedCid, cfg.ownerInviteFingerprint ?? '');
|
|
661
|
+
r = advanceSaga(id, 'create_members', 3);
|
|
662
|
+
}
|
|
538
663
|
const actions = [];
|
|
539
|
-
if (r
|
|
664
|
+
if (r?.saga.error)
|
|
540
665
|
actions.push(`Last error: ${r.saga.error}`);
|
|
541
|
-
if (r
|
|
666
|
+
if (r?.saga.recovery_hint)
|
|
542
667
|
actions.push(r.saga.recovery_hint);
|
|
543
|
-
if (r
|
|
668
|
+
if (r?.provisioning_detail === 'waiting_cowork')
|
|
544
669
|
actions.push('Check ours-cowork service status');
|
|
545
|
-
if (r
|
|
670
|
+
if (r?.provisioning_detail === 'waiting_owner_invite')
|
|
546
671
|
actions.push('Rotate rooms.owner.public_invite in config, then re-run recover');
|
|
547
672
|
if (opts.json) {
|
|
548
|
-
console.log(JSON.stringify({ schema_version: 1, room: r, recovery_actions: actions }, null, 2));
|
|
673
|
+
console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null, recovery_actions: actions }, null, 2));
|
|
549
674
|
return;
|
|
550
675
|
}
|
|
551
|
-
console.log(`Room ${
|
|
676
|
+
console.log(`Room ${cowork.room_id} · ${cowork.state}${r ? ` · saga: ${r.saga.phase}` : ''}`);
|
|
552
677
|
if (actions.length) {
|
|
553
678
|
console.log('Recovery:');
|
|
554
679
|
for (const a of actions)
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Typed client for the ours-cowork v1 private management-socket protocol.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* JSON RPC. All methods fail closed: if Cowork is unreachable or returns
|
|
7
|
-
* an unexpected response, the operation fails rather than guessing.
|
|
8
|
-
*
|
|
9
|
-
* BLOCKED: The management socket protocol is defined in ours-cowork but
|
|
10
|
-
* the exact prerelease wire format needs verification. This adapter
|
|
11
|
-
* defines the interface Fleet needs and provides a fail-closed stub
|
|
12
|
-
* that reports the blocker. Replace the stub with socket calls once
|
|
13
|
-
* the Cowork management socket contract is verified.
|
|
4
|
+
* Cowork owns room state. Fleet only invokes the versioned JSON RPC exposed
|
|
5
|
+
* by Cowork's Unix socket and projects the response into orchestration data.
|
|
14
6
|
*/
|
|
7
|
+
import type { Socket } from 'node:net';
|
|
15
8
|
export interface CoworkRoomCreateResult {
|
|
16
9
|
room_id: string;
|
|
17
10
|
identity_name: string;
|
|
@@ -44,8 +37,8 @@ export interface CoworkAdapter {
|
|
|
44
37
|
available(): Promise<boolean>;
|
|
45
38
|
createRoom(opts: {
|
|
46
39
|
room_name: string;
|
|
47
|
-
goal
|
|
48
|
-
briefing
|
|
40
|
+
goal: string;
|
|
41
|
+
briefing: string;
|
|
49
42
|
quiet_membership?: boolean;
|
|
50
43
|
anonymous?: boolean;
|
|
51
44
|
}): Promise<CoworkRoomCreateResult>;
|
|
@@ -57,24 +50,28 @@ export interface CoworkAdapter {
|
|
|
57
50
|
role: string;
|
|
58
51
|
min_accepts: number;
|
|
59
52
|
}): Promise<CoworkInviteResult>;
|
|
60
|
-
acceptAsIdentity(roomId: string, invite: string, identityCid: string, opts: {
|
|
61
|
-
role: string;
|
|
62
|
-
}): Promise<CoworkInviteAcceptResult>;
|
|
63
53
|
getRoom(roomId: string): Promise<CoworkRoomInfo | undefined>;
|
|
64
54
|
listRooms(): Promise<CoworkRoomInfo[]>;
|
|
65
55
|
closeRoom(roomId: string): Promise<void>;
|
|
66
56
|
getSeats(roomId: string): Promise<CoworkSeatInfo[]>;
|
|
57
|
+
recoverRoom(roomId: string): Promise<CoworkRoomInfo>;
|
|
67
58
|
}
|
|
68
59
|
export declare class CoworkUnavailableError extends Error {
|
|
69
|
-
constructor(message?: string);
|
|
60
|
+
constructor(message?: string, options?: ErrorOptions);
|
|
70
61
|
}
|
|
71
62
|
export declare class CoworkProtocolError extends Error {
|
|
72
63
|
readonly operation: string;
|
|
73
|
-
|
|
64
|
+
readonly code: string;
|
|
65
|
+
constructor(operation: string, message: string, code?: string, options?: ErrorOptions);
|
|
74
66
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
67
|
+
export interface CoworkAdapterOptions {
|
|
68
|
+
configPath?: string;
|
|
69
|
+
socketPath?: string;
|
|
70
|
+
timeoutMs?: number;
|
|
71
|
+
env?: NodeJS.ProcessEnv;
|
|
72
|
+
home?: string;
|
|
73
|
+
connect?: (path: string) => Socket;
|
|
74
|
+
}
|
|
75
|
+
/** Resolve the same config/state inputs as ours-cowork and return its Unix socket. */
|
|
76
|
+
export declare function resolveCoworkSocketPath(options?: CoworkAdapterOptions): string;
|
|
77
|
+
export declare function createCoworkAdapter(options?: CoworkAdapterOptions): CoworkAdapter;
|
|
@@ -1,48 +1,272 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Typed client for the ours-cowork v1 private management-socket protocol.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* JSON RPC. All methods fail closed: if Cowork is unreachable or returns
|
|
7
|
-
* an unexpected response, the operation fails rather than guessing.
|
|
8
|
-
*
|
|
9
|
-
* BLOCKED: The management socket protocol is defined in ours-cowork but
|
|
10
|
-
* the exact prerelease wire format needs verification. This adapter
|
|
11
|
-
* defines the interface Fleet needs and provides a fail-closed stub
|
|
12
|
-
* that reports the blocker. Replace the stub with socket calls once
|
|
13
|
-
* the Cowork management socket contract is verified.
|
|
4
|
+
* Cowork owns room state. Fleet only invokes the versioned JSON RPC exposed
|
|
5
|
+
* by Cowork's Unix socket and projects the response into orchestration data.
|
|
14
6
|
*/
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join, resolve } from 'node:path';
|
|
10
|
+
import { createConnection } from 'node:net';
|
|
11
|
+
const RPC_VERSION = 1;
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
13
|
+
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
15
14
|
export class CoworkUnavailableError extends Error {
|
|
16
|
-
constructor(message = 'Cowork management socket is not reachable') {
|
|
17
|
-
super(message);
|
|
15
|
+
constructor(message = 'Cowork management socket is not reachable', options) {
|
|
16
|
+
super(message, options);
|
|
17
|
+
this.name = 'CoworkUnavailableError';
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
export class CoworkProtocolError extends Error {
|
|
21
21
|
operation;
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
code;
|
|
23
|
+
constructor(operation, message, code = 'protocol_error', options) {
|
|
24
|
+
super(`cowork ${operation}: ${message}`, options);
|
|
24
25
|
this.operation = operation;
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.name = 'CoworkProtocolError';
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
throw new
|
|
35
|
-
|
|
30
|
+
function object(value, operation, label) {
|
|
31
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
32
|
+
throw new CoworkProtocolError(operation, `${label} must be an object`);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function string(value, operation, label) {
|
|
36
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
37
|
+
throw new CoworkProtocolError(operation, `${label} must be a non-empty string`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function text(value, operation, label) {
|
|
41
|
+
if (typeof value !== 'string')
|
|
42
|
+
throw new CoworkProtocolError(operation, `${label} must be a string`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function roomState(value, operation) {
|
|
46
|
+
if (value !== 'provisioning' && value !== 'active' && value !== 'closing' && value !== 'closed')
|
|
47
|
+
throw new CoworkProtocolError(operation, 'room state is invalid');
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function seatState(value, operation) {
|
|
51
|
+
if (value !== 'pending' && value !== 'active' && value !== 'removed')
|
|
52
|
+
throw new CoworkProtocolError(operation, 'seat state is invalid');
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function projectSeat(value, operation) {
|
|
56
|
+
const seat = object(value, operation, 'seat');
|
|
57
|
+
return {
|
|
58
|
+
identity_cid: string(seat.identity, operation, 'seat.identity'),
|
|
59
|
+
role: string(seat.role, operation, 'seat.role'),
|
|
60
|
+
seat_state: seatState(seat.state, operation),
|
|
36
61
|
};
|
|
62
|
+
}
|
|
63
|
+
function projectRoom(value, operation) {
|
|
64
|
+
const room = object(value, operation, 'room');
|
|
65
|
+
if (!Array.isArray(room.seats))
|
|
66
|
+
throw new CoworkProtocolError(operation, 'room.seats must be an array');
|
|
67
|
+
const mission = room.mission === undefined ? undefined : object(room.mission, operation, 'room.mission');
|
|
68
|
+
return {
|
|
69
|
+
room_id: string(room.room_id, operation, 'room.room_id'),
|
|
70
|
+
identity_name: string(room.identity_name, operation, 'room.identity_name'),
|
|
71
|
+
// Cowork reserves an empty CID only for its durable packet_pending
|
|
72
|
+
// recovery sentinel; list/show must preserve that recoverable state.
|
|
73
|
+
identity_cid: text(room.identity_cid, operation, 'room.identity_cid'),
|
|
74
|
+
room_name: string(room.room_name, operation, 'room.room_name'),
|
|
75
|
+
state: roomState(room.state, operation),
|
|
76
|
+
seats: room.seats.map((seat) => projectSeat(seat, operation)),
|
|
77
|
+
...(typeof mission?.goal === 'string' ? { goal: mission.goal } : {}),
|
|
78
|
+
...(typeof mission?.briefing === 'string' ? { briefing: mission.briefing } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** Resolve the same config/state inputs as ours-cowork and return its Unix socket. */
|
|
82
|
+
export function resolveCoworkSocketPath(options = {}) {
|
|
83
|
+
if (options.socketPath)
|
|
84
|
+
return resolve(options.socketPath);
|
|
85
|
+
const env = options.env ?? process.env;
|
|
86
|
+
const home = options.home ?? homedir();
|
|
87
|
+
const configuredPath = options.configPath ?? env.OURS_COWORK_CONFIG;
|
|
88
|
+
const configPath = resolve(configuredPath ?? join(home, '.ours-cowork', 'config.json'));
|
|
89
|
+
let stateDir = resolve(home, '.ours-cowork');
|
|
90
|
+
if (existsSync(configPath)) {
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw new CoworkProtocolError('config', `malformed config at ${configPath}`, 'invalid_config', { cause: error });
|
|
97
|
+
}
|
|
98
|
+
const config = object(parsed, 'config', 'config');
|
|
99
|
+
if (config.version !== 1 || typeof config.stateDir !== 'string' || !config.stateDir)
|
|
100
|
+
throw new CoworkProtocolError('config', `invalid Cowork v1 config at ${configPath}`, 'invalid_config');
|
|
101
|
+
stateDir = resolve(config.stateDir);
|
|
102
|
+
}
|
|
103
|
+
else if (configuredPath !== undefined) {
|
|
104
|
+
throw new CoworkProtocolError('config', `configured Cowork config does not exist: ${configPath}`, 'invalid_config');
|
|
105
|
+
}
|
|
106
|
+
if (env.OURS_COWORK_STATE_DIR)
|
|
107
|
+
stateDir = resolve(env.OURS_COWORK_STATE_DIR);
|
|
108
|
+
return join(stateDir, 'management.sock');
|
|
109
|
+
}
|
|
110
|
+
function rpcCall(socketPath, method, params, timeoutMs, connect) {
|
|
111
|
+
const id = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
112
|
+
const request = `${JSON.stringify({ version: RPC_VERSION, id, method, params })}\n`;
|
|
113
|
+
return new Promise((resolveCall, rejectCall) => {
|
|
114
|
+
const socket = connect(socketPath);
|
|
115
|
+
let bytes = '';
|
|
116
|
+
let size = 0;
|
|
117
|
+
let settled = false;
|
|
118
|
+
let connected = false;
|
|
119
|
+
const finishError = (error) => {
|
|
120
|
+
if (settled)
|
|
121
|
+
return;
|
|
122
|
+
settled = true;
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
socket.destroy();
|
|
125
|
+
rejectCall(error);
|
|
126
|
+
};
|
|
127
|
+
const timer = setTimeout(() => finishError(new CoworkUnavailableError(connected ? 'Cowork did not answer the management socket' : 'Cowork management socket is not reachable')), timeoutMs);
|
|
128
|
+
socket.setEncoding('utf8');
|
|
129
|
+
socket.once('connect', () => {
|
|
130
|
+
connected = true;
|
|
131
|
+
socket.write(request);
|
|
132
|
+
});
|
|
133
|
+
socket.on('data', (chunk) => {
|
|
134
|
+
if (settled)
|
|
135
|
+
return;
|
|
136
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
137
|
+
size += Buffer.byteLength(text);
|
|
138
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
139
|
+
finishError(new CoworkProtocolError(method, 'response exceeded 4 MiB'));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
bytes += text;
|
|
143
|
+
const newline = bytes.indexOf('\n');
|
|
144
|
+
if (newline < 0)
|
|
145
|
+
return;
|
|
146
|
+
if (bytes.slice(newline + 1).trim() !== '') {
|
|
147
|
+
finishError(new CoworkProtocolError(method, 'daemon returned more than one response'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
let response;
|
|
151
|
+
try {
|
|
152
|
+
response = object(JSON.parse(bytes.slice(0, newline)), method, 'RPC response');
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
finishError(error instanceof CoworkProtocolError
|
|
156
|
+
? error : new CoworkProtocolError(method, 'daemon returned malformed JSON', 'protocol_error', { cause: error }));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (response.version !== RPC_VERSION || response.id !== id
|
|
160
|
+
|| (Object.hasOwn(response, 'error') === Object.hasOwn(response, 'result'))) {
|
|
161
|
+
finishError(new CoworkProtocolError(method, 'daemon returned an invalid RPC response'));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
settled = true;
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
socket.destroy();
|
|
167
|
+
if (Object.hasOwn(response, 'error')) {
|
|
168
|
+
const error = object(response.error, method, 'RPC error');
|
|
169
|
+
rejectCall(new CoworkProtocolError(method, typeof error.message === 'string' ? error.message : 'unknown RPC error', typeof error.code === 'string' ? error.code : 'rpc_error'));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
resolveCall(response.result);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
socket.once('end', () => {
|
|
176
|
+
if (!settled)
|
|
177
|
+
finishError(new CoworkProtocolError(method, 'daemon closed without a complete response'));
|
|
178
|
+
});
|
|
179
|
+
socket.once('error', (error) => {
|
|
180
|
+
if (!settled)
|
|
181
|
+
finishError(new CoworkUnavailableError(error.code === 'EACCES' || error.code === 'EPERM'
|
|
182
|
+
? 'Cowork management socket access denied' : 'Cowork management socket is not reachable', { cause: error }));
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
export function createCoworkAdapter(options = {}) {
|
|
187
|
+
const socketPath = resolveCoworkSocketPath(options);
|
|
188
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
189
|
+
const connect = options.connect ?? ((path) => createConnection(path));
|
|
190
|
+
const call = (method, params) => rpcCall(socketPath, method, params, timeoutMs, connect);
|
|
37
191
|
return {
|
|
38
|
-
async available() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
192
|
+
async available() {
|
|
193
|
+
try {
|
|
194
|
+
await call('room.list', {});
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
async createRoom(opts) {
|
|
202
|
+
const result = projectRoom(await call('room.create', {
|
|
203
|
+
name: opts.room_name,
|
|
204
|
+
goal: opts.goal,
|
|
205
|
+
briefing: opts.briefing,
|
|
206
|
+
...(opts.quiet_membership === undefined ? {} : { quiet_membership: opts.quiet_membership }),
|
|
207
|
+
...(opts.anonymous === undefined ? {} : { anonymous: opts.anonymous }),
|
|
208
|
+
}), 'room.create');
|
|
209
|
+
if (!result.identity_cid)
|
|
210
|
+
throw new CoworkProtocolError('room.create', 'created room did not establish an identity CID');
|
|
211
|
+
return { room_id: result.room_id, identity_name: result.identity_name, identity_cid: result.identity_cid };
|
|
212
|
+
},
|
|
213
|
+
async acceptInvite(roomId, invite, opts) {
|
|
214
|
+
const receipt = object(await call('room.accept', {
|
|
215
|
+
room_id: roomId,
|
|
216
|
+
role: opts.role,
|
|
217
|
+
invite,
|
|
218
|
+
expected_cid: opts.expected_cid,
|
|
219
|
+
}), 'room.accept', 'receipt');
|
|
220
|
+
const state = seatState(receipt.state, 'room.accept');
|
|
221
|
+
if (state === 'removed')
|
|
222
|
+
throw new CoworkProtocolError('room.accept', 'accepted seat cannot be removed');
|
|
223
|
+
return {
|
|
224
|
+
seat_cid: string(receipt.identity, 'room.accept', 'receipt.identity'),
|
|
225
|
+
seat_state: state,
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
async issueInvite(roomId, opts) {
|
|
229
|
+
const receipt = object(await call('room.invite', {
|
|
230
|
+
room_id: roomId,
|
|
231
|
+
mode: 'public',
|
|
232
|
+
role: opts.role,
|
|
233
|
+
min_accepts: opts.min_accepts,
|
|
234
|
+
}), 'room.invite', 'receipt');
|
|
235
|
+
const invite = object(receipt.invite, 'room.invite', 'receipt.invite');
|
|
236
|
+
return {
|
|
237
|
+
invite: string(receipt.blob, 'room.invite', 'receipt.blob'),
|
|
238
|
+
min_accepts: typeof invite.min_accepts === 'number'
|
|
239
|
+
? invite.min_accepts : opts.min_accepts,
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
async getRoom(roomId) {
|
|
243
|
+
try {
|
|
244
|
+
return projectRoom(await call('room.show', { room_id: roomId }), 'room.show');
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (error instanceof CoworkProtocolError && error.code === 'not_found')
|
|
248
|
+
return undefined;
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
async listRooms() {
|
|
253
|
+
const result = await call('room.list', {});
|
|
254
|
+
if (!Array.isArray(result))
|
|
255
|
+
throw new CoworkProtocolError('room.list', 'result must be an array');
|
|
256
|
+
return result.map((room) => projectRoom(room, 'room.list'));
|
|
257
|
+
},
|
|
258
|
+
async closeRoom(roomId) { await call('room.close', { room_id: roomId }); },
|
|
259
|
+
async getSeats(roomId) {
|
|
260
|
+
const result = await call('room.participants', { room_id: roomId });
|
|
261
|
+
if (!Array.isArray(result))
|
|
262
|
+
throw new CoworkProtocolError('room.participants', 'result must be an array');
|
|
263
|
+
return result.map((seat) => projectSeat(seat, 'room.participants'));
|
|
264
|
+
},
|
|
265
|
+
async recoverRoom(roomId) {
|
|
266
|
+
// Cowork performs packet/state reconciliation during daemon recovery.
|
|
267
|
+
// Its `room.recover` RPC is specifically invite-receipt recovery, so a
|
|
268
|
+
// Fleet reconciliation pass must read `room.show`, not mutate invites.
|
|
269
|
+
return projectRoom(await call('room.show', { room_id: roomId }), 'room.show');
|
|
270
|
+
},
|
|
47
271
|
};
|
|
48
272
|
}
|
|
@@ -23,6 +23,7 @@ export function createRoomRecord(input) {
|
|
|
23
23
|
const record = {
|
|
24
24
|
room_id: input.room_id,
|
|
25
25
|
room_name: input.room_name,
|
|
26
|
+
room_identity_cid: input.room_identity_cid,
|
|
26
27
|
task_id: input.task_id,
|
|
27
28
|
template_snapshot: input.template_snapshot,
|
|
28
29
|
saga: { phase: 'persist_intent', step_index: 0 },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "0.19.0-nightly.
|
|
3
|
+
"version": "0.19.0-nightly.3",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, tmux or ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|