@ours.network/fleet 0.18.1 → 0.19.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 +86 -90
- package/dist/application/fleet-query-service.js +0 -12
- package/dist/application/role-creation-service.js +10 -7
- package/dist/application/types.d.ts +0 -11
- package/dist/briefing.js +6 -15
- package/dist/build-info.json +5 -5
- package/dist/cli.js +12 -37
- package/dist/config.d.ts +14 -6
- package/dist/config.js +58 -31
- package/dist/creation.d.ts +38 -22
- package/dist/creation.js +111 -24
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +71 -94
- package/dist/doctor.d.ts +5 -1
- package/dist/doctor.js +143 -12
- package/dist/fleet-proxy.d.ts +0 -5
- package/dist/harness/acp-agent.js +6 -11
- package/dist/harness/claude-code.js +11 -200
- package/dist/harness/codex.d.ts +1 -4
- package/dist/harness/codex.js +12 -70
- package/dist/harness/types.d.ts +4 -54
- package/dist/loops/manager.d.ts +1 -30
- package/dist/loops/manager.js +6 -69
- package/dist/loops/state.d.ts +0 -18
- package/dist/loops/state.js +0 -4
- package/dist/monitor.js +1 -1
- package/dist/ops.d.ts +3 -0
- package/dist/ops.js +8 -3
- package/dist/owner-channel/attachments.d.ts +25 -2
- package/dist/owner-channel/attachments.js +61 -5
- package/dist/owner-channel/channel.d.ts +36 -30
- package/dist/owner-channel/channel.js +338 -291
- package/dist/owner-channel/commands.d.ts +9 -0
- package/dist/owner-channel/commands.js +393 -0
- package/dist/owner-channel/message-recovery.d.ts +25 -0
- package/dist/owner-channel/message-recovery.js +114 -0
- package/dist/owner-channel/notices.d.ts +0 -7
- package/dist/owner-channel/notices.js +0 -9
- package/dist/owner-channel/ours-client.d.ts +148 -0
- package/dist/owner-channel/ours-client.js +231 -0
- package/dist/rooms-tasks/cli.d.ts +4 -0
- package/dist/rooms-tasks/cli.js +1102 -0
- package/dist/rooms-tasks/close.d.ts +21 -0
- package/dist/rooms-tasks/close.js +159 -0
- package/dist/rooms-tasks/config.d.ts +15 -0
- package/dist/rooms-tasks/config.js +174 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +77 -0
- package/dist/rooms-tasks/cowork-adapter.js +272 -0
- package/dist/rooms-tasks/external-worker.d.ts +2 -0
- package/dist/rooms-tasks/external-worker.js +40 -0
- package/dist/rooms-tasks/index.d.ts +10 -0
- package/dist/rooms-tasks/index.js +10 -0
- package/dist/rooms-tasks/provision.d.ts +21 -0
- package/dist/rooms-tasks/provision.js +243 -0
- package/dist/rooms-tasks/room-state.d.ts +28 -0
- package/dist/rooms-tasks/room-state.js +221 -0
- package/dist/rooms-tasks/task-state.d.ts +43 -0
- package/dist/rooms-tasks/task-state.js +277 -0
- package/dist/rooms-tasks/templates.d.ts +6 -0
- package/dist/rooms-tasks/templates.js +94 -0
- package/dist/rooms-tasks/terminal.d.ts +23 -0
- package/dist/rooms-tasks/terminal.js +53 -0
- package/dist/rooms-tasks/types.d.ts +187 -0
- package/dist/rooms-tasks/types.js +20 -0
- package/dist/runner.d.ts +0 -48
- package/dist/runner.js +94 -236
- package/dist/session/acp.d.ts +0 -104
- package/dist/session/acp.js +10 -213
- package/dist/session/conversation-normalizer.d.ts +0 -6
- package/dist/session/conversation-normalizer.js +10 -153
- package/dist/session/conversation-types.d.ts +4 -23
- package/dist/session/types.d.ts +0 -35
- package/dist/spawn.js +26 -33
- package/dist/supervisor/systemd.js +29 -2
- package/dist/temp-lifecycle.d.ts +9 -2
- package/dist/temp-lifecycle.js +61 -0
- package/dist/watchdog/briefing.js +0 -7
- package/dist/watchdog/run.js +3 -3
- package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
- package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +1 -7
- package/dist/worklog.js +39 -191
- package/package.json +3 -1
- package/dist/model-env.d.ts +0 -71
- package/dist/model-env.js +0 -106
- package/dist/owner-channel/mcp.d.ts +0 -24
- package/dist/owner-channel/mcp.js +0 -145
- package/dist/session/activity.d.ts +0 -31
- package/dist/session/activity.js +0 -48
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { withFileLock } from '../atomic-file.js';
|
|
2
|
+
import type { CoworkAdapter } from './cowork-adapter.js';
|
|
3
|
+
import type { RoomMemberSeat, RoomOrchestrationRecord } from './types.js';
|
|
4
|
+
export interface RoomCloseDeps {
|
|
5
|
+
inspectMember?(seat: RoomMemberSeat): Promise<{
|
|
6
|
+
launchId: string;
|
|
7
|
+
}>;
|
|
8
|
+
requestStop?(role: string): Promise<void>;
|
|
9
|
+
waitForLivenessAbsent?(role: string, launchId: string): Promise<void>;
|
|
10
|
+
secureArchive?(role: string, launchId: string): Promise<string>;
|
|
11
|
+
removeIdentity?(seat: RoomMemberSeat): Promise<void>;
|
|
12
|
+
lock?: typeof withFileLock;
|
|
13
|
+
}
|
|
14
|
+
/** One forward-only room close saga shared by every Fleet entry point. */
|
|
15
|
+
export declare function acceptManagedRoomClose(roomId: string): Promise<RoomOrchestrationRecord>;
|
|
16
|
+
export declare function recordManagedRoomCloseError(roomId: string, error: string, recoveryHint: string): Promise<RoomOrchestrationRecord>;
|
|
17
|
+
export declare function closeManagedRoom(input: {
|
|
18
|
+
roomId: string;
|
|
19
|
+
cowork: Pick<CoworkAdapter, 'closeRoom'>;
|
|
20
|
+
deps?: RoomCloseDeps;
|
|
21
|
+
}): Promise<RoomOrchestrationRecord>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { attachOursClient } from '@ours.network/sdk/client';
|
|
5
|
+
import { withFileLock } from '../atomic-file.js';
|
|
6
|
+
import { agentDir, stateRoot } from '../paths.js';
|
|
7
|
+
import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, } from '../temp-lifecycle.js';
|
|
8
|
+
import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, getRoomRecord, setRoomCloseError, } from './room-state.js';
|
|
9
|
+
const CLOSE_LOCK_STALE_MS = 5 * 60_000;
|
|
10
|
+
const STOP_POLLS = 50;
|
|
11
|
+
const STOP_POLL_MS = 100;
|
|
12
|
+
function roomCloseLockPath(roomId) {
|
|
13
|
+
return join(stateRoot(), 'locks', 'room-close', encodeURIComponent(roomId));
|
|
14
|
+
}
|
|
15
|
+
function errorText(error) {
|
|
16
|
+
return error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
function exactMemberIdentity(seat) {
|
|
19
|
+
const dir = agentDir(seat.role_name, true);
|
|
20
|
+
const identityPath = join(dir, '.identity');
|
|
21
|
+
if (!existsSync(dir) || !existsSync(identityPath)) {
|
|
22
|
+
throw new Error(`room member '${seat.role_name}' has no live Fleet temp-state identity proof; refusing retirement`);
|
|
23
|
+
}
|
|
24
|
+
const identity = readFileSync(identityPath, 'utf8').trim();
|
|
25
|
+
if (identity !== seat.role_name) {
|
|
26
|
+
throw new Error(`room member '${seat.role_name}' temp state binds identity '${identity}'; refusing mismatched retirement`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function inspectMember(seat) {
|
|
30
|
+
exactMemberIdentity(seat);
|
|
31
|
+
const supervisor = readTempSupervisor(agentDir(seat.role_name, true));
|
|
32
|
+
if (!supervisor || supervisor.role !== seat.role_name) {
|
|
33
|
+
throw new Error(`room member '${seat.role_name}' has no exact Fleet supervisor ownership proof`);
|
|
34
|
+
}
|
|
35
|
+
return { launchId: supervisor.launchId };
|
|
36
|
+
}
|
|
37
|
+
async function waitForLivenessAbsent(role, launchId, lifecycleDeps = {}) {
|
|
38
|
+
const dir = agentDir(role, true);
|
|
39
|
+
const sleep = lifecycleDeps.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
|
|
40
|
+
for (let attempt = 0; attempt < STOP_POLLS; attempt++) {
|
|
41
|
+
if (!existsSync(dir))
|
|
42
|
+
return; // secureStoppedTempArchive proves the exact archive next.
|
|
43
|
+
const supervisor = readTempSupervisor(dir);
|
|
44
|
+
if (!supervisor || supervisor.role !== role || supervisor.launchId !== launchId) {
|
|
45
|
+
throw new Error(`temporary role '${role}' changed while waiting for launch ${launchId} to stop`);
|
|
46
|
+
}
|
|
47
|
+
const state = await tempSupervisorLiveness(dir, lifecycleDeps);
|
|
48
|
+
if (state === 'stopped')
|
|
49
|
+
return;
|
|
50
|
+
if (attempt < STOP_POLLS - 1)
|
|
51
|
+
await sleep(STOP_POLL_MS);
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`temporary role '${role}' did not reach proven stopped liveness`);
|
|
54
|
+
}
|
|
55
|
+
async function withIdentityClient(work) {
|
|
56
|
+
const client = await attachOursClient({
|
|
57
|
+
env: process.env,
|
|
58
|
+
leaseToken: `ours-fleet-room-close-${process.pid}-${randomUUID()}`,
|
|
59
|
+
clientPid: process.pid,
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
return await work(client);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await client.releaseLease().catch(() => { });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function listedIdentity(rows, name) {
|
|
69
|
+
return rows.find(row => row.name === name);
|
|
70
|
+
}
|
|
71
|
+
async function removeExactMemberIdentity(seat) {
|
|
72
|
+
await withIdentityClient(async (client) => {
|
|
73
|
+
const before = listedIdentity(await client.listIdentities(), seat.role_name);
|
|
74
|
+
if (!before)
|
|
75
|
+
return;
|
|
76
|
+
if (before.cid?.toLowerCase() !== seat.identity_cid.toLowerCase()) {
|
|
77
|
+
throw new Error(`room member '${seat.role_name}' identity CID mismatch: recorded ${seat.identity_cid}, found ${before.cid ?? 'none'}`);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await client.removeIdentity({ name: seat.role_name });
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const code = error instanceof Error && error.name === 'OursError'
|
|
84
|
+
? error.code
|
|
85
|
+
: undefined;
|
|
86
|
+
if (code !== 'NO_SUCH_IDENTITY')
|
|
87
|
+
throw error;
|
|
88
|
+
const after = listedIdentity(await client.listIdentities(), seat.role_name);
|
|
89
|
+
if (after)
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
const after = listedIdentity(await client.listIdentities(), seat.role_name);
|
|
93
|
+
if (after) {
|
|
94
|
+
throw new Error(`room member '${seat.role_name}' identity still exists after remove_identity`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function retireMember(roomId, seat, deps) {
|
|
99
|
+
let room = getRoomRecord(roomId);
|
|
100
|
+
let current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
101
|
+
let retirement = current.retirement;
|
|
102
|
+
if (!retirement) {
|
|
103
|
+
const ownership = await (deps.inspectMember ?? inspectMember)(current);
|
|
104
|
+
room = advanceMemberRetirement(roomId, current.role_name, 'stop_requested', ownership.launchId);
|
|
105
|
+
current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
106
|
+
retirement = current.retirement;
|
|
107
|
+
}
|
|
108
|
+
if (retirement.phase === 'stop_requested') {
|
|
109
|
+
await (deps.requestStop ?? (async (role) => { await stopTempSupervisor(role); }))(current.role_name);
|
|
110
|
+
await (deps.waitForLivenessAbsent ?? waitForLivenessAbsent)(current.role_name, retirement.launch_id);
|
|
111
|
+
room = advanceMemberRetirement(roomId, current.role_name, 'liveness_absent', retirement.launch_id);
|
|
112
|
+
current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
113
|
+
retirement = current.retirement;
|
|
114
|
+
}
|
|
115
|
+
if (retirement.phase === 'liveness_absent') {
|
|
116
|
+
const archivePath = await (deps.secureArchive ?? secureStoppedTempArchive)(current.role_name, retirement.launch_id);
|
|
117
|
+
room = advanceMemberRetirement(roomId, current.role_name, 'archive_secured', retirement.launch_id, archivePath);
|
|
118
|
+
current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
119
|
+
retirement = current.retirement;
|
|
120
|
+
}
|
|
121
|
+
if (retirement.phase === 'archive_secured') {
|
|
122
|
+
await (deps.removeIdentity ?? removeExactMemberIdentity)(current);
|
|
123
|
+
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', retirement.launch_id, retirement.archive_path);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** One forward-only room close saga shared by every Fleet entry point. */
|
|
127
|
+
export function acceptManagedRoomClose(roomId) {
|
|
128
|
+
return withFileLock(roomCloseLockPath(roomId), () => beginRoomClose(roomId), {}, CLOSE_LOCK_STALE_MS);
|
|
129
|
+
}
|
|
130
|
+
export function recordManagedRoomCloseError(roomId, error, recoveryHint) {
|
|
131
|
+
return withFileLock(roomCloseLockPath(roomId), () => setRoomCloseError(roomId, error, recoveryHint), {}, CLOSE_LOCK_STALE_MS);
|
|
132
|
+
}
|
|
133
|
+
export async function closeManagedRoom(input) {
|
|
134
|
+
const deps = input.deps ?? {};
|
|
135
|
+
const lock = deps.lock ?? withFileLock;
|
|
136
|
+
return lock(roomCloseLockPath(input.roomId), async () => {
|
|
137
|
+
let room = beginRoomClose(input.roomId);
|
|
138
|
+
if (room.state === 'closed')
|
|
139
|
+
return room;
|
|
140
|
+
try {
|
|
141
|
+
if (room.close?.phase === 'retire_members') {
|
|
142
|
+
for (const seat of room.member_seats) {
|
|
143
|
+
if (seat.retirement?.phase === 'identity_absent')
|
|
144
|
+
continue;
|
|
145
|
+
await retireMember(input.roomId, seat, deps);
|
|
146
|
+
}
|
|
147
|
+
room = advanceRoomClose(input.roomId, 'close_cowork');
|
|
148
|
+
}
|
|
149
|
+
if (room.close?.phase === 'close_cowork') {
|
|
150
|
+
await input.cowork.closeRoom(input.roomId);
|
|
151
|
+
}
|
|
152
|
+
return closeRoom(input.roomId);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
setRoomCloseError(input.roomId, errorText(error), `Retry 'ours-fleet room close ${input.roomId} ${input.roomId}' or run room recover.`);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}, {}, CLOSE_LOCK_STALE_MS);
|
|
159
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RoomsConfig, TasksConfig, RoomTemplatesConfig } from './types.js';
|
|
2
|
+
export declare class RoomsTasksConfigError extends Error {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
constructor(path: string, message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function fingerprint(invite: string): string;
|
|
7
|
+
export declare function validateRoomsConfig(raw: unknown, vars: Record<string, string>, path: string): RoomsConfig & {
|
|
8
|
+
_invite?: {
|
|
9
|
+
value: string;
|
|
10
|
+
fingerprint: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export declare function validateTasksConfig(raw: unknown, path: string): TasksConfig;
|
|
14
|
+
export declare function validateRoomTemplatesConfig(raw: unknown, path: string): RoomTemplatesConfig;
|
|
15
|
+
export declare const FLEET_D_ALLOWED_KEYS: readonly ["roles", "rooms", "room_templates", "tasks"];
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
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, TEMPLATE_OVERRIDE_KEYS as TOK, } from './types.js';
|
|
4
|
+
import { BUILTIN_TEMPLATES } from './templates.js';
|
|
5
|
+
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
6
|
+
const CID_RE = /^[0-9a-fA-F]{64}$/;
|
|
7
|
+
export class RoomsTasksConfigError extends Error {
|
|
8
|
+
path;
|
|
9
|
+
constructor(path, message) {
|
|
10
|
+
super(`${path}: ${message}`);
|
|
11
|
+
this.path = path;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function rejectUnknown(obj, allowed, path, section) {
|
|
15
|
+
const bad = Object.keys(obj).filter(k => !allowed.includes(k));
|
|
16
|
+
if (bad.length)
|
|
17
|
+
throw new RoomsTasksConfigError(path, `${section} has unknown key(s) ${bad.join(', ')}; allowed: ${allowed.join(', ')}`);
|
|
18
|
+
}
|
|
19
|
+
export function fingerprint(invite) {
|
|
20
|
+
return createHash('sha256').update(invite.trim()).digest('hex');
|
|
21
|
+
}
|
|
22
|
+
function resolveInvite(owner, vars, path) {
|
|
23
|
+
const inline = owner.public_invite;
|
|
24
|
+
const file = owner.public_invite_file;
|
|
25
|
+
if (inline && file)
|
|
26
|
+
throw new RoomsTasksConfigError(path, 'rooms.owner: exactly one of public_invite or public_invite_file');
|
|
27
|
+
if (!inline && !file)
|
|
28
|
+
return undefined;
|
|
29
|
+
let raw;
|
|
30
|
+
if (file) {
|
|
31
|
+
const resolved = file.replace(/\$\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m));
|
|
32
|
+
if (!existsSync(resolved))
|
|
33
|
+
throw new RoomsTasksConfigError(path, `rooms.owner.public_invite_file: not found: ${resolved}`);
|
|
34
|
+
raw = readFileSync(resolved, 'utf8').trim();
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
raw = inline;
|
|
38
|
+
}
|
|
39
|
+
if (!raw)
|
|
40
|
+
throw new RoomsTasksConfigError(path, 'rooms.owner: invite is empty');
|
|
41
|
+
return { value: raw, fingerprint: fingerprint(raw) };
|
|
42
|
+
}
|
|
43
|
+
export function validateRoomsConfig(raw, vars, path) {
|
|
44
|
+
if (!isPlainObject(raw))
|
|
45
|
+
throw new RoomsTasksConfigError(path, 'rooms: must be a mapping');
|
|
46
|
+
// Migration-only input: provider selection was exposed before rooms shipped,
|
|
47
|
+
// even though cowork was the sole implementation. Accept the one historical
|
|
48
|
+
// value without carrying it into the current schema or resolved config.
|
|
49
|
+
if (Object.hasOwn(raw, 'provider') && raw.provider !== 'cowork')
|
|
50
|
+
throw new RoomsTasksConfigError(path, 'rooms.provider: only the legacy value \'cowork\' can be migrated; remove this field because ours-cowork is always used');
|
|
51
|
+
const { provider: _legacyProvider, ...current } = raw;
|
|
52
|
+
rejectUnknown(current, RK, path, 'rooms');
|
|
53
|
+
let cowork;
|
|
54
|
+
if (raw.cowork !== undefined) {
|
|
55
|
+
if (!isPlainObject(raw.cowork))
|
|
56
|
+
throw new RoomsTasksConfigError(path, 'rooms.cowork: must be a mapping');
|
|
57
|
+
rejectUnknown(raw.cowork, RCK, path, 'rooms.cowork');
|
|
58
|
+
cowork = { config: raw.cowork.config };
|
|
59
|
+
}
|
|
60
|
+
if (!isPlainObject(raw.owner))
|
|
61
|
+
throw new RoomsTasksConfigError(path, 'rooms.owner: required and must be a mapping');
|
|
62
|
+
rejectUnknown(raw.owner, ROK, path, 'rooms.owner');
|
|
63
|
+
const ownerProvider = raw.owner.provider ?? 'messenger-server';
|
|
64
|
+
const expectedCid = raw.owner.expected_cid;
|
|
65
|
+
if (!expectedCid)
|
|
66
|
+
throw new RoomsTasksConfigError(path, 'rooms.owner.expected_cid: required');
|
|
67
|
+
if (!CID_RE.test(expectedCid))
|
|
68
|
+
throw new RoomsTasksConfigError(path, 'rooms.owner.expected_cid: must be exactly 64 hexadecimal characters');
|
|
69
|
+
const ownerRole = raw.owner.role ?? 'Owner';
|
|
70
|
+
const invite = resolveInvite(raw.owner, vars, path);
|
|
71
|
+
const owner = {
|
|
72
|
+
provider: ownerProvider,
|
|
73
|
+
expected_cid: expectedCid.toLowerCase(),
|
|
74
|
+
role: ownerRole,
|
|
75
|
+
...(raw.owner.public_invite ? { public_invite: '[REDACTED]' } : {}),
|
|
76
|
+
...(raw.owner.public_invite_file ? { public_invite_file: raw.owner.public_invite_file } : {}),
|
|
77
|
+
};
|
|
78
|
+
let defaults;
|
|
79
|
+
if (raw.defaults !== undefined) {
|
|
80
|
+
if (!isPlainObject(raw.defaults))
|
|
81
|
+
throw new RoomsTasksConfigError(path, 'rooms.defaults: must be a mapping');
|
|
82
|
+
rejectUnknown(raw.defaults, RDK, path, 'rooms.defaults');
|
|
83
|
+
defaults = {
|
|
84
|
+
template: raw.defaults.template,
|
|
85
|
+
attach_owner: raw.defaults.attach_owner,
|
|
86
|
+
close_when_task_done: raw.defaults.close_when_task_done,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return { cowork, owner, defaults, _invite: invite };
|
|
90
|
+
}
|
|
91
|
+
export function validateTasksConfig(raw, path) {
|
|
92
|
+
if (!isPlainObject(raw))
|
|
93
|
+
throw new RoomsTasksConfigError(path, 'tasks: must be a mapping');
|
|
94
|
+
rejectUnknown(raw, TK, path, 'tasks');
|
|
95
|
+
const createMode = raw.create_mode;
|
|
96
|
+
if (createMode !== undefined && createMode !== 'start' && createMode !== 'backlog')
|
|
97
|
+
throw new RoomsTasksConfigError(path, `tasks.create_mode: must be 'start' or 'backlog'`);
|
|
98
|
+
if (raw.close_room_on_done !== undefined && typeof raw.close_room_on_done !== 'boolean')
|
|
99
|
+
throw new RoomsTasksConfigError(path, 'tasks.close_room_on_done: must be a boolean');
|
|
100
|
+
if (raw.retain_completed_for !== undefined && typeof raw.retain_completed_for !== 'string')
|
|
101
|
+
throw new RoomsTasksConfigError(path, 'tasks.retain_completed_for: must be a duration string');
|
|
102
|
+
return {
|
|
103
|
+
default_room_template: raw.default_room_template,
|
|
104
|
+
create_mode: createMode,
|
|
105
|
+
close_room_on_done: raw.close_room_on_done,
|
|
106
|
+
retain_completed_for: raw.retain_completed_for,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function validateRoomTemplatesConfig(raw, path) {
|
|
110
|
+
if (!isPlainObject(raw))
|
|
111
|
+
throw new RoomsTasksConfigError(path, 'room_templates: must be a mapping');
|
|
112
|
+
const result = {};
|
|
113
|
+
for (const [name, tplRaw] of Object.entries(raw)) {
|
|
114
|
+
if (!/^[a-z0-9_-]+$/.test(name))
|
|
115
|
+
throw new RoomsTasksConfigError(path, `room_templates: invalid template name '${name}' (allowed: [a-z0-9_-])`);
|
|
116
|
+
if (!isPlainObject(tplRaw))
|
|
117
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}: must be a mapping`);
|
|
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
|
+
const version = tplRaw.version;
|
|
123
|
+
if (version === undefined || !Number.isInteger(version) || version < 1)
|
|
124
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.version: required positive integer`);
|
|
125
|
+
if (!tplRaw.description || typeof tplRaw.description !== 'string')
|
|
126
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.description: required string`);
|
|
127
|
+
if (!Array.isArray(tplRaw.members) || !tplRaw.members.length)
|
|
128
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members: required non-empty array`);
|
|
129
|
+
const members = tplRaw.members.map((m, i) => {
|
|
130
|
+
if (!isPlainObject(m))
|
|
131
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}]: must be a mapping`);
|
|
132
|
+
rejectUnknown(m, TMK, path, `room_templates.${name}.members[${i}]`);
|
|
133
|
+
if (!m.slot || typeof m.slot !== 'string')
|
|
134
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].slot: required string`);
|
|
135
|
+
if (!m.role || typeof m.role !== 'string')
|
|
136
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].role: required string`);
|
|
137
|
+
if (!Number.isInteger(m.count) || m.count < 1)
|
|
138
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].count: required positive integer`);
|
|
139
|
+
if (!m.role_ref || typeof m.role_ref !== 'string')
|
|
140
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].role_ref: required string`);
|
|
141
|
+
if (m.overrides !== undefined) {
|
|
142
|
+
if (!isPlainObject(m.overrides))
|
|
143
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].overrides: must be a mapping`);
|
|
144
|
+
rejectUnknown(m.overrides, TOK, path, `room_templates.${name}.members[${i}].overrides`);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
slot: m.slot,
|
|
148
|
+
role: m.role,
|
|
149
|
+
count: m.count,
|
|
150
|
+
role_ref: m.role_ref,
|
|
151
|
+
overrides: m.overrides,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
let room;
|
|
155
|
+
if (tplRaw.room !== undefined) {
|
|
156
|
+
if (!isPlainObject(tplRaw.room))
|
|
157
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.room: must be a mapping`);
|
|
158
|
+
room = {
|
|
159
|
+
quiet_membership: tplRaw.room.quiet_membership,
|
|
160
|
+
anonymous: tplRaw.room.anonymous,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
result[name] = {
|
|
164
|
+
name,
|
|
165
|
+
version: version,
|
|
166
|
+
description: tplRaw.description,
|
|
167
|
+
room,
|
|
168
|
+
contract: tplRaw.contract,
|
|
169
|
+
members,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
export const FLEET_D_ALLOWED_KEYS = ['roles', 'rooms', 'room_templates', 'tasks'];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed client for the ours-cowork v1 private management-socket protocol.
|
|
3
|
+
*
|
|
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.
|
|
6
|
+
*/
|
|
7
|
+
import type { Socket } from 'node:net';
|
|
8
|
+
export interface CoworkRoomCreateResult {
|
|
9
|
+
room_id: string;
|
|
10
|
+
identity_name: string;
|
|
11
|
+
identity_cid: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CoworkInviteAcceptResult {
|
|
14
|
+
seat_cid: string;
|
|
15
|
+
seat_state: 'pending' | 'active';
|
|
16
|
+
}
|
|
17
|
+
export interface CoworkInviteResult {
|
|
18
|
+
invite: string;
|
|
19
|
+
min_accepts: number;
|
|
20
|
+
}
|
|
21
|
+
export interface CoworkSeatInfo {
|
|
22
|
+
identity_cid: string;
|
|
23
|
+
role: string;
|
|
24
|
+
seat_state: 'pending' | 'active' | 'removed';
|
|
25
|
+
}
|
|
26
|
+
export interface CoworkRoomInfo {
|
|
27
|
+
room_id: string;
|
|
28
|
+
identity_name: string;
|
|
29
|
+
identity_cid: string;
|
|
30
|
+
room_name: string;
|
|
31
|
+
state: 'provisioning' | 'active' | 'closing' | 'closed';
|
|
32
|
+
seats: CoworkSeatInfo[];
|
|
33
|
+
goal?: string;
|
|
34
|
+
briefing?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface CoworkAdapter {
|
|
37
|
+
available(): Promise<boolean>;
|
|
38
|
+
createRoom(opts: {
|
|
39
|
+
room_name: string;
|
|
40
|
+
goal: string;
|
|
41
|
+
briefing: string;
|
|
42
|
+
quiet_membership?: boolean;
|
|
43
|
+
anonymous?: boolean;
|
|
44
|
+
}): Promise<CoworkRoomCreateResult>;
|
|
45
|
+
acceptInvite(roomId: string, invite: string, opts: {
|
|
46
|
+
role: string;
|
|
47
|
+
expected_cid: string;
|
|
48
|
+
}): Promise<CoworkInviteAcceptResult>;
|
|
49
|
+
issueInvite(roomId: string, opts: {
|
|
50
|
+
role: string;
|
|
51
|
+
min_accepts: number;
|
|
52
|
+
}): Promise<CoworkInviteResult>;
|
|
53
|
+
getRoom(roomId: string): Promise<CoworkRoomInfo | undefined>;
|
|
54
|
+
listRooms(): Promise<CoworkRoomInfo[]>;
|
|
55
|
+
closeRoom(roomId: string): Promise<void>;
|
|
56
|
+
getSeats(roomId: string): Promise<CoworkSeatInfo[]>;
|
|
57
|
+
recoverRoom(roomId: string): Promise<CoworkRoomInfo>;
|
|
58
|
+
}
|
|
59
|
+
export declare class CoworkUnavailableError extends Error {
|
|
60
|
+
constructor(message?: string, options?: ErrorOptions);
|
|
61
|
+
}
|
|
62
|
+
export declare class CoworkProtocolError extends Error {
|
|
63
|
+
readonly operation: string;
|
|
64
|
+
readonly code: string;
|
|
65
|
+
constructor(operation: string, message: string, code?: string, options?: ErrorOptions);
|
|
66
|
+
}
|
|
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;
|