@ours.network/fleet 1.2.0-nightly.12 → 1.2.0-nightly.14
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 +2 -0
- package/dist/application/model-catalog.d.ts +1 -1
- package/dist/application/model-catalog.js +5 -2
- package/dist/application/task-room-service.d.ts +3 -0
- package/dist/application/task-room-service.js +1 -1
- package/dist/atomic-file.d.ts +5 -0
- package/dist/atomic-file.js +62 -1
- package/dist/build-info.json +4 -4
- package/dist/erased-resources.d.ts +9 -0
- package/dist/erased-resources.js +87 -0
- package/dist/fleet-command-audit.d.ts +1 -0
- package/dist/fleet-command-audit.js +9 -5
- package/dist/init-wizard.js +10 -3
- package/dist/owner-channel/lifecycle-outbox.js +4 -3
- package/dist/rooms-tasks/close.d.ts +2 -2
- package/dist/rooms-tasks/close.js +128 -22
- package/dist/rooms-tasks/deletion.d.ts +1 -1
- package/dist/rooms-tasks/deletion.js +41 -9
- package/dist/rooms-tasks/erasure.d.ts +6 -0
- package/dist/rooms-tasks/erasure.js +169 -0
- package/dist/rooms-tasks/room-state.d.ts +3 -1
- package/dist/rooms-tasks/room-state.js +37 -7
- package/dist/rooms-tasks/task-state.d.ts +10 -4
- package/dist/rooms-tasks/task-state.js +54 -12
- package/dist/rooms-tasks/types.d.ts +2 -0
- package/dist/temp-lifecycle.d.ts +5 -0
- package/dist/temp-lifecycle.js +28 -12
- package/docs/task-workspaces.md +34 -2
- package/package.json +1 -1
- package/presets/README.md +66 -0
- package/presets/brain-catalog.json +7 -2
- package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
- package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
- package/presets/manifest.json +2 -2
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { detachDeletedRoom } from './task-state.js';
|
|
2
|
+
import { binderKey } from '../agent-ours/state.js';
|
|
3
|
+
import { eraseMemberArtifacts } from './erasure.js';
|
|
1
4
|
import { deleteWorkspace, assertWorkspaceDeletable } from './workspace.js';
|
|
2
5
|
import { collectWorkspaceArchives } from './workspace-artifacts.js';
|
|
3
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -9,7 +12,7 @@ import { agentDir, stateRoot } from '../paths.js';
|
|
|
9
12
|
import { readClientProfile } from '../client-profile.js';
|
|
10
13
|
import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, tempArchiveForLaunch, tempArchiveForCreationAction, } from '../temp-lifecycle.js';
|
|
11
14
|
import { CoworkProtocolError } from './cowork-adapter.js';
|
|
12
|
-
import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, deleteRoomRecord, getRoomRecord, listRoomRecords, setRoomCloseError, } from './room-state.js';
|
|
15
|
+
import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, deleteRoomRecord, getRoomRecord, listRoomRecords, setRoomCloseError, recordRetirementMemberCids, } from './room-state.js';
|
|
13
16
|
export const CLOSE_LOCK_STALE_MS = 5 * 60_000;
|
|
14
17
|
const STOP_POLLS = 50;
|
|
15
18
|
const STOP_POLL_MS = 100;
|
|
@@ -22,6 +25,20 @@ function errorText(error) {
|
|
|
22
25
|
function exactMemberIdentity(seat) {
|
|
23
26
|
const dir = agentDir(seat.role_name, true);
|
|
24
27
|
const identityPath = join(dir, '.identity');
|
|
28
|
+
if (existsSync(dir) && !existsSync(identityPath) && seat.launch?.action_id && seat.launch.launch_id) {
|
|
29
|
+
const supervisor = readTempSupervisor(dir);
|
|
30
|
+
let creation;
|
|
31
|
+
try {
|
|
32
|
+
creation = JSON.parse(readFileSync(join(dir, 'creation.json'), 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code !== 'ENOENT')
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
if (supervisor?.role === seat.role_name && supervisor.launchId === seat.launch.launch_id
|
|
39
|
+
&& creation?.role === seat.role_name && creation.creationActionId === seat.launch.action_id)
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
25
42
|
if (!existsSync(dir) || !existsSync(identityPath)) {
|
|
26
43
|
throw new Error(`room member '${seat.role_name}' has no live Fleet temp-state identity proof; refusing retirement`);
|
|
27
44
|
}
|
|
@@ -33,7 +50,8 @@ function exactMemberIdentity(seat) {
|
|
|
33
50
|
export async function inspectMember(seat) {
|
|
34
51
|
exactMemberIdentity(seat);
|
|
35
52
|
const supervisor = readTempSupervisor(agentDir(seat.role_name, true));
|
|
36
|
-
if (!supervisor || supervisor.role !== seat.role_name
|
|
53
|
+
if (!supervisor || supervisor.role !== seat.role_name
|
|
54
|
+
|| (seat.launch?.launch_id && supervisor.launchId !== seat.launch.launch_id)) {
|
|
37
55
|
throw new Error(`room member '${seat.role_name}' has no exact Fleet supervisor ownership proof`);
|
|
38
56
|
}
|
|
39
57
|
return { launchId: supervisor.launchId };
|
|
@@ -93,14 +111,19 @@ export async function identityCidPresent(cid) {
|
|
|
93
111
|
}
|
|
94
112
|
export async function removeExactMemberIdentity(seat) {
|
|
95
113
|
await withIdentityClient(async (client) => {
|
|
96
|
-
const
|
|
97
|
-
|
|
114
|
+
const rows = await client.listIdentities();
|
|
115
|
+
const before = listedIdentity(rows, seat.role_name);
|
|
116
|
+
if (!before) {
|
|
117
|
+
if (rows.some(row => row.name === seat.role_name || (seat.identity_cid && 'cid' in row
|
|
118
|
+
&& row.cid.toLowerCase() === seat.identity_cid.toLowerCase())))
|
|
119
|
+
throw new Error(`room member '${seat.role_name}' identity absence is not proven`);
|
|
98
120
|
return;
|
|
121
|
+
}
|
|
99
122
|
if (!seat.identity_cid) {
|
|
100
|
-
throw new Error(`room member '${seat.role_name}' exists without a recorded authenticated CID; refusing removal`);
|
|
123
|
+
throw new Error(`room member '${seat.role_name}' exists without a recorded authenticated CID; identity absence is not proven; refusing removal`);
|
|
101
124
|
}
|
|
102
125
|
if (before.cid?.toLowerCase() !== seat.identity_cid.toLowerCase()) {
|
|
103
|
-
throw new Error(`room member '${seat.role_name}' identity CID mismatch: recorded ${seat.identity_cid}, found ${before.cid ?? 'none'}`);
|
|
126
|
+
throw new Error(`room member '${seat.role_name}' identity absence is not proven: CID mismatch: recorded ${seat.identity_cid}, found ${before.cid ?? 'none'}`);
|
|
104
127
|
}
|
|
105
128
|
try {
|
|
106
129
|
await client.removeIdentity({ name: seat.role_name });
|
|
@@ -115,8 +138,9 @@ export async function removeExactMemberIdentity(seat) {
|
|
|
115
138
|
if (after)
|
|
116
139
|
throw error;
|
|
117
140
|
}
|
|
118
|
-
const after =
|
|
119
|
-
if (after
|
|
141
|
+
const after = await client.listIdentities();
|
|
142
|
+
if (after.some(row => row.name === seat.role_name || (seat.identity_cid && 'cid' in row
|
|
143
|
+
&& row.cid.toLowerCase() === seat.identity_cid.toLowerCase()))) {
|
|
120
144
|
throw new Error(`room member '${seat.role_name}' identity still exists after remove_identity`);
|
|
121
145
|
}
|
|
122
146
|
});
|
|
@@ -135,9 +159,9 @@ async function retireMember(roomId, seat, deps) {
|
|
|
135
159
|
let current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
136
160
|
let retirement = current.retirement;
|
|
137
161
|
if (retirement?.phase === 'identity_absent') {
|
|
138
|
-
if (
|
|
162
|
+
if (!deps.inspectMember) {
|
|
139
163
|
if (existsSync(agentDir(current.role_name, true)))
|
|
140
|
-
throw new Error(
|
|
164
|
+
throw new Error('Retired member has replacement launch live state');
|
|
141
165
|
await assertMemberIdentityAbsent(current);
|
|
142
166
|
}
|
|
143
167
|
return;
|
|
@@ -146,6 +170,8 @@ async function retireMember(roomId, seat, deps) {
|
|
|
146
170
|
if (!existsSync(agentDir(current.role_name, true)) && current.launch?.launch_id && current.launch.action_id) {
|
|
147
171
|
const archived = tempArchiveForLaunch(current.role_name, current.launch.launch_id);
|
|
148
172
|
const created = tempArchiveForCreationAction(current.role_name, current.launch.action_id);
|
|
173
|
+
if (archived && (!created || created.path !== archived || created.launchId !== current.launch.launch_id))
|
|
174
|
+
throw new Error('Archived member creation ownership mismatch');
|
|
149
175
|
if (archived && created?.path === archived && created.launchId === current.launch.launch_id) {
|
|
150
176
|
if (readFileSync(join(archived, '.identity'), 'utf8').trim() !== current.role_name)
|
|
151
177
|
throw new Error(`room member '${current.role_name}' archive identity mismatch`);
|
|
@@ -153,6 +179,7 @@ async function retireMember(roomId, seat, deps) {
|
|
|
153
179
|
throw new Error(`room member '${current.role_name}' archived supervisor is not proven stopped`);
|
|
154
180
|
// A terminated launch can archive itself before room retirement begins.
|
|
155
181
|
// Accept its exact durable provenance only when no identity needs removal.
|
|
182
|
+
await (deps.removeIdentity ?? removeExactMemberIdentity)(current);
|
|
156
183
|
await assertMemberIdentityAbsent(current);
|
|
157
184
|
const latest = getRoomRecord(roomId)?.member_seats.find(seat => seat.role_name === current.role_name);
|
|
158
185
|
if (existsSync(agentDir(current.role_name, true))
|
|
@@ -164,14 +191,6 @@ async function retireMember(roomId, seat, deps) {
|
|
|
164
191
|
return;
|
|
165
192
|
}
|
|
166
193
|
}
|
|
167
|
-
if (current.launch?.state === 'failed' && !existsSync(agentDir(current.role_name, true))) {
|
|
168
|
-
// A failure before applyRole (for example invite-secret validation) has
|
|
169
|
-
// no supervisor to stop or archive. Settle only proven absence; this
|
|
170
|
-
// path never removes an identity based on missing local evidence.
|
|
171
|
-
await assertMemberIdentityAbsent(current);
|
|
172
|
-
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', 'failed-launch-absent');
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
194
|
if (current.launch?.state === 'pending' && current.launch.attempt === 0) {
|
|
176
195
|
if (existsSync(agentDir(current.role_name, true))) {
|
|
177
196
|
throw new Error(`never-launched room member '${current.role_name}' unexpectedly has Fleet temp state`);
|
|
@@ -180,12 +199,31 @@ async function retireMember(roomId, seat, deps) {
|
|
|
180
199
|
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', 'never-launched');
|
|
181
200
|
return;
|
|
182
201
|
}
|
|
202
|
+
if (!deps.inspectMember && !existsSync(agentDir(current.role_name, true))) {
|
|
203
|
+
await (deps.removeIdentity ?? removeExactMemberIdentity)(current);
|
|
204
|
+
if (existsSync(agentDir(current.role_name, true)))
|
|
205
|
+
throw new Error(`room member '${current.role_name}' acquired replacement state during retirement`);
|
|
206
|
+
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', 'absent-verified');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
183
209
|
const ownership = await (deps.inspectMember ?? inspectMember)(current);
|
|
184
210
|
room = advanceMemberRetirement(roomId, current.role_name, 'stop_requested', ownership.launchId);
|
|
185
211
|
current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
186
212
|
retirement = current.retirement;
|
|
187
213
|
}
|
|
214
|
+
if (['stop_requested', 'liveness_absent', 'archive_secured'].includes(retirement.phase) && !deps.requestStop
|
|
215
|
+
&& !existsSync(agentDir(current.role_name, true))) {
|
|
216
|
+
const archive = tempArchiveForLaunch(current.role_name, retirement.launch_id);
|
|
217
|
+
if (archive && await tempSupervisorLiveness(archive) !== 'stopped')
|
|
218
|
+
throw new Error('Archived supervisor is not proven stopped');
|
|
219
|
+
await (deps.removeIdentity ?? removeExactMemberIdentity)(current);
|
|
220
|
+
await assertMemberIdentityAbsent(current);
|
|
221
|
+
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', retirement.launch_id, archive, !archive);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
188
224
|
if (retirement.phase === 'stop_requested') {
|
|
225
|
+
if (!deps.inspectMember && (await inspectMember(current)).launchId !== retirement.launch_id)
|
|
226
|
+
throw new Error('Member launch changed before stop');
|
|
189
227
|
await (deps.requestStop ?? (async (role) => { await stopTempSupervisor(role); }))(current.role_name);
|
|
190
228
|
await (deps.waitForLivenessAbsent ?? waitForLivenessAbsent)(current.role_name, retirement.launch_id);
|
|
191
229
|
room = advanceMemberRetirement(roomId, current.role_name, 'liveness_absent', retirement.launch_id);
|
|
@@ -215,17 +253,31 @@ export async function closeManagedRoom(input) {
|
|
|
215
253
|
const lock = deps.lock ?? withFileLock;
|
|
216
254
|
return lock(roomCloseLockPath(input.roomId), async () => {
|
|
217
255
|
let room = beginRoomClose(input.roomId);
|
|
218
|
-
if (room.state === 'closed')
|
|
256
|
+
if (room.state === 'closed') {
|
|
257
|
+
if (!deps.inspectMember)
|
|
258
|
+
for (const seat of room.member_seats) {
|
|
259
|
+
if (existsSync(agentDir(seat.role_name, true)))
|
|
260
|
+
throw new Error('Closed room has replacement live state');
|
|
261
|
+
await assertMemberIdentityAbsent(seat);
|
|
262
|
+
}
|
|
219
263
|
return room;
|
|
264
|
+
}
|
|
220
265
|
try {
|
|
221
266
|
if (room.close?.phase === 'retire_members') {
|
|
267
|
+
room = await recoverMemberCids(room, input.cowork);
|
|
222
268
|
for (const seat of room.member_seats) {
|
|
223
269
|
await retireMember(input.roomId, seat, deps);
|
|
224
270
|
}
|
|
225
271
|
room = advanceRoomClose(input.roomId, 'close_cowork');
|
|
226
272
|
}
|
|
227
273
|
if (room.close?.phase === 'close_cowork') {
|
|
228
|
-
|
|
274
|
+
try {
|
|
275
|
+
await input.cowork.closeRoom(input.roomId);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
229
281
|
}
|
|
230
282
|
return closeRoom(input.roomId);
|
|
231
283
|
}
|
|
@@ -258,7 +310,8 @@ export async function deleteLegacyClosedRooms(input) {
|
|
|
258
310
|
}
|
|
259
311
|
/** Retire live resources through the existing cursor, then delete retained state. */
|
|
260
312
|
export async function deleteManagedRoom(input) {
|
|
261
|
-
|
|
313
|
+
if (getRoomRecord(input.roomId))
|
|
314
|
+
await closeManagedRoom(input);
|
|
262
315
|
return withFileLock(roomCloseLockPath(input.roomId), async () => {
|
|
263
316
|
try {
|
|
264
317
|
try {
|
|
@@ -276,12 +329,65 @@ export async function deleteManagedRoom(input) {
|
|
|
276
329
|
collectWorkspaceArchives(retained.workspace);
|
|
277
330
|
deleteWorkspace(retained.workspace, 'room', input.roomId);
|
|
278
331
|
}
|
|
332
|
+
if (retained)
|
|
333
|
+
await eraseMemberArtifacts('room', input.roomId, retained.member_seats, [input.roomId]);
|
|
334
|
+
if (retained?.task_id)
|
|
335
|
+
detachDeletedRoom(retained.task_id, input.roomId);
|
|
279
336
|
deleteRoomRecord(input.roomId);
|
|
280
337
|
return { room_id: input.roomId, deleted: true };
|
|
281
338
|
}
|
|
282
339
|
catch (error) {
|
|
283
|
-
|
|
340
|
+
if (getRoomRecord(input.roomId))
|
|
341
|
+
setRoomCloseError(input.roomId, errorText(error), `Fix the cleanup error and explicitly retry room delete ${input.roomId} ${input.roomId}.`);
|
|
284
342
|
throw error;
|
|
285
343
|
}
|
|
286
344
|
}, {}, CLOSE_LOCK_STALE_MS);
|
|
287
345
|
}
|
|
346
|
+
/** Recover only an authenticated seat tied to this exact room and issued invite.
|
|
347
|
+
* Display names and the current daemon name inventory are never binding proof.
|
|
348
|
+
*/
|
|
349
|
+
async function recoverMemberCids(room, cowork) {
|
|
350
|
+
if (!room.member_seats.some(seat => !seat.identity_cid))
|
|
351
|
+
return room;
|
|
352
|
+
const profile = room.member_seats.some(seat => seat.launch?.action_id) ? readClientProfile(process.env) : undefined;
|
|
353
|
+
let remote;
|
|
354
|
+
if (cowork.getRoom && room.room_identity_cid) {
|
|
355
|
+
try {
|
|
356
|
+
remote = await cowork.getRoom(room.room_id);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
if (remote && (remote.room_id !== room.room_id || remote.identity_cid.toLowerCase() !== room.room_identity_cid.toLowerCase()))
|
|
363
|
+
throw new Error('Room identity mismatch during deletion recovery');
|
|
364
|
+
}
|
|
365
|
+
const seats = room.member_seats.map(seat => {
|
|
366
|
+
let runtimeCid;
|
|
367
|
+
if (profile && seat.launch?.action_id) {
|
|
368
|
+
const dir = join(stateRoot(), 'private-ours', binderKey(profile.expectedInstanceId, seat.role_name));
|
|
369
|
+
try {
|
|
370
|
+
const state = JSON.parse(readFileSync(join(dir, 'state.json'), 'utf8'));
|
|
371
|
+
const instance = JSON.parse(readFileSync(join(dir, 'instance.json'), 'utf8'));
|
|
372
|
+
if (state.name === seat.role_name && state.daemon === profile.expectedInstanceId
|
|
373
|
+
&& state.lifetime === 'temporary' && state.action === seat.launch.action_id
|
|
374
|
+
&& instance.role === seat.role_name && instance.temporary === true && instance.instance === state.instance)
|
|
375
|
+
runtimeCid = state.cid;
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (error.code !== 'ENOENT')
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const matches = seat.invite_id ? remote?.seats.filter(value => value.invite_id === seat.invite_id && value.role === seat.cowork_role) ?? [] : [];
|
|
383
|
+
if (matches.length > 1)
|
|
384
|
+
throw new Error('Ambiguous authenticated member identity during deletion recovery');
|
|
385
|
+
const candidates = [seat.identity_cid, runtimeCid, matches[0]?.identity_cid].filter((v) => !!v);
|
|
386
|
+
if (candidates.some(cid => !/^[a-f0-9]{64}$/i.test(cid)))
|
|
387
|
+
throw new Error('Invalid authenticated member CID');
|
|
388
|
+
if (new Set(candidates.map(cid => cid.toLowerCase())).size > 1)
|
|
389
|
+
throw new Error('Contradictory authenticated member CID evidence');
|
|
390
|
+
return candidates.length ? { ...seat, identity_cid: candidates[0] } : seat;
|
|
391
|
+
});
|
|
392
|
+
return recordRetirementMemberCids(room.room_id, seats);
|
|
393
|
+
}
|
|
@@ -3,7 +3,7 @@ import { type CoworkAdapter } from './cowork-adapter.js';
|
|
|
3
3
|
import { type TaskDeletionAcceptance } from './task-state.js';
|
|
4
4
|
import type { TaskDeletionActor, TaskRecord } from './types.js';
|
|
5
5
|
export { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
|
|
6
|
-
type DeletionCowork = Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'
|
|
6
|
+
type DeletionCowork = Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'> & Partial<Pick<CoworkAdapter, 'getRoom'>>;
|
|
7
7
|
export interface TaskDeletionSettleDeps {
|
|
8
8
|
roomClose?: RoomCloseDeps;
|
|
9
9
|
/** Test seam for the temp-state existence proof. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { eraseMemberArtifacts } from './erasure.js';
|
|
1
2
|
import { proveArchivedAbsence, verifyArchivedAbsence, verifyArchivedMemberStillAbsent } from './archived-absence.js';
|
|
2
3
|
import { existsSync } from 'node:fs';
|
|
3
4
|
import { withFileLock } from '../atomic-file.js';
|
|
@@ -5,12 +6,12 @@ import { agentDir } from '../paths.js';
|
|
|
5
6
|
import { deleteWorkspace, assertWorkspaceDeletable } from './workspace.js';
|
|
6
7
|
import { collectWorkspaceArchives } from './workspace-artifacts.js';
|
|
7
8
|
import { secureStoppedTempArchive, stopTempSupervisor } from '../temp-lifecycle.js';
|
|
8
|
-
import { closeManagedRoom, identityCidPresent, inspectMember, removeExactMemberIdentity, waitForLivenessAbsent, } from './close.js';
|
|
9
|
+
import { closeManagedRoom, identityCidPresent, inspectMember, assertMemberIdentityAbsent, removeExactMemberIdentity, waitForLivenessAbsent, } from './close.js';
|
|
9
10
|
import { CoworkProtocolError } from './cowork-adapter.js';
|
|
10
11
|
import { deleteRoomRecord, getRoomRecord, listRoomRecords } from './room-state.js';
|
|
11
12
|
import { acquireLaunchSnapshotLock, releaseLaunchSnapshotForDeletingTask } from './launch-snapshot.js';
|
|
12
13
|
import { TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath } from './terminal.js';
|
|
13
|
-
import { advanceTaskDeletionMember, beginTaskDeletionIntent, completeTaskDeletionReceipt, ensureTaskDeletionReceipt, getDeletingTask, importTaskDeletionRetirementEvidence, setTaskDeletionError, TaskStateError,
|
|
14
|
+
import { advanceTaskDeletionMember, settleAbsentTaskDeletionMember, beginTaskDeletionIntent, completeTaskDeletionReceipt, ensureTaskDeletionReceipt, getDeletingTask, importTaskDeletionRetirementEvidence, setTaskDeletionError, TaskStateError, beginTaskArchiveCleanup, unlinkDeletedTask, upsertTaskDeletionMembersFromSeats, } from './task-state.js';
|
|
14
15
|
export { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
|
|
15
16
|
import { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
|
|
16
17
|
/**
|
|
@@ -43,6 +44,7 @@ function tolerantCowork(cowork) {
|
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
46
|
return {
|
|
47
|
+
...(cowork.getRoom ? { getRoom: cowork.getRoom.bind(cowork) } : {}),
|
|
46
48
|
closeRoom: roomId => tolerate(() => cowork.closeRoom(roomId)),
|
|
47
49
|
deleteRoom: roomId => tolerate(() => cowork.deleteRoom(roomId)),
|
|
48
50
|
};
|
|
@@ -51,6 +53,7 @@ function cursorSeat(cursor) {
|
|
|
51
53
|
return {
|
|
52
54
|
role_name: cursor.name, identity_cid: cursor.identity_cid,
|
|
53
55
|
slot: cursor.name, cowork_role: 'member', seat_state: 'active',
|
|
56
|
+
...(cursor.launch_id ? { retirement: { phase: 'identity_absent', launch_id: cursor.launch_id, archive_path: cursor.archive_path, updated_at: cursor.updated_at } } : {}),
|
|
54
57
|
};
|
|
55
58
|
}
|
|
56
59
|
/**
|
|
@@ -63,8 +66,15 @@ function cursorSeat(cursor) {
|
|
|
63
66
|
async function retireCursorMember(taskId, cursor, deps) {
|
|
64
67
|
let phase = cursor.phase;
|
|
65
68
|
let launchId = cursor.launch_id;
|
|
66
|
-
if (phase === 'identity_absent')
|
|
69
|
+
if (phase === 'identity_absent') {
|
|
70
|
+
if (!deps.roomClose?.inspectMember) {
|
|
71
|
+
if ((deps.hasTempState ?? (name => existsSync(agentDir(name, true))))(cursor.name))
|
|
72
|
+
throw new Error('Retired deletion member has replacement live state');
|
|
73
|
+
if (await (deps.identityCidPresent ?? identityCidPresent)(cursor.identity_cid))
|
|
74
|
+
throw new Error('Retired deletion member identity reappeared');
|
|
75
|
+
}
|
|
67
76
|
return;
|
|
77
|
+
}
|
|
68
78
|
const seat = cursorSeat(cursor);
|
|
69
79
|
const roomClose = deps.roomClose ?? {};
|
|
70
80
|
if (phase === 'pending') {
|
|
@@ -88,7 +98,16 @@ async function retireCursorMember(taskId, cursor, deps) {
|
|
|
88
98
|
phase = 'stop_requested';
|
|
89
99
|
launchId = ownership.launchId;
|
|
90
100
|
}
|
|
101
|
+
if (!roomClose.inspectMember && !existsSync(agentDir(cursor.name, true))) {
|
|
102
|
+
await (roomClose.removeIdentity ?? removeExactMemberIdentity)(seat);
|
|
103
|
+
if (existsSync(agentDir(cursor.name, true)))
|
|
104
|
+
throw new Error('Deletion member acquired replacement state');
|
|
105
|
+
settleAbsentTaskDeletionMember(taskId, cursor.name);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
91
108
|
if (phase === 'stop_requested') {
|
|
109
|
+
if (!roomClose.inspectMember && (await inspectMember(seat)).launchId !== launchId)
|
|
110
|
+
throw new Error('Deletion member launch changed before stop');
|
|
92
111
|
await (roomClose.requestStop
|
|
93
112
|
?? (async (role) => { await stopTempSupervisor(role); }))(cursor.name);
|
|
94
113
|
await (roomClose.waitForLivenessAbsent ?? waitForLivenessAbsent)(cursor.name, launchId);
|
|
@@ -140,8 +159,6 @@ export async function settleTaskDeletion(input) {
|
|
|
140
159
|
// aborts settlement (fail closed).
|
|
141
160
|
ensureTaskDeletionReceipt(taskId);
|
|
142
161
|
const records = listRoomRecords().filter(room => room.task_id === taskId);
|
|
143
|
-
if (task.deletion.workspace_cleanup_started_at && records.length)
|
|
144
|
-
throw new TaskStateError(`task ${taskId} room records reappeared after workspace cleanup began`);
|
|
145
162
|
const recordIds = new Set(records.map(room => room.room_id));
|
|
146
163
|
const recordedRoomId = task.deletion.room_id ?? task.room_id;
|
|
147
164
|
const needsCowork = records.length > 0 || recordedRoomId !== undefined;
|
|
@@ -153,8 +170,15 @@ export async function settleTaskDeletion(input) {
|
|
|
153
170
|
const seats = closed?.member_seats ?? record.member_seats;
|
|
154
171
|
const proofs = [];
|
|
155
172
|
for (const seat of seats) {
|
|
156
|
-
if (!seat.identity_cid && seat.retirement?.
|
|
157
|
-
|
|
173
|
+
if (!seat.identity_cid && !seat.retirement?.absence_verified && !['never-launched', 'absent-verified'].includes(seat.retirement?.launch_id ?? '')) {
|
|
174
|
+
const prior = task.deletion.archived_absences?.find(proof => proof.name === seat.role_name);
|
|
175
|
+
if (task.deletion.workspace_cleanup_started_at && prior && !existsSync(prior.archive_path)) {
|
|
176
|
+
await verifyArchivedMemberStillAbsent(prior);
|
|
177
|
+
proofs.push(prior);
|
|
178
|
+
}
|
|
179
|
+
else
|
|
180
|
+
proofs.push(await proveArchivedAbsence(seat));
|
|
181
|
+
}
|
|
158
182
|
}
|
|
159
183
|
// No await between the last live-state fence and durable checkpoint.
|
|
160
184
|
for (const proof of proofs) {
|
|
@@ -166,7 +190,9 @@ export async function settleTaskDeletion(input) {
|
|
|
166
190
|
importTaskDeletionRetirementEvidence(taskId, seats, proofs);
|
|
167
191
|
await cowork.deleteRoom(record.room_id);
|
|
168
192
|
for (const proof of proofs)
|
|
169
|
-
await verifyArchivedAbsence(proof);
|
|
193
|
+
await (task.deletion.workspace_cleanup_started_at && !existsSync(proof.archive_path) ? verifyArchivedMemberStillAbsent : verifyArchivedAbsence)(proof);
|
|
194
|
+
beginTaskArchiveCleanup(taskId);
|
|
195
|
+
await eraseMemberArtifacts('room', record.room_id, seats, [record.room_id]);
|
|
170
196
|
for (const proof of proofs)
|
|
171
197
|
if (existsSync(agentDir(proof.name, true)))
|
|
172
198
|
throw new Error('Archived member replacement before room unlink');
|
|
@@ -216,11 +242,17 @@ export async function settleTaskDeletion(input) {
|
|
|
216
242
|
for (const proof of task.deletion.archived_absences ?? [])
|
|
217
243
|
if (existsSync(agentDir(proof.name, true)))
|
|
218
244
|
throw new Error('Archived member replacement before task unlink');
|
|
245
|
+
for (const name of task.deletion.absent_members ?? []) {
|
|
246
|
+
if (existsSync(agentDir(name, true)))
|
|
247
|
+
throw new Error('Absent member acquired replacement state');
|
|
248
|
+
await assertMemberIdentityAbsent({ role_name: name, slot: name, cowork_role: 'member', seat_state: 'removed' });
|
|
249
|
+
}
|
|
219
250
|
if (cleanup.snapshotHash)
|
|
220
251
|
releaseLaunchSnapshotForDeletingTask(cleanup.snapshotHash, taskId);
|
|
252
|
+
await eraseMemberArtifacts('task', taskId, task.deletion.members.map(cursorSeat), task.deletion.room_id ? [task.deletion.room_id] : []);
|
|
221
253
|
if (task.workspace) {
|
|
222
254
|
assertWorkspaceDeletable(task.workspace, 'task', taskId);
|
|
223
|
-
|
|
255
|
+
beginTaskArchiveCleanup(taskId);
|
|
224
256
|
collectWorkspaceArchives(task.workspace);
|
|
225
257
|
deleteWorkspace(task.workspace, 'task', taskId);
|
|
226
258
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RoomMemberSeat } from './types.js';
|
|
2
|
+
/** Erase only proven launch artifacts. Each atomic rename transfers ownership to
|
|
3
|
+
* a random, durable tombstone before recursive removal can destroy its proof.
|
|
4
|
+
* The caller holds the task/room lifecycle lock and has retired all writers.
|
|
5
|
+
*/
|
|
6
|
+
export declare function eraseMemberArtifacts(owner: 'room' | 'task', id: string, seats: readonly RoomMemberSeat[], roomIds: readonly string[]): Promise<void>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { eraseResourcePresentations } from '../erased-resources.js';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync } from 'node:fs';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { dirname, join, relative } from 'node:path';
|
|
5
|
+
import { parse } from 'yaml';
|
|
6
|
+
import { replaceFileAtomically } from '../atomic-file.js';
|
|
7
|
+
import { agentDir, stateRoot } from '../paths.js';
|
|
8
|
+
import { readTempSupervisor, tempSupervisorLiveness, eraseTerminationEvents } from '../temp-lifecycle.js';
|
|
9
|
+
import { assertSafeAncestors, auditWorkspaceGit } from './workspace.js';
|
|
10
|
+
function entries(path) {
|
|
11
|
+
try {
|
|
12
|
+
return readdirSync(path);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT')
|
|
16
|
+
return [];
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function json(path) {
|
|
21
|
+
try {
|
|
22
|
+
if (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink())
|
|
23
|
+
return undefined;
|
|
24
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT')
|
|
28
|
+
return undefined;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function fingerprint(path) {
|
|
33
|
+
const stat = lstatSync(path);
|
|
34
|
+
const files = stat.isDirectory() ? ['creation.json', '.temp-supervisor.json', '.identity', 'role.yaml', 'state.json', 'instance.json'] : [''];
|
|
35
|
+
const hash = createHash('sha256');
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const target = file ? join(path, file) : path;
|
|
38
|
+
if (existsSync(target)) {
|
|
39
|
+
if (!lstatSync(target).isFile() || lstatSync(target).isSymbolicLink())
|
|
40
|
+
throw new Error('Unsafe erasure proof');
|
|
41
|
+
hash.update(file).update(readFileSync(target));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return hash.digest('hex');
|
|
45
|
+
}
|
|
46
|
+
/** Erase only proven launch artifacts. Each atomic rename transfers ownership to
|
|
47
|
+
* a random, durable tombstone before recursive removal can destroy its proof.
|
|
48
|
+
* The caller holds the task/room lifecycle lock and has retired all writers.
|
|
49
|
+
*/
|
|
50
|
+
export async function eraseMemberArtifacts(owner, id, seats, roomIds) {
|
|
51
|
+
if (!/^[a-zA-Z0-9_-]{1,80}$/.test(id))
|
|
52
|
+
throw new Error('Invalid erasure owner');
|
|
53
|
+
const root = stateRoot();
|
|
54
|
+
const manifestPath = join(root, 'erasure', `${owner}-${id}.json`);
|
|
55
|
+
assertSafeAncestors(dirname(manifestPath));
|
|
56
|
+
let manifest = json(manifestPath);
|
|
57
|
+
const names = new Set(seats.map(s => s.role_name));
|
|
58
|
+
for (const name of names) {
|
|
59
|
+
if (!/^[a-zA-Z0-9_-]{1,120}$/.test(name))
|
|
60
|
+
throw new Error('Invalid erasure member');
|
|
61
|
+
if (existsSync(agentDir(name, true)))
|
|
62
|
+
throw new Error(`Member '${name}' still has live state during erasure`);
|
|
63
|
+
}
|
|
64
|
+
if (!manifest) {
|
|
65
|
+
const paths = [];
|
|
66
|
+
const launches = seats.flatMap(seat => {
|
|
67
|
+
const launchId = seat.launch?.launch_id ?? seat.retirement?.launch_id;
|
|
68
|
+
return launchId ? [{ role: seat.role_name, launchId }] : [];
|
|
69
|
+
});
|
|
70
|
+
const actions = new Map(seats.map(s => [s.role_name, new Set([s.launch?.action_id].filter((v) => !!v))]));
|
|
71
|
+
const recovery = join(root, 'recovery', 'temporary');
|
|
72
|
+
assertSafeAncestors(recovery);
|
|
73
|
+
for (const entry of entries(recovery)) {
|
|
74
|
+
const path = join(recovery, entry);
|
|
75
|
+
const stat = lstatSync(path);
|
|
76
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
77
|
+
continue;
|
|
78
|
+
const supervisor = readTempSupervisor(path);
|
|
79
|
+
if (!supervisor || !names.has(supervisor.role))
|
|
80
|
+
continue;
|
|
81
|
+
const seat = seats.find(s => s.role_name === supervisor.role);
|
|
82
|
+
const creation = json(join(path, 'creation.json'));
|
|
83
|
+
let room;
|
|
84
|
+
const roleFile = join(path, 'role.yaml');
|
|
85
|
+
if (existsSync(roleFile) && lstatSync(roleFile).isFile() && !lstatSync(roleFile).isSymbolicLink()) {
|
|
86
|
+
const role = parse(readFileSync(roleFile, 'utf8'));
|
|
87
|
+
if (role?.name === supervisor.role)
|
|
88
|
+
room = role.roomMemberStartup?.room_id;
|
|
89
|
+
}
|
|
90
|
+
const exactLaunch = seat.launch?.launch_id === supervisor.launchId || seat.retirement?.launch_id === supervisor.launchId;
|
|
91
|
+
const exactAction = creation?.role === supervisor.role && creation.creationActionId === seat.launch?.action_id;
|
|
92
|
+
// Older attempts belong only when the launch descriptor pins the room.
|
|
93
|
+
if (!(exactLaunch && (exactAction || !seat.launch?.action_id)) && !(room && roomIds.includes(room)))
|
|
94
|
+
continue;
|
|
95
|
+
if (existsSync(join(path, '.identity')) && readFileSync(join(path, '.identity'), 'utf8').trim() !== supervisor.role)
|
|
96
|
+
throw new Error('Archive identity mismatch during erasure');
|
|
97
|
+
if (await tempSupervisorLiveness(path) !== 'stopped')
|
|
98
|
+
throw new Error('Archive writer is not stopped');
|
|
99
|
+
if (creation?.role === supervisor.role && typeof creation.creationActionId === 'string')
|
|
100
|
+
actions.get(supervisor.role).add(creation.creationActionId);
|
|
101
|
+
auditWorkspaceGit(path);
|
|
102
|
+
launches.push({ role: supervisor.role, launchId: supervisor.launchId });
|
|
103
|
+
paths.push(path);
|
|
104
|
+
}
|
|
105
|
+
const privateRoot = join(root, 'private-ours');
|
|
106
|
+
assertSafeAncestors(privateRoot);
|
|
107
|
+
for (const entry of entries(privateRoot)) {
|
|
108
|
+
const path = join(privateRoot, entry);
|
|
109
|
+
if (!lstatSync(path).isDirectory() || lstatSync(path).isSymbolicLink())
|
|
110
|
+
continue;
|
|
111
|
+
if (entry === 'launches' || entry === 'room-inputs') {
|
|
112
|
+
for (const file of entries(path)) {
|
|
113
|
+
const child = join(path, file), value = json(child);
|
|
114
|
+
if (!value)
|
|
115
|
+
continue;
|
|
116
|
+
const launchOwned = entry === 'launches' && value.role === value.identity
|
|
117
|
+
&& actions.get(value.role)?.has(value.action);
|
|
118
|
+
const roomOwned = entry === 'room-inputs' && roomIds.includes(value.room_id)
|
|
119
|
+
&& names.has(value.identity_name);
|
|
120
|
+
const readyOwned = entry === 'room-inputs' && file.endsWith('.ready.json')
|
|
121
|
+
&& roomIds.includes(value.room) && seats.some(seat => seat.invite_id === value.invite && (!seat.identity_cid || seat.identity_cid === value.cid));
|
|
122
|
+
if (launchOwned || roomOwned || readyOwned)
|
|
123
|
+
paths.push(child);
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const state = json(join(path, 'state.json')), instance = json(join(path, 'instance.json'));
|
|
128
|
+
if (state?.lifetime === 'temporary' && actions.get(state.name)?.has(state.action)
|
|
129
|
+
&& instance?.role === state.name && instance?.temporary === true && instance?.instance === state.instance) {
|
|
130
|
+
auditWorkspaceGit(path);
|
|
131
|
+
paths.push(path);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
manifest = { launches, version: 1, token: randomUUID(), paths, phases: {}, stamps: Object.fromEntries(paths.map(path => {
|
|
135
|
+
const stat = lstatSync(path);
|
|
136
|
+
return [path, { dev: stat.dev, ino: stat.ino, proof: fingerprint(path) }];
|
|
137
|
+
})) };
|
|
138
|
+
mkdirSync(dirname(manifestPath), { recursive: true, mode: 0o700 });
|
|
139
|
+
replaceFileAtomically(manifestPath, JSON.stringify(manifest));
|
|
140
|
+
}
|
|
141
|
+
if (manifest.version !== 1 || !/^[a-f0-9-]{36}$/.test(manifest.token) || !Array.isArray(manifest.paths))
|
|
142
|
+
throw new Error('Invalid erasure manifest');
|
|
143
|
+
for (const path of manifest.paths) {
|
|
144
|
+
const rel = relative(root, path);
|
|
145
|
+
if (!rel.startsWith('recovery/temporary/') && !rel.startsWith('private-ours/'))
|
|
146
|
+
throw new Error('Invalid erasure artifact path');
|
|
147
|
+
if (rel.split('/').includes('..'))
|
|
148
|
+
throw new Error('Invalid erasure artifact traversal');
|
|
149
|
+
assertSafeAncestors(dirname(path));
|
|
150
|
+
const tombstone = `${path}.erasing-${manifest.token}`;
|
|
151
|
+
if (manifest.phases[path] === 'removed')
|
|
152
|
+
continue;
|
|
153
|
+
if (existsSync(path) && !existsSync(tombstone) && manifest.phases[path] !== 'renamed') {
|
|
154
|
+
const stat = lstatSync(path), stamp = manifest.stamps[path];
|
|
155
|
+
if (stat.isSymbolicLink() || !stamp || stamp.dev !== stat.dev || stamp.ino !== stat.ino || stamp.proof !== fingerprint(path))
|
|
156
|
+
throw new Error('Erasure source ownership changed');
|
|
157
|
+
renameSync(path, tombstone);
|
|
158
|
+
}
|
|
159
|
+
manifest.phases[path] = 'renamed';
|
|
160
|
+
replaceFileAtomically(manifestPath, JSON.stringify(manifest));
|
|
161
|
+
rmSync(tombstone, { recursive: true, force: true });
|
|
162
|
+
manifest.phases[path] = 'removed';
|
|
163
|
+
replaceFileAtomically(manifestPath, JSON.stringify(manifest));
|
|
164
|
+
}
|
|
165
|
+
eraseResourcePresentations([{ kind: owner, id }, ...roomIds.map(id => ({ kind: 'room', id })),
|
|
166
|
+
...seats.map(seat => ({ kind: 'agent', id: seat.role_name }))]);
|
|
167
|
+
eraseTerminationEvents(manifest.launches);
|
|
168
|
+
rmSync(manifestPath, { force: true });
|
|
169
|
+
}
|
|
@@ -31,6 +31,8 @@ export declare function updateMemberStartup(id: string, roleName: string, update
|
|
|
31
31
|
export declare function activateRoom(id: string): RoomOrchestrationRecord;
|
|
32
32
|
export declare function closeRoom(id: string): RoomOrchestrationRecord;
|
|
33
33
|
export declare function beginRoomClose(id: string): RoomOrchestrationRecord;
|
|
34
|
-
export declare function advanceMemberRetirement(id: string, roleName: string, phase: MemberRetirementPhase, launchId: string, archivePath?: string): RoomOrchestrationRecord;
|
|
34
|
+
export declare function advanceMemberRetirement(id: string, roleName: string, phase: MemberRetirementPhase, launchId: string, archivePath?: string, absenceVerified?: boolean): RoomOrchestrationRecord;
|
|
35
35
|
export declare function advanceRoomClose(id: string, phase: RoomClosePhase): RoomOrchestrationRecord;
|
|
36
36
|
export declare function setRoomCloseError(id: string, error: string, recoveryHint: string): RoomOrchestrationRecord;
|
|
37
|
+
/** Settlement-only binding recovery; never publishes a seat or changes a launch. */
|
|
38
|
+
export declare function recordRetirementMemberCids(id: string, recovered: ReadonlyArray<RoomMemberSeat>): RoomOrchestrationRecord;
|