@ours.network/fleet 1.2.0-nightly.2 → 1.2.0-nightly.21
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 +152 -29
- 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/correspondence.d.ts +10 -0
- package/dist/agent-ours/correspondence.js +1 -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 +72 -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 +59 -0
- package/dist/agent-ours/runtime.js +283 -0
- package/dist/agent-ours/service.d.ts +23 -0
- package/dist/agent-ours/service.js +372 -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/conversation-history.d.ts +6 -0
- package/dist/application/conversation-history.js +65 -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/room-layout-definitions.d.ts +45 -0
- package/dist/application/room-layout-definitions.js +130 -0
- package/dist/application/session-control.d.ts +8 -0
- package/dist/application/session-control.js +21 -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 +89 -0
- package/dist/application/task-layouts.js +248 -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.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 +65 -2
- package/dist/client-profile.d.ts +5 -16
- package/dist/client-profile.js +4 -112
- package/dist/config.d.ts +4 -0
- package/dist/config.js +7 -0
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +5 -16
- 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 +43 -23
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +46 -59
- package/dist/erased-resources.d.ts +9 -0
- package/dist/erased-resources.js +87 -0
- package/dist/fleet-command-audit.d.ts +1 -0
- package/dist/fleet-command-audit.js +12 -7
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +4 -2
- 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-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/init-wizard.js +20 -9
- package/dist/monitor.d.ts +1 -34
- package/dist/monitor.js +24 -151
- 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/preset-migration.js +15 -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.js +3 -1
- package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
- package/dist/rooms-tasks/cowork-adapter.js +11 -117
- package/dist/rooms-tasks/cowork-http.d.ts +3 -0
- package/dist/rooms-tasks/cowork-http.js +70 -0
- 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-cli.d.ts +2 -0
- package/dist/rooms-tasks/layout-cli.js +81 -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 +39 -0
- package/dist/rooms-tasks/layout-service.js +173 -0
- package/dist/rooms-tasks/layout.d.ts +102 -0
- package/dist/rooms-tasks/layout.js +227 -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 +182 -48
- 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 +23 -3
- package/dist/runner.js +574 -494
- package/dist/session/acp.d.ts +3 -0
- package/dist/session/acp.js +7 -0
- package/dist/session/arbiter.d.ts +3 -0
- package/dist/session/arbiter.js +20 -2
- package/dist/session/codex-app-server-transport.js +1 -1
- package/dist/session/codex-app-server.d.ts +3 -0
- package/dist/session/codex-app-server.js +25 -1
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +36 -2
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-types.d.ts +5 -3
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +3 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/spawn.js +19 -16
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -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/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/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +4 -1
- package/dist/web/auth.js +23 -2
- package/dist/web/control.js +2 -1
- 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 +20 -0
- package/dist/web/prefix-gateway.js +150 -0
- package/dist/web/runtime.d.ts +4 -0
- package/dist/web/runtime.js +19 -14
- package/dist/web/server.d.ts +7 -0
- package/dist/web/server.js +293 -25
- package/docs/room-layouts.md +92 -0
- package/docs/task-workspaces.md +130 -0
- package/examples/room-layouts/fresh.yaml +27 -0
- package/examples/room-layouts/shared.yaml +19 -0
- package/package.json +17 -13
- package/presets/README.md +78 -0
- package/presets/brain-catalog.json +8 -2
- package/presets/fleet/agent_templates/Engineer.yaml +5 -0
- 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 +22 -0
- 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
|
@@ -54,7 +54,9 @@ export function validateRoomsConfig(raw, vars, path) {
|
|
|
54
54
|
if (!isPlainObject(raw.cowork))
|
|
55
55
|
throw new RoomsTasksConfigError(path, 'rooms.cowork: must be a mapping');
|
|
56
56
|
rejectUnknown(raw.cowork, RCK, path, 'rooms.cowork');
|
|
57
|
-
|
|
57
|
+
if (raw.cowork.config !== undefined)
|
|
58
|
+
throw new RoomsTasksConfigError(path, 'rooms.cowork.config is no longer supported; remove it and configure ~/.ours-client/profile.json for all services');
|
|
59
|
+
cowork = {};
|
|
58
60
|
}
|
|
59
61
|
if (!isPlainObject(raw.owner))
|
|
60
62
|
throw new RoomsTasksConfigError(path, 'rooms.owner: required and must be a mapping');
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Typed client for the ours-cowork v1
|
|
2
|
+
* Typed client for the ours-cowork v1 gateway management protocol.
|
|
3
3
|
*
|
|
4
4
|
* Cowork owns room state. Fleet only invokes the versioned JSON RPC exposed
|
|
5
|
-
*
|
|
5
|
+
* through the shared HTTP gateway and projects the response into orchestration data.
|
|
6
6
|
*/
|
|
7
7
|
import type { Socket } from 'node:net';
|
|
8
8
|
import type { RoomHistoryEvidence } from './types.js';
|
|
@@ -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;
|
|
@@ -104,6 +108,4 @@ export interface CoworkAdapterOptions {
|
|
|
104
108
|
home?: string;
|
|
105
109
|
connect?: (path: string) => Socket;
|
|
106
110
|
}
|
|
107
|
-
/** Resolve the same config/state inputs as ours-cowork and return its Unix socket. */
|
|
108
|
-
export declare function resolveCoworkSocketPath(options?: CoworkAdapterOptions): string;
|
|
109
111
|
export declare function createCoworkAdapter(options?: CoworkAdapterOptions): CoworkAdapter;
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Typed client for the ours-cowork v1
|
|
2
|
+
* Typed client for the ours-cowork v1 gateway management protocol.
|
|
3
3
|
*
|
|
4
4
|
* Cowork owns room state. Fleet only invokes the versioned JSON RPC exposed
|
|
5
|
-
*
|
|
5
|
+
* through the shared HTTP gateway and projects the response into orchestration data.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { join, resolve } from 'node:path';
|
|
10
|
-
import { createConnection } from 'node:net';
|
|
11
|
-
const RPC_VERSION = 1;
|
|
7
|
+
import { readClientProfile } from '../client-profile.js';
|
|
8
|
+
import { coworkHttpCall } from './cowork-http.js';
|
|
12
9
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
13
|
-
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
14
10
|
export class CoworkUnavailableError extends Error {
|
|
15
|
-
constructor(message = 'Cowork management
|
|
11
|
+
constructor(message = 'Cowork gateway management is not reachable', options) {
|
|
16
12
|
super(message, options);
|
|
17
13
|
this.name = 'CoworkUnavailableError';
|
|
18
14
|
}
|
|
@@ -169,116 +165,13 @@ function projectHistoryRecord(value) {
|
|
|
169
165
|
}
|
|
170
166
|
return undefined;
|
|
171
167
|
}
|
|
172
|
-
/** Resolve the same config/state inputs as ours-cowork and return its Unix socket. */
|
|
173
|
-
export function resolveCoworkSocketPath(options = {}) {
|
|
174
|
-
if (options.socketPath)
|
|
175
|
-
return resolve(options.socketPath);
|
|
176
|
-
const env = options.env ?? process.env;
|
|
177
|
-
const home = options.home ?? homedir();
|
|
178
|
-
const configuredPath = options.configPath ?? env.OURS_COWORK_CONFIG;
|
|
179
|
-
const configPath = resolve(configuredPath ?? join(home, '.ours-cowork', 'config.json'));
|
|
180
|
-
let stateDir = resolve(home, '.ours-cowork');
|
|
181
|
-
if (existsSync(configPath)) {
|
|
182
|
-
let parsed;
|
|
183
|
-
try {
|
|
184
|
-
parsed = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
throw new CoworkProtocolError('config', `malformed config at ${configPath}`, 'invalid_config', { cause: error });
|
|
188
|
-
}
|
|
189
|
-
const config = object(parsed, 'config', 'config');
|
|
190
|
-
if (config.version !== 1 || typeof config.stateDir !== 'string' || !config.stateDir)
|
|
191
|
-
throw new CoworkProtocolError('config', `invalid Cowork v1 config at ${configPath}`, 'invalid_config');
|
|
192
|
-
stateDir = resolve(config.stateDir);
|
|
193
|
-
}
|
|
194
|
-
else if (configuredPath !== undefined) {
|
|
195
|
-
throw new CoworkProtocolError('config', `configured Cowork config does not exist: ${configPath}`, 'invalid_config');
|
|
196
|
-
}
|
|
197
|
-
if (env.OURS_COWORK_STATE_DIR)
|
|
198
|
-
stateDir = resolve(env.OURS_COWORK_STATE_DIR);
|
|
199
|
-
return join(stateDir, 'management.sock');
|
|
200
|
-
}
|
|
201
|
-
function rpcCall(socketPath, method, params, timeoutMs, connect) {
|
|
202
|
-
const id = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
203
|
-
const request = `${JSON.stringify({ version: RPC_VERSION, id, method, params })}\n`;
|
|
204
|
-
return new Promise((resolveCall, rejectCall) => {
|
|
205
|
-
const socket = connect(socketPath);
|
|
206
|
-
let bytes = '';
|
|
207
|
-
let size = 0;
|
|
208
|
-
let settled = false;
|
|
209
|
-
let connected = false;
|
|
210
|
-
const finishError = (error) => {
|
|
211
|
-
if (settled)
|
|
212
|
-
return;
|
|
213
|
-
settled = true;
|
|
214
|
-
clearTimeout(timer);
|
|
215
|
-
socket.destroy();
|
|
216
|
-
rejectCall(error);
|
|
217
|
-
};
|
|
218
|
-
const timer = setTimeout(() => finishError(new CoworkUnavailableError(connected ? 'Cowork did not answer the management socket' : 'Cowork management socket is not reachable')), timeoutMs);
|
|
219
|
-
socket.setEncoding('utf8');
|
|
220
|
-
socket.once('connect', () => {
|
|
221
|
-
connected = true;
|
|
222
|
-
socket.write(request);
|
|
223
|
-
});
|
|
224
|
-
socket.on('data', (chunk) => {
|
|
225
|
-
if (settled)
|
|
226
|
-
return;
|
|
227
|
-
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
228
|
-
size += Buffer.byteLength(text);
|
|
229
|
-
if (size > MAX_RESPONSE_BYTES) {
|
|
230
|
-
finishError(new CoworkProtocolError(method, 'response exceeded 4 MiB'));
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
bytes += text;
|
|
234
|
-
const newline = bytes.indexOf('\n');
|
|
235
|
-
if (newline < 0)
|
|
236
|
-
return;
|
|
237
|
-
if (bytes.slice(newline + 1).trim() !== '') {
|
|
238
|
-
finishError(new CoworkProtocolError(method, 'daemon returned more than one response'));
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
let response;
|
|
242
|
-
try {
|
|
243
|
-
response = object(JSON.parse(bytes.slice(0, newline)), method, 'RPC response');
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
finishError(error instanceof CoworkProtocolError
|
|
247
|
-
? error : new CoworkProtocolError(method, 'daemon returned malformed JSON', 'protocol_error', { cause: error }));
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
if (response.version !== RPC_VERSION || response.id !== id
|
|
251
|
-
|| (Object.hasOwn(response, 'error') === Object.hasOwn(response, 'result'))) {
|
|
252
|
-
finishError(new CoworkProtocolError(method, 'daemon returned an invalid RPC response'));
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
settled = true;
|
|
256
|
-
clearTimeout(timer);
|
|
257
|
-
socket.destroy();
|
|
258
|
-
if (Object.hasOwn(response, 'error')) {
|
|
259
|
-
const error = object(response.error, method, 'RPC error');
|
|
260
|
-
rejectCall(new CoworkProtocolError(method, typeof error.message === 'string' ? error.message : 'unknown RPC error', typeof error.code === 'string' ? error.code : 'rpc_error'));
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
resolveCall(response.result);
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
socket.once('end', () => {
|
|
267
|
-
if (!settled)
|
|
268
|
-
finishError(new CoworkProtocolError(method, 'daemon closed without a complete response'));
|
|
269
|
-
});
|
|
270
|
-
socket.once('error', (error) => {
|
|
271
|
-
if (!settled)
|
|
272
|
-
finishError(new CoworkUnavailableError(error.code === 'EACCES' || error.code === 'EPERM'
|
|
273
|
-
? 'Cowork management socket access denied' : 'Cowork management socket is not reachable', { cause: error }));
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
168
|
export function createCoworkAdapter(options = {}) {
|
|
278
|
-
const
|
|
169
|
+
const env = options.env ?? process.env;
|
|
170
|
+
if (options.socketPath !== undefined || options.configPath !== undefined)
|
|
171
|
+
throw new CoworkProtocolError('config', 'Local Cowork selectors are unsupported; configure the shared gateway client profile', 'invalid_config');
|
|
172
|
+
const profile = readClientProfile({ ...env, HOME: options.home ?? env.HOME });
|
|
279
173
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
280
|
-
const
|
|
281
|
-
const call = (method, params) => rpcCall(socketPath, method, params, timeoutMs, connect);
|
|
174
|
+
const call = (method, params) => coworkHttpCall(profile, method, params, timeoutMs);
|
|
282
175
|
return {
|
|
283
176
|
async available() {
|
|
284
177
|
try {
|
|
@@ -296,6 +189,7 @@ export function createCoworkAdapter(options = {}) {
|
|
|
296
189
|
briefing: opts.briefing,
|
|
297
190
|
...(opts.quiet_membership === undefined ? {} : { quiet_membership: opts.quiet_membership }),
|
|
298
191
|
...(opts.anonymous === undefined ? {} : { anonymous: opts.anonymous }),
|
|
192
|
+
...(opts.activation_requirements === undefined ? {} : { activation_requirements: opts.activation_requirements }),
|
|
299
193
|
}), 'room.create');
|
|
300
194
|
if (!result.identity_cid)
|
|
301
195
|
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
|
+
}
|
|
@@ -3,7 +3,7 @@ import { type CoworkAdapter } from './cowork-adapter.js';
|
|
|
3
3
|
import { type TaskDeletionAcceptance } from './task-state.js';
|
|
4
4
|
import type { TaskDeletionActor, TaskRecord } from './types.js';
|
|
5
5
|
export { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
|
|
6
|
-
type DeletionCowork = Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'
|
|
6
|
+
type DeletionCowork = Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'> & Partial<Pick<CoworkAdapter, 'getRoom'>>;
|
|
7
7
|
export interface TaskDeletionSettleDeps {
|
|
8
8
|
roomClose?: RoomCloseDeps;
|
|
9
9
|
/** Test seam for the temp-state existence proof. */
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import { eraseMemberArtifacts } from './erasure.js';
|
|
2
|
+
import { proveArchivedAbsence, verifyArchivedAbsence, verifyArchivedMemberStillAbsent } from './archived-absence.js';
|
|
1
3
|
import { existsSync } from 'node:fs';
|
|
2
4
|
import { withFileLock } from '../atomic-file.js';
|
|
3
5
|
import { agentDir } from '../paths.js';
|
|
6
|
+
import { deleteWorkspace, assertWorkspaceDeletable } from './workspace.js';
|
|
7
|
+
import { collectWorkspaceArchives } from './workspace-artifacts.js';
|
|
4
8
|
import { secureStoppedTempArchive, stopTempSupervisor } from '../temp-lifecycle.js';
|
|
5
|
-
import { closeManagedRoom, identityCidPresent, inspectMember, removeExactMemberIdentity, waitForLivenessAbsent, } from './close.js';
|
|
9
|
+
import { closeManagedRoom, identityCidPresent, inspectMember, assertMemberIdentityAbsent, removeExactMemberIdentity, waitForLivenessAbsent, } from './close.js';
|
|
6
10
|
import { CoworkProtocolError } from './cowork-adapter.js';
|
|
7
11
|
import { deleteRoomRecord, getRoomRecord, listRoomRecords } from './room-state.js';
|
|
8
12
|
import { acquireLaunchSnapshotLock, releaseLaunchSnapshotForDeletingTask } from './launch-snapshot.js';
|
|
9
13
|
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';
|
|
14
|
+
import { advanceTaskDeletionMember, settleAbsentTaskDeletionMember, beginTaskDeletionIntent, completeTaskDeletionReceipt, ensureTaskDeletionReceipt, getDeletingTask, importTaskDeletionRetirementEvidence, setTaskDeletionError, TaskStateError, beginTaskArchiveCleanup, unlinkDeletedTask, upsertTaskDeletionMembersFromSeats, } from './task-state.js';
|
|
11
15
|
export { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
|
|
12
16
|
import { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
|
|
13
17
|
/**
|
|
@@ -40,6 +44,7 @@ function tolerantCowork(cowork) {
|
|
|
40
44
|
}
|
|
41
45
|
};
|
|
42
46
|
return {
|
|
47
|
+
...(cowork.getRoom ? { getRoom: cowork.getRoom.bind(cowork) } : {}),
|
|
43
48
|
closeRoom: roomId => tolerate(() => cowork.closeRoom(roomId)),
|
|
44
49
|
deleteRoom: roomId => tolerate(() => cowork.deleteRoom(roomId)),
|
|
45
50
|
};
|
|
@@ -48,6 +53,7 @@ function cursorSeat(cursor) {
|
|
|
48
53
|
return {
|
|
49
54
|
role_name: cursor.name, identity_cid: cursor.identity_cid,
|
|
50
55
|
slot: cursor.name, cowork_role: 'member', seat_state: 'active',
|
|
56
|
+
...(cursor.launch_id ? { retirement: { phase: 'identity_absent', launch_id: cursor.launch_id, archive_path: cursor.archive_path, updated_at: cursor.updated_at } } : {}),
|
|
51
57
|
};
|
|
52
58
|
}
|
|
53
59
|
/**
|
|
@@ -60,8 +66,15 @@ function cursorSeat(cursor) {
|
|
|
60
66
|
async function retireCursorMember(taskId, cursor, deps) {
|
|
61
67
|
let phase = cursor.phase;
|
|
62
68
|
let launchId = cursor.launch_id;
|
|
63
|
-
if (phase === 'identity_absent')
|
|
69
|
+
if (phase === 'identity_absent') {
|
|
70
|
+
if (!deps.roomClose?.inspectMember) {
|
|
71
|
+
if ((deps.hasTempState ?? (name => existsSync(agentDir(name, true))))(cursor.name))
|
|
72
|
+
throw new Error('Retired deletion member has replacement live state');
|
|
73
|
+
if (await (deps.identityCidPresent ?? identityCidPresent)(cursor.identity_cid))
|
|
74
|
+
throw new Error('Retired deletion member identity reappeared');
|
|
75
|
+
}
|
|
64
76
|
return;
|
|
77
|
+
}
|
|
65
78
|
const seat = cursorSeat(cursor);
|
|
66
79
|
const roomClose = deps.roomClose ?? {};
|
|
67
80
|
if (phase === 'pending') {
|
|
@@ -85,7 +98,16 @@ async function retireCursorMember(taskId, cursor, deps) {
|
|
|
85
98
|
phase = 'stop_requested';
|
|
86
99
|
launchId = ownership.launchId;
|
|
87
100
|
}
|
|
101
|
+
if (!roomClose.inspectMember && !existsSync(agentDir(cursor.name, true))) {
|
|
102
|
+
await (roomClose.removeIdentity ?? removeExactMemberIdentity)(seat);
|
|
103
|
+
if (existsSync(agentDir(cursor.name, true)))
|
|
104
|
+
throw new Error('Deletion member acquired replacement state');
|
|
105
|
+
settleAbsentTaskDeletionMember(taskId, cursor.name);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
88
108
|
if (phase === 'stop_requested') {
|
|
109
|
+
if (!roomClose.inspectMember && (await inspectMember(seat)).launchId !== launchId)
|
|
110
|
+
throw new Error('Deletion member launch changed before stop');
|
|
89
111
|
await (roomClose.requestStop
|
|
90
112
|
?? (async (role) => { await stopTempSupervisor(role); }))(cursor.name);
|
|
91
113
|
await (roomClose.waitForLivenessAbsent ?? waitForLivenessAbsent)(cursor.name, launchId);
|
|
@@ -145,10 +167,40 @@ export async function settleTaskDeletion(input) {
|
|
|
145
167
|
upsertTaskDeletionMembersFromSeats(taskId, record.member_seats);
|
|
146
168
|
await closeManagedRoom({ roomId: record.room_id, cowork: cowork, deps: deps.roomClose });
|
|
147
169
|
const closed = getRoomRecord(record.room_id);
|
|
148
|
-
|
|
170
|
+
const seats = closed?.member_seats ?? record.member_seats;
|
|
171
|
+
const proofs = [];
|
|
172
|
+
for (const seat of seats) {
|
|
173
|
+
if (!seat.identity_cid && !seat.retirement?.absence_verified && !['never-launched', 'absent-verified'].includes(seat.retirement?.launch_id ?? '')) {
|
|
174
|
+
const prior = task.deletion.archived_absences?.find(proof => proof.name === seat.role_name);
|
|
175
|
+
if (task.deletion.workspace_cleanup_started_at && prior && !existsSync(prior.archive_path)) {
|
|
176
|
+
await verifyArchivedMemberStillAbsent(prior);
|
|
177
|
+
proofs.push(prior);
|
|
178
|
+
}
|
|
179
|
+
else
|
|
180
|
+
proofs.push(await proveArchivedAbsence(seat));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// No await between the last live-state fence and durable checkpoint.
|
|
184
|
+
for (const proof of proofs) {
|
|
185
|
+
const latest = getRoomRecord(record.room_id)?.member_seats.find(seat => seat.role_name === proof.name);
|
|
186
|
+
if (existsSync(agentDir(proof.name, true)) || latest?.identity_cid
|
|
187
|
+
|| latest?.launch?.launch_id !== proof.launch_id || latest.launch.action_id !== proof.action_id)
|
|
188
|
+
throw new Error('Archived absence seat changed before checkpoint');
|
|
189
|
+
}
|
|
190
|
+
importTaskDeletionRetirementEvidence(taskId, seats, proofs);
|
|
149
191
|
await cowork.deleteRoom(record.room_id);
|
|
192
|
+
for (const proof of proofs)
|
|
193
|
+
await (task.deletion.workspace_cleanup_started_at && !existsSync(proof.archive_path) ? verifyArchivedMemberStillAbsent : verifyArchivedAbsence)(proof);
|
|
194
|
+
beginTaskArchiveCleanup(taskId);
|
|
195
|
+
await eraseMemberArtifacts('room', record.room_id, seats, [record.room_id]);
|
|
196
|
+
for (const proof of proofs)
|
|
197
|
+
if (existsSync(agentDir(proof.name, true)))
|
|
198
|
+
throw new Error('Archived member replacement before room unlink');
|
|
150
199
|
deleteRoomRecord(record.room_id);
|
|
151
200
|
}
|
|
201
|
+
const deletion = getDeletingTask(taskId).deletion;
|
|
202
|
+
for (const proof of deletion.archived_absences ?? [])
|
|
203
|
+
await (deletion.workspace_cleanup_started_at && !existsSync(proof.archive_path) ? verifyArchivedMemberStillAbsent : verifyArchivedAbsence)(proof);
|
|
152
204
|
// Members whose room record is gone (crash after record deletion, or
|
|
153
205
|
// legacy state): resume from the durable cursors.
|
|
154
206
|
for (const cursor of getDeletingTask(taskId).deletion.members) {
|
|
@@ -176,7 +228,7 @@ export async function settleTaskDeletion(input) {
|
|
|
176
228
|
completeTaskDeletionReceipt(taskId);
|
|
177
229
|
return { task_id: taskId, deleted: false };
|
|
178
230
|
}
|
|
179
|
-
const finalize = () => withFileLock(taskOperationLockPath(taskId), () => {
|
|
231
|
+
const finalize = () => withFileLock(taskOperationLockPath(taskId), async () => {
|
|
180
232
|
try {
|
|
181
233
|
const task = getDeletingTask(taskId);
|
|
182
234
|
if (task.deletion?.status !== 'pending')
|
|
@@ -185,8 +237,25 @@ export async function settleTaskDeletion(input) {
|
|
|
185
237
|
throw new TaskStateError(`task ${taskId} room records reappeared during deletion finalization`);
|
|
186
238
|
if (task.deletion.members.some(member => member.phase !== 'identity_absent'))
|
|
187
239
|
throw new TaskStateError(`task ${taskId} has unretired members at deletion finalization`);
|
|
240
|
+
for (const proof of task.deletion.archived_absences ?? [])
|
|
241
|
+
await (task.deletion.workspace_cleanup_started_at && !existsSync(proof.archive_path) ? verifyArchivedMemberStillAbsent : verifyArchivedAbsence)(proof);
|
|
242
|
+
for (const proof of task.deletion.archived_absences ?? [])
|
|
243
|
+
if (existsSync(agentDir(proof.name, true)))
|
|
244
|
+
throw new Error('Archived member replacement before task unlink');
|
|
245
|
+
for (const name of task.deletion.absent_members ?? []) {
|
|
246
|
+
if (existsSync(agentDir(name, true)))
|
|
247
|
+
throw new Error('Absent member acquired replacement state');
|
|
248
|
+
await assertMemberIdentityAbsent({ role_name: name, slot: name, cowork_role: 'member', seat_state: 'removed' });
|
|
249
|
+
}
|
|
188
250
|
if (cleanup.snapshotHash)
|
|
189
251
|
releaseLaunchSnapshotForDeletingTask(cleanup.snapshotHash, taskId);
|
|
252
|
+
await eraseMemberArtifacts('task', taskId, task.deletion.members.map(cursorSeat), task.deletion.room_id ? [task.deletion.room_id] : []);
|
|
253
|
+
if (task.workspace) {
|
|
254
|
+
assertWorkspaceDeletable(task.workspace, 'task', taskId);
|
|
255
|
+
beginTaskArchiveCleanup(taskId);
|
|
256
|
+
collectWorkspaceArchives(task.workspace);
|
|
257
|
+
deleteWorkspace(task.workspace, 'task', taskId);
|
|
258
|
+
}
|
|
190
259
|
unlinkDeletedTask(taskId);
|
|
191
260
|
}
|
|
192
261
|
catch (error) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RoomMemberSeat } from './types.js';
|
|
2
|
+
/** Erase only proven launch artifacts. Each atomic rename transfers ownership to
|
|
3
|
+
* a random, durable tombstone before recursive removal can destroy its proof.
|
|
4
|
+
* The caller holds the task/room lifecycle lock and has retired all writers.
|
|
5
|
+
*/
|
|
6
|
+
export declare function eraseMemberArtifacts(owner: 'room' | 'task', id: string, seats: readonly RoomMemberSeat[], roomIds: readonly string[]): Promise<void>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { eraseResourcePresentations } from '../erased-resources.js';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync } from 'node:fs';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { dirname, join, relative } from 'node:path';
|
|
5
|
+
import { parse } from 'yaml';
|
|
6
|
+
import { replaceFileAtomically } from '../atomic-file.js';
|
|
7
|
+
import { agentDir, stateRoot } from '../paths.js';
|
|
8
|
+
import { readTempSupervisor, tempSupervisorLiveness, eraseTerminationEvents } from '../temp-lifecycle.js';
|
|
9
|
+
import { assertSafeAncestors, auditWorkspaceGit } from './workspace.js';
|
|
10
|
+
function entries(path) {
|
|
11
|
+
try {
|
|
12
|
+
return readdirSync(path);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT')
|
|
16
|
+
return [];
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function json(path) {
|
|
21
|
+
try {
|
|
22
|
+
if (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink())
|
|
23
|
+
return undefined;
|
|
24
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT')
|
|
28
|
+
return undefined;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function fingerprint(path) {
|
|
33
|
+
const stat = lstatSync(path);
|
|
34
|
+
const files = stat.isDirectory() ? ['creation.json', '.temp-supervisor.json', '.identity', 'role.yaml', 'state.json', 'instance.json'] : [''];
|
|
35
|
+
const hash = createHash('sha256');
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const target = file ? join(path, file) : path;
|
|
38
|
+
if (existsSync(target)) {
|
|
39
|
+
if (!lstatSync(target).isFile() || lstatSync(target).isSymbolicLink())
|
|
40
|
+
throw new Error('Unsafe erasure proof');
|
|
41
|
+
hash.update(file).update(readFileSync(target));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return hash.digest('hex');
|
|
45
|
+
}
|
|
46
|
+
/** Erase only proven launch artifacts. Each atomic rename transfers ownership to
|
|
47
|
+
* a random, durable tombstone before recursive removal can destroy its proof.
|
|
48
|
+
* The caller holds the task/room lifecycle lock and has retired all writers.
|
|
49
|
+
*/
|
|
50
|
+
export async function eraseMemberArtifacts(owner, id, seats, roomIds) {
|
|
51
|
+
if (!/^[a-zA-Z0-9_-]{1,80}$/.test(id))
|
|
52
|
+
throw new Error('Invalid erasure owner');
|
|
53
|
+
const root = stateRoot();
|
|
54
|
+
const manifestPath = join(root, 'erasure', `${owner}-${id}.json`);
|
|
55
|
+
assertSafeAncestors(dirname(manifestPath));
|
|
56
|
+
let manifest = json(manifestPath);
|
|
57
|
+
const names = new Set(seats.map(s => s.role_name));
|
|
58
|
+
for (const name of names) {
|
|
59
|
+
if (!/^[a-zA-Z0-9_-]{1,120}$/.test(name))
|
|
60
|
+
throw new Error('Invalid erasure member');
|
|
61
|
+
if (existsSync(agentDir(name, true)))
|
|
62
|
+
throw new Error(`Member '${name}' still has live state during erasure`);
|
|
63
|
+
}
|
|
64
|
+
if (!manifest) {
|
|
65
|
+
const paths = [];
|
|
66
|
+
const launches = seats.flatMap(seat => {
|
|
67
|
+
const launchId = seat.launch?.launch_id ?? seat.retirement?.launch_id;
|
|
68
|
+
return launchId ? [{ role: seat.role_name, launchId }] : [];
|
|
69
|
+
});
|
|
70
|
+
const actions = new Map(seats.map(s => [s.role_name, new Set([s.launch?.action_id].filter((v) => !!v))]));
|
|
71
|
+
const recovery = join(root, 'recovery', 'temporary');
|
|
72
|
+
assertSafeAncestors(recovery);
|
|
73
|
+
for (const entry of entries(recovery)) {
|
|
74
|
+
const path = join(recovery, entry);
|
|
75
|
+
const stat = lstatSync(path);
|
|
76
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
77
|
+
continue;
|
|
78
|
+
const supervisor = readTempSupervisor(path);
|
|
79
|
+
if (!supervisor || !names.has(supervisor.role))
|
|
80
|
+
continue;
|
|
81
|
+
const seat = seats.find(s => s.role_name === supervisor.role);
|
|
82
|
+
const creation = json(join(path, 'creation.json'));
|
|
83
|
+
let room;
|
|
84
|
+
const roleFile = join(path, 'role.yaml');
|
|
85
|
+
if (existsSync(roleFile) && lstatSync(roleFile).isFile() && !lstatSync(roleFile).isSymbolicLink()) {
|
|
86
|
+
const role = parse(readFileSync(roleFile, 'utf8'));
|
|
87
|
+
if (role?.name === supervisor.role)
|
|
88
|
+
room = role.roomMemberStartup?.room_id;
|
|
89
|
+
}
|
|
90
|
+
const exactLaunch = seat.launch?.launch_id === supervisor.launchId || seat.retirement?.launch_id === supervisor.launchId;
|
|
91
|
+
const exactAction = creation?.role === supervisor.role && creation.creationActionId === seat.launch?.action_id;
|
|
92
|
+
// Older attempts belong only when the launch descriptor pins the room.
|
|
93
|
+
if (!(exactLaunch && (exactAction || !seat.launch?.action_id)) && !(room && roomIds.includes(room)))
|
|
94
|
+
continue;
|
|
95
|
+
if (existsSync(join(path, '.identity')) && readFileSync(join(path, '.identity'), 'utf8').trim() !== supervisor.role)
|
|
96
|
+
throw new Error('Archive identity mismatch during erasure');
|
|
97
|
+
if (await tempSupervisorLiveness(path) !== 'stopped')
|
|
98
|
+
throw new Error('Archive writer is not stopped');
|
|
99
|
+
if (creation?.role === supervisor.role && typeof creation.creationActionId === 'string')
|
|
100
|
+
actions.get(supervisor.role).add(creation.creationActionId);
|
|
101
|
+
auditWorkspaceGit(path);
|
|
102
|
+
launches.push({ role: supervisor.role, launchId: supervisor.launchId });
|
|
103
|
+
paths.push(path);
|
|
104
|
+
}
|
|
105
|
+
const privateRoot = join(root, 'private-ours');
|
|
106
|
+
assertSafeAncestors(privateRoot);
|
|
107
|
+
for (const entry of entries(privateRoot)) {
|
|
108
|
+
const path = join(privateRoot, entry);
|
|
109
|
+
if (!lstatSync(path).isDirectory() || lstatSync(path).isSymbolicLink())
|
|
110
|
+
continue;
|
|
111
|
+
if (entry === 'launches' || entry === 'room-inputs') {
|
|
112
|
+
for (const file of entries(path)) {
|
|
113
|
+
const child = join(path, file), value = json(child);
|
|
114
|
+
if (!value)
|
|
115
|
+
continue;
|
|
116
|
+
const launchOwned = entry === 'launches' && value.role === value.identity
|
|
117
|
+
&& actions.get(value.role)?.has(value.action);
|
|
118
|
+
const roomOwned = entry === 'room-inputs' && roomIds.includes(value.room_id)
|
|
119
|
+
&& names.has(value.identity_name);
|
|
120
|
+
const readyOwned = entry === 'room-inputs' && file.endsWith('.ready.json')
|
|
121
|
+
&& roomIds.includes(value.room) && seats.some(seat => seat.invite_id === value.invite && (!seat.identity_cid || seat.identity_cid === value.cid));
|
|
122
|
+
if (launchOwned || roomOwned || readyOwned)
|
|
123
|
+
paths.push(child);
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const state = json(join(path, 'state.json')), instance = json(join(path, 'instance.json'));
|
|
128
|
+
if (state?.lifetime === 'temporary' && actions.get(state.name)?.has(state.action)
|
|
129
|
+
&& instance?.role === state.name && instance?.temporary === true && instance?.instance === state.instance) {
|
|
130
|
+
auditWorkspaceGit(path);
|
|
131
|
+
paths.push(path);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
manifest = { launches, version: 1, token: randomUUID(), paths, phases: {}, stamps: Object.fromEntries(paths.map(path => {
|
|
135
|
+
const stat = lstatSync(path);
|
|
136
|
+
return [path, { dev: stat.dev, ino: stat.ino, proof: fingerprint(path) }];
|
|
137
|
+
})) };
|
|
138
|
+
mkdirSync(dirname(manifestPath), { recursive: true, mode: 0o700 });
|
|
139
|
+
replaceFileAtomically(manifestPath, JSON.stringify(manifest));
|
|
140
|
+
}
|
|
141
|
+
if (manifest.version !== 1 || !/^[a-f0-9-]{36}$/.test(manifest.token) || !Array.isArray(manifest.paths))
|
|
142
|
+
throw new Error('Invalid erasure manifest');
|
|
143
|
+
for (const path of manifest.paths) {
|
|
144
|
+
const rel = relative(root, path);
|
|
145
|
+
if (!rel.startsWith('recovery/temporary/') && !rel.startsWith('private-ours/'))
|
|
146
|
+
throw new Error('Invalid erasure artifact path');
|
|
147
|
+
if (rel.split('/').includes('..'))
|
|
148
|
+
throw new Error('Invalid erasure artifact traversal');
|
|
149
|
+
assertSafeAncestors(dirname(path));
|
|
150
|
+
const tombstone = `${path}.erasing-${manifest.token}`;
|
|
151
|
+
if (manifest.phases[path] === 'removed')
|
|
152
|
+
continue;
|
|
153
|
+
if (existsSync(path) && !existsSync(tombstone) && manifest.phases[path] !== 'renamed') {
|
|
154
|
+
const stat = lstatSync(path), stamp = manifest.stamps[path];
|
|
155
|
+
if (stat.isSymbolicLink() || !stamp || stamp.dev !== stat.dev || stamp.ino !== stat.ino || stamp.proof !== fingerprint(path))
|
|
156
|
+
throw new Error('Erasure source ownership changed');
|
|
157
|
+
renameSync(path, tombstone);
|
|
158
|
+
}
|
|
159
|
+
manifest.phases[path] = 'renamed';
|
|
160
|
+
replaceFileAtomically(manifestPath, JSON.stringify(manifest));
|
|
161
|
+
rmSync(tombstone, { recursive: true, force: true });
|
|
162
|
+
manifest.phases[path] = 'removed';
|
|
163
|
+
replaceFileAtomically(manifestPath, JSON.stringify(manifest));
|
|
164
|
+
}
|
|
165
|
+
eraseResourcePresentations([{ kind: owner, id }, ...roomIds.map(id => ({ kind: 'room', id })),
|
|
166
|
+
...seats.map(seat => ({ kind: 'agent', id: seat.role_name }))]);
|
|
167
|
+
eraseTerminationEvents(manifest.launches);
|
|
168
|
+
rmSync(manifestPath, { force: true });
|
|
169
|
+
}
|
|
@@ -8,4 +8,4 @@ export declare function presentFleetWorkerLifecycle(presentations: FleetAuditPre
|
|
|
8
8
|
sleep(ms: number): Promise<void>;
|
|
9
9
|
}): Promise<void>;
|
|
10
10
|
/** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
|
|
11
|
-
export declare function launchFleetWorker(args: string[], operation: string, configPath?: string): Promise<void>;
|
|
11
|
+
export declare function launchFleetWorker(args: string[], operation: string, configPath?: string, binPath?: string): Promise<void>;
|
|
@@ -9,8 +9,7 @@ const LIFECYCLE_ACCEPT_TIMEOUT_MS = 10 * 60_000;
|
|
|
9
9
|
const LIFECYCLE_RETRY_MS = 250;
|
|
10
10
|
export function fleetWorkerEnv(source = process.env) {
|
|
11
11
|
const inheritedKeys = [
|
|
12
|
-
'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_CONFIG',
|
|
13
|
-
'OURS_PORT', 'OURS_STATE_DIR', 'OURS_API_TOKEN', 'OURS_COWORK_CONFIG',
|
|
12
|
+
'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_FLEET_SOCKET_ROOT', 'OURS_CONFIG',
|
|
14
13
|
];
|
|
15
14
|
const env = Object.fromEntries(inheritedKeys.flatMap(key => source[key] === undefined ? [] : [[key, source[key]]]));
|
|
16
15
|
const lifecycleStateDir = source[FLEET_WORKER_LIFECYCLE_STATE_DIR_ENV]
|
|
@@ -52,8 +51,8 @@ export async function presentFleetWorkerLifecycle(presentations, deps = {
|
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
53
|
/** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
|
|
55
|
-
export function launchFleetWorker(args, operation, configPath) {
|
|
56
|
-
const cli = [
|
|
54
|
+
export function launchFleetWorker(args, operation, configPath, binPath = process.argv[1]) {
|
|
55
|
+
const cli = [binPath, ...args, ...(configPath ? ['-c', configPath] : [])];
|
|
57
56
|
const env = fleetWorkerEnv();
|
|
58
57
|
const inherited = Object.entries(env).map(([key, value]) => `${key}=${value}`);
|
|
59
58
|
const suffix = randomUUID().slice(0, 8);
|