@ours.network/fleet 1.2.0-nightly.54 → 1.2.0-nightly.55
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 +20 -12
- package/dist/agent-ours/service.d.ts +4 -1
- package/dist/agent-ours/service.js +31 -7
- package/dist/application/log-service.js +8 -8
- package/dist/application/role-creation-service.js +2 -1
- package/dist/application/role-removal-service.js +7 -1
- package/dist/application/role-repository.js +17 -0
- package/dist/application/task-layouts.js +12 -2
- package/dist/application/task-room-service.js +19 -17
- package/dist/application/types.d.ts +2 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +31 -3
- package/dist/config.d.ts +2 -0
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/codex-session.js +2 -2
- package/dist/managed-recovery.d.ts +13 -0
- package/dist/managed-recovery.js +31 -0
- package/dist/ops.d.ts +2 -0
- package/dist/ops.js +15 -2
- package/dist/rooms-tasks/cli.js +21 -0
- package/dist/rooms-tasks/close.d.ts +2 -0
- package/dist/rooms-tasks/close.js +66 -1
- package/dist/rooms-tasks/deletion.js +10 -0
- package/dist/rooms-tasks/erasure.js +4 -2
- package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
- package/dist/rooms-tasks/layout-member-retirement.js +90 -0
- package/dist/rooms-tasks/layout-service.d.ts +1 -0
- package/dist/rooms-tasks/layout-service.js +31 -13
- package/dist/rooms-tasks/layout.d.ts +7 -0
- package/dist/rooms-tasks/layout.js +35 -0
- package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
- package/dist/rooms-tasks/legacy-task-member.js +116 -0
- package/dist/rooms-tasks/member-ownership.d.ts +5 -0
- package/dist/rooms-tasks/member-ownership.js +40 -0
- package/dist/rooms-tasks/provision.js +26 -1
- package/dist/rooms-tasks/recovery.d.ts +19 -0
- package/dist/rooms-tasks/recovery.js +110 -0
- package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
- package/dist/rooms-tasks/task-service-retirement.js +21 -0
- package/dist/rooms-tasks/task-state.d.ts +1 -0
- package/dist/rooms-tasks/task-state.js +16 -1
- package/dist/rooms-tasks/types.d.ts +3 -0
- package/dist/rooms-tasks/workspace.d.ts +2 -0
- package/dist/rooms-tasks/workspace.js +5 -0
- package/dist/runner.d.ts +6 -0
- package/dist/runner.js +160 -22
- package/dist/session/acp.d.ts +1 -0
- package/dist/session/acp.js +7 -0
- package/dist/session/arbiter.d.ts +5 -0
- package/dist/session/arbiter.js +16 -2
- package/dist/session/codex-app-server.d.ts +1 -0
- package/dist/session/codex-app-server.js +5 -0
- package/dist/session/types.d.ts +2 -0
- package/dist/spawn.d.ts +5 -1
- package/dist/spawn.js +32 -3
- package/dist/startup-readiness.d.ts +1 -1
- package/dist/startup-readiness.js +3 -3
- package/dist/supervisor/adoption.d.ts +8 -0
- package/dist/supervisor/adoption.js +78 -0
- package/dist/supervisor/catalog.d.ts +62 -0
- package/dist/supervisor/catalog.js +254 -0
- package/dist/supervisor/fleet.d.ts +8 -0
- package/dist/supervisor/fleet.js +89 -0
- package/dist/supervisor/index.js +3 -6
- package/dist/supervisor/launchd.d.ts +1 -1
- package/dist/supervisor/launchd.js +24 -12
- package/dist/supervisor/legacy.d.ts +35 -0
- package/dist/supervisor/legacy.js +524 -0
- package/dist/supervisor/manager.d.ts +15 -0
- package/dist/supervisor/manager.js +258 -0
- package/dist/supervisor/scope.d.ts +4 -0
- package/dist/supervisor/scope.js +10 -0
- package/dist/supervisor/systemd.d.ts +2 -2
- package/dist/supervisor/systemd.js +14 -14
- package/dist/task-supervision.d.ts +9 -0
- package/dist/task-supervision.js +145 -0
- package/dist/temp-lifecycle.d.ts +17 -7
- package/dist/temp-lifecycle.js +85 -53
- package/dist/temp-supervisor-recovery.d.ts +1 -1
- package/dist/temp-supervisor-recovery.js +2 -2
- package/docs/fleet-migration.md +54 -0
- package/docs/task-workspaces.md +26 -0
- package/package.json +4 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { detachDeletedRoom } from './task-state.js';
|
|
2
2
|
import { binderKey } from '../agent-ours/state.js';
|
|
3
|
+
import { assertMemberNotPermanent } from './member-ownership.js';
|
|
4
|
+
import { retireTaskMemberService } from './task-service-retirement.js';
|
|
3
5
|
import { eraseMemberArtifacts } from './erasure.js';
|
|
4
6
|
import { deleteWorkspace, assertWorkspaceDeletable } from './workspace.js';
|
|
5
7
|
import { collectWorkspaceArchives } from './workspace-artifacts.js';
|
|
6
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
7
9
|
import { join } from 'node:path';
|
|
8
10
|
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import { parse } from 'yaml';
|
|
12
|
+
import { privateRuntimeRoot, releaseManagedAgent } from '../agent-ours/service.js';
|
|
9
13
|
import { attachOursClient } from '@ours.network/sdk/client';
|
|
10
14
|
import { withFileLock } from '../atomic-file.js';
|
|
11
15
|
import { agentDir, stateRoot } from '../paths.js';
|
|
@@ -48,6 +52,7 @@ function exactMemberIdentity(seat) {
|
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
export async function inspectMember(seat) {
|
|
55
|
+
assertMemberNotPermanent(seat.role_name);
|
|
51
56
|
exactMemberIdentity(seat);
|
|
52
57
|
const supervisor = readTempSupervisor(agentDir(seat.role_name, true));
|
|
53
58
|
if (!supervisor || supervisor.role !== seat.role_name
|
|
@@ -105,7 +110,46 @@ export async function identityCidPresent(cid) {
|
|
|
105
110
|
return rows.some(row => 'cid' in row && row.cid.toLowerCase() === cid.toLowerCase());
|
|
106
111
|
});
|
|
107
112
|
}
|
|
113
|
+
/** A managed temporary identity belongs to its durable external lease even
|
|
114
|
+
* after worker exit. Retire that exact logical owner before generic removal. */
|
|
115
|
+
async function releaseRetainedMemberRuntime(seat) {
|
|
116
|
+
const live = agentDir(seat.role_name, true);
|
|
117
|
+
const source = existsSync(live) ? live
|
|
118
|
+
: (seat.launch?.launch_id ? tempArchiveForLaunch(seat.role_name, seat.launch.launch_id) : undefined);
|
|
119
|
+
if (!source) {
|
|
120
|
+
if (seat.launch?.task_supervised)
|
|
121
|
+
throw Error('MEMBER_RUNTIME_RETIREMENT_SOURCE_MISSING');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const path = join(source, 'role.yaml');
|
|
125
|
+
if (!existsSync(path))
|
|
126
|
+
return; // pre-managed compatibility evidence has no runtime
|
|
127
|
+
const stat = lstatSync(path);
|
|
128
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024)
|
|
129
|
+
throw Error('MEMBER_RUNTIME_UNSAFE_PROOF');
|
|
130
|
+
const role = parse(readFileSync(path, 'utf8'));
|
|
131
|
+
const profile = readClientProfile({ ...process.env, ...role.env });
|
|
132
|
+
const runtime = join(privateRuntimeRoot(), binderKey(profile.expectedInstanceId, seat.role_name));
|
|
133
|
+
const metadata = readTempSupervisor(source);
|
|
134
|
+
if (!existsSync(join(runtime, 'instance.json'))) {
|
|
135
|
+
if (metadata?.kind === 'fleet-managed')
|
|
136
|
+
throw Error('MEMBER_RUNTIME_RETIREMENT_MISSING');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const creationPath = join(source, 'creation.json'), creationStat = lstatSync(creationPath);
|
|
140
|
+
if (!creationStat.isFile() || creationStat.isSymbolicLink() || creationStat.size > 256 * 1024)
|
|
141
|
+
throw Error('MEMBER_RUNTIME_UNSAFE_PROOF');
|
|
142
|
+
const creation = JSON.parse(readFileSync(creationPath, 'utf8'));
|
|
143
|
+
if (role.name !== seat.role_name || role.identity !== seat.role_name || !seat.identity_cid
|
|
144
|
+
|| !seat.launch?.action_id || !seat.launch.launch_id || creation.role !== seat.role_name
|
|
145
|
+
|| creation.creationActionId !== seat.launch.action_id || metadata?.role !== seat.role_name
|
|
146
|
+
|| metadata.launchId !== seat.launch.launch_id
|
|
147
|
+
|| (metadata.taskOwner && metadata.taskOwner.creationActionId !== seat.launch.action_id))
|
|
148
|
+
throw Error('MEMBER_RUNTIME_RETIREMENT_MISMATCH');
|
|
149
|
+
await releaseManagedAgent(role, { cid: seat.identity_cid, action: seat.launch.action_id });
|
|
150
|
+
}
|
|
108
151
|
export async function removeExactMemberIdentity(seat) {
|
|
152
|
+
assertMemberNotPermanent(seat.role_name);
|
|
109
153
|
await withIdentityClient(async (client) => {
|
|
110
154
|
const rows = await client.listIdentities();
|
|
111
155
|
const before = listedIdentity(rows, seat.role_name);
|
|
@@ -113,6 +157,11 @@ export async function removeExactMemberIdentity(seat) {
|
|
|
113
157
|
if (rows.some(row => row.name === seat.role_name || (seat.identity_cid && 'cid' in row
|
|
114
158
|
&& row.cid.toLowerCase() === seat.identity_cid.toLowerCase())))
|
|
115
159
|
throw new Error(`room member '${seat.role_name}' identity absence is not proven`);
|
|
160
|
+
// A terminal release can remove the SDK identity before its response is
|
|
161
|
+
// recorded. Finish the exact retained launch's release journal on retry.
|
|
162
|
+
if (existsSync(agentDir(seat.role_name, true)) || (seat.launch?.launch_id
|
|
163
|
+
&& tempArchiveForLaunch(seat.role_name, seat.launch.launch_id)))
|
|
164
|
+
await releaseRetainedMemberRuntime(seat);
|
|
116
165
|
return;
|
|
117
166
|
}
|
|
118
167
|
if (!seat.identity_cid) {
|
|
@@ -121,7 +170,9 @@ export async function removeExactMemberIdentity(seat) {
|
|
|
121
170
|
if (before.cid?.toLowerCase() !== seat.identity_cid.toLowerCase()) {
|
|
122
171
|
throw new Error(`room member '${seat.role_name}' identity absence is not proven: CID mismatch: recorded ${seat.identity_cid}, found ${before.cid ?? 'none'}`);
|
|
123
172
|
}
|
|
173
|
+
await releaseRetainedMemberRuntime(seat);
|
|
124
174
|
try {
|
|
175
|
+
assertMemberNotPermanent(seat.role_name);
|
|
125
176
|
await client.removeIdentity({ name: seat.role_name });
|
|
126
177
|
}
|
|
127
178
|
catch (error) {
|
|
@@ -150,9 +201,19 @@ export async function assertMemberIdentityAbsent(seat) {
|
|
|
150
201
|
}
|
|
151
202
|
});
|
|
152
203
|
}
|
|
204
|
+
async function retireMemberRegistration(room, current, deps) {
|
|
205
|
+
if (room.task_id)
|
|
206
|
+
await (deps.retireTaskService ?? retireTaskMemberService)(current.role_name, {
|
|
207
|
+
taskId: room.task_id, creationActionId: current.launch?.action_id,
|
|
208
|
+
taskSupervised: current.launch?.task_supervised,
|
|
209
|
+
...(current.launch?.launch_id ? { launchId: current.launch.launch_id } : {}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
153
212
|
async function retireMember(roomId, seat, deps) {
|
|
213
|
+
assertMemberNotPermanent(seat.role_name);
|
|
154
214
|
let room = getRoomRecord(roomId);
|
|
155
215
|
let current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
216
|
+
await retireMemberRegistration(room, current, deps);
|
|
156
217
|
let retirement = current.retirement;
|
|
157
218
|
if (retirement?.phase === 'identity_absent') {
|
|
158
219
|
if (!deps.inspectMember) {
|
|
@@ -250,6 +311,10 @@ export async function closeManagedRoom(input) {
|
|
|
250
311
|
return lock(roomCloseLockPath(input.roomId), async () => {
|
|
251
312
|
let room = beginRoomClose(input.roomId);
|
|
252
313
|
if (room.state === 'closed') {
|
|
314
|
+
for (const seat of room.member_seats) {
|
|
315
|
+
assertMemberNotPermanent(seat.role_name);
|
|
316
|
+
await retireMemberRegistration(room, seat, deps);
|
|
317
|
+
}
|
|
253
318
|
if (!deps.inspectMember)
|
|
254
319
|
for (const seat of room.member_seats) {
|
|
255
320
|
if (existsSync(agentDir(seat.role_name, true)))
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { eraseMemberArtifacts } from './erasure.js';
|
|
2
|
+
import { assertMemberNotPermanent } from './member-ownership.js';
|
|
3
|
+
import { retireTaskMemberService } from './task-service-retirement.js';
|
|
4
|
+
import { assertTaskRegistrationsAbsent } from '../supervisor/catalog.js';
|
|
2
5
|
import { proveArchivedAbsence, verifyArchivedAbsence, verifyArchivedMemberStillAbsent } from './archived-absence.js';
|
|
3
6
|
import { existsSync } from 'node:fs';
|
|
4
7
|
import { withFileLock } from '../atomic-file.js';
|
|
@@ -64,6 +67,12 @@ function cursorSeat(cursor) {
|
|
|
64
67
|
* the recorded CID surviving under any name blocks settlement.
|
|
65
68
|
*/
|
|
66
69
|
async function retireCursorMember(taskId, cursor, deps) {
|
|
70
|
+
assertMemberNotPermanent(cursor.name);
|
|
71
|
+
await (deps.roomClose?.retireTaskService ?? retireTaskMemberService)(cursor.name, { taskId,
|
|
72
|
+
creationActionId: cursor.action_id,
|
|
73
|
+
taskSupervised: cursor.task_supervised,
|
|
74
|
+
...((cursor.launch_id && !['never-launched', 'absent-verified'].includes(cursor.launch_id)) ? { launchId: cursor.launch_id } : {}),
|
|
75
|
+
});
|
|
67
76
|
let phase = cursor.phase;
|
|
68
77
|
let launchId = cursor.launch_id;
|
|
69
78
|
if (phase === 'identity_absent') {
|
|
@@ -249,6 +258,7 @@ export async function settleTaskDeletion(input) {
|
|
|
249
258
|
}
|
|
250
259
|
if (cleanup.snapshotHash)
|
|
251
260
|
releaseLaunchSnapshotForDeletingTask(cleanup.snapshotHash, taskId);
|
|
261
|
+
assertTaskRegistrationsAbsent(taskId);
|
|
252
262
|
await eraseMemberArtifacts('task', taskId, task.deletion.members.map(cursorSeat), task.deletion.room_id ? [task.deletion.room_id] : []);
|
|
253
263
|
if (task.workspace) {
|
|
254
264
|
assertWorkspaceDeletable(task.workspace, 'task', taskId);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { eraseResourcePresentations } from '../erased-resources.js';
|
|
2
|
+
import { assertMemberNotPermanent } from './member-ownership.js';
|
|
2
3
|
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync } from 'node:fs';
|
|
3
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
5
|
import { dirname, join, relative } from 'node:path';
|
|
5
6
|
import { parse } from 'yaml';
|
|
6
7
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
7
8
|
import { agentDir, stateRoot } from '../paths.js';
|
|
8
|
-
import {
|
|
9
|
+
import { readTempSupervisorForScan, tempSupervisorLiveness, eraseTerminationEvents } from '../temp-lifecycle.js';
|
|
9
10
|
import { assertSafeAncestors, auditWorkspaceGit, prepareOwnedDeletionTree } from './workspace.js';
|
|
10
11
|
function entries(path) {
|
|
11
12
|
try {
|
|
@@ -58,6 +59,7 @@ export async function eraseMemberArtifacts(owner, id, seats, roomIds) {
|
|
|
58
59
|
for (const name of names) {
|
|
59
60
|
if (!/^[a-zA-Z0-9_-]{1,120}$/.test(name))
|
|
60
61
|
throw new Error('Invalid erasure member');
|
|
62
|
+
assertMemberNotPermanent(name);
|
|
61
63
|
if (existsSync(agentDir(name, true)))
|
|
62
64
|
throw new Error(`Member '${name}' still has live state during erasure`);
|
|
63
65
|
}
|
|
@@ -75,7 +77,7 @@ export async function eraseMemberArtifacts(owner, id, seats, roomIds) {
|
|
|
75
77
|
const stat = lstatSync(path);
|
|
76
78
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
77
79
|
continue;
|
|
78
|
-
const supervisor =
|
|
80
|
+
const supervisor = readTempSupervisorForScan(path);
|
|
79
81
|
if (!supervisor || !names.has(supervisor.role))
|
|
80
82
|
continue;
|
|
81
83
|
const seat = seats.find(s => s.role_name === supervisor.role);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type TempLifecycleDeps } from '../temp-lifecycle.js';
|
|
2
|
+
import type { LayoutInstance, RoomLayoutState } from './layout.js';
|
|
3
|
+
import type { RoomMemberSeat } from './types.js';
|
|
4
|
+
export declare function layoutOwnedMemberName(runId: string, key: string): string;
|
|
5
|
+
/** Retire a local layout factory result, never a borrowed binding. The runtime
|
|
6
|
+
* instance and CID fence replacement supervisors even when already stopped.
|
|
7
|
+
*/
|
|
8
|
+
export declare function retireOwnedLayoutMember(instance: LayoutInstance, runId: string, key: string, lifecycle?: TempLifecycleDeps, taskSupervised?: boolean): Promise<void>;
|
|
9
|
+
/** Exact archived launches used by the existing artifact erasure saga. */
|
|
10
|
+
export declare function ownedLayoutRetirementSeats(state: RoomLayoutState, runId: string): RoomMemberSeat[];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
import { binderKey } from '../agent-ours/state.js';
|
|
6
|
+
import { readClientProfile } from '../client-profile.js';
|
|
7
|
+
import { readProvenance } from '../creation.js';
|
|
8
|
+
import { agentDir, stateRoot } from '../paths.js';
|
|
9
|
+
import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempArchiveForCreationAction } from '../temp-lifecycle.js';
|
|
10
|
+
import { assertMemberIdentityAbsent, removeExactMemberIdentity, waitForLivenessAbsent } from './close.js';
|
|
11
|
+
import { assertMemberNotPermanent } from './member-ownership.js';
|
|
12
|
+
import { retireTaskMemberService } from './task-service-retirement.js';
|
|
13
|
+
function proof(path) {
|
|
14
|
+
const stat = lstatSync(path);
|
|
15
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024)
|
|
16
|
+
throw Error('LAYOUT_MEMBER_UNSAFE_PROOF');
|
|
17
|
+
return readFileSync(path, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
export function layoutOwnedMemberName(runId, key) {
|
|
20
|
+
return `layout-${createHash('sha256').update(runId + ':' + key).digest('hex').slice(0, 16)}`;
|
|
21
|
+
}
|
|
22
|
+
function assertOwned(instance, runId, key) {
|
|
23
|
+
if (instance.remote || !instance.agent || instance.temporary !== true
|
|
24
|
+
|| instance.agent !== layoutOwnedMemberName(runId, key) || instance.supervisor !== stateRoot())
|
|
25
|
+
throw Error('LAYOUT_MEMBER_NOT_OWNED');
|
|
26
|
+
assertMemberNotPermanent(instance.agent);
|
|
27
|
+
}
|
|
28
|
+
function seatFor(instance, key, action, launchId) {
|
|
29
|
+
return { role_name: instance.agent, identity_cid: instance.cid, slot: key, cowork_role: key, seat_state: 'removed',
|
|
30
|
+
launch: { state: 'stopped', action_id: action, launch_id: launchId, attempt: 1, updated_at: '' } };
|
|
31
|
+
}
|
|
32
|
+
/** Retire a local layout factory result, never a borrowed binding. The runtime
|
|
33
|
+
* instance and CID fence replacement supervisors even when already stopped.
|
|
34
|
+
*/
|
|
35
|
+
export async function retireOwnedLayoutMember(instance, runId, key, lifecycle = {}, taskSupervised = false) {
|
|
36
|
+
assertOwned(instance, runId, key);
|
|
37
|
+
const action = `${runId}:${key}`, dir = agentDir(instance.agent, true);
|
|
38
|
+
const archived = tempArchiveForCreationAction(instance.agent, action);
|
|
39
|
+
if (!existsSync(dir) && !archived) {
|
|
40
|
+
if (runId.startsWith('task-'))
|
|
41
|
+
await retireTaskMemberService(instance.agent, { taskId: runId.slice(5), creationActionId: action, taskSupervised }, lifecycle.exec);
|
|
42
|
+
// A prior erasure may already have consumed its archive. No live state plus
|
|
43
|
+
// authoritative identity absence is required before accepting completion.
|
|
44
|
+
await assertMemberIdentityAbsent({ role_name: instance.agent, identity_cid: instance.cid,
|
|
45
|
+
slot: key, cowork_role: key, seat_state: 'removed' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const source = existsSync(dir) ? dir : archived.path;
|
|
49
|
+
const supervisor = readTempSupervisor(source), provenance = readProvenance(source);
|
|
50
|
+
if (!supervisor || supervisor.role !== instance.agent || provenance?.role !== instance.agent
|
|
51
|
+
|| provenance.creationActionId !== action || proof(join(source, '.identity')).trim() !== instance.agent)
|
|
52
|
+
throw Error('LAYOUT_MEMBER_LAUNCH_MISMATCH');
|
|
53
|
+
{
|
|
54
|
+
const role = parse(proof(join(source, 'role.yaml')));
|
|
55
|
+
if (role?.name !== instance.agent || role.identity !== instance.agent)
|
|
56
|
+
throw Error('LAYOUT_MEMBER_ROLE_MISMATCH');
|
|
57
|
+
const profile = readClientProfile({ ...process.env, ...role.env });
|
|
58
|
+
const privateDir = join(stateRoot(), 'private-ours', binderKey(profile.expectedInstanceId, instance.agent));
|
|
59
|
+
const runtime = JSON.parse(proof(join(privateDir, 'state.json')));
|
|
60
|
+
const runtimeInstance = JSON.parse(proof(join(privateDir, 'instance.json')));
|
|
61
|
+
if (runtime.instance !== instance.launch || runtimeInstance.instance !== instance.launch
|
|
62
|
+
|| runtimeInstance.role !== instance.agent || runtimeInstance.temporary !== true
|
|
63
|
+
|| runtime.name !== instance.agent || runtime.daemon !== profile.expectedInstanceId
|
|
64
|
+
|| runtime.lifetime !== 'temporary' || runtime.action !== action
|
|
65
|
+
|| runtime.cid?.toLowerCase() !== instance.cid.toLowerCase())
|
|
66
|
+
throw Error('LAYOUT_MEMBER_INSTANCE_MISMATCH');
|
|
67
|
+
if (runId.startsWith('task-'))
|
|
68
|
+
await retireTaskMemberService(instance.agent, { taskId: runId.slice(5), creationActionId: action, launchId: supervisor.launchId, taskSupervised }, lifecycle.exec);
|
|
69
|
+
if (existsSync(dir)) {
|
|
70
|
+
await stopTempSupervisor(instance.agent, lifecycle);
|
|
71
|
+
await waitForLivenessAbsent(instance.agent, supervisor.launchId, lifecycle);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
await secureStoppedTempArchive(instance.agent, supervisor.launchId, lifecycle);
|
|
75
|
+
await removeExactMemberIdentity(seatFor(instance, key, action, supervisor.launchId));
|
|
76
|
+
await assertMemberIdentityAbsent(seatFor(instance, key, action, supervisor.launchId));
|
|
77
|
+
}
|
|
78
|
+
/** Exact archived launches used by the existing artifact erasure saga. */
|
|
79
|
+
export function ownedLayoutRetirementSeats(state, runId) {
|
|
80
|
+
return Object.entries(state.participants).flatMap(([key, participant]) => {
|
|
81
|
+
if (!participant.owned || !participant.instance)
|
|
82
|
+
return [];
|
|
83
|
+
const instance = participant.instance;
|
|
84
|
+
assertOwned(instance, runId, key);
|
|
85
|
+
if (!participant.retired)
|
|
86
|
+
throw Error('LAYOUT_MEMBER_NOT_RETIRED');
|
|
87
|
+
const action = `${runId}:${key}`, archive = tempArchiveForCreationAction(instance.agent, action);
|
|
88
|
+
return [seatFor(instance, key, action, archive?.launchId ?? 'absent-verified')];
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -5,6 +5,7 @@ export declare class NativeLayoutSupervisor implements LayoutSupervisor {
|
|
|
5
5
|
private configPath;
|
|
6
6
|
private runId;
|
|
7
7
|
private templates;
|
|
8
|
+
readonly taskSupervised: boolean;
|
|
8
9
|
readonly id: string;
|
|
9
10
|
constructor(configPath: string | undefined, runId: string, templates: Record<string, AgentTemplateDefinition>);
|
|
10
11
|
private request;
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { randomUUID
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { loadConfig, loadRoomsConfig, splitRootFor } from '../config.js';
|
|
6
6
|
import { agentDir, stateRoot, defaultConfigPath } from '../paths.js';
|
|
7
7
|
import { controlRequest } from '../session/control.js';
|
|
8
8
|
import { spawnTemp } from '../spawn.js';
|
|
9
|
-
import { tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
10
9
|
import { layoutSupervisorId } from './layout-control.js';
|
|
11
10
|
import { RoomLayout } from './layout.js';
|
|
12
11
|
import { assertLayoutFile, validLayoutKey, readRoomLayout, readRoomLayouts } from './layout-config.js';
|
|
13
12
|
import { createCoworkAdapter } from './cowork-adapter.js';
|
|
14
13
|
import { callRemoteLayoutBinding } from './layout-binding-client.js';
|
|
15
14
|
import { readClientProfile } from '../client-profile.js';
|
|
15
|
+
import { tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
16
|
+
import { layoutOwnedMemberName, retireOwnedLayoutMember } from './layout-member-retirement.js';
|
|
16
17
|
export class NativeLayoutSupervisor {
|
|
17
18
|
configPath;
|
|
18
19
|
runId;
|
|
19
20
|
templates;
|
|
21
|
+
taskSupervised;
|
|
20
22
|
id;
|
|
21
23
|
constructor(configPath, runId, templates) {
|
|
22
24
|
this.configPath = configPath;
|
|
@@ -24,6 +26,7 @@ export class NativeLayoutSupervisor {
|
|
|
24
26
|
this.templates = templates;
|
|
25
27
|
mkdirSync(stateRoot(), { recursive: true, mode: 0o700 });
|
|
26
28
|
this.id = layoutSupervisorId();
|
|
29
|
+
this.taskSupervised = runId.startsWith('task-') && process.env.OURS_FLEET_SUPERVISOR !== 'none';
|
|
27
30
|
}
|
|
28
31
|
async request(agent, temporary, request) {
|
|
29
32
|
if (!/^[A-Za-z0-9_-]+$/.test(agent))
|
|
@@ -48,8 +51,14 @@ export class NativeLayoutSupervisor {
|
|
|
48
51
|
const definition = this.templates[template];
|
|
49
52
|
if (!definition)
|
|
50
53
|
throw Error(`agent template not found in run snapshot: ${template}`);
|
|
51
|
-
const name =
|
|
54
|
+
const name = layoutOwnedMemberName(this.runId, key);
|
|
55
|
+
const taskId = this.runId.startsWith('task-') ? this.runId.slice(5) : undefined;
|
|
56
|
+
const task = taskId ? (await import('./task-state.js')).getTask(taskId) : undefined;
|
|
57
|
+
if (task && task.layout?.run_id !== this.runId)
|
|
58
|
+
throw Error('TASK_LAYOUT_OWNER_MISMATCH');
|
|
52
59
|
await spawnTemp({ name, temp: true, configPath: this.configPath, agentDefinition: definition,
|
|
60
|
+
...(task ? { taskOwner: { taskId: task.task_id, layout: { runId: this.runId, participant: key },
|
|
61
|
+
creationActionId: `${this.runId}:${key}` } } : {}),
|
|
53
62
|
creationActionId: `${this.runId}:${key}`, surface: 'cli' }, fileURLToPath(new URL('../cli.js', import.meta.url)));
|
|
54
63
|
const deadline = Date.now() + 120_000;
|
|
55
64
|
let last;
|
|
@@ -90,17 +99,26 @@ export class NativeLayoutSupervisor {
|
|
|
90
99
|
throw Error('cannot retire a borrowed cross-Fleet instance');
|
|
91
100
|
if (i.supervisor !== this.id || !i.agent || !/^[A-Za-z0-9_-]+$/.test(i.agent) || i.temporary !== true)
|
|
92
101
|
throw Error('invalid local temporary layout instance');
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
if (!this.runId.startsWith('task-')) {
|
|
103
|
+
const dir = agentDir(i.agent, true);
|
|
104
|
+
const stopped = async () => !existsSync(dir) || await tempSupervisorLiveness(dir) === 'stopped';
|
|
105
|
+
if (await stopped())
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
await this.call(i, { action: 'retire' });
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (!await stopped())
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
96
114
|
return;
|
|
97
|
-
try {
|
|
98
|
-
await this.call(i, { action: 'retire' });
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
if (!await stopped())
|
|
102
|
-
throw error;
|
|
103
115
|
}
|
|
116
|
+
const snapshot = JSON.parse(readFileSync(layoutRunPath(this.runId), 'utf8'));
|
|
117
|
+
const owned = Object.entries(snapshot.participants).filter(([, participant]) => participant.owned
|
|
118
|
+
&& participant.instance?.agent === i.agent && JSON.stringify(participant.instance) === JSON.stringify(i));
|
|
119
|
+
if (owned.length !== 1)
|
|
120
|
+
throw Error('LAYOUT_MEMBER_NOT_OWNED');
|
|
121
|
+
await retireOwnedLayoutMember(i, this.runId, owned[0][0], {}, owned[0][1].task_supervised);
|
|
104
122
|
}
|
|
105
123
|
}
|
|
106
124
|
export const layoutRunPath = (id) => {
|
|
@@ -49,6 +49,7 @@ export interface LayoutAssignment {
|
|
|
49
49
|
contract?: string;
|
|
50
50
|
}
|
|
51
51
|
export interface LayoutSupervisor {
|
|
52
|
+
readonly taskSupervised?: boolean;
|
|
52
53
|
id: string;
|
|
53
54
|
/** Must verify current launch, CID, live session and standalone ownership. */
|
|
54
55
|
verify(instance: LayoutInstance): Promise<void>;
|
|
@@ -67,6 +68,7 @@ type ParticipantState = {
|
|
|
67
68
|
retired?: boolean;
|
|
68
69
|
participant?: string;
|
|
69
70
|
room?: string;
|
|
71
|
+
task_supervised?: boolean;
|
|
70
72
|
};
|
|
71
73
|
type RoomState = {
|
|
72
74
|
spec: LayoutRoom;
|
|
@@ -74,6 +76,7 @@ type RoomState = {
|
|
|
74
76
|
ready: string[];
|
|
75
77
|
state: 'provisioning' | 'active' | 'closed';
|
|
76
78
|
owner_ready?: boolean;
|
|
79
|
+
deleted?: boolean;
|
|
77
80
|
};
|
|
78
81
|
export interface RoomLayoutState {
|
|
79
82
|
version: 1;
|
|
@@ -110,5 +113,9 @@ export declare class RoomLayout {
|
|
|
110
113
|
private archiveRoom;
|
|
111
114
|
closeRoom(actor: string, key: string): Promise<void>;
|
|
112
115
|
close(actor: string): Promise<void>;
|
|
116
|
+
/** Explicit deletion retains a per-room checkpoint until the run is erased.
|
|
117
|
+
* Finish/cancel continue to use close(), preserving archived rooms.
|
|
118
|
+
*/
|
|
119
|
+
delete(actor: string): Promise<void>;
|
|
113
120
|
}
|
|
114
121
|
export {};
|
|
@@ -6,6 +6,7 @@ import { createHash } from 'node:crypto';
|
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
7
|
import { canonicalJson } from '../canonical-json.js';
|
|
8
8
|
import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
|
|
9
|
+
import { CoworkProtocolError } from './cowork-adapter.js';
|
|
9
10
|
const ownerReference = (owner) => ({ cid: owner.cid, role: owner.role, fingerprint: createHash('sha256').update(owner.invite).digest('hex') });
|
|
10
11
|
const own = (o, key) => Object.hasOwn(o, key);
|
|
11
12
|
function need(ok, message) { if (!ok)
|
|
@@ -133,6 +134,8 @@ export class RoomLayout {
|
|
|
133
134
|
if (!p.instance) {
|
|
134
135
|
const template = s.definition.participants[member]?.agent_template;
|
|
135
136
|
need(template, `no factory for ${member}`);
|
|
137
|
+
if (this.supervisor.taskSupervised)
|
|
138
|
+
p.task_supervised = true;
|
|
136
139
|
await this.mutation(s, `spawn:${resolvedKey}`, () => this.supervisor.spawn(resolvedKey, template), i => { p.instance = i; });
|
|
137
140
|
await this.verify(p.instance);
|
|
138
141
|
}
|
|
@@ -247,4 +250,36 @@ export class RoomLayout {
|
|
|
247
250
|
this.save(s);
|
|
248
251
|
}, true);
|
|
249
252
|
}
|
|
253
|
+
/** Explicit deletion retains a per-room checkpoint until the run is erased.
|
|
254
|
+
* Finish/cancel continue to use close(), preserving archived rooms.
|
|
255
|
+
*/
|
|
256
|
+
async delete(actor) {
|
|
257
|
+
await this.close(actor);
|
|
258
|
+
await this.operation(actor, async (state) => {
|
|
259
|
+
for (const room of Object.values(state.rooms)) {
|
|
260
|
+
if (!room.native || room.deleted)
|
|
261
|
+
continue;
|
|
262
|
+
let remote;
|
|
263
|
+
try {
|
|
264
|
+
remote = await this.cowork.getRoom(room.native.room_id);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
if (remote) {
|
|
271
|
+
need(remote.identity_cid === room.native.identity_cid, 'native room identity changed during deletion');
|
|
272
|
+
try {
|
|
273
|
+
await this.cowork.deleteRoom(room.native.room_id);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
room.deleted = true;
|
|
281
|
+
this.save(state);
|
|
282
|
+
}
|
|
283
|
+
}, true);
|
|
284
|
+
}
|
|
250
285
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
2
|
+
export interface LegacyTaskOwner {
|
|
3
|
+
taskId: string;
|
|
4
|
+
roomId: string;
|
|
5
|
+
roomIdentityCid: string;
|
|
6
|
+
creationActionId: string;
|
|
7
|
+
}
|
|
8
|
+
interface AdoptionDeps {
|
|
9
|
+
liveness?: typeof tempSupervisorLiveness;
|
|
10
|
+
verifyIdentity?(name: string, cid: string): Promise<void>;
|
|
11
|
+
/** Crash seam: the role snapshot is durable before the supervisor owner. */
|
|
12
|
+
beforeOwnerCommit?(): void;
|
|
13
|
+
}
|
|
14
|
+
/** Caller holds task and room operation locks. No service is started here.
|
|
15
|
+
* Running legacy supervisors remain untouched; stopped exact state receives
|
|
16
|
+
* only ownership metadata, preserving launch, identity and conversation.
|
|
17
|
+
*/
|
|
18
|
+
export declare function adoptLegacyTaskMember(owner: LegacyTaskOwner, name: string, deps?: AdoptionDeps): Promise<'adopted' | 'running-legacy' | 'already-durable'>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { lstatSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { parse, stringify } from 'yaml';
|
|
5
|
+
import { attachOursClient } from '@ours.network/sdk/client';
|
|
6
|
+
import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
|
|
7
|
+
import { binderKey } from '../agent-ours/state.js';
|
|
8
|
+
import { readClientProfile } from '../client-profile.js';
|
|
9
|
+
import { readProvenance } from '../creation.js';
|
|
10
|
+
import { agentDir, stateRoot } from '../paths.js';
|
|
11
|
+
import { readTempSupervisor, tempSupervisorLiveness, TEMP_SUPERVISOR_FILE } from '../temp-lifecycle.js';
|
|
12
|
+
import { getTask } from './task-state.js';
|
|
13
|
+
import { getRoomRecord, updateMemberStartup } from './room-state.js';
|
|
14
|
+
import { assertMemberNotPermanent } from './member-ownership.js';
|
|
15
|
+
function bounded(path) {
|
|
16
|
+
const stat = lstatSync(path);
|
|
17
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024)
|
|
18
|
+
throw Error('LEGACY_TASK_MEMBER_UNSAFE_PROOF');
|
|
19
|
+
return readFileSync(path, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
async function verifyIdentity(name, cid, env) {
|
|
22
|
+
const profile = readClientProfile(env);
|
|
23
|
+
const client = await attachOursClient({ endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
|
|
24
|
+
credentialPath: profile.credentialPath, sessionMode: 'external', env: {}, leaseToken: `task-migration-${randomUUID()}` });
|
|
25
|
+
try {
|
|
26
|
+
const identities = await client.listIdentities();
|
|
27
|
+
const found = identities.find(value => value.name === name);
|
|
28
|
+
if (!found || !('cid' in found) || found.cid.toLowerCase() !== cid.toLowerCase())
|
|
29
|
+
throw Error('LEGACY_TASK_MEMBER_IDENTITY_MISMATCH');
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
try {
|
|
33
|
+
await client.releaseLease();
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await client.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Caller holds task and room operation locks. No service is started here.
|
|
41
|
+
* Running legacy supervisors remain untouched; stopped exact state receives
|
|
42
|
+
* only ownership metadata, preserving launch, identity and conversation.
|
|
43
|
+
*/
|
|
44
|
+
export async function adoptLegacyTaskMember(owner, name, deps = {}) {
|
|
45
|
+
return withFileLock(join(stateRoot(), 'locks', 'temp-supervisors', encodeURIComponent(name)), async () => {
|
|
46
|
+
assertMemberNotPermanent(name);
|
|
47
|
+
const dir = agentDir(name, true), task = getTask(owner.taskId), room = getRoomRecord(owner.roomId);
|
|
48
|
+
const seat = room?.member_seats.find(value => value.role_name === name);
|
|
49
|
+
const supervisor = readTempSupervisor(dir);
|
|
50
|
+
const rolePath = join(dir, 'role.yaml'), roleBytes = bounded(rolePath);
|
|
51
|
+
const role = parse(roleBytes);
|
|
52
|
+
const startup = role?.roomMemberStartup, provenance = readProvenance(dir);
|
|
53
|
+
if (task.deletion || task.terminal_intent || !['provisioning', 'active', 'review'].includes(task.state)
|
|
54
|
+
|| !room || room.close || !['provisioning', 'active'].includes(room.state) || room.task_id !== owner.taskId
|
|
55
|
+
|| task.room_id !== owner.roomId || room.room_identity_cid !== owner.roomIdentityCid
|
|
56
|
+
|| task.room_identity_cid !== owner.roomIdentityCid || !seat || seat.retirement
|
|
57
|
+
|| !supervisor || supervisor.role !== name || seat.launch?.launch_id !== supervisor.launchId
|
|
58
|
+
|| seat.launch.action_id !== owner.creationActionId || provenance?.creationActionId !== owner.creationActionId
|
|
59
|
+
|| provenance.role !== name || role.name !== name || role.identity !== name
|
|
60
|
+
|| startup?.identity_name !== name || startup.room_id !== owner.roomId
|
|
61
|
+
|| startup.room_identity_cid !== owner.roomIdentityCid || startup.invite_id !== seat.invite_id
|
|
62
|
+
|| startup.role !== seat.cowork_role || startup.invite !== ''
|
|
63
|
+
|| (startup.task_id !== undefined && startup.task_id !== owner.taskId)
|
|
64
|
+
|| (room.workspace && (role.cwd !== room.workspace.path || JSON.stringify(startup.workspace) !== JSON.stringify(room.workspace)))
|
|
65
|
+
|| bounded(join(dir, '.identity')).trim() !== name)
|
|
66
|
+
throw Error('LEGACY_TASK_MEMBER_OWNERSHIP_MISMATCH');
|
|
67
|
+
if (supervisor.taskOwner) {
|
|
68
|
+
if (Object.entries(owner).some(([key, value]) => supervisor.taskOwner[key] !== value)
|
|
69
|
+
|| startup.task_id !== owner.taskId)
|
|
70
|
+
throw Error('LEGACY_TASK_MEMBER_OWNER_MISMATCH');
|
|
71
|
+
if (supervisor.kind !== 'detached' && process.env.OURS_FLEET_SUPERVISOR !== 'none')
|
|
72
|
+
updateMemberStartup(owner.roomId, name, { launch: { ...seat.launch, task_supervised: true } });
|
|
73
|
+
return 'already-durable';
|
|
74
|
+
}
|
|
75
|
+
const live = await (deps.liveness ?? tempSupervisorLiveness)(dir);
|
|
76
|
+
if (live === 'running')
|
|
77
|
+
return 'running-legacy';
|
|
78
|
+
if (live !== 'stopped')
|
|
79
|
+
throw Error('LEGACY_TASK_MEMBER_LIVENESS_UNKNOWN');
|
|
80
|
+
const env = { ...process.env, ...role.env };
|
|
81
|
+
const profile = readClientProfile(env);
|
|
82
|
+
const privateDir = join(stateRoot(), 'private-ours', binderKey(profile.expectedInstanceId, name));
|
|
83
|
+
const state = JSON.parse(bounded(join(privateDir, 'state.json')));
|
|
84
|
+
const instance = JSON.parse(bounded(join(privateDir, 'instance.json')));
|
|
85
|
+
const externalOwner = JSON.parse(bounded(join(privateDir, 'owner.json')));
|
|
86
|
+
const launch = JSON.parse(bounded(join(stateRoot(), 'private-ours', 'launches', binderKey('temporary', name) + '.json')));
|
|
87
|
+
if (!seat.identity_cid || state.name !== name || state.daemon !== profile.expectedInstanceId
|
|
88
|
+
|| state.lifetime !== 'temporary' || state.action !== owner.creationActionId
|
|
89
|
+
|| state.cid?.toLowerCase() !== seat.identity_cid.toLowerCase()
|
|
90
|
+
|| !['READY', 'SERVING', 'RECOVERING'].includes(state.phase)
|
|
91
|
+
|| instance.role !== name || instance.temporary !== true || instance.instance !== state.instance
|
|
92
|
+
|| externalOwner.instance !== state.instance || typeof externalOwner.token !== 'string' || !externalOwner.token
|
|
93
|
+
|| state.room?.id !== owner.roomId || state.room.cid !== owner.roomIdentityCid
|
|
94
|
+
|| state.room.agentCid.toLowerCase() !== seat.identity_cid.toLowerCase() || state.room.action !== seat.invite_id
|
|
95
|
+
|| launch.role !== name || launch.identity !== name || launch.action !== owner.creationActionId)
|
|
96
|
+
throw Error('LEGACY_TASK_MEMBER_RUNTIME_NOT_RESUMABLE');
|
|
97
|
+
const sessionPath = join(dir, role.session === 'codex-app-server' ? '.session-id' : '.acp-session-id');
|
|
98
|
+
if (!bounded(sessionPath).trim())
|
|
99
|
+
throw Error('LEGACY_TASK_MEMBER_CONVERSATION_MISSING');
|
|
100
|
+
if (deps.verifyIdentity)
|
|
101
|
+
await deps.verifyIdentity(name, seat.identity_cid);
|
|
102
|
+
else
|
|
103
|
+
await verifyIdentity(name, seat.identity_cid, env);
|
|
104
|
+
assertMemberNotPermanent(name);
|
|
105
|
+
if (bounded(rolePath) !== roleBytes || JSON.stringify(readTempSupervisor(dir)) !== JSON.stringify(supervisor))
|
|
106
|
+
throw Error('LEGACY_TASK_MEMBER_STATE_CHANGED');
|
|
107
|
+
// Crash after this write is healed by retrying the same stopped adoption.
|
|
108
|
+
startup.task_id = owner.taskId;
|
|
109
|
+
replaceFileAtomically(rolePath, stringify(role));
|
|
110
|
+
if (supervisor.kind !== 'detached' && process.env.OURS_FLEET_SUPERVISOR !== 'none')
|
|
111
|
+
updateMemberStartup(owner.roomId, name, { launch: { ...seat.launch, task_supervised: true } });
|
|
112
|
+
deps.beforeOwnerCommit?.();
|
|
113
|
+
replaceFileAtomically(join(dir, TEMP_SUPERVISOR_FILE), JSON.stringify({ ...supervisor, taskOwner: owner }, null, 2) + '\n');
|
|
114
|
+
return 'adopted';
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** A room seat is never authority to retire a standalone persistent agent.
|
|
2
|
+
* Check both role-name collisions and a permanent role using the seat's
|
|
3
|
+
* identity name. Missing legacy task state must not bypass this fence.
|
|
4
|
+
*/
|
|
5
|
+
export declare function assertMemberNotPermanent(name: string): void;
|