@ours.network/fleet 1.2.0-nightly.1 → 1.2.0-nightly.11
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 +110 -21
- package/dist/agent-ours/bridge.d.ts +3 -0
- package/dist/agent-ours/bridge.js +178 -0
- package/dist/agent-ours/controller.d.ts +14 -0
- package/dist/agent-ours/controller.js +60 -0
- package/dist/agent-ours/file-rpc.d.ts +23 -0
- package/dist/agent-ours/file-rpc.js +123 -0
- package/dist/agent-ours/harness.d.ts +14 -0
- package/dist/agent-ours/harness.js +64 -0
- package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
- package/dist/agent-ours/mcp-endpoint.js +111 -0
- package/dist/agent-ours/runtime.d.ts +53 -0
- package/dist/agent-ours/runtime.js +247 -0
- package/dist/agent-ours/service.d.ts +23 -0
- package/dist/agent-ours/service.js +364 -0
- package/dist/agent-ours/state.d.ts +45 -0
- package/dist/agent-ours/state.js +95 -0
- package/dist/agent-ours/wire.d.ts +14 -0
- package/dist/agent-ours/wire.js +55 -0
- package/dist/agent-recovery-gate.js +1 -1
- package/dist/application/legacy-supervisor-identity.d.ts +7 -0
- package/dist/application/legacy-supervisor-identity.js +43 -0
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +121 -0
- package/dist/application/task-room-service.d.ts +6 -2
- package/dist/application/task-room-service.js +78 -21
- package/dist/briefing.js +10 -59
- package/dist/build-info.json +5 -4
- package/dist/capabilities.d.ts +2 -1
- package/dist/capabilities.js +2 -0
- package/dist/cli.js +63 -2
- package/dist/client-profile.d.ts +1 -0
- package/dist/client-profile.js +18 -3
- package/dist/config.d.ts +1 -0
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +3 -14
- package/dist/docs.d.ts +3 -3
- package/dist/docs.js +32 -23
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +29 -10
- package/dist/fleet-command-audit.js +3 -2
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +4 -2
- package/dist/harness/codex-session.js +9 -2
- package/dist/harness/codex.d.ts +3 -2
- package/dist/harness/codex.js +4 -3
- package/dist/harness/hermes-session.js +7 -1
- package/dist/owner-channel/channel.d.ts +3 -0
- package/dist/owner-channel/channel.js +24 -4
- package/dist/owner-channel/commands.js +5 -0
- package/dist/rooms-tasks/archived-absence.d.ts +8 -0
- package/dist/rooms-tasks/archived-absence.js +48 -0
- package/dist/rooms-tasks/cli.js +6 -1
- package/dist/rooms-tasks/close.d.ts +3 -0
- package/dist/rooms-tasks/close.js +95 -17
- package/dist/rooms-tasks/cowork-adapter.d.ts +4 -0
- package/dist/rooms-tasks/cowork-adapter.js +10 -2
- package/dist/rooms-tasks/cowork-http.d.ts +3 -0
- package/dist/rooms-tasks/cowork-http.js +70 -0
- package/dist/rooms-tasks/deletion.js +40 -3
- package/dist/rooms-tasks/live-readiness.d.ts +18 -0
- package/dist/rooms-tasks/live-readiness.js +75 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +2 -0
- package/dist/rooms-tasks/provision.d.ts +11 -0
- package/dist/rooms-tasks/provision.js +138 -26
- package/dist/rooms-tasks/room-state.js +13 -0
- package/dist/rooms-tasks/task-state.d.ts +13 -3
- package/dist/rooms-tasks/task-state.js +42 -6
- package/dist/rooms-tasks/types.d.ts +12 -0
- package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
- package/dist/rooms-tasks/workspace-artifacts.js +93 -0
- package/dist/rooms-tasks/workspace.d.ts +26 -0
- package/dist/rooms-tasks/workspace.js +195 -0
- package/dist/runner.d.ts +14 -0
- package/dist/runner.js +527 -495
- package/dist/session/codex-app-server-transport.js +1 -1
- package/dist/spawn.js +19 -16
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -0
- package/dist/supervisor/systemd.js +5 -0
- package/dist/temp-supervisor-recovery.d.ts +3 -0
- package/dist/temp-supervisor-recovery.js +46 -0
- package/dist/watchdog/briefing.js +3 -16
- package/dist/watchdog/run.js +19 -5
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +18 -1
- package/docs/task-workspaces.md +98 -0
- package/package.json +6 -3
- package/presets/fleet/roles/Coordinator.yaml +22 -0
|
@@ -53,6 +53,12 @@ export class HermesAgentSessionAdapter {
|
|
|
53
53
|
// composition; no second ambient merge may reintroduce provider secrets.
|
|
54
54
|
const env = hermesChildEnvironment(role, preparedHome(prep), launch.env, launch.env);
|
|
55
55
|
delete env.OURS_AUTOSTART;
|
|
56
|
+
if (options.managedOurs) {
|
|
57
|
+
for (const key of Object.keys(env))
|
|
58
|
+
if (key.startsWith('OURS_') && !key.startsWith('OURS_FLEET_'))
|
|
59
|
+
delete env[key];
|
|
60
|
+
Object.assign(env, { FLEET_OURS_MANAGED: '1' });
|
|
61
|
+
}
|
|
56
62
|
await this.checks.preflight?.(options, env);
|
|
57
63
|
const nativeProvider = await this.checks.expectedProvider(role, prep);
|
|
58
64
|
if (typeof nativeProvider !== 'string' || !nativeProvider.trim())
|
|
@@ -68,7 +74,7 @@ export class HermesAgentSessionAdapter {
|
|
|
68
74
|
permissions: options.permissions, modeId, requireMode: true,
|
|
69
75
|
permissionMode: { fleetMode: options.permissionMode.fleetMode, nativeMode: modeId },
|
|
70
76
|
permissionTimeoutMs: 50_000,
|
|
71
|
-
mcpServers: hermesMcpServers(role, env),
|
|
77
|
+
mcpServers: options.managedOurs ? [...hermesMcpServers(role, env).filter(s => s.name !== 'ours'), options.managedOurs.server] : hermesMcpServers(role, env),
|
|
72
78
|
scrubObsoleteOursAutostart: true,
|
|
73
79
|
validateStartupResponse: async (initialized, created) => {
|
|
74
80
|
await this.checks.validateArtifact(initialized);
|
|
@@ -14,6 +14,8 @@ export interface OwnerChannelOptions {
|
|
|
14
14
|
harness: string;
|
|
15
15
|
config: OwnerChannelConfig;
|
|
16
16
|
session: AgentSession;
|
|
17
|
+
/** Ordinary owner input must not cancel the initial briefing turn. */
|
|
18
|
+
startupPending?: () => boolean;
|
|
17
19
|
stateDir: string;
|
|
18
20
|
env?: Record<string, string>;
|
|
19
21
|
log(line: string): void;
|
|
@@ -269,6 +271,7 @@ export declare class OwnerChannel implements OwnerChannelHandle {
|
|
|
269
271
|
private attachmentGroups;
|
|
270
272
|
private handleAttachmentGroup;
|
|
271
273
|
private handle;
|
|
274
|
+
private ownerPromptPolicy;
|
|
272
275
|
/**
|
|
273
276
|
* Deterministic command path: the message never becomes an agent prompt.
|
|
274
277
|
* Authorization already happened — the managed-agent relay branch and the
|
|
@@ -1123,6 +1123,13 @@ export class OwnerChannel {
|
|
|
1123
1123
|
const retrieved = unread.length
|
|
1124
1124
|
? parseRetrievedAttachments(await this.client.getFiles(unread.map(file => file.wireId)), unread)
|
|
1125
1125
|
: [];
|
|
1126
|
+
// getFiles returns daemon-local paths, which need not exist in Fleet's
|
|
1127
|
+
// filesystem (for example with an HTTP daemon in a container). Fetch
|
|
1128
|
+
// through the bound client and keep the daemon's integrity metadata for
|
|
1129
|
+
// admission below; never trust or remap the returned filesystem path.
|
|
1130
|
+
for (const file of retrieved) {
|
|
1131
|
+
file.path = await writeRecoveredAttachment(requestDir, file.wireId, await this.client.fetchFile(file.wireId));
|
|
1132
|
+
}
|
|
1126
1133
|
for (const file of historyRecovered) {
|
|
1127
1134
|
if (!group.recovery)
|
|
1128
1135
|
throw new Error('unexpected read attachment without recovery route');
|
|
@@ -1136,8 +1143,7 @@ export class OwnerChannel {
|
|
|
1136
1143
|
await mkdir(outbox, { recursive: true, mode: 0o700 });
|
|
1137
1144
|
const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
|
|
1138
1145
|
const queued = await queueSessionPrompt(this.options.session, this.ownerAttachmentPrompt(sender, originWireId, requestId, admitted, group.caption), {
|
|
1139
|
-
|
|
1140
|
-
...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
|
|
1146
|
+
...this.ownerPromptPolicy(),
|
|
1141
1147
|
origin: { kind: 'owner', requestId,
|
|
1142
1148
|
...(group.caption ? { displayText: String(group.caption.text ?? '') } : {}) },
|
|
1143
1149
|
});
|
|
@@ -1263,8 +1269,7 @@ export class OwnerChannel {
|
|
|
1263
1269
|
const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
|
|
1264
1270
|
try {
|
|
1265
1271
|
queued = await queueSessionPrompt(this.options.session, this.ownerPrompt(sender, text, wireId), {
|
|
1266
|
-
|
|
1267
|
-
...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
|
|
1272
|
+
...this.ownerPromptPolicy(),
|
|
1268
1273
|
origin: { kind: 'owner', requestId, displayText: text },
|
|
1269
1274
|
});
|
|
1270
1275
|
}
|
|
@@ -1309,6 +1314,14 @@ export class OwnerChannel {
|
|
|
1309
1314
|
this.completionTasks.add(task);
|
|
1310
1315
|
return true;
|
|
1311
1316
|
}
|
|
1317
|
+
ownerPromptPolicy() {
|
|
1318
|
+
if (this.options.startupPending?.())
|
|
1319
|
+
return { interrupt: false, steer: true };
|
|
1320
|
+
return {
|
|
1321
|
+
interrupt: this.options.config.interrupt,
|
|
1322
|
+
...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1312
1325
|
/**
|
|
1313
1326
|
* Deterministic command path: the message never becomes an agent prompt.
|
|
1314
1327
|
* Authorization already happened — the managed-agent relay branch and the
|
|
@@ -1762,6 +1775,13 @@ export class OwnerChannel {
|
|
|
1762
1775
|
const retrieved = unread.length
|
|
1763
1776
|
? parseRetrievedAttachments(await this.client.getFiles(unread.map(file => file.wireId)), unread)
|
|
1764
1777
|
: [];
|
|
1778
|
+
// getFiles returns daemon-local paths, which need not exist in Fleet's
|
|
1779
|
+
// filesystem (for example with an HTTP daemon in a container). Fetch
|
|
1780
|
+
// through the bound client and keep the daemon's integrity metadata for
|
|
1781
|
+
// admission below; never trust or remap the returned filesystem path.
|
|
1782
|
+
for (const file of retrieved) {
|
|
1783
|
+
file.path = await writeRecoveredAttachment(requestDir, file.wireId, await this.client.fetchFile(file.wireId));
|
|
1784
|
+
}
|
|
1765
1785
|
for (const file of historyRecovered) {
|
|
1766
1786
|
if (!group.recovery)
|
|
1767
1787
|
throw new Error('unexpected read attachment without recovery route');
|
|
@@ -47,6 +47,11 @@ function isKnownOwnerRoomState(message) {
|
|
|
47
47
|
].some(pattern => pattern.test(message));
|
|
48
48
|
}
|
|
49
49
|
function ownerTaskFailure(error) {
|
|
50
|
+
if (error instanceof TaskRoomApplicationError && error.code === 'task_not_ready')
|
|
51
|
+
return {
|
|
52
|
+
kind: 'state', detail: error.message,
|
|
53
|
+
action: 'Ask Fleet Coordinator to inspect room and member status and choose supported recovery.',
|
|
54
|
+
};
|
|
50
55
|
if (error instanceof TaskListError)
|
|
51
56
|
return {
|
|
52
57
|
kind: error.code === 'list_not_found' ? 'not_found'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ArchivedMemberAbsence, RoomMemberSeat } from './types.js';
|
|
2
|
+
/** Revalidate live facts; a persisted timestamp is never cleanup authority. */
|
|
3
|
+
export declare function verifyArchivedAbsence(proof: ArchivedMemberAbsence): Promise<void>;
|
|
4
|
+
/** After the verified archive has been consumed by explicit workspace deletion,
|
|
5
|
+
* still fence replacement live state and a recreated daemon identity on every retry.
|
|
6
|
+
*/
|
|
7
|
+
export declare function verifyArchivedMemberStillAbsent(proof: ArchivedMemberAbsence): Promise<void>;
|
|
8
|
+
export declare function proveArchivedAbsence(seat: RoomMemberSeat): Promise<ArchivedMemberAbsence>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { agentDir } from '../paths.js';
|
|
4
|
+
import { tempArchiveForLaunch, tempArchiveForCreationAction, tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
5
|
+
import { assertMemberIdentityAbsent } from './close.js';
|
|
6
|
+
/** Revalidate live facts; a persisted timestamp is never cleanup authority. */
|
|
7
|
+
export async function verifyArchivedAbsence(proof) {
|
|
8
|
+
const absent = () => {
|
|
9
|
+
if (existsSync(agentDir(proof.name, true)))
|
|
10
|
+
throw new Error('Archived member has replacement live state');
|
|
11
|
+
};
|
|
12
|
+
absent();
|
|
13
|
+
const archive = tempArchiveForLaunch(proof.name, proof.launch_id);
|
|
14
|
+
const action = tempArchiveForCreationAction(proof.name, proof.action_id);
|
|
15
|
+
if (!archive || archive !== proof.archive_path || action?.path !== archive
|
|
16
|
+
|| action.launchId !== proof.launch_id
|
|
17
|
+
|| readFileSync(join(archive, '.identity'), 'utf8').trim() !== proof.name)
|
|
18
|
+
throw new Error('Archived member absence ownership proof mismatch');
|
|
19
|
+
if (await tempSupervisorLiveness(archive) !== 'stopped')
|
|
20
|
+
throw new Error('Archived member supervisor absence is not proven');
|
|
21
|
+
await verifyArchivedMemberStillAbsent(proof);
|
|
22
|
+
}
|
|
23
|
+
/** After the verified archive has been consumed by explicit workspace deletion,
|
|
24
|
+
* still fence replacement live state and a recreated daemon identity on every retry.
|
|
25
|
+
*/
|
|
26
|
+
export async function verifyArchivedMemberStillAbsent(proof) {
|
|
27
|
+
const absent = () => {
|
|
28
|
+
if (existsSync(agentDir(proof.name, true)))
|
|
29
|
+
throw new Error('Archived member has replacement live state');
|
|
30
|
+
};
|
|
31
|
+
absent();
|
|
32
|
+
await assertMemberIdentityAbsent({
|
|
33
|
+
role_name: proof.name, slot: 'archived', cowork_role: 'archived', seat_state: 'removed',
|
|
34
|
+
});
|
|
35
|
+
absent();
|
|
36
|
+
}
|
|
37
|
+
export async function proveArchivedAbsence(seat) {
|
|
38
|
+
if (seat.identity_cid || seat.retirement?.phase !== 'identity_absent'
|
|
39
|
+
|| !seat.launch?.launch_id || !seat.launch.action_id
|
|
40
|
+
|| seat.launch.launch_id !== seat.retirement.launch_id || !seat.retirement.archive_path)
|
|
41
|
+
throw new Error('Missing exact archived member absence evidence');
|
|
42
|
+
const proof = {
|
|
43
|
+
name: seat.role_name, launch_id: seat.launch.launch_id, action_id: seat.launch.action_id,
|
|
44
|
+
archive_path: seat.retirement.archive_path, checked_at: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
await verifyArchivedAbsence(proof);
|
|
47
|
+
return proof;
|
|
48
|
+
}
|
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -75,6 +75,11 @@ async function presentDetachedProvisioningOutcome(outcome) {
|
|
|
75
75
|
function taskRoomPublicFailure(error) {
|
|
76
76
|
const f = error.fields;
|
|
77
77
|
switch (error.code) {
|
|
78
|
+
case 'task_not_ready': return {
|
|
79
|
+
legacy: `task readiness ${f.readiness}: ${f.reason}`, kind: 'state',
|
|
80
|
+
detail: `Task ${f.task} readiness is ${f.readiness}: ${f.reason}${f.member ? ` (${f.member})` : ''}.`,
|
|
81
|
+
action: 'Ask Fleet Coordinator to inspect room and member status and choose supported recovery. Do not blindly relaunch agents or reuse invites.',
|
|
82
|
+
};
|
|
78
83
|
case 'task_confirmation_mismatch': return {
|
|
79
84
|
legacy: 'confirmation ID must match task ID', kind: 'usage',
|
|
80
85
|
detail: 'The two task IDs must match.', action: 'Repeat the same task ID twice.',
|
|
@@ -1152,7 +1157,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1152
1157
|
const app = taskRoomService(opts.configuration);
|
|
1153
1158
|
for (;;) {
|
|
1154
1159
|
const before = app.taskProvisioningOutcome(id);
|
|
1155
|
-
if (before.kind !== 'in_progress') {
|
|
1160
|
+
if (before.kind !== 'in_progress' || before.next_action) {
|
|
1156
1161
|
await presentDetachedProvisioningOutcome(before);
|
|
1157
1162
|
return;
|
|
1158
1163
|
}
|
|
@@ -2,6 +2,8 @@ import { withFileLock } from '../atomic-file.js';
|
|
|
2
2
|
import { type TempLifecycleDeps } from '../temp-lifecycle.js';
|
|
3
3
|
import { type CoworkAdapter } from './cowork-adapter.js';
|
|
4
4
|
import type { RoomMemberSeat, RoomOrchestrationRecord } from './types.js';
|
|
5
|
+
export declare const CLOSE_LOCK_STALE_MS: number;
|
|
6
|
+
export declare function roomCloseLockPath(roomId: string): string;
|
|
5
7
|
export interface RoomCloseDeps {
|
|
6
8
|
inspectMember?(seat: RoomMemberSeat): Promise<{
|
|
7
9
|
launchId: string;
|
|
@@ -19,6 +21,7 @@ export declare function waitForLivenessAbsent(role: string, launchId: string, li
|
|
|
19
21
|
/** Report whether any daemon identity — under any name — carries this exact CID. */
|
|
20
22
|
export declare function identityCidPresent(cid: string): Promise<boolean>;
|
|
21
23
|
export declare function removeExactMemberIdentity(seat: RoomMemberSeat): Promise<void>;
|
|
24
|
+
export declare function assertMemberIdentityAbsent(seat: RoomMemberSeat): Promise<void>;
|
|
22
25
|
/** One forward-only room close saga shared by every Fleet entry point. */
|
|
23
26
|
export declare function acceptManagedRoomClose(roomId: string): Promise<RoomOrchestrationRecord>;
|
|
24
27
|
export declare function recordManagedRoomCloseError(roomId: string, error: string, recoveryHint: string): Promise<RoomOrchestrationRecord>;
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
import { deleteWorkspace, assertWorkspaceDeletable } from './workspace.js';
|
|
2
|
+
import { collectWorkspaceArchives } from './workspace-artifacts.js';
|
|
1
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
4
|
import { join } from 'node:path';
|
|
3
5
|
import { randomUUID } from 'node:crypto';
|
|
4
6
|
import { attachOursClient } from '@ours.network/sdk/client';
|
|
5
7
|
import { withFileLock } from '../atomic-file.js';
|
|
6
8
|
import { agentDir, stateRoot } from '../paths.js';
|
|
7
|
-
import {
|
|
9
|
+
import { readClientProfile } from '../client-profile.js';
|
|
10
|
+
import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, tempArchiveForLaunch, tempArchiveForCreationAction, } from '../temp-lifecycle.js';
|
|
8
11
|
import { CoworkProtocolError } from './cowork-adapter.js';
|
|
9
12
|
import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, deleteRoomRecord, getRoomRecord, listRoomRecords, setRoomCloseError, } from './room-state.js';
|
|
10
|
-
const CLOSE_LOCK_STALE_MS = 5 * 60_000;
|
|
13
|
+
export const CLOSE_LOCK_STALE_MS = 5 * 60_000;
|
|
11
14
|
const STOP_POLLS = 50;
|
|
12
15
|
const STOP_POLL_MS = 100;
|
|
13
|
-
function roomCloseLockPath(roomId) {
|
|
16
|
+
export function roomCloseLockPath(roomId) {
|
|
14
17
|
return join(stateRoot(), 'locks', 'room-close', encodeURIComponent(roomId));
|
|
15
18
|
}
|
|
16
19
|
function errorText(error) {
|
|
@@ -54,16 +57,28 @@ export async function waitForLivenessAbsent(role, launchId, lifecycleDeps = {})
|
|
|
54
57
|
throw new Error(`temporary role '${role}' did not reach proven stopped liveness`);
|
|
55
58
|
}
|
|
56
59
|
async function withIdentityClient(work) {
|
|
57
|
-
const
|
|
60
|
+
const profile = readClientProfile(process.env);
|
|
61
|
+
const leaseToken = `ours-fleet-room-close-${process.pid}-${randomUUID()}`;
|
|
62
|
+
const client = await attachOursClient(profile ? {
|
|
63
|
+
endpoint: profile.endpoint,
|
|
64
|
+
expectedInstanceId: profile.expectedInstanceId,
|
|
65
|
+
credentialPath: profile.credentialPath,
|
|
66
|
+
sessionMode: 'external', env: {}, leaseToken,
|
|
67
|
+
} : {
|
|
58
68
|
env: process.env,
|
|
59
|
-
leaseToken
|
|
69
|
+
leaseToken,
|
|
60
70
|
clientPid: process.pid,
|
|
61
71
|
});
|
|
62
72
|
try {
|
|
63
73
|
return await work(client);
|
|
64
74
|
}
|
|
65
75
|
finally {
|
|
66
|
-
|
|
76
|
+
try {
|
|
77
|
+
await client.releaseLease().catch(() => { });
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await client.close();
|
|
81
|
+
}
|
|
67
82
|
}
|
|
68
83
|
}
|
|
69
84
|
function listedIdentity(rows, name) {
|
|
@@ -106,11 +121,57 @@ export async function removeExactMemberIdentity(seat) {
|
|
|
106
121
|
}
|
|
107
122
|
});
|
|
108
123
|
}
|
|
124
|
+
export async function assertMemberIdentityAbsent(seat) {
|
|
125
|
+
await withIdentityClient(async (client) => {
|
|
126
|
+
const rows = await client.listIdentities();
|
|
127
|
+
if (rows.some(row => row.name === seat.role_name ||
|
|
128
|
+
(seat.identity_cid && 'cid' in row && row.cid.toLowerCase() === seat.identity_cid.toLowerCase()))) {
|
|
129
|
+
throw new Error(`room member '${seat.role_name}' identity absence is not proven; refusing retirement`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
109
133
|
async function retireMember(roomId, seat, deps) {
|
|
110
134
|
let room = getRoomRecord(roomId);
|
|
111
135
|
let current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
112
136
|
let retirement = current.retirement;
|
|
137
|
+
if (retirement?.phase === 'identity_absent') {
|
|
138
|
+
if (current.launch?.launch_id && current.launch.launch_id !== retirement.launch_id) {
|
|
139
|
+
if (existsSync(agentDir(current.role_name, true)))
|
|
140
|
+
throw new Error(`room member '${current.role_name}' has a replacement launch after retirement; retire that exact temporary role before retrying`);
|
|
141
|
+
await assertMemberIdentityAbsent(current);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
113
145
|
if (!retirement) {
|
|
146
|
+
if (!existsSync(agentDir(current.role_name, true)) && current.launch?.launch_id && current.launch.action_id) {
|
|
147
|
+
const archived = tempArchiveForLaunch(current.role_name, current.launch.launch_id);
|
|
148
|
+
const created = tempArchiveForCreationAction(current.role_name, current.launch.action_id);
|
|
149
|
+
if (archived && created?.path === archived && created.launchId === current.launch.launch_id) {
|
|
150
|
+
if (readFileSync(join(archived, '.identity'), 'utf8').trim() !== current.role_name)
|
|
151
|
+
throw new Error(`room member '${current.role_name}' archive identity mismatch`);
|
|
152
|
+
if (await tempSupervisorLiveness(archived) !== 'stopped')
|
|
153
|
+
throw new Error(`room member '${current.role_name}' archived supervisor is not proven stopped`);
|
|
154
|
+
// A terminated launch can archive itself before room retirement begins.
|
|
155
|
+
// Accept its exact durable provenance only when no identity needs removal.
|
|
156
|
+
await assertMemberIdentityAbsent(current);
|
|
157
|
+
const latest = getRoomRecord(roomId)?.member_seats.find(seat => seat.role_name === current.role_name);
|
|
158
|
+
if (existsSync(agentDir(current.role_name, true))
|
|
159
|
+
|| latest?.launch?.launch_id !== current.launch.launch_id
|
|
160
|
+
|| latest?.launch?.action_id !== current.launch.action_id
|
|
161
|
+
|| latest?.identity_cid !== current.identity_cid)
|
|
162
|
+
throw new Error(`room member '${current.role_name}' changed during archived retirement proof`);
|
|
163
|
+
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', current.launch.launch_id, archived);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
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
|
+
}
|
|
114
175
|
if (current.launch?.state === 'pending' && current.launch.attempt === 0) {
|
|
115
176
|
if (existsSync(agentDir(current.role_name, true))) {
|
|
116
177
|
throw new Error(`never-launched room member '${current.role_name}' unexpectedly has Fleet temp state`);
|
|
@@ -159,8 +220,6 @@ export async function closeManagedRoom(input) {
|
|
|
159
220
|
try {
|
|
160
221
|
if (room.close?.phase === 'retire_members') {
|
|
161
222
|
for (const seat of room.member_seats) {
|
|
162
|
-
if (seat.retirement?.phase === 'identity_absent')
|
|
163
|
-
continue;
|
|
164
223
|
await retireMember(input.roomId, seat, deps);
|
|
165
224
|
}
|
|
166
225
|
room = advanceRoomClose(input.roomId, 'close_cowork');
|
|
@@ -180,6 +239,9 @@ export async function closeManagedRoom(input) {
|
|
|
180
239
|
export async function deleteLegacyClosedRooms(input) {
|
|
181
240
|
const deleted = [];
|
|
182
241
|
for (const room of listRoomRecords({ state: 'closed' })) {
|
|
242
|
+
// This compatibility sweep is retirement, never explicit workspace deletion.
|
|
243
|
+
if (room.workspace)
|
|
244
|
+
continue;
|
|
183
245
|
try {
|
|
184
246
|
await input.cowork.deleteRoom(room.room_id);
|
|
185
247
|
}
|
|
@@ -197,13 +259,29 @@ export async function deleteLegacyClosedRooms(input) {
|
|
|
197
259
|
/** Retire live resources through the existing cursor, then delete retained state. */
|
|
198
260
|
export async function deleteManagedRoom(input) {
|
|
199
261
|
await closeManagedRoom(input);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
262
|
+
return withFileLock(roomCloseLockPath(input.roomId), async () => {
|
|
263
|
+
try {
|
|
264
|
+
try {
|
|
265
|
+
await input.cowork.deleteRoom(input.roomId);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
// An earlier explicit delete may have removed the remote room before
|
|
269
|
+
// local filesystem cleanup failed. Never restore it or repeat creation.
|
|
270
|
+
if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
const retained = getRoomRecord(input.roomId);
|
|
274
|
+
if (retained?.workspace && !retained.task_id) {
|
|
275
|
+
assertWorkspaceDeletable(retained.workspace, 'room', input.roomId);
|
|
276
|
+
collectWorkspaceArchives(retained.workspace);
|
|
277
|
+
deleteWorkspace(retained.workspace, 'room', input.roomId);
|
|
278
|
+
}
|
|
279
|
+
deleteRoomRecord(input.roomId);
|
|
280
|
+
return { room_id: input.roomId, deleted: true };
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
setRoomCloseError(input.roomId, errorText(error), `Fix the cleanup error and explicitly retry room delete ${input.roomId} ${input.roomId}.`);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}, {}, CLOSE_LOCK_STALE_MS);
|
|
209
287
|
}
|
|
@@ -58,6 +58,10 @@ export interface CoworkAdapter {
|
|
|
58
58
|
briefing: string;
|
|
59
59
|
quiet_membership?: boolean;
|
|
60
60
|
anonymous?: boolean;
|
|
61
|
+
activation_requirements?: Array<{
|
|
62
|
+
role: string;
|
|
63
|
+
count: number;
|
|
64
|
+
}>;
|
|
61
65
|
}): Promise<CoworkRoomCreateResult>;
|
|
62
66
|
acceptInvite(roomId: string, invite: string, opts: {
|
|
63
67
|
role: string;
|
|
@@ -8,6 +8,8 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { join, resolve } from 'node:path';
|
|
10
10
|
import { createConnection } from 'node:net';
|
|
11
|
+
import { readClientProfile } from '../client-profile.js';
|
|
12
|
+
import { coworkHttpCall } from './cowork-http.js';
|
|
11
13
|
const RPC_VERSION = 1;
|
|
12
14
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
13
15
|
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
@@ -275,10 +277,15 @@ function rpcCall(socketPath, method, params, timeoutMs, connect) {
|
|
|
275
277
|
});
|
|
276
278
|
}
|
|
277
279
|
export function createCoworkAdapter(options = {}) {
|
|
278
|
-
const
|
|
280
|
+
const env = options.env ?? process.env;
|
|
281
|
+
const explicitLocal = options.socketPath !== undefined || options.configPath !== undefined
|
|
282
|
+
|| env.OURS_COWORK_CONFIG !== undefined || !!env.OURS_COWORK_STATE_DIR;
|
|
283
|
+
const profile = explicitLocal ? undefined : readClientProfile({ ...env, HOME: options.home ?? env.HOME });
|
|
284
|
+
const remote = profile?.serverUrl ? profile : undefined;
|
|
285
|
+
const socketPath = remote ? undefined : resolveCoworkSocketPath(options);
|
|
279
286
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
280
287
|
const connect = options.connect ?? ((path) => createConnection(path));
|
|
281
|
-
const call = (method, params) => rpcCall(socketPath, method, params, timeoutMs, connect);
|
|
288
|
+
const call = (method, params) => remote ? coworkHttpCall(remote, method, params, timeoutMs) : rpcCall(socketPath, method, params, timeoutMs, connect);
|
|
282
289
|
return {
|
|
283
290
|
async available() {
|
|
284
291
|
try {
|
|
@@ -296,6 +303,7 @@ export function createCoworkAdapter(options = {}) {
|
|
|
296
303
|
briefing: opts.briefing,
|
|
297
304
|
...(opts.quiet_membership === undefined ? {} : { quiet_membership: opts.quiet_membership }),
|
|
298
305
|
...(opts.anonymous === undefined ? {} : { anonymous: opts.anonymous }),
|
|
306
|
+
...(opts.activation_requirements === undefined ? {} : { activation_requirements: opts.activation_requirements }),
|
|
299
307
|
}), 'room.create');
|
|
300
308
|
if (!result.identity_cid)
|
|
301
309
|
throw new CoworkProtocolError('room.create', 'created room did not establish an identity CID');
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ExplicitClientProfile } from '../client-profile.js';
|
|
2
|
+
/** Same RPC protocol as the local socket; never replay a possibly delivered mutation. */
|
|
3
|
+
export declare function coworkHttpCall(profile: ExplicitClientProfile, method: string, params: Record<string, unknown>, timeoutMs: number): Promise<unknown>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { attachOursClient } from '@ours.network/sdk/client';
|
|
3
|
+
import { readPrivateFile } from '@ours.network/sdk/connector';
|
|
4
|
+
import { CoworkProtocolError, CoworkUnavailableError } from './cowork-adapter.js';
|
|
5
|
+
/** Same RPC protocol as the local socket; never replay a possibly delivered mutation. */
|
|
6
|
+
export async function coworkHttpCall(profile, method, params, timeoutMs) {
|
|
7
|
+
const id = randomUUID();
|
|
8
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
9
|
+
const body = JSON.stringify({ version: 1, id, method, params });
|
|
10
|
+
if (Buffer.byteLength(body) > 1024 * 1024)
|
|
11
|
+
throw new CoworkProtocolError(method, 'request exceeded 1 MiB');
|
|
12
|
+
try {
|
|
13
|
+
const client = await attachOursClient({ endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
|
|
14
|
+
credentialPath: profile.credentialPath, sessionMode: 'external', leaseToken: `fleet-cowork-check-${id}`, env: {}, requestSignal: signal });
|
|
15
|
+
await client.close();
|
|
16
|
+
const token = readPrivateFile(profile.credentialPath, 4096).toString('utf8').trim();
|
|
17
|
+
const response = await fetch(profile.serverUrl + '/cowork/management/rpc', {
|
|
18
|
+
method: 'POST', redirect: 'error', signal,
|
|
19
|
+
headers: { 'content-type': 'application/json', 'x-ours-api-token': token },
|
|
20
|
+
body,
|
|
21
|
+
});
|
|
22
|
+
if (response.status === 401 || response.status === 403) {
|
|
23
|
+
await response.body?.cancel();
|
|
24
|
+
throw new CoworkProtocolError(method, 'server credential was rejected', 'unauthorized');
|
|
25
|
+
}
|
|
26
|
+
const reader = response.body?.getReader();
|
|
27
|
+
if (!reader)
|
|
28
|
+
throw new CoworkProtocolError(method, 'empty HTTP response');
|
|
29
|
+
const chunks = [];
|
|
30
|
+
let size = 0;
|
|
31
|
+
try {
|
|
32
|
+
for (;;) {
|
|
33
|
+
const { done, value } = await reader.read();
|
|
34
|
+
if (done)
|
|
35
|
+
break;
|
|
36
|
+
size += value.length;
|
|
37
|
+
if (size > 4 * 1024 * 1024)
|
|
38
|
+
throw new CoworkProtocolError(method, 'response exceeded 4 MiB');
|
|
39
|
+
chunks.push(value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await reader.cancel();
|
|
44
|
+
}
|
|
45
|
+
let rpc;
|
|
46
|
+
try {
|
|
47
|
+
rpc = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new CoworkProtocolError(method, 'server returned malformed JSON');
|
|
51
|
+
}
|
|
52
|
+
if (!rpc || Array.isArray(rpc) || rpc.version !== 1 || rpc.id !== id
|
|
53
|
+
|| Object.hasOwn(rpc, 'result') === Object.hasOwn(rpc, 'error'))
|
|
54
|
+
throw new CoworkProtocolError(method, 'server returned an invalid RPC response');
|
|
55
|
+
if (Object.hasOwn(rpc, 'error')) {
|
|
56
|
+
const error = rpc.error;
|
|
57
|
+
if (!error || typeof error.code !== 'string' || typeof error.message !== 'string')
|
|
58
|
+
throw new CoworkProtocolError(method, 'server returned an invalid RPC error');
|
|
59
|
+
throw new CoworkProtocolError(method, error.message, error.code);
|
|
60
|
+
}
|
|
61
|
+
if (!response.ok)
|
|
62
|
+
throw new CoworkProtocolError(method, `management answered HTTP ${response.status}`);
|
|
63
|
+
return rpc.result;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof CoworkProtocolError)
|
|
67
|
+
throw error;
|
|
68
|
+
throw new CoworkUnavailableError('Cowork HTTP management failed; check the selected server and credential', { cause: error });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { proveArchivedAbsence, verifyArchivedAbsence, verifyArchivedMemberStillAbsent } from './archived-absence.js';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
3
|
import { withFileLock } from '../atomic-file.js';
|
|
3
4
|
import { agentDir } from '../paths.js';
|
|
5
|
+
import { deleteWorkspace, assertWorkspaceDeletable } from './workspace.js';
|
|
6
|
+
import { collectWorkspaceArchives } from './workspace-artifacts.js';
|
|
4
7
|
import { secureStoppedTempArchive, stopTempSupervisor } from '../temp-lifecycle.js';
|
|
5
8
|
import { closeManagedRoom, identityCidPresent, inspectMember, removeExactMemberIdentity, waitForLivenessAbsent, } from './close.js';
|
|
6
9
|
import { CoworkProtocolError } from './cowork-adapter.js';
|
|
7
10
|
import { deleteRoomRecord, getRoomRecord, listRoomRecords } from './room-state.js';
|
|
8
11
|
import { acquireLaunchSnapshotLock, releaseLaunchSnapshotForDeletingTask } from './launch-snapshot.js';
|
|
9
12
|
import { TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath } from './terminal.js';
|
|
10
|
-
import { advanceTaskDeletionMember, beginTaskDeletionIntent, completeTaskDeletionReceipt, ensureTaskDeletionReceipt, getDeletingTask, importTaskDeletionRetirementEvidence, setTaskDeletionError, TaskStateError, unlinkDeletedTask, upsertTaskDeletionMembersFromSeats, } from './task-state.js';
|
|
13
|
+
import { advanceTaskDeletionMember, beginTaskDeletionIntent, completeTaskDeletionReceipt, ensureTaskDeletionReceipt, getDeletingTask, importTaskDeletionRetirementEvidence, setTaskDeletionError, TaskStateError, beginTaskWorkspaceCleanup, unlinkDeletedTask, upsertTaskDeletionMembersFromSeats, } from './task-state.js';
|
|
11
14
|
export { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
|
|
12
15
|
import { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
|
|
13
16
|
/**
|
|
@@ -137,6 +140,8 @@ export async function settleTaskDeletion(input) {
|
|
|
137
140
|
// aborts settlement (fail closed).
|
|
138
141
|
ensureTaskDeletionReceipt(taskId);
|
|
139
142
|
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`);
|
|
140
145
|
const recordIds = new Set(records.map(room => room.room_id));
|
|
141
146
|
const recordedRoomId = task.deletion.room_id ?? task.room_id;
|
|
142
147
|
const needsCowork = records.length > 0 || recordedRoomId !== undefined;
|
|
@@ -145,10 +150,31 @@ export async function settleTaskDeletion(input) {
|
|
|
145
150
|
upsertTaskDeletionMembersFromSeats(taskId, record.member_seats);
|
|
146
151
|
await closeManagedRoom({ roomId: record.room_id, cowork: cowork, deps: deps.roomClose });
|
|
147
152
|
const closed = getRoomRecord(record.room_id);
|
|
148
|
-
|
|
153
|
+
const seats = closed?.member_seats ?? record.member_seats;
|
|
154
|
+
const proofs = [];
|
|
155
|
+
for (const seat of seats) {
|
|
156
|
+
if (!seat.identity_cid && seat.retirement?.launch_id !== 'never-launched')
|
|
157
|
+
proofs.push(await proveArchivedAbsence(seat));
|
|
158
|
+
}
|
|
159
|
+
// No await between the last live-state fence and durable checkpoint.
|
|
160
|
+
for (const proof of proofs) {
|
|
161
|
+
const latest = getRoomRecord(record.room_id)?.member_seats.find(seat => seat.role_name === proof.name);
|
|
162
|
+
if (existsSync(agentDir(proof.name, true)) || latest?.identity_cid
|
|
163
|
+
|| latest?.launch?.launch_id !== proof.launch_id || latest.launch.action_id !== proof.action_id)
|
|
164
|
+
throw new Error('Archived absence seat changed before checkpoint');
|
|
165
|
+
}
|
|
166
|
+
importTaskDeletionRetirementEvidence(taskId, seats, proofs);
|
|
149
167
|
await cowork.deleteRoom(record.room_id);
|
|
168
|
+
for (const proof of proofs)
|
|
169
|
+
await verifyArchivedAbsence(proof);
|
|
170
|
+
for (const proof of proofs)
|
|
171
|
+
if (existsSync(agentDir(proof.name, true)))
|
|
172
|
+
throw new Error('Archived member replacement before room unlink');
|
|
150
173
|
deleteRoomRecord(record.room_id);
|
|
151
174
|
}
|
|
175
|
+
const deletion = getDeletingTask(taskId).deletion;
|
|
176
|
+
for (const proof of deletion.archived_absences ?? [])
|
|
177
|
+
await (deletion.workspace_cleanup_started_at && !existsSync(proof.archive_path) ? verifyArchivedMemberStillAbsent : verifyArchivedAbsence)(proof);
|
|
152
178
|
// Members whose room record is gone (crash after record deletion, or
|
|
153
179
|
// legacy state): resume from the durable cursors.
|
|
154
180
|
for (const cursor of getDeletingTask(taskId).deletion.members) {
|
|
@@ -176,7 +202,7 @@ export async function settleTaskDeletion(input) {
|
|
|
176
202
|
completeTaskDeletionReceipt(taskId);
|
|
177
203
|
return { task_id: taskId, deleted: false };
|
|
178
204
|
}
|
|
179
|
-
const finalize = () => withFileLock(taskOperationLockPath(taskId), () => {
|
|
205
|
+
const finalize = () => withFileLock(taskOperationLockPath(taskId), async () => {
|
|
180
206
|
try {
|
|
181
207
|
const task = getDeletingTask(taskId);
|
|
182
208
|
if (task.deletion?.status !== 'pending')
|
|
@@ -185,8 +211,19 @@ export async function settleTaskDeletion(input) {
|
|
|
185
211
|
throw new TaskStateError(`task ${taskId} room records reappeared during deletion finalization`);
|
|
186
212
|
if (task.deletion.members.some(member => member.phase !== 'identity_absent'))
|
|
187
213
|
throw new TaskStateError(`task ${taskId} has unretired members at deletion finalization`);
|
|
214
|
+
for (const proof of task.deletion.archived_absences ?? [])
|
|
215
|
+
await (task.deletion.workspace_cleanup_started_at && !existsSync(proof.archive_path) ? verifyArchivedMemberStillAbsent : verifyArchivedAbsence)(proof);
|
|
216
|
+
for (const proof of task.deletion.archived_absences ?? [])
|
|
217
|
+
if (existsSync(agentDir(proof.name, true)))
|
|
218
|
+
throw new Error('Archived member replacement before task unlink');
|
|
188
219
|
if (cleanup.snapshotHash)
|
|
189
220
|
releaseLaunchSnapshotForDeletingTask(cleanup.snapshotHash, taskId);
|
|
221
|
+
if (task.workspace) {
|
|
222
|
+
assertWorkspaceDeletable(task.workspace, 'task', taskId);
|
|
223
|
+
beginTaskWorkspaceCleanup(taskId);
|
|
224
|
+
collectWorkspaceArchives(task.workspace);
|
|
225
|
+
deleteWorkspace(task.workspace, 'task', taskId);
|
|
226
|
+
}
|
|
190
227
|
unlinkDeletedTask(taskId);
|
|
191
228
|
}
|
|
192
229
|
catch (error) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readRoomReadiness } from '../agent-ours/service.js';
|
|
2
|
+
import { controlRequest } from '../session/control.js';
|
|
3
|
+
import { readTempSupervisor, tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
4
|
+
import type { CoworkAdapter } from './cowork-adapter.js';
|
|
5
|
+
import type { RoomOrchestrationRecord, TaskRecord } from './types.js';
|
|
6
|
+
export type TaskReadinessIssue = {
|
|
7
|
+
state: 'degraded' | 'unknown';
|
|
8
|
+
reason: string;
|
|
9
|
+
member?: string;
|
|
10
|
+
};
|
|
11
|
+
export interface LiveReadinessDeps {
|
|
12
|
+
supervisor?: typeof readTempSupervisor;
|
|
13
|
+
liveness?: typeof tempSupervisorLiveness;
|
|
14
|
+
readiness?: typeof readRoomReadiness;
|
|
15
|
+
control?: typeof controlRequest;
|
|
16
|
+
}
|
|
17
|
+
/** Read-only observation. A failed probe never grants authority to replace a seat. */
|
|
18
|
+
export declare function taskLiveReadiness(task: TaskRecord, room: RoomOrchestrationRecord | undefined, cowork: Pick<CoworkAdapter, 'getRoom'>, deps?: LiveReadinessDeps): Promise<TaskReadinessIssue | undefined>;
|