@ours.network/fleet 1.2.0-nightly.53 → 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 +22 -12
- package/dist/agent-ours/bridge.js +10 -3
- package/dist/agent-ours/file-rpc.d.ts +8 -0
- package/dist/agent-ours/file-rpc.js +11 -1
- package/dist/agent-ours/harness.js +47 -3
- package/dist/agent-ours/mcp-endpoint.d.ts +5 -0
- package/dist/agent-ours/mcp-endpoint.js +4 -0
- package/dist/agent-ours/service.d.ts +6 -1
- package/dist/agent-ours/service.js +73 -8
- package/dist/application/conversation-history.d.ts +2 -0
- package/dist/application/conversation-history.js +25 -2
- 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/supervisor-ours-tools.js +3 -1
- 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 +11 -0
- package/dist/config.js +13 -2
- package/dist/file-delivery/reader.d.ts +17 -0
- package/dist/file-delivery/reader.js +94 -0
- package/dist/file-delivery/store.d.ts +15 -0
- package/dist/file-delivery/store.js +124 -0
- package/dist/file-delivery/types.d.ts +19 -0
- package/dist/file-delivery/types.js +1 -0
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/codex-app-server-proxy.js +6 -2
- package/dist/harness/codex-file-inventory.d.ts +15 -0
- package/dist/harness/codex-file-inventory.js +113 -0
- package/dist/harness/codex-session.js +2 -2
- package/dist/harness/codex.js +2 -1
- package/dist/isolation/policy.js +4 -1
- 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 +161 -22
- package/dist/session/acp.d.ts +5 -0
- package/dist/session/acp.js +68 -8
- 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/conversation-types.d.ts +4 -2
- package/dist/session/types.d.ts +3 -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/dist/web/server.js +24 -0
- package/docs/acp-files.md +102 -0
- package/docs/fleet-migration.md +54 -0
- package/docs/task-workspaces.md +26 -0
- package/package.json +7 -3
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const object = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
2
|
+
const parse = (line) => { try {
|
|
3
|
+
const v = JSON.parse(line);
|
|
4
|
+
return object(v) ? v : undefined;
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return undefined;
|
|
8
|
+
} };
|
|
9
|
+
export class CodexFileInventory {
|
|
10
|
+
sendToCodex;
|
|
11
|
+
emitToClient;
|
|
12
|
+
checks = new Map();
|
|
13
|
+
sequence = 0;
|
|
14
|
+
constructor(sendToCodex, emitToClient) {
|
|
15
|
+
this.sendToCodex = sendToCodex;
|
|
16
|
+
this.emitToClient = emitToClient;
|
|
17
|
+
}
|
|
18
|
+
/** Hold only a turn/start; unrelated tools/configuration are preserved. */
|
|
19
|
+
observeClientLine(line) {
|
|
20
|
+
const message = parse(line);
|
|
21
|
+
if (message?.method !== 'turn/start')
|
|
22
|
+
return true;
|
|
23
|
+
const threadId = object(message.params) ? message.params.threadId : undefined;
|
|
24
|
+
if (typeof threadId !== 'string' || !threadId || message.id === undefined || this.checks.size >= 8) {
|
|
25
|
+
this.reject(message);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const check = { request: message, threadId, servers: [], cursors: new Set(), timer: setTimeout(() => {
|
|
29
|
+
for (const [id, c] of this.checks)
|
|
30
|
+
if (c === check)
|
|
31
|
+
this.checks.delete(id);
|
|
32
|
+
this.reject(message);
|
|
33
|
+
}, 30_000) };
|
|
34
|
+
this.query(check);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
observeServerLine(line) {
|
|
38
|
+
const message = parse(line), id = message?.id;
|
|
39
|
+
if (typeof id !== 'string')
|
|
40
|
+
return true;
|
|
41
|
+
const check = this.checks.get(id);
|
|
42
|
+
if (!check)
|
|
43
|
+
return true;
|
|
44
|
+
this.checks.delete(id);
|
|
45
|
+
const result = message?.result;
|
|
46
|
+
if (!object(result) || !Array.isArray(result.data) || result.data.some(s => !object(s)) || check.servers.length + result.data.length > 1000) {
|
|
47
|
+
this.finish(check, false);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
check.servers.push(...result.data);
|
|
51
|
+
if (result.nextCursor !== null) {
|
|
52
|
+
const cursor = result.nextCursor;
|
|
53
|
+
if (typeof cursor !== 'string' || !cursor || check.cursors.has(cursor) || check.cursors.size >= 10)
|
|
54
|
+
this.finish(check, false);
|
|
55
|
+
else {
|
|
56
|
+
check.cursors.add(cursor);
|
|
57
|
+
this.query(check, cursor);
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
this.finish(check, this.qualified(check.servers));
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
close() { for (const c of this.checks.values())
|
|
65
|
+
clearTimeout(c.timer); this.checks.clear(); }
|
|
66
|
+
query(check, cursor) {
|
|
67
|
+
const id = `ours-fleet-file-inventory-${++this.sequence}`;
|
|
68
|
+
this.checks.set(id, check);
|
|
69
|
+
this.sendToCodex(JSON.stringify({ id, method: 'mcpServerStatus/list', params: { threadId: check.threadId, detail: 'full', limit: 100, ...(cursor ? { cursor } : {}) } }));
|
|
70
|
+
}
|
|
71
|
+
qualified(servers) {
|
|
72
|
+
let managed = 0;
|
|
73
|
+
for (const server of servers) {
|
|
74
|
+
// Unknown or failed discovery cannot establish absence of another connector.
|
|
75
|
+
if (server.toolsError !== null || !object(server.tools))
|
|
76
|
+
return false;
|
|
77
|
+
if (!object(server.serverInfo) || typeof server.serverInfo.name !== 'string')
|
|
78
|
+
return false;
|
|
79
|
+
const tools = Object.values(server.tools).filter(object);
|
|
80
|
+
const names = new Set(tools.map(t => t.name));
|
|
81
|
+
if (server.name !== 'ours') {
|
|
82
|
+
if (server.serverInfo.name === 'ours')
|
|
83
|
+
return false;
|
|
84
|
+
if (tools.some(t => t.name === 'send_file' && object(t.inputSchema) && object(t.inputSchema.properties) && 'contact' in t.inputSchema.properties))
|
|
85
|
+
return false;
|
|
86
|
+
if (names.has('current_identity') && names.has('send_file') && names.has('get_messages'))
|
|
87
|
+
return false;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
managed++;
|
|
91
|
+
if (server.serverInfo.name !== 'ours' || server.runtimeStatus !== 'connected')
|
|
92
|
+
return false;
|
|
93
|
+
const files = tools.filter(t => t.name === 'send_file');
|
|
94
|
+
if (files.length !== 1 || !object(files[0].inputSchema))
|
|
95
|
+
return false;
|
|
96
|
+
const schema = files[0].inputSchema;
|
|
97
|
+
if (!object(schema.properties) || !object(schema.properties.contact) || schema.additionalProperties !== false
|
|
98
|
+
|| (Array.isArray(schema.required) && schema.required.includes('contact')))
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return managed === 1;
|
|
102
|
+
}
|
|
103
|
+
finish(check, allowed) {
|
|
104
|
+
clearTimeout(check.timer);
|
|
105
|
+
if (allowed)
|
|
106
|
+
this.sendToCodex(JSON.stringify(check.request));
|
|
107
|
+
else
|
|
108
|
+
this.reject(check.request);
|
|
109
|
+
}
|
|
110
|
+
reject(request) {
|
|
111
|
+
this.emitToClient(JSON.stringify({ id: request.id, error: { code: -32000, message: 'CURRENT_CHAT_DELIVERY_UNAVAILABLE: unqualified MCP inventory' } }));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -32,7 +32,7 @@ export class CodexAgentSessionAdapter {
|
|
|
32
32
|
if (role.session === 'codex-app-server')
|
|
33
33
|
return this.nativeTransport({
|
|
34
34
|
name: role.name, argv: launch.argv, cwd: options.cwd, env: launch.env,
|
|
35
|
-
stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
|
|
35
|
+
stateDir: options.stateDir, mode: options.mode, requireResume: options.requireResume, permissions: options.permissions,
|
|
36
36
|
permissionMode: options.permissionMode,
|
|
37
37
|
model: role.model, effort: role.effort,
|
|
38
38
|
approvalPolicy: this.strategy.approvalPolicy(role),
|
|
@@ -44,7 +44,7 @@ export class CodexAgentSessionAdapter {
|
|
|
44
44
|
return this.transport({
|
|
45
45
|
name: role.name, harness: role.harness,
|
|
46
46
|
argv: launch.argv, cwd: options.cwd, env: launch.env,
|
|
47
|
-
stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
|
|
47
|
+
stateDir: options.stateDir, mode: options.mode, requireResume: options.requireResume, permissions: options.permissions,
|
|
48
48
|
modeId: this.strategy.permissionModeId(role),
|
|
49
49
|
mcpServers: options.managedOurs ? [...(this.strategy.mcpServers(role) ?? []).filter(s => s.name !== 'ours'), options.managedOurs.server] : this.strategy.mcpServers(role),
|
|
50
50
|
inheritEnvironment: options.managedOurs ? false : undefined,
|
package/dist/harness/codex.js
CHANGED
|
@@ -262,6 +262,7 @@ function codexAcpEnvironment(role, dirs) {
|
|
|
262
262
|
if (!source)
|
|
263
263
|
throw new Error('Codex app-server proxy is missing; rebuild ours-fleet');
|
|
264
264
|
writeFileSync(proxyModule, readFileSync(source, 'utf8'), { mode: 0o600 });
|
|
265
|
+
writeFileSync(join(runtimeDir, 'codex-file-inventory.js'), readFileSync(join(dirname(source), 'codex-file-inventory.js'), 'utf8'), { mode: 0o600 });
|
|
265
266
|
const windows = process.platform === 'win32';
|
|
266
267
|
const command = join(runtimeDir, windows ? 'codex-app-server-proxy.cmd' : 'codex-app-server-proxy');
|
|
267
268
|
const script = windows
|
|
@@ -482,7 +483,7 @@ export function makeCodexAdapter(exec = realExec, transport, nativeTransport) {
|
|
|
482
483
|
catch (error) {
|
|
483
484
|
throw new Error(`Codex ACP runtime check failed: ${error.message}; check CODEX_PATH or reinstall Fleet with optional dependencies`);
|
|
484
485
|
}
|
|
485
|
-
const minimum = role.model === 'gpt-6-astra' || resolution.version === '1.10.0'
|
|
486
|
+
const minimum = role.file_delivery?.enabled ? '0.159.0' : role.model === 'gpt-6-astra' || resolution.version === '1.10.0'
|
|
486
487
|
? '0.153.3' : '0.145.0';
|
|
487
488
|
if (!codexVersionAtLeast(runtime.version, minimum))
|
|
488
489
|
throw new Error(`${role.model ?? 'Codex ACP'} requires a newer Codex (>=${minimum}): ACP selects ${runtime.executable} (${runtime.version}); upgrade Fleet or set CODEX_PATH to a supported executable in the agent/service environment`);
|
package/dist/isolation/policy.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stateRoot } from '../paths.js';
|
|
1
2
|
import { realpathSync } from 'node:fs';
|
|
2
3
|
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
3
4
|
import { BACKENDS, ON_UNAVAILABLE, NETWORK_MODES, } from './types.js';
|
|
@@ -57,7 +58,7 @@ const SYSTEM_RO = ['/usr', '/bin', '/sbin', '/lib', '/lib64', '/etc'];
|
|
|
57
58
|
/** Ephemeral scratch mounts. */
|
|
58
59
|
const scratchTmpfs = (home) => ['/tmp', join(home, '.cache')];
|
|
59
60
|
/** Home-relative sensitive paths never exposed (the blocklist's teeth). */
|
|
60
|
-
const SENSITIVE_HOME = ['.ssh', '.aws', '.docker', '.gnupg', '.ours', 'fleet.yaml', 'fleet.d'];
|
|
61
|
+
const SENSITIVE_HOME = ['.ssh', '.aws', '.docker', '.gnupg', '.ours', '.ours-fleet/private-ours', '.ours-fleet/private-file-delivery', 'fleet.yaml', 'fleet.d'];
|
|
61
62
|
const ISOLATION_KEYS = ['backend', 'on_unavailable', 'fs', 'network', 'allow_hosts', 'resources', 'secrets'];
|
|
62
63
|
const FS_KEYS = ['read', 'write'];
|
|
63
64
|
const RESOURCE_KEYS = ['cpu', 'mem', 'pids'];
|
|
@@ -189,6 +190,8 @@ export function resolveIsolation(cfg, ctx) {
|
|
|
189
190
|
const agentsRoot = dirname(stateDir);
|
|
190
191
|
const blocklist = [
|
|
191
192
|
...SENSITIVE_HOME.map(p => join(home, p)),
|
|
193
|
+
join(stateRoot(), 'private-ours'),
|
|
194
|
+
join(stateRoot(), 'private-file-delivery'),
|
|
192
195
|
agentsRoot, // sibling agents' state dirs (this agent's own is explicitly mounted)
|
|
193
196
|
];
|
|
194
197
|
// ENFORCE the list, before anything builds a backend argv. Until now it was
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Body-free recovery cursor. Dispatching is uncertain after a crash, never
|
|
2
|
+
* permission to replay an initial task which may already have caused effects. */
|
|
3
|
+
export interface ManagedRecovery {
|
|
4
|
+
version: 1;
|
|
5
|
+
session: 'pending' | 'established';
|
|
6
|
+
readiness: boolean;
|
|
7
|
+
initial: 'pending' | 'dispatching' | 'completed';
|
|
8
|
+
active: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const managedRecoveryPath: (dir: string) => string;
|
|
11
|
+
export declare function readManagedRecovery(dir: string): ManagedRecovery | undefined;
|
|
12
|
+
export declare function writeManagedRecovery(dir: string, value: ManagedRecovery): void;
|
|
13
|
+
export declare function firstManagedSessionMayStart(dir: string): boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { lstatSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { replaceFileAtomically } from './atomic-file.js';
|
|
4
|
+
export const managedRecoveryPath = (dir) => join(dir, '.managed-recovery.json');
|
|
5
|
+
export function readManagedRecovery(dir) {
|
|
6
|
+
const path = managedRecoveryPath(dir);
|
|
7
|
+
let stat;
|
|
8
|
+
try {
|
|
9
|
+
stat = lstatSync(path);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (error.code === 'ENOENT')
|
|
13
|
+
return;
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096)
|
|
17
|
+
throw Error('MANAGED_RECOVERY_UNSAFE_PROOF');
|
|
18
|
+
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
19
|
+
if (value.version !== 1 || !['pending', 'established'].includes(value.session)
|
|
20
|
+
|| !['pending', 'dispatching', 'completed'].includes(value.initial)
|
|
21
|
+
|| typeof value.readiness !== 'boolean' || typeof value.active !== 'boolean')
|
|
22
|
+
throw Error('MANAGED_RECOVERY_INVALID_PROOF');
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
export function writeManagedRecovery(dir, value) {
|
|
26
|
+
replaceFileAtomically(managedRecoveryPath(dir), JSON.stringify(value) + '\n');
|
|
27
|
+
}
|
|
28
|
+
export function firstManagedSessionMayStart(dir) {
|
|
29
|
+
const proof = readManagedRecovery(dir);
|
|
30
|
+
return Boolean(proof?.session === 'pending' && proof.initial === 'pending' && !proof.readiness && !proof.active);
|
|
31
|
+
}
|
package/dist/ops.d.ts
CHANGED
|
@@ -54,5 +54,7 @@ export declare function up(cfg: FleetConfig, names: string[], deps: OpsDeps, con
|
|
|
54
54
|
export declare function down(cfg: FleetConfig, names: string[], deps: OpsDeps): Promise<void>;
|
|
55
55
|
/** Re-sync from config + bounce. mode 'keep' resumes context; 'fresh' wipes it. */
|
|
56
56
|
export declare function restartRoles(cfg: FleetConfig, names: string[], deps: OpsDeps, mode: 'keep' | 'fresh', configPath?: string): Promise<void>;
|
|
57
|
+
/** Read-only admission check shared by CLI removal and web preview, before any archive. */
|
|
58
|
+
export declare function assertStandaloneRoleRemoval(cfg: FleetConfig, name: string): void;
|
|
57
59
|
/** Stop + forget a role: unit, state dir, and its fleet.d file when spawned. */
|
|
58
60
|
export declare function rmRole(cfg: FleetConfig, name: string, deps: OpsDeps): Promise<void>;
|
package/dist/ops.js
CHANGED
|
@@ -7,8 +7,8 @@ import { findRole } from './config.js';
|
|
|
7
7
|
import { getAdapter } from './harness/registry.js';
|
|
8
8
|
import { generateBriefing } from './briefing.js';
|
|
9
9
|
import { managedCliBriefing } from './managed-cli.js';
|
|
10
|
-
import { resetRestartLedger } from './runner.js';
|
|
11
|
-
import { archiveTempState, stopTempSupervisor, tempSupervisorLiveness, } from './temp-lifecycle.js';
|
|
10
|
+
import { resetRestartLedger, loadTempRole } from './runner.js';
|
|
11
|
+
import { archiveTempState, stopTempSupervisor, tempSupervisorLiveness, readTempSupervisor, } from './temp-lifecycle.js';
|
|
12
12
|
import { realExec } from './exec.js';
|
|
13
13
|
import { daemonIdentityProvisioner, reconcilePermanentRoleIdentities, } from './creation.js';
|
|
14
14
|
import { provesGeneratedAgentSource } from './generated-agent-source.js';
|
|
@@ -223,8 +223,21 @@ export async function restartRoles(cfg, names, deps, mode, configPath) {
|
|
|
223
223
|
: `↻ ${role.name} — restarted (resumes; briefing re-synced)`);
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
|
+
/** Read-only admission check shared by CLI removal and web preview, before any archive. */
|
|
227
|
+
export function assertStandaloneRoleRemoval(cfg, name) {
|
|
228
|
+
if (cfg.roles.some(role => role.name === name) || !/^[A-Za-z0-9_-]+$/.test(name))
|
|
229
|
+
return;
|
|
230
|
+
const dir = agentDir(name, true);
|
|
231
|
+
if (!existsSync(dir))
|
|
232
|
+
return;
|
|
233
|
+
const taskId = readTempSupervisor(dir)?.taskOwner?.taskId
|
|
234
|
+
?? (existsSync(join(dir, 'role.yaml')) ? loadTempRole(name).roomMemberStartup?.task_id : undefined);
|
|
235
|
+
if (taskId)
|
|
236
|
+
throw Error(`TASK_MEMBER_IS_TASK_OWNED: ${name} belongs to task ${taskId}; use the owning task lifecycle; if its record is missing or corrupt, preserve evidence and restore the original records before retrying task delete`);
|
|
237
|
+
}
|
|
226
238
|
/** Stop + forget a role: unit, state dir, and its fleet.d file when spawned. */
|
|
227
239
|
export async function rmRole(cfg, name, deps) {
|
|
240
|
+
assertStandaloneRoleRemoval(cfg, name);
|
|
228
241
|
const temporaryDir = /^[A-Za-z0-9_-]+$/.test(name) ? agentDir(name, true) : '';
|
|
229
242
|
const configured = cfg.roles.find(role => role.name === name);
|
|
230
243
|
if (!configured && temporaryDir && existsSync(temporaryDir)) {
|
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -891,6 +891,27 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
891
891
|
dieTaskRoom(e);
|
|
892
892
|
}
|
|
893
893
|
});
|
|
894
|
+
cOpt(taskCmd.command('recover-members <id>'))
|
|
895
|
+
.description('resume proven stopped task members in-place; never create identities or replay invites')
|
|
896
|
+
.option('--json', 'JSON output')
|
|
897
|
+
.action(async (id, opts) => {
|
|
898
|
+
try {
|
|
899
|
+
const { recoverTaskMembers } = await import('./recovery.js');
|
|
900
|
+
const task = getTask(id), cfg = loadConfig(opts.configuration);
|
|
901
|
+
const members = await recoverTaskMembers({ taskId: id, binPath: getBinPath(),
|
|
902
|
+
cowork: task.layout ? undefined : createCoworkAdapter({ configPath: cfg.rooms?.cowork?.config }) });
|
|
903
|
+
if (opts.json)
|
|
904
|
+
console.log(JSON.stringify({ schema_version: 1, task_id: id, members }, null, 2));
|
|
905
|
+
else
|
|
906
|
+
for (const member of members)
|
|
907
|
+
console.log(`${member.name}: ${member.status}`);
|
|
908
|
+
}
|
|
909
|
+
catch (e) {
|
|
910
|
+
if (opts.json)
|
|
911
|
+
die(e);
|
|
912
|
+
dieTaskRoom(e);
|
|
913
|
+
}
|
|
914
|
+
});
|
|
894
915
|
cOpt(taskCmd.command('start <id>'))
|
|
895
916
|
.description('idempotently select a plan, provision, and start a task')
|
|
896
917
|
.option('--template <name>', 'room template')
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { retireTaskMemberService } from './task-service-retirement.js';
|
|
1
2
|
import { withFileLock } from '../atomic-file.js';
|
|
2
3
|
import { type TempLifecycleDeps } from '../temp-lifecycle.js';
|
|
3
4
|
import { type CoworkAdapter } from './cowork-adapter.js';
|
|
@@ -5,6 +6,7 @@ import type { RoomMemberSeat, RoomOrchestrationRecord } from './types.js';
|
|
|
5
6
|
export declare const CLOSE_LOCK_STALE_MS: number;
|
|
6
7
|
export declare function roomCloseLockPath(roomId: string): string;
|
|
7
8
|
export interface RoomCloseDeps {
|
|
9
|
+
retireTaskService?: typeof retireTaskMemberService;
|
|
8
10
|
inspectMember?(seat: RoomMemberSeat): Promise<{
|
|
9
11
|
launchId: string;
|
|
10
12
|
}>;
|
|
@@ -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;
|