@ours.network/fleet 1.2.0-nightly.5 → 1.2.0-nightly.50
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 +263 -53
- package/dist/account-origin.d.ts +2 -0
- package/dist/account-origin.js +6 -0
- package/dist/agent-ours/bridge.js +9 -0
- package/dist/agent-ours/correspondence.d.ts +10 -0
- package/dist/agent-ours/correspondence.js +1 -0
- package/dist/agent-ours/harness.js +8 -0
- package/dist/agent-ours/runtime.d.ts +6 -0
- package/dist/agent-ours/runtime.js +36 -0
- package/dist/agent-ours/service.js +33 -13
- package/dist/application/codex-models.d.ts +15 -0
- package/dist/application/codex-models.js +56 -0
- package/dist/application/conversation-history.d.ts +25 -0
- package/dist/application/conversation-history.js +189 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +2 -2
- package/dist/application/legacy-supervisor-identity.d.ts +7 -0
- package/dist/application/legacy-supervisor-identity.js +43 -0
- package/dist/application/model-catalog.d.ts +1 -1
- package/dist/application/model-catalog.js +5 -2
- package/dist/application/preset-provenance.d.ts +19 -0
- package/dist/application/preset-provenance.js +59 -0
- package/dist/application/role-creation-service.js +1 -1
- package/dist/application/role-removal-service.js +2 -0
- package/dist/application/role-repository.js +2 -2
- package/dist/application/room-layout-definitions.d.ts +45 -0
- package/dist/application/room-layout-definitions.js +130 -0
- package/dist/application/session-control.d.ts +30 -0
- package/dist/application/session-control.js +72 -3
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +113 -0
- package/dist/application/task-layouts.d.ts +91 -0
- package/dist/application/task-layouts.js +249 -0
- package/dist/application/task-room-service.d.ts +99 -3
- package/dist/application/task-room-service.js +296 -31
- package/dist/atomic-file.d.ts +5 -0
- package/dist/atomic-file.js +62 -1
- package/dist/briefing.d.ts +2 -0
- package/dist/briefing.js +8 -1
- package/dist/build-info.json +7 -4
- package/dist/capabilities.d.ts +6 -1
- package/dist/capabilities.js +8 -0
- package/dist/cli.js +376 -34
- package/dist/client-profile.d.ts +5 -17
- package/dist/client-profile.js +4 -127
- package/dist/config.d.ts +28 -0
- package/dist/config.js +41 -6
- package/dist/creation.js +2 -2
- package/dist/daemon-recovery.d.ts +1 -8
- package/dist/daemon-recovery.js +8 -113
- package/dist/docs.d.ts +3 -3
- package/dist/docs.js +126 -22
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +55 -60
- package/dist/erased-resources.d.ts +9 -0
- package/dist/erased-resources.js +87 -0
- package/dist/fleet-command-audit.d.ts +7 -0
- package/dist/fleet-command-audit.js +26 -9
- package/dist/harness/claude-code.js +9 -1
- package/dist/harness/claude-runtime.d.ts +12 -0
- package/dist/harness/claude-runtime.js +43 -0
- package/dist/harness/codex-app-server-proxy.d.ts +4 -1
- package/dist/harness/codex-app-server-proxy.js +41 -5
- package/dist/harness/codex-reasoning.d.ts +5 -0
- package/dist/harness/codex-reasoning.js +12 -0
- package/dist/harness/codex-runtime.d.ts +11 -1
- package/dist/harness/codex-runtime.js +31 -4
- package/dist/harness/codex.d.ts +3 -2
- package/dist/harness/codex.js +20 -11
- package/dist/init-wizard.d.ts +2 -0
- package/dist/init-wizard.js +38 -19
- package/dist/isolation/policy.js +2 -0
- package/dist/isolation/types.d.ts +5 -0
- package/dist/managed-cli.d.ts +253 -0
- package/dist/managed-cli.js +923 -0
- package/dist/minimal-setup.d.ts +2 -0
- package/dist/minimal-setup.js +44 -0
- package/dist/monitor.d.ts +1 -34
- package/dist/monitor.js +24 -151
- package/dist/notifications/fleet-producer.d.ts +19 -0
- package/dist/notifications/fleet-producer.js +208 -0
- package/dist/notifications/outbox.d.ts +46 -0
- package/dist/notifications/outbox.js +158 -0
- package/dist/notifications/target-cleanup.d.ts +8 -0
- package/dist/notifications/target-cleanup.js +41 -0
- package/dist/ops.js +5 -1
- package/dist/owner-channel/channel.d.ts +3 -0
- package/dist/owner-channel/channel.js +34 -6
- package/dist/owner-channel/commands.d.ts +4 -0
- package/dist/owner-channel/commands.js +8 -0
- package/dist/owner-channel/lifecycle-outbox.js +4 -3
- package/dist/owner-channel/ours-client.js +2 -17
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +2 -0
- package/dist/preset-migration.js +25 -0
- package/dist/resolved-plan.js +1 -0
- package/dist/rooms-tasks/add-member.d.ts +48 -0
- package/dist/rooms-tasks/add-member.js +83 -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 +34 -5
- package/dist/rooms-tasks/close.d.ts +5 -2
- package/dist/rooms-tasks/close.js +208 -28
- package/dist/rooms-tasks/config.d.ts +1 -1
- package/dist/rooms-tasks/config.js +11 -4
- package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
- package/dist/rooms-tasks/cowork-adapter.js +8 -121
- package/dist/rooms-tasks/deletion.d.ts +1 -1
- package/dist/rooms-tasks/deletion.js +74 -5
- package/dist/rooms-tasks/erasure.d.ts +6 -0
- package/dist/rooms-tasks/erasure.js +169 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -1
- package/dist/rooms-tasks/external-worker.js +3 -4
- package/dist/rooms-tasks/layout-binding-client.d.ts +11 -0
- package/dist/rooms-tasks/layout-binding-client.js +56 -0
- package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
- package/dist/rooms-tasks/layout-binding-grants.js +128 -0
- package/dist/rooms-tasks/layout-cli.d.ts +4 -0
- package/dist/rooms-tasks/layout-cli.js +107 -0
- package/dist/rooms-tasks/layout-config.d.ts +11 -0
- package/dist/rooms-tasks/layout-config.js +89 -0
- package/dist/rooms-tasks/layout-control.d.ts +21 -0
- package/dist/rooms-tasks/layout-control.js +52 -0
- package/dist/rooms-tasks/layout-service.d.ts +42 -0
- package/dist/rooms-tasks/layout-service.js +185 -0
- package/dist/rooms-tasks/layout.d.ts +114 -0
- package/dist/rooms-tasks/layout.js +250 -0
- package/dist/rooms-tasks/live-readiness.d.ts +18 -0
- package/dist/rooms-tasks/live-readiness.js +85 -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 +14 -0
- package/dist/rooms-tasks/provision.js +173 -46
- package/dist/rooms-tasks/room-state.d.ts +4 -2
- package/dist/rooms-tasks/room-state.js +53 -8
- package/dist/rooms-tasks/task-state.d.ts +42 -7
- package/dist/rooms-tasks/task-state.js +153 -23
- package/dist/rooms-tasks/types.d.ts +28 -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 +19 -3
- package/dist/runner.js +128 -45
- package/dist/session/acp.d.ts +9 -1
- package/dist/session/acp.js +31 -6
- package/dist/session/arbiter.d.ts +5 -0
- package/dist/session/arbiter.js +28 -2
- package/dist/session/codex-app-server.d.ts +5 -0
- package/dist/session/codex-app-server.js +36 -3
- package/dist/session/control.d.ts +12 -1
- package/dist/session/control.js +68 -3
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-store.d.ts +1 -1
- package/dist/session/conversation-store.js +7 -11
- package/dist/session/conversation-types.d.ts +7 -3
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +6 -0
- package/dist/setup-tunnel-preflight.d.ts +34 -0
- package/dist/setup-tunnel-preflight.js +62 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/spawn.js +8 -1
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -0
- package/dist/subscriptions/cli.d.ts +91 -0
- package/dist/subscriptions/cli.js +223 -0
- package/dist/subscriptions/launch.d.ts +37 -0
- package/dist/subscriptions/launch.js +93 -0
- package/dist/subscriptions/login.d.ts +75 -0
- package/dist/subscriptions/login.js +182 -0
- package/dist/subscriptions/observed.d.ts +7 -0
- package/dist/subscriptions/observed.js +44 -0
- package/dist/subscriptions/probe.d.ts +10 -0
- package/dist/subscriptions/probe.js +73 -0
- package/dist/subscriptions/service.d.ts +102 -0
- package/dist/subscriptions/service.js +364 -0
- package/dist/subscriptions/store.d.ts +71 -0
- package/dist/subscriptions/store.js +140 -0
- package/dist/supervisor/launchd.js +2 -1
- package/dist/supervisor/systemd.js +9 -23
- package/dist/temp-idle.d.ts +17 -0
- package/dist/temp-idle.js +42 -0
- package/dist/temp-lifecycle.d.ts +6 -1
- package/dist/temp-lifecycle.js +29 -13
- package/dist/tunnel-setup-completion.d.ts +7 -0
- package/dist/tunnel-setup-completion.js +13 -0
- package/dist/tunnel-setup-input.d.ts +19 -0
- package/dist/tunnel-setup-input.js +124 -0
- package/dist/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +29 -1
- package/dist/web/auth.js +82 -7
- package/dist/web/available-port.d.ts +2 -0
- package/dist/web/available-port.js +23 -0
- package/dist/web/control.d.ts +3 -3
- package/dist/web/control.js +10 -8
- package/dist/web/fleet-config-service.d.ts +4 -1
- package/dist/web/fleet-config-service.js +46 -15
- package/dist/web/prefix-gateway.d.ts +32 -0
- package/dist/web/prefix-gateway.js +234 -0
- package/dist/web/runtime.d.ts +3 -1
- package/dist/web/runtime.js +116 -31
- package/dist/web/server.d.ts +16 -0
- package/dist/web/server.js +588 -53
- package/dist/web/service.d.ts +11 -2
- package/dist/web/service.js +30 -4
- package/dist/web/workspace-devices.d.ts +36 -0
- package/dist/web/workspace-devices.js +101 -0
- package/dist/web/workspace-tunnel.d.ts +1 -0
- package/dist/web/workspace-tunnel.js +34 -0
- package/dist/workspace-enrollment.d.ts +82 -0
- package/dist/workspace-enrollment.js +566 -0
- package/dist/workspace-replacement.d.ts +66 -0
- package/dist/workspace-replacement.js +220 -0
- package/docs/live-voice.md +13 -0
- package/docs/room-layouts.md +227 -0
- package/docs/task-workspaces.md +130 -0
- package/docs/validation/managed-cli-permissions.md +421 -0
- package/examples/room-layouts/fresh.yaml +27 -0
- package/examples/room-layouts/scoped.yaml +21 -0
- package/examples/room-layouts/shared.yaml +19 -0
- package/package.json +15 -18
- package/presets/README.md +78 -0
- package/presets/brain-catalog.json +8 -2
- package/presets/fleet/agent_templates/Critic.yaml +1 -1
- package/presets/fleet/agent_templates/Developer.yaml +1 -1
- package/presets/fleet/agent_templates/Engineer.yaml +5 -0
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +1 -1
- package/presets/fleet/agents/FleetCoordinator.yaml +1 -1
- 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-1-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
- 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/fleet/roles/Coordinator.yaml +34 -11
- package/presets/fleet/roles/Engineer.yaml +40 -0
- package/presets/fleet/room_layouts/engineering.yaml +25 -0
- package/presets/fleet/room_layouts/pair.yaml +24 -0
- package/presets/fleet/room_layouts/single.yaml +17 -0
- package/presets/fleet/room_layouts/team.yaml +30 -0
- package/presets/fleet/room_templates/engineering.yaml +19 -0
- package/presets/manifest.json +2 -2
- package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
- package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
- package/dist/web-app/icons/ours-fleet.svg +0 -4
- package/dist/web-app/index.html +0 -17
- package/dist/web-app/manifest.webmanifest +0 -15
- package/dist/web-app/offline.html +0 -18
- package/dist/web-app/sw.js +0 -51
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
|
|
5
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
6
|
+
import { stateRoot } from '../paths.js';
|
|
7
|
+
import { getTask, TaskStateError } from './task-state.js';
|
|
8
|
+
import { getRoomRecord } from './room-state.js';
|
|
9
|
+
import { prepareExecutionPlan } from './member-overrides.js';
|
|
10
|
+
import { sealTemplateSnapshot } from './templates.js';
|
|
11
|
+
import { provisionMembers } from './provision.js';
|
|
12
|
+
export class MemberAdditionRejected extends TaskStateError {
|
|
13
|
+
accepted = false;
|
|
14
|
+
}
|
|
15
|
+
const running = new Set();
|
|
16
|
+
function pathFor(taskId, requestId) {
|
|
17
|
+
if (!/^[a-z0-9]{10,40}$/.test(taskId) || !/^[a-zA-Z0-9_-]{8,80}$/.test(requestId))
|
|
18
|
+
throw new TaskStateError('Invalid task or request ID');
|
|
19
|
+
return join(stateRoot(), 'member-additions', taskId, requestId + '.json');
|
|
20
|
+
}
|
|
21
|
+
export function memberAddition(taskId, requestId) {
|
|
22
|
+
getTask(taskId);
|
|
23
|
+
const path = pathFor(taskId, requestId);
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
throw new TaskStateError('Member request not found');
|
|
26
|
+
const receipt = JSON.parse(readFileSync(path, 'utf8'));
|
|
27
|
+
const { hash, ...view } = receipt;
|
|
28
|
+
return receipt.state === 'running' && !running.has(path) ? { ...view, state: 'attention', error: 'The server stopped observing this launch. Inspect the existing agent before any recovery.' } : view;
|
|
29
|
+
}
|
|
30
|
+
export async function requestMemberAddition(input) {
|
|
31
|
+
const { request: r, taskId } = input;
|
|
32
|
+
if (!r || typeof r.slot !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(r.slot)
|
|
33
|
+
|| ![r.role, r.brain, r.agentTemplate].every(v => typeof v === 'string' && v.length > 0 && v.length <= 128))
|
|
34
|
+
// No receipt lookup has happened yet: this request ID may already be accepted.
|
|
35
|
+
throw new TaskStateError('Slot, Role, Brain and Agent Template are required');
|
|
36
|
+
const path = pathFor(taskId, r.requestId);
|
|
37
|
+
const hash = createHash('sha256').update(canonicalJson(r)).digest('hex');
|
|
38
|
+
mkdirSync(join(stateRoot(), 'member-additions', taskId), { recursive: true, mode: 0o700 });
|
|
39
|
+
return withFileLock(path + '.lock', async () => {
|
|
40
|
+
if (existsSync(path)) {
|
|
41
|
+
const old = JSON.parse(readFileSync(path, 'utf8'));
|
|
42
|
+
if (old.hash !== hash)
|
|
43
|
+
throw new TaskStateError('Request ID was already used with different settings');
|
|
44
|
+
return memberAddition(taskId, r.requestId);
|
|
45
|
+
}
|
|
46
|
+
const task = getTask(taskId);
|
|
47
|
+
const room = task.room_id && getRoomRecord(task.room_id);
|
|
48
|
+
if (!room || !['active', 'review'].includes(task.state) || task.terminal_intent || room.state !== 'active'
|
|
49
|
+
|| room.task_id !== taskId || room.room_identity_cid !== task.room_identity_cid)
|
|
50
|
+
throw new MemberAdditionRejected('Adding an agent requires an active task room');
|
|
51
|
+
const roleId = `${taskId.slice(0, 8)}-${r.slot}-1`;
|
|
52
|
+
if (room.member_seats.some(s => s.role_name === roleId))
|
|
53
|
+
throw new MemberAdditionRejected('This member slot already exists');
|
|
54
|
+
const plan = (() => {
|
|
55
|
+
try {
|
|
56
|
+
return prepareExecutionPlan({ name: 'additional-member', version: 1, description: 'Additional task participant',
|
|
57
|
+
contract: room.template_snapshot?.contract, members: [{ slot: r.slot, role: r.role, count: 1, agent_template: r.agentTemplate }] }, input.cfg, { [r.slot]: { role: r.role, brain: r.brain } });
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
throw new MemberAdditionRejected(error instanceof Error ? error.message : 'Invalid member configuration');
|
|
61
|
+
}
|
|
62
|
+
})();
|
|
63
|
+
const template = sealTemplateSnapshot(plan.snapshot, input.cfg.agentTemplates ?? {}, plan.launchDefinitions);
|
|
64
|
+
const receipt = { taskId, requestId: r.requestId, roleId, hash, state: 'running' };
|
|
65
|
+
const save = () => replaceFileAtomically(path, JSON.stringify(receipt) + '\n', 0o600);
|
|
66
|
+
save();
|
|
67
|
+
running.add(path);
|
|
68
|
+
void (input.provision ?? provisionMembers)({ cfg: input.cfg, cowork: input.cowork, configPath: input.configPath, binPath: input.binPath,
|
|
69
|
+
taskId, roomId: room.room_id, template, goal: task.title, brief: task.brief, append: true }).then(result => {
|
|
70
|
+
const seat = result.member_seats.find(s => s.role_name === roleId);
|
|
71
|
+
receipt.state = seat?.seat_state === 'active' && getTask(taskId).member_roles.some(m => m.name === roleId) ? 'succeeded' : 'attention';
|
|
72
|
+
if (receipt.state === 'attention')
|
|
73
|
+
receipt.error = 'Admission is not confirmed. Inspect the existing launch; no automatic retry was made.';
|
|
74
|
+
}).catch(() => { receipt.state = 'failed'; receipt.error = 'Member launch failed. Inspect the task and agent logs before recovery.'; })
|
|
75
|
+
.finally(() => { try {
|
|
76
|
+
save();
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
running.delete(path);
|
|
80
|
+
} }).catch(() => { running.delete(path); });
|
|
81
|
+
return memberAddition(taskId, r.requestId);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -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.',
|
|
@@ -515,10 +520,10 @@ function taskRoomService(configuration) {
|
|
|
515
520
|
function allTemplates(cfg) {
|
|
516
521
|
return cfg.roomTemplates ?? {};
|
|
517
522
|
}
|
|
518
|
-
function coworkFor(cfg) {
|
|
523
|
+
function coworkFor(cfg, options) {
|
|
519
524
|
if (!cfg.rooms)
|
|
520
525
|
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
521
|
-
return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
526
|
+
return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config, ...options });
|
|
522
527
|
}
|
|
523
528
|
function resolveRoomTemplate(cfg, name) {
|
|
524
529
|
if (!name)
|
|
@@ -827,16 +832,18 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
827
832
|
dieTaskRoom(e);
|
|
828
833
|
}
|
|
829
834
|
});
|
|
830
|
-
taskCmd.command('show <id>')
|
|
835
|
+
cOpt(taskCmd.command('show <id>'))
|
|
831
836
|
.description('show task details')
|
|
832
837
|
.option('--json', 'JSON output')
|
|
833
838
|
.action(async (id, opts) => {
|
|
834
839
|
try {
|
|
835
|
-
const service = taskRoomService();
|
|
840
|
+
const service = taskRoomService(opts.configuration);
|
|
836
841
|
const { task: t, orchestration: room } = service.getTask(id);
|
|
842
|
+
const readiness = t.state === 'active' ? await service.observeTaskReadiness(id) : undefined;
|
|
837
843
|
if (opts.json) {
|
|
838
844
|
console.log(JSON.stringify({
|
|
839
845
|
schema_version: 1, task: t, orchestration: room ?? null,
|
|
846
|
+
...(readiness ? { readiness } : {}),
|
|
840
847
|
}, null, 2));
|
|
841
848
|
return;
|
|
842
849
|
}
|
|
@@ -846,6 +853,8 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
846
853
|
{ label: 'ID', value: t.task_id, kind: 'code' },
|
|
847
854
|
{ label: 'Title', value: t.title },
|
|
848
855
|
{ label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
|
|
856
|
+
...(readiness ? [{ label: 'Readiness', value: readiness.state },
|
|
857
|
+
...('reason' in readiness ? [{ label: 'Reason', value: readiness.reason }] : [])] : []),
|
|
849
858
|
...(t.blocked ? [{ label: 'Blocked', value: t.blocked.reason }] : []),
|
|
850
859
|
...(t.template ? [{ label: 'Template', value: `${t.template.name}@${t.template.version}`, kind: 'code' }] : []),
|
|
851
860
|
...(t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []),
|
|
@@ -1152,7 +1161,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1152
1161
|
const app = taskRoomService(opts.configuration);
|
|
1153
1162
|
for (;;) {
|
|
1154
1163
|
const before = app.taskProvisioningOutcome(id);
|
|
1155
|
-
if (before.kind !== 'in_progress') {
|
|
1164
|
+
if (before.kind !== 'in_progress' || before.next_action) {
|
|
1156
1165
|
await presentDetachedProvisioningOutcome(before);
|
|
1157
1166
|
return;
|
|
1158
1167
|
}
|
|
@@ -1176,6 +1185,26 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1176
1185
|
dieTaskRoom(e);
|
|
1177
1186
|
}
|
|
1178
1187
|
});
|
|
1188
|
+
cOpt(taskCmd.command('_layout <id> <operation> [room]', { hidden: true }))
|
|
1189
|
+
.description('internal: run an accepted layout room operation (open, close-room, close)')
|
|
1190
|
+
.option('--json', 'JSON output')
|
|
1191
|
+
.option('--operation-id <id>', 'accepted operation ID; status updates apply only while it is current')
|
|
1192
|
+
.action(async (id, operation, room, opts) => {
|
|
1193
|
+
try {
|
|
1194
|
+
if (!['open', 'close-room', 'close'].includes(operation))
|
|
1195
|
+
throw new Error(`unknown layout operation: ${operation}`);
|
|
1196
|
+
const view = await taskRoomService(opts.configuration).runTaskLayoutOperation({
|
|
1197
|
+
actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
|
|
1198
|
+
operation: operation, room, operationId: opts.operationId,
|
|
1199
|
+
});
|
|
1200
|
+
console.log(JSON.stringify({ schema_version: 1, layout: view }, null, 2));
|
|
1201
|
+
}
|
|
1202
|
+
catch (e) {
|
|
1203
|
+
if (opts.json)
|
|
1204
|
+
die(e);
|
|
1205
|
+
dieTaskRoom(e);
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1179
1208
|
cOpt(taskCmd.command('_settle_delete <id>', { hidden: true }))
|
|
1180
1209
|
.description('internal: settle a previously accepted task deletion')
|
|
1181
1210
|
.option('--json', 'JSON output')
|
|
@@ -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,12 +21,13 @@ 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>;
|
|
25
28
|
export declare function closeManagedRoom(input: {
|
|
26
29
|
roomId: string;
|
|
27
|
-
cowork: Pick<CoworkAdapter, 'closeRoom'
|
|
30
|
+
cowork: Pick<CoworkAdapter, 'closeRoom'> & Partial<Pick<CoworkAdapter, 'getRoom'>>;
|
|
28
31
|
deps?: RoomCloseDeps;
|
|
29
32
|
}): Promise<RoomOrchestrationRecord>;
|
|
30
33
|
export interface ManagedRoomDeleteResult {
|
|
@@ -38,6 +41,6 @@ export declare function deleteLegacyClosedRooms(input: {
|
|
|
38
41
|
/** Retire live resources through the existing cursor, then delete retained state. */
|
|
39
42
|
export declare function deleteManagedRoom(input: {
|
|
40
43
|
roomId: string;
|
|
41
|
-
cowork: Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'
|
|
44
|
+
cowork: Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'> & Partial<Pick<CoworkAdapter, 'getRoom'>>;
|
|
42
45
|
deps?: RoomCloseDeps;
|
|
43
46
|
}): Promise<ManagedRoomDeleteResult>;
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
+
import { detachDeletedRoom } from './task-state.js';
|
|
2
|
+
import { binderKey } from '../agent-ours/state.js';
|
|
3
|
+
import { eraseMemberArtifacts } from './erasure.js';
|
|
4
|
+
import { deleteWorkspace, assertWorkspaceDeletable } from './workspace.js';
|
|
5
|
+
import { collectWorkspaceArchives } from './workspace-artifacts.js';
|
|
1
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
7
|
import { join } from 'node:path';
|
|
3
8
|
import { randomUUID } from 'node:crypto';
|
|
4
9
|
import { attachOursClient } from '@ours.network/sdk/client';
|
|
5
10
|
import { withFileLock } from '../atomic-file.js';
|
|
6
11
|
import { agentDir, stateRoot } from '../paths.js';
|
|
7
|
-
import {
|
|
12
|
+
import { readClientProfile } from '../client-profile.js';
|
|
13
|
+
import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, tempArchiveForLaunch, tempArchiveForCreationAction, } from '../temp-lifecycle.js';
|
|
8
14
|
import { CoworkProtocolError } from './cowork-adapter.js';
|
|
9
|
-
import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, deleteRoomRecord, getRoomRecord, listRoomRecords, setRoomCloseError, } from './room-state.js';
|
|
10
|
-
const CLOSE_LOCK_STALE_MS = 5 * 60_000;
|
|
15
|
+
import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, deleteRoomRecord, getRoomRecord, listRoomRecords, setRoomCloseError, recordRetirementMemberCids, } from './room-state.js';
|
|
16
|
+
export const CLOSE_LOCK_STALE_MS = 5 * 60_000;
|
|
11
17
|
const STOP_POLLS = 50;
|
|
12
18
|
const STOP_POLL_MS = 100;
|
|
13
|
-
function roomCloseLockPath(roomId) {
|
|
19
|
+
export function roomCloseLockPath(roomId) {
|
|
14
20
|
return join(stateRoot(), 'locks', 'room-close', encodeURIComponent(roomId));
|
|
15
21
|
}
|
|
16
22
|
function errorText(error) {
|
|
@@ -19,6 +25,20 @@ function errorText(error) {
|
|
|
19
25
|
function exactMemberIdentity(seat) {
|
|
20
26
|
const dir = agentDir(seat.role_name, true);
|
|
21
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
|
+
}
|
|
22
42
|
if (!existsSync(dir) || !existsSync(identityPath)) {
|
|
23
43
|
throw new Error(`room member '${seat.role_name}' has no live Fleet temp-state identity proof; refusing retirement`);
|
|
24
44
|
}
|
|
@@ -30,7 +50,8 @@ function exactMemberIdentity(seat) {
|
|
|
30
50
|
export async function inspectMember(seat) {
|
|
31
51
|
exactMemberIdentity(seat);
|
|
32
52
|
const supervisor = readTempSupervisor(agentDir(seat.role_name, true));
|
|
33
|
-
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)) {
|
|
34
55
|
throw new Error(`room member '${seat.role_name}' has no exact Fleet supervisor ownership proof`);
|
|
35
56
|
}
|
|
36
57
|
return { launchId: supervisor.launchId };
|
|
@@ -54,16 +75,24 @@ export async function waitForLivenessAbsent(role, launchId, lifecycleDeps = {})
|
|
|
54
75
|
throw new Error(`temporary role '${role}' did not reach proven stopped liveness`);
|
|
55
76
|
}
|
|
56
77
|
async function withIdentityClient(work) {
|
|
78
|
+
const profile = readClientProfile(process.env);
|
|
79
|
+
const leaseToken = `ours-fleet-room-close-${process.pid}-${randomUUID()}`;
|
|
57
80
|
const client = await attachOursClient({
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
81
|
+
endpoint: profile.endpoint,
|
|
82
|
+
expectedInstanceId: profile.expectedInstanceId,
|
|
83
|
+
credentialPath: profile.credentialPath,
|
|
84
|
+
sessionMode: 'external', env: {}, leaseToken,
|
|
61
85
|
});
|
|
62
86
|
try {
|
|
63
87
|
return await work(client);
|
|
64
88
|
}
|
|
65
89
|
finally {
|
|
66
|
-
|
|
90
|
+
try {
|
|
91
|
+
await client.releaseLease().catch(() => { });
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await client.close();
|
|
95
|
+
}
|
|
67
96
|
}
|
|
68
97
|
}
|
|
69
98
|
function listedIdentity(rows, name) {
|
|
@@ -78,14 +107,19 @@ export async function identityCidPresent(cid) {
|
|
|
78
107
|
}
|
|
79
108
|
export async function removeExactMemberIdentity(seat) {
|
|
80
109
|
await withIdentityClient(async (client) => {
|
|
81
|
-
const
|
|
82
|
-
|
|
110
|
+
const rows = await client.listIdentities();
|
|
111
|
+
const before = listedIdentity(rows, seat.role_name);
|
|
112
|
+
if (!before) {
|
|
113
|
+
if (rows.some(row => row.name === seat.role_name || (seat.identity_cid && 'cid' in row
|
|
114
|
+
&& row.cid.toLowerCase() === seat.identity_cid.toLowerCase())))
|
|
115
|
+
throw new Error(`room member '${seat.role_name}' identity absence is not proven`);
|
|
83
116
|
return;
|
|
117
|
+
}
|
|
84
118
|
if (!seat.identity_cid) {
|
|
85
|
-
throw new Error(`room member '${seat.role_name}' exists without a recorded authenticated CID; refusing removal`);
|
|
119
|
+
throw new Error(`room member '${seat.role_name}' exists without a recorded authenticated CID; identity absence is not proven; refusing removal`);
|
|
86
120
|
}
|
|
87
121
|
if (before.cid?.toLowerCase() !== seat.identity_cid.toLowerCase()) {
|
|
88
|
-
throw new Error(`room member '${seat.role_name}' identity CID mismatch: recorded ${seat.identity_cid}, found ${before.cid ?? 'none'}`);
|
|
122
|
+
throw new Error(`room member '${seat.role_name}' identity absence is not proven: CID mismatch: recorded ${seat.identity_cid}, found ${before.cid ?? 'none'}`);
|
|
89
123
|
}
|
|
90
124
|
try {
|
|
91
125
|
await client.removeIdentity({ name: seat.role_name });
|
|
@@ -100,17 +134,59 @@ export async function removeExactMemberIdentity(seat) {
|
|
|
100
134
|
if (after)
|
|
101
135
|
throw error;
|
|
102
136
|
}
|
|
103
|
-
const after =
|
|
104
|
-
if (after
|
|
137
|
+
const after = await client.listIdentities();
|
|
138
|
+
if (after.some(row => row.name === seat.role_name || (seat.identity_cid && 'cid' in row
|
|
139
|
+
&& row.cid.toLowerCase() === seat.identity_cid.toLowerCase()))) {
|
|
105
140
|
throw new Error(`room member '${seat.role_name}' identity still exists after remove_identity`);
|
|
106
141
|
}
|
|
107
142
|
});
|
|
108
143
|
}
|
|
144
|
+
export async function assertMemberIdentityAbsent(seat) {
|
|
145
|
+
await withIdentityClient(async (client) => {
|
|
146
|
+
const rows = await client.listIdentities();
|
|
147
|
+
if (rows.some(row => row.name === seat.role_name ||
|
|
148
|
+
(seat.identity_cid && 'cid' in row && row.cid.toLowerCase() === seat.identity_cid.toLowerCase()))) {
|
|
149
|
+
throw new Error(`room member '${seat.role_name}' identity absence is not proven; refusing retirement`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
109
153
|
async function retireMember(roomId, seat, deps) {
|
|
110
154
|
let room = getRoomRecord(roomId);
|
|
111
155
|
let current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
112
156
|
let retirement = current.retirement;
|
|
157
|
+
if (retirement?.phase === 'identity_absent') {
|
|
158
|
+
if (!deps.inspectMember) {
|
|
159
|
+
if (existsSync(agentDir(current.role_name, true)))
|
|
160
|
+
throw new Error('Retired member has replacement launch live state');
|
|
161
|
+
await assertMemberIdentityAbsent(current);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
113
165
|
if (!retirement) {
|
|
166
|
+
if (!existsSync(agentDir(current.role_name, true)) && current.launch?.launch_id && current.launch.action_id) {
|
|
167
|
+
const archived = tempArchiveForLaunch(current.role_name, current.launch.launch_id);
|
|
168
|
+
const created = tempArchiveForCreationAction(current.role_name, current.launch.action_id);
|
|
169
|
+
if (archived && (!created || created.path !== archived || created.launchId !== current.launch.launch_id))
|
|
170
|
+
throw new Error('Archived member creation ownership mismatch');
|
|
171
|
+
if (archived && created?.path === archived && created.launchId === current.launch.launch_id) {
|
|
172
|
+
if (readFileSync(join(archived, '.identity'), 'utf8').trim() !== current.role_name)
|
|
173
|
+
throw new Error(`room member '${current.role_name}' archive identity mismatch`);
|
|
174
|
+
if (await tempSupervisorLiveness(archived) !== 'stopped')
|
|
175
|
+
throw new Error(`room member '${current.role_name}' archived supervisor is not proven stopped`);
|
|
176
|
+
// A terminated launch can archive itself before room retirement begins.
|
|
177
|
+
// Accept its exact durable provenance only when no identity needs removal.
|
|
178
|
+
await (deps.removeIdentity ?? removeExactMemberIdentity)(current);
|
|
179
|
+
await assertMemberIdentityAbsent(current);
|
|
180
|
+
const latest = getRoomRecord(roomId)?.member_seats.find(seat => seat.role_name === current.role_name);
|
|
181
|
+
if (existsSync(agentDir(current.role_name, true))
|
|
182
|
+
|| latest?.launch?.launch_id !== current.launch.launch_id
|
|
183
|
+
|| latest?.launch?.action_id !== current.launch.action_id
|
|
184
|
+
|| latest?.identity_cid !== current.identity_cid)
|
|
185
|
+
throw new Error(`room member '${current.role_name}' changed during archived retirement proof`);
|
|
186
|
+
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', current.launch.launch_id, archived);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
114
190
|
if (current.launch?.state === 'pending' && current.launch.attempt === 0) {
|
|
115
191
|
if (existsSync(agentDir(current.role_name, true))) {
|
|
116
192
|
throw new Error(`never-launched room member '${current.role_name}' unexpectedly has Fleet temp state`);
|
|
@@ -119,12 +195,31 @@ async function retireMember(roomId, seat, deps) {
|
|
|
119
195
|
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', 'never-launched');
|
|
120
196
|
return;
|
|
121
197
|
}
|
|
198
|
+
if (!deps.inspectMember && !existsSync(agentDir(current.role_name, true))) {
|
|
199
|
+
await (deps.removeIdentity ?? removeExactMemberIdentity)(current);
|
|
200
|
+
if (existsSync(agentDir(current.role_name, true)))
|
|
201
|
+
throw new Error(`room member '${current.role_name}' acquired replacement state during retirement`);
|
|
202
|
+
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', 'absent-verified');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
122
205
|
const ownership = await (deps.inspectMember ?? inspectMember)(current);
|
|
123
206
|
room = advanceMemberRetirement(roomId, current.role_name, 'stop_requested', ownership.launchId);
|
|
124
207
|
current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
125
208
|
retirement = current.retirement;
|
|
126
209
|
}
|
|
210
|
+
if (['stop_requested', 'liveness_absent', 'archive_secured'].includes(retirement.phase) && !deps.requestStop
|
|
211
|
+
&& !existsSync(agentDir(current.role_name, true))) {
|
|
212
|
+
const archive = tempArchiveForLaunch(current.role_name, retirement.launch_id);
|
|
213
|
+
if (archive && await tempSupervisorLiveness(archive) !== 'stopped')
|
|
214
|
+
throw new Error('Archived supervisor is not proven stopped');
|
|
215
|
+
await (deps.removeIdentity ?? removeExactMemberIdentity)(current);
|
|
216
|
+
await assertMemberIdentityAbsent(current);
|
|
217
|
+
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', retirement.launch_id, archive, !archive);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
127
220
|
if (retirement.phase === 'stop_requested') {
|
|
221
|
+
if (!deps.inspectMember && (await inspectMember(current)).launchId !== retirement.launch_id)
|
|
222
|
+
throw new Error('Member launch changed before stop');
|
|
128
223
|
await (deps.requestStop ?? (async (role) => { await stopTempSupervisor(role); }))(current.role_name);
|
|
129
224
|
await (deps.waitForLivenessAbsent ?? waitForLivenessAbsent)(current.role_name, retirement.launch_id);
|
|
130
225
|
room = advanceMemberRetirement(roomId, current.role_name, 'liveness_absent', retirement.launch_id);
|
|
@@ -154,19 +249,31 @@ export async function closeManagedRoom(input) {
|
|
|
154
249
|
const lock = deps.lock ?? withFileLock;
|
|
155
250
|
return lock(roomCloseLockPath(input.roomId), async () => {
|
|
156
251
|
let room = beginRoomClose(input.roomId);
|
|
157
|
-
if (room.state === 'closed')
|
|
252
|
+
if (room.state === 'closed') {
|
|
253
|
+
if (!deps.inspectMember)
|
|
254
|
+
for (const seat of room.member_seats) {
|
|
255
|
+
if (existsSync(agentDir(seat.role_name, true)))
|
|
256
|
+
throw new Error('Closed room has replacement live state');
|
|
257
|
+
await assertMemberIdentityAbsent(seat);
|
|
258
|
+
}
|
|
158
259
|
return room;
|
|
260
|
+
}
|
|
159
261
|
try {
|
|
160
262
|
if (room.close?.phase === 'retire_members') {
|
|
263
|
+
room = await recoverMemberCids(room, input.cowork);
|
|
161
264
|
for (const seat of room.member_seats) {
|
|
162
|
-
if (seat.retirement?.phase === 'identity_absent')
|
|
163
|
-
continue;
|
|
164
265
|
await retireMember(input.roomId, seat, deps);
|
|
165
266
|
}
|
|
166
267
|
room = advanceRoomClose(input.roomId, 'close_cowork');
|
|
167
268
|
}
|
|
168
269
|
if (room.close?.phase === 'close_cowork') {
|
|
169
|
-
|
|
270
|
+
try {
|
|
271
|
+
await input.cowork.closeRoom(input.roomId);
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
170
277
|
}
|
|
171
278
|
return closeRoom(input.roomId);
|
|
172
279
|
}
|
|
@@ -180,6 +287,9 @@ export async function closeManagedRoom(input) {
|
|
|
180
287
|
export async function deleteLegacyClosedRooms(input) {
|
|
181
288
|
const deleted = [];
|
|
182
289
|
for (const room of listRoomRecords({ state: 'closed' })) {
|
|
290
|
+
// This compatibility sweep is retirement, never explicit workspace deletion.
|
|
291
|
+
if (room.workspace)
|
|
292
|
+
continue;
|
|
183
293
|
try {
|
|
184
294
|
await input.cowork.deleteRoom(room.room_id);
|
|
185
295
|
}
|
|
@@ -196,14 +306,84 @@ export async function deleteLegacyClosedRooms(input) {
|
|
|
196
306
|
}
|
|
197
307
|
/** Retire live resources through the existing cursor, then delete retained state. */
|
|
198
308
|
export async function deleteManagedRoom(input) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
309
|
+
if (getRoomRecord(input.roomId))
|
|
310
|
+
await closeManagedRoom(input);
|
|
311
|
+
return withFileLock(roomCloseLockPath(input.roomId), async () => {
|
|
312
|
+
try {
|
|
313
|
+
try {
|
|
314
|
+
await input.cowork.deleteRoom(input.roomId);
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
// An earlier explicit delete may have removed the remote room before
|
|
318
|
+
// local filesystem cleanup failed. Never restore it or repeat creation.
|
|
319
|
+
if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
const retained = getRoomRecord(input.roomId);
|
|
323
|
+
if (retained?.workspace && !retained.task_id) {
|
|
324
|
+
assertWorkspaceDeletable(retained.workspace, 'room', input.roomId);
|
|
325
|
+
collectWorkspaceArchives(retained.workspace);
|
|
326
|
+
deleteWorkspace(retained.workspace, 'room', input.roomId);
|
|
327
|
+
}
|
|
328
|
+
if (retained)
|
|
329
|
+
await eraseMemberArtifacts('room', input.roomId, retained.member_seats, [input.roomId]);
|
|
330
|
+
if (retained?.task_id)
|
|
331
|
+
detachDeletedRoom(retained.task_id, input.roomId);
|
|
332
|
+
deleteRoomRecord(input.roomId);
|
|
333
|
+
return { room_id: input.roomId, deleted: true };
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (getRoomRecord(input.roomId))
|
|
337
|
+
setRoomCloseError(input.roomId, errorText(error), `Fix the cleanup error and explicitly retry room delete ${input.roomId} ${input.roomId}.`);
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
}, {}, CLOSE_LOCK_STALE_MS);
|
|
341
|
+
}
|
|
342
|
+
/** Recover only an authenticated seat tied to this exact room and issued invite.
|
|
343
|
+
* Display names and the current daemon name inventory are never binding proof.
|
|
344
|
+
*/
|
|
345
|
+
async function recoverMemberCids(room, cowork) {
|
|
346
|
+
if (!room.member_seats.some(seat => !seat.identity_cid))
|
|
347
|
+
return room;
|
|
348
|
+
const profile = room.member_seats.some(seat => seat.launch?.action_id) ? readClientProfile(process.env) : undefined;
|
|
349
|
+
let remote;
|
|
350
|
+
if (cowork.getRoom && room.room_identity_cid) {
|
|
351
|
+
try {
|
|
352
|
+
remote = await cowork.getRoom(room.room_id);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
if (remote && (remote.room_id !== room.room_id || remote.identity_cid.toLowerCase() !== room.room_identity_cid.toLowerCase()))
|
|
359
|
+
throw new Error('Room identity mismatch during deletion recovery');
|
|
206
360
|
}
|
|
207
|
-
|
|
208
|
-
|
|
361
|
+
const seats = room.member_seats.map(seat => {
|
|
362
|
+
let runtimeCid;
|
|
363
|
+
if (profile && seat.launch?.action_id) {
|
|
364
|
+
const dir = join(stateRoot(), 'private-ours', binderKey(profile.expectedInstanceId, seat.role_name));
|
|
365
|
+
try {
|
|
366
|
+
const state = JSON.parse(readFileSync(join(dir, 'state.json'), 'utf8'));
|
|
367
|
+
const instance = JSON.parse(readFileSync(join(dir, 'instance.json'), 'utf8'));
|
|
368
|
+
if (state.name === seat.role_name && state.daemon === profile.expectedInstanceId
|
|
369
|
+
&& state.lifetime === 'temporary' && state.action === seat.launch.action_id
|
|
370
|
+
&& instance.role === seat.role_name && instance.temporary === true && instance.instance === state.instance)
|
|
371
|
+
runtimeCid = state.cid;
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (error.code !== 'ENOENT')
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const matches = seat.invite_id ? remote?.seats.filter(value => value.invite_id === seat.invite_id && value.role === seat.cowork_role) ?? [] : [];
|
|
379
|
+
if (matches.length > 1)
|
|
380
|
+
throw new Error('Ambiguous authenticated member identity during deletion recovery');
|
|
381
|
+
const candidates = [seat.identity_cid, runtimeCid, matches[0]?.identity_cid].filter((v) => !!v);
|
|
382
|
+
if (candidates.some(cid => !/^[a-f0-9]{64}$/i.test(cid)))
|
|
383
|
+
throw new Error('Invalid authenticated member CID');
|
|
384
|
+
if (new Set(candidates.map(cid => cid.toLowerCase())).size > 1)
|
|
385
|
+
throw new Error('Contradictory authenticated member CID evidence');
|
|
386
|
+
return candidates.length ? { ...seat, identity_cid: candidates[0] } : seat;
|
|
387
|
+
});
|
|
388
|
+
return recordRetirementMemberCids(room.room_id, seats);
|
|
209
389
|
}
|
|
@@ -4,7 +4,7 @@ export declare class RoomsTasksConfigError extends Error {
|
|
|
4
4
|
constructor(path: string, message: string);
|
|
5
5
|
}
|
|
6
6
|
export declare function fingerprint(invite: string): string;
|
|
7
|
-
export declare function validateRoomsConfig(raw: unknown, vars: Record<string, string>, path: string): RoomsConfig & {
|
|
7
|
+
export declare function validateRoomsConfig(raw: unknown, vars: Record<string, string>, path: string, deferredInviteFile?: string): RoomsConfig & {
|
|
8
8
|
_invite?: {
|
|
9
9
|
value: string;
|
|
10
10
|
fingerprint: string;
|