@ours.network/fleet 1.2.0-nightly.1 → 1.2.0-nightly.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +110 -21
  2. package/dist/agent-ours/bridge.d.ts +3 -0
  3. package/dist/agent-ours/bridge.js +178 -0
  4. package/dist/agent-ours/controller.d.ts +14 -0
  5. package/dist/agent-ours/controller.js +60 -0
  6. package/dist/agent-ours/file-rpc.d.ts +23 -0
  7. package/dist/agent-ours/file-rpc.js +123 -0
  8. package/dist/agent-ours/harness.d.ts +14 -0
  9. package/dist/agent-ours/harness.js +64 -0
  10. package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
  11. package/dist/agent-ours/mcp-endpoint.js +111 -0
  12. package/dist/agent-ours/runtime.d.ts +53 -0
  13. package/dist/agent-ours/runtime.js +247 -0
  14. package/dist/agent-ours/service.d.ts +23 -0
  15. package/dist/agent-ours/service.js +364 -0
  16. package/dist/agent-ours/state.d.ts +45 -0
  17. package/dist/agent-ours/state.js +95 -0
  18. package/dist/agent-ours/wire.d.ts +14 -0
  19. package/dist/agent-ours/wire.js +55 -0
  20. package/dist/agent-recovery-gate.js +1 -1
  21. package/dist/application/legacy-supervisor-identity.d.ts +7 -0
  22. package/dist/application/legacy-supervisor-identity.js +43 -0
  23. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  24. package/dist/application/supervisor-ours-tools.js +121 -0
  25. package/dist/application/task-room-service.d.ts +6 -2
  26. package/dist/application/task-room-service.js +78 -21
  27. package/dist/briefing.js +10 -59
  28. package/dist/build-info.json +5 -4
  29. package/dist/capabilities.d.ts +2 -1
  30. package/dist/capabilities.js +2 -0
  31. package/dist/cli.js +63 -2
  32. package/dist/client-profile.d.ts +1 -0
  33. package/dist/client-profile.js +18 -3
  34. package/dist/config.d.ts +1 -0
  35. package/dist/creation.d.ts +1 -1
  36. package/dist/creation.js +3 -14
  37. package/dist/docs.d.ts +3 -3
  38. package/dist/docs.js +32 -23
  39. package/dist/doctor.d.ts +1 -1
  40. package/dist/doctor.js +29 -10
  41. package/dist/fleet-command-audit.js +3 -2
  42. package/dist/harness/agent-session.d.ts +2 -0
  43. package/dist/harness/claude-code-session.js +4 -2
  44. package/dist/harness/codex-session.js +9 -2
  45. package/dist/harness/codex.d.ts +3 -2
  46. package/dist/harness/codex.js +4 -3
  47. package/dist/harness/hermes-session.js +7 -1
  48. package/dist/owner-channel/channel.d.ts +3 -0
  49. package/dist/owner-channel/channel.js +24 -4
  50. package/dist/owner-channel/commands.js +5 -0
  51. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  52. package/dist/rooms-tasks/archived-absence.js +48 -0
  53. package/dist/rooms-tasks/cli.js +6 -1
  54. package/dist/rooms-tasks/close.d.ts +3 -0
  55. package/dist/rooms-tasks/close.js +95 -17
  56. package/dist/rooms-tasks/cowork-adapter.d.ts +4 -0
  57. package/dist/rooms-tasks/cowork-adapter.js +10 -2
  58. package/dist/rooms-tasks/cowork-http.d.ts +3 -0
  59. package/dist/rooms-tasks/cowork-http.js +70 -0
  60. package/dist/rooms-tasks/deletion.js +40 -3
  61. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  62. package/dist/rooms-tasks/live-readiness.js +75 -0
  63. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  64. package/dist/rooms-tasks/member-startup.js +2 -0
  65. package/dist/rooms-tasks/provision.d.ts +11 -0
  66. package/dist/rooms-tasks/provision.js +138 -26
  67. package/dist/rooms-tasks/room-state.js +13 -0
  68. package/dist/rooms-tasks/task-state.d.ts +13 -3
  69. package/dist/rooms-tasks/task-state.js +42 -6
  70. package/dist/rooms-tasks/types.d.ts +12 -0
  71. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  72. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  73. package/dist/rooms-tasks/workspace.d.ts +26 -0
  74. package/dist/rooms-tasks/workspace.js +195 -0
  75. package/dist/runner.d.ts +14 -0
  76. package/dist/runner.js +527 -495
  77. package/dist/session/codex-app-server-transport.js +1 -1
  78. package/dist/spawn.js +19 -16
  79. package/dist/startup-readiness.d.ts +13 -0
  80. package/dist/startup-readiness.js +38 -0
  81. package/dist/supervisor/systemd.js +5 -0
  82. package/dist/temp-supervisor-recovery.d.ts +3 -0
  83. package/dist/temp-supervisor-recovery.js +46 -0
  84. package/dist/watchdog/briefing.js +3 -16
  85. package/dist/watchdog/run.js +19 -5
  86. package/dist/web/server.d.ts +2 -0
  87. package/dist/web/server.js +18 -1
  88. package/docs/task-workspaces.md +98 -0
  89. package/package.json +6 -3
  90. package/presets/fleet/roles/Coordinator.yaml +22 -0
@@ -0,0 +1,364 @@
1
+ import { randomBytes, randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { attachOursClient } from '@ours.network/sdk/client';
5
+ import { ApplicationIdentityStore } from '@ours.network/mcp/application-identities';
6
+ import { readClientProfile } from '../client-profile.js';
7
+ import { stateRoot } from '../paths.js';
8
+ import { createCoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
9
+ import { RuntimeController } from './controller.js';
10
+ import { atomicPrivateWrite, binderKey } from './state.js';
11
+ import { AgentOursRuntime } from './runtime.js';
12
+ import { startMcpEndpoint } from './mcp-endpoint.js';
13
+ export const privateRuntimeRoot = () => join(stateRoot(), 'private-ours');
14
+ function roomSecretPath(role) {
15
+ const startup = role.roomMemberStartup;
16
+ return join(privateRuntimeRoot(), 'room-inputs', binderKey(startup.room_identity_cid, JSON.stringify([role.name, startup.invite_id])) + '.json');
17
+ }
18
+ function matchesRoomSecret(secret, role) {
19
+ const startup = role.roomMemberStartup;
20
+ return secret.room_id === startup.room_id && secret.room_identity_cid === startup.room_identity_cid
21
+ && secret.invite_id === startup.invite_id && secret.identity_name === role.identity
22
+ && secret.role === startup.role;
23
+ }
24
+ /** Only trusted launch orchestration may write this descriptor, never the child. */
25
+ export function storeRoomSecret(role) {
26
+ const startup = role.roomMemberStartup;
27
+ if (!startup?.invite)
28
+ return;
29
+ if (startup.identity_name !== role.identity)
30
+ throw Error('ROOM_SECRET_MISMATCH');
31
+ const root = join(privateRuntimeRoot(), 'room-inputs');
32
+ mkdirSync(root, { recursive: true, mode: 0o700 });
33
+ // Failed attempts retain private evidence. A new invite must never overwrite
34
+ // or consume a previous attempt's descriptor.
35
+ const path = roomSecretPath(role);
36
+ if (existsSync(path)) {
37
+ const old = JSON.parse(readFileSync(path, 'utf8'));
38
+ if (!matchesRoomSecret(old, role) || old.invite !== startup.invite)
39
+ throw Error('ROOM_SECRET_COLLISION');
40
+ return;
41
+ }
42
+ atomicPrivateWrite(path, startup);
43
+ }
44
+ export function storeTemporaryLaunch(role, action) {
45
+ const root = join(privateRuntimeRoot(), 'launches');
46
+ mkdirSync(root, { recursive: true, mode: 0o700 });
47
+ atomicPrivateWrite(join(root, binderKey('temporary', role.name) + '.json'), {
48
+ role: role.name,
49
+ identity: role.identity,
50
+ action,
51
+ });
52
+ }
53
+ export async function prepareManagedAgent(role, stateDir, temporary) {
54
+ if (role.owner_channel?.identity === role.identity)
55
+ throw Error('OWNER_AGENT_IDENTITY_COLLISION');
56
+ const profile = readClientProfile({ ...process.env, ...role.env });
57
+ if (!profile)
58
+ throw Error('MANAGED_OURS_REQUIRES_EXPLICIT_DAEMON_PROFILE');
59
+ const root = privateRuntimeRoot(), key = binderKey(profile.expectedInstanceId, role.identity), dir = join(root, key);
60
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
61
+ const instancePath = join(dir, 'instance.json');
62
+ const launch = temporary
63
+ ? JSON.parse(readFileSync(join(root, 'launches', binderKey('temporary', role.name) + '.json'), 'utf8'))
64
+ : undefined;
65
+ if (launch &&
66
+ (launch.role !== role.name ||
67
+ launch.identity !== role.identity ||
68
+ typeof launch.action !== 'string'))
69
+ throw Error('INVALID_TEMP_LAUNCH');
70
+ let instance;
71
+ if (existsSync(instancePath)) {
72
+ const old = JSON.parse(readFileSync(instancePath, 'utf8'));
73
+ if (old.role !== role.name || old.temporary !== temporary)
74
+ throw Error('IDENTITY_ASSIGNED_TO_OTHER_ROLE');
75
+ instance = old.instance;
76
+ }
77
+ else {
78
+ instance = randomUUID();
79
+ try {
80
+ writeFileSync(instancePath, JSON.stringify({ instance, role: role.name, temporary }), {
81
+ flag: 'wx',
82
+ mode: 0o600,
83
+ });
84
+ }
85
+ catch (error) {
86
+ if (error.code !== 'EEXIST')
87
+ throw error;
88
+ const old = JSON.parse(readFileSync(instancePath, 'utf8'));
89
+ if (old.role !== role.name || old.temporary !== temporary)
90
+ throw Error('IDENTITY_ASSIGNED_TO_OTHER_ROLE');
91
+ instance = old.instance;
92
+ }
93
+ }
94
+ const controller = await RuntimeController.acquire(root, profile.expectedInstanceId, role.identity, instance);
95
+ let closeClient;
96
+ let partialEndpoint;
97
+ try {
98
+ let prior = controller.journal.read();
99
+ if (prior?.phase === 'RELEASED') {
100
+ if (temporary && launch.action === prior.action)
101
+ throw Error('TERMINAL_TEMP_REQUIRES_NEW_LAUNCH');
102
+ instance = controller.successor();
103
+ atomicPrivateWrite(instancePath, { instance, role: role.name, temporary });
104
+ if (temporary && existsSync(join(dir, 'identity-pin.json')))
105
+ unlinkSync(join(dir, 'identity-pin.json'));
106
+ prior = undefined;
107
+ }
108
+ const generation = prior ? controller.resumeGeneration() : 1;
109
+ const tokenPath = join(dir, 'owner.json');
110
+ let token;
111
+ if (existsSync(tokenPath)) {
112
+ const owner = JSON.parse(readFileSync(tokenPath, 'utf8'));
113
+ if (owner.instance !== instance || typeof owner.token !== 'string')
114
+ throw Error('OWNER_RECORD_MISMATCH');
115
+ token = owner.token;
116
+ }
117
+ else {
118
+ if (prior)
119
+ throw Error('OWNER_RECORD_MISSING');
120
+ token = randomBytes(32).toString('hex');
121
+ atomicPrivateWrite(tokenPath, { instance, token });
122
+ }
123
+ const client = await attachOursClient({
124
+ endpoint: profile.endpoint,
125
+ expectedInstanceId: profile.expectedInstanceId,
126
+ credentialPath: profile.credentialPath,
127
+ sessionMode: 'external',
128
+ leaseToken: token,
129
+ requiredCapabilities: ['external-sessions-v1', 'root-first-identities-v1'],
130
+ });
131
+ closeClient = () => client.close();
132
+ const pinPath = join(dir, 'identity-pin.json');
133
+ const pin = existsSync(pinPath) ? JSON.parse(readFileSync(pinPath, 'utf8')) : undefined;
134
+ if (pin &&
135
+ (pin.daemon !== profile.expectedInstanceId ||
136
+ pin.name !== role.identity ||
137
+ typeof pin.cid !== 'string'))
138
+ throw Error('INVALID_IDENTITY_PIN');
139
+ const runtime = new AgentOursRuntime({
140
+ instance,
141
+ generation,
142
+ daemon: profile.expectedInstanceId,
143
+ name: role.identity,
144
+ lifetime: temporary ? 'temporary' : 'permanent',
145
+ action: launch?.action ?? prior?.action ?? randomUUID(),
146
+ expectedCid: pin?.cid,
147
+ allowCreate: true,
148
+ bio: role.bio ?? '',
149
+ }, {
150
+ client,
151
+ journal: controller.journal,
152
+ assertFence: controller.assertFence,
153
+ now: Date.now,
154
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
155
+ });
156
+ let room;
157
+ if (role.roomMemberStartup) {
158
+ const startup = role.roomMemberStartup;
159
+ const legacyPath = join(root, 'room-inputs', binderKey(startup.room_identity_cid, role.name) + '.json');
160
+ const currentPath = roomSecretPath(role);
161
+ const path = existsSync(currentPath) ? currentPath : legacyPath;
162
+ const cowork = createCoworkAdapter();
163
+ room = {
164
+ id: startup.room_id,
165
+ cid: startup.room_identity_cid,
166
+ seat: startup.invite_id,
167
+ action: startup.invite_id,
168
+ redeem: async (attached) => {
169
+ const secret = JSON.parse(readFileSync(path, 'utf8'));
170
+ if (!matchesRoomSecret(secret, role))
171
+ throw Error('ROOM_SECRET_MISMATCH');
172
+ return attached.addContact({ invite: secret.invite });
173
+ },
174
+ observe: async (agentCid) => {
175
+ const [contacts, observed] = await Promise.all([
176
+ client.listContacts(),
177
+ cowork.getRoom(startup.room_id),
178
+ ]);
179
+ if (!observed || observed.identity_cid !== startup.room_identity_cid)
180
+ return 'mismatch';
181
+ const seat = observed.seats.find((s) => s.invite_id === startup.invite_id);
182
+ if (seat && (seat.identity_cid !== agentCid || seat.role !== startup.role))
183
+ return 'mismatch';
184
+ return seat?.seat_state === 'active' &&
185
+ contacts.contacts.some((c) => c.container_id === startup.room_identity_cid)
186
+ ? 'established'
187
+ : 'pending';
188
+ },
189
+ discardSecret: async () => {
190
+ if (existsSync(path))
191
+ unlinkSync(path);
192
+ },
193
+ };
194
+ }
195
+ await runtime.prepare(room);
196
+ if (role.roomMemberStartup) {
197
+ const startup = role.roomMemberStartup;
198
+ atomicPrivateWrite(join(root, 'room-inputs', binderKey(startup.room_identity_cid, role.name) + '.ready.json'), { room: startup.room_id, invite: startup.invite_id, cid: runtime.snapshot.cid, generation });
199
+ }
200
+ if (!pin)
201
+ atomicPrivateWrite(pinPath, {
202
+ daemon: profile.expectedInstanceId,
203
+ name: role.identity,
204
+ cid: runtime.snapshot.cid,
205
+ });
206
+ const identities = new ApplicationIdentityStore({ instanceId: profile.expectedInstanceId }, { path: join(dir, 'application.json') });
207
+ await identities.add(role.identity);
208
+ const bridgeDir = join(stateDir, '.ours-bridge');
209
+ mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
210
+ const capability = randomBytes(32).toString('hex'), socket = join(bridgeDir, `g${generation}.sock`);
211
+ const endpoint = await startMcpEndpoint({
212
+ socket,
213
+ capability,
214
+ generation,
215
+ runtime,
216
+ client,
217
+ identities,
218
+ remoteDaemonFiles: true,
219
+ });
220
+ partialEndpoint = endpoint;
221
+ const descriptor = join(bridgeDir, 'descriptor.json');
222
+ atomicPrivateWrite(descriptor, { socket, capability, generation, role: role.name, identity: role.identity, cid: runtime.snapshot.cid });
223
+ return {
224
+ runtime,
225
+ descriptor,
226
+ privatePaths: [root, profile.configPath, profile.credentialPath],
227
+ close: async (terminal) => {
228
+ try {
229
+ await endpoint.close();
230
+ if (terminal)
231
+ await runtime.terminal();
232
+ else
233
+ await runtime.suspend();
234
+ }
235
+ finally {
236
+ try {
237
+ await client.close();
238
+ }
239
+ finally {
240
+ controller.unlock();
241
+ }
242
+ }
243
+ },
244
+ };
245
+ }
246
+ catch (error) {
247
+ try {
248
+ await partialEndpoint?.close();
249
+ }
250
+ finally {
251
+ try {
252
+ await closeClient?.();
253
+ }
254
+ finally {
255
+ controller.unlock();
256
+ }
257
+ }
258
+ throw error;
259
+ }
260
+ }
261
+ /** Explicit trusted inventory migration: read and pin existing permanent CID, never bind. */
262
+ export async function preparePermanentAssignment(role) {
263
+ const profile = readClientProfile({ ...process.env, ...role.env });
264
+ if (!profile)
265
+ throw Error('MANAGED_OURS_REQUIRES_EXPLICIT_DAEMON_PROFILE');
266
+ const client = await attachOursClient({
267
+ endpoint: profile.endpoint,
268
+ expectedInstanceId: profile.expectedInstanceId,
269
+ credentialPath: profile.credentialPath,
270
+ sessionMode: 'local',
271
+ requiredCapabilities: ['local-pid-v1', 'root-first-identities-v1'],
272
+ });
273
+ try {
274
+ const rows = await client.listIdentities(), row = rows.find((r) => r.name === role.identity);
275
+ if (!row)
276
+ return 'unverified';
277
+ if (!('cid' in row) || row.kind !== 'role' || row.temp)
278
+ throw Error('PERMANENT_ASSIGNMENT_NOT_A_ROLE');
279
+ const dir = join(privateRuntimeRoot(), binderKey(profile.expectedInstanceId, role.identity));
280
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
281
+ const path = join(dir, 'identity-pin.json');
282
+ if (existsSync(path)) {
283
+ const old = JSON.parse(readFileSync(path, 'utf8'));
284
+ if (old.cid !== row.cid ||
285
+ old.daemon !== profile.expectedInstanceId ||
286
+ old.name !== role.identity)
287
+ throw Error('PERMANENT_ASSIGNMENT_PIN_MISMATCH');
288
+ }
289
+ else
290
+ atomicPrivateWrite(path, {
291
+ daemon: profile.expectedInstanceId,
292
+ name: role.identity,
293
+ cid: row.cid,
294
+ });
295
+ return 'verified';
296
+ }
297
+ finally {
298
+ await client.close();
299
+ }
300
+ }
301
+ export function readRoomReadiness(roomCid, name) {
302
+ try {
303
+ return JSON.parse(readFileSync(join(privateRuntimeRoot(), 'room-inputs', binderKey(roomCid, name) + '.ready.json'), 'utf8'));
304
+ }
305
+ catch (error) {
306
+ if (error.code === 'ENOENT')
307
+ return;
308
+ throw error;
309
+ }
310
+ }
311
+ /** Explicit logical-instance termination, independent of harness/bridge transport. */
312
+ export async function releaseManagedAgent(role) {
313
+ const profile = readClientProfile({ ...process.env, ...role.env });
314
+ if (!profile)
315
+ throw Error('MANAGED_OURS_REQUIRES_EXPLICIT_DAEMON_PROFILE');
316
+ const root = privateRuntimeRoot(), dir = join(root, binderKey(profile.expectedInstanceId, role.identity));
317
+ if (!existsSync(join(dir, 'instance.json')))
318
+ return;
319
+ const instance = JSON.parse(readFileSync(join(dir, 'instance.json'), 'utf8'));
320
+ if (instance.role !== role.name)
321
+ throw Error('IDENTITY_ASSIGNED_TO_OTHER_ROLE');
322
+ const controller = await RuntimeController.acquire(root, profile.expectedInstanceId, role.identity, instance.instance);
323
+ try {
324
+ const state = controller.journal.read();
325
+ if (!state || state.phase === 'RELEASED')
326
+ return;
327
+ const owner = JSON.parse(readFileSync(join(dir, 'owner.json'), 'utf8'));
328
+ if (owner.instance !== state.instance)
329
+ throw Error('OWNER_RECORD_MISMATCH');
330
+ const client = await attachOursClient({
331
+ endpoint: profile.endpoint,
332
+ expectedInstanceId: profile.expectedInstanceId,
333
+ credentialPath: profile.credentialPath,
334
+ sessionMode: 'external',
335
+ leaseToken: owner.token,
336
+ requiredCapabilities: ['external-sessions-v1', 'root-first-identities-v1'],
337
+ });
338
+ try {
339
+ const runtime = new AgentOursRuntime({
340
+ instance: state.instance,
341
+ generation: state.generation,
342
+ daemon: state.daemon,
343
+ name: state.name,
344
+ lifetime: state.lifetime,
345
+ action: state.action,
346
+ allowCreate: false,
347
+ bio: '',
348
+ }, {
349
+ client,
350
+ journal: controller.journal,
351
+ assertFence: controller.assertFence,
352
+ now: Date.now,
353
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
354
+ });
355
+ await runtime.terminal();
356
+ }
357
+ finally {
358
+ await client.close();
359
+ }
360
+ }
361
+ finally {
362
+ controller.unlock();
363
+ }
364
+ }
@@ -0,0 +1,45 @@
1
+ export type Phase = 'PREPARING' | 'OWNED' | 'ROOM_PENDING' | 'READY' | 'SERVING' | 'RECOVERING' | 'QUIESCING' | 'TERMINAL_INTENT' | 'RELEASED' | 'FAILED' | 'CLEANUP_PENDING';
2
+ export interface RuntimeState {
3
+ version: 1;
4
+ instance: string;
5
+ generation: number;
6
+ daemon: string;
7
+ name: string;
8
+ cid?: string;
9
+ lifetime: 'permanent' | 'temporary';
10
+ action: string;
11
+ phase: Phase;
12
+ revision: number;
13
+ updatedAt: string;
14
+ admissionIntent?: {
15
+ id: string;
16
+ cid: string;
17
+ seat: string;
18
+ action: string;
19
+ agentCid: string;
20
+ };
21
+ releaseAck?: {
22
+ released: string[];
23
+ closed: string[];
24
+ attempted: number;
25
+ notified: number;
26
+ failed: number;
27
+ };
28
+ room?: {
29
+ id: string;
30
+ cid: string;
31
+ seat: string;
32
+ agentCid: string;
33
+ action: string;
34
+ };
35
+ }
36
+ export declare function atomicPrivateWrite(path: string, value: unknown): void;
37
+ export declare function binderKey(daemon: string, identity: string): string;
38
+ /** Supervisor-owned durable state; retained across harness reconnects. */
39
+ export declare class RuntimeJournal {
40
+ readonly privateDir: string;
41
+ readonly path: string;
42
+ constructor(privateDir: string);
43
+ read(): RuntimeState | undefined;
44
+ commit(next: RuntimeState, expectedRevision?: number): void;
45
+ }
@@ -0,0 +1,95 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ const phases = [
5
+ 'PREPARING',
6
+ 'OWNED',
7
+ 'ROOM_PENDING',
8
+ 'READY',
9
+ 'SERVING',
10
+ 'RECOVERING',
11
+ 'QUIESCING',
12
+ 'TERMINAL_INTENT',
13
+ 'RELEASED',
14
+ 'FAILED',
15
+ 'CLEANUP_PENDING',
16
+ ];
17
+ export function atomicPrivateWrite(path, value) {
18
+ const tmp = `${path}.${randomUUID()}.tmp`;
19
+ const fd = openSync(tmp, 'wx', 0o600);
20
+ try {
21
+ writeFileSync(fd, JSON.stringify(value) + '\n');
22
+ fsyncSync(fd);
23
+ }
24
+ finally {
25
+ closeSync(fd);
26
+ }
27
+ renameSync(tmp, path);
28
+ const parent = openSync(join(path, '..'), 'r');
29
+ try {
30
+ fsyncSync(parent);
31
+ }
32
+ finally {
33
+ closeSync(parent);
34
+ }
35
+ }
36
+ export function binderKey(daemon, identity) {
37
+ return createHash('sha256')
38
+ .update(JSON.stringify([daemon, identity]))
39
+ .digest('hex');
40
+ }
41
+ /** Supervisor-owned durable state; retained across harness reconnects. */
42
+ export class RuntimeJournal {
43
+ privateDir;
44
+ path;
45
+ constructor(privateDir) {
46
+ this.privateDir = privateDir;
47
+ mkdirSync(privateDir, { recursive: true, mode: 0o700 });
48
+ this.path = join(privateDir, 'state.json');
49
+ }
50
+ read() {
51
+ let raw;
52
+ try {
53
+ raw = readFileSync(this.path, 'utf8');
54
+ }
55
+ catch (error) {
56
+ if (error.code === 'ENOENT')
57
+ return;
58
+ throw error;
59
+ }
60
+ const s = JSON.parse(raw);
61
+ if (s.version !== 1 ||
62
+ !s.instance ||
63
+ !s.daemon ||
64
+ !s.name ||
65
+ !s.action ||
66
+ !phases.includes(s.phase) ||
67
+ !Number.isSafeInteger(s.generation) ||
68
+ s.generation < 1 ||
69
+ !Number.isSafeInteger(s.revision) ||
70
+ !['permanent', 'temporary'].includes(s.lifetime))
71
+ throw Error('CORRUPT_RUNTIME_JOURNAL');
72
+ return s;
73
+ }
74
+ commit(next, expectedRevision) {
75
+ const current = this.read();
76
+ if (current?.revision !== expectedRevision)
77
+ throw Error('STALE_RUNTIME_REVISION');
78
+ if (current &&
79
+ (current.instance !== next.instance ||
80
+ current.daemon !== next.daemon ||
81
+ current.name !== next.name ||
82
+ current.lifetime !== next.lifetime ||
83
+ next.generation < current.generation))
84
+ throw Error('RUNTIME_IDENTITY_MISMATCH');
85
+ if (current &&
86
+ ['TERMINAL_INTENT', 'CLEANUP_PENDING', 'RELEASED'].includes(current.phase) &&
87
+ !['TERMINAL_INTENT', 'CLEANUP_PENDING', 'RELEASED'].includes(next.phase))
88
+ throw Error('RETIRED_RUNTIME');
89
+ atomicPrivateWrite(this.path, {
90
+ ...next,
91
+ revision: (expectedRevision ?? 0) + 1,
92
+ updatedAt: new Date().toISOString(),
93
+ });
94
+ }
95
+ }
@@ -0,0 +1,14 @@
1
+ import type { Socket } from 'node:net';
2
+ export declare const MAX_FRAME_BYTES: number;
3
+ export declare const MAX_CHUNK_BYTES: number;
4
+ /** One bounded framed channel. No request replay after disconnect. */
5
+ export declare class Wire {
6
+ readonly socket: Socket;
7
+ private buffer;
8
+ private ended;
9
+ onFrame: (frame: unknown) => void;
10
+ onClose: () => void;
11
+ constructor(socket: Socket);
12
+ send(frame: unknown): Promise<void>;
13
+ close(): void;
14
+ }
@@ -0,0 +1,55 @@
1
+ export const MAX_FRAME_BYTES = 1024 * 1024;
2
+ export const MAX_CHUNK_BYTES = 64 * 1024;
3
+ /** One bounded framed channel. No request replay after disconnect. */
4
+ export class Wire {
5
+ socket;
6
+ buffer = Buffer.alloc(0);
7
+ ended = false;
8
+ onFrame = () => { };
9
+ onClose = () => { };
10
+ constructor(socket) {
11
+ this.socket = socket;
12
+ socket.on('data', (chunk) => {
13
+ this.buffer = Buffer.concat([this.buffer, chunk]);
14
+ for (;;) {
15
+ const end = this.buffer.indexOf(10);
16
+ if (end < 0) {
17
+ if (this.buffer.length > MAX_FRAME_BYTES)
18
+ socket.destroy();
19
+ return;
20
+ }
21
+ if (end > MAX_FRAME_BYTES) {
22
+ socket.destroy();
23
+ return;
24
+ }
25
+ const line = this.buffer.subarray(0, end);
26
+ this.buffer = this.buffer.subarray(end + 1);
27
+ try {
28
+ this.onFrame(JSON.parse(line.toString('utf8')));
29
+ }
30
+ catch {
31
+ socket.destroy();
32
+ return;
33
+ }
34
+ }
35
+ });
36
+ socket.on('error', () => socket.destroy());
37
+ socket.on('close', () => {
38
+ if (!this.ended) {
39
+ this.ended = true;
40
+ this.onClose();
41
+ }
42
+ });
43
+ }
44
+ async send(frame) {
45
+ if (this.ended || this.socket.destroyed)
46
+ throw Error('BRIDGE_DISCONNECTED');
47
+ const bytes = Buffer.from(JSON.stringify(frame) + '\n');
48
+ if (bytes.length > MAX_FRAME_BYTES)
49
+ throw Error('FRAME_TOO_LARGE');
50
+ await new Promise((resolve, reject) => this.socket.write(bytes, (error) => (error ? reject(error) : resolve())));
51
+ }
52
+ close() {
53
+ this.socket.destroy();
54
+ }
55
+ }
@@ -118,7 +118,7 @@ export async function recoverAgentIdentity(session, identity) {
118
118
  const unsubscribe = session.subscribeConversation(event => events.push(event));
119
119
  try {
120
120
  const queued = await session.queuePrompt([
121
- '[fleet-recovery] The shared ours daemon restarted.',
121
+ '[fleet-recovery] The shared ours-daemon restarted.',
122
122
  `Call ours choose_identity with name ${JSON.stringify(identity)} and force false.`,
123
123
  'Then call current_identity, then get_messages. Complete all three in that order.',
124
124
  'Do not create/delete identities, force-bind, interrupt, or restart any service/session.',
@@ -0,0 +1,7 @@
1
+ /** Read-only compatibility proof for supervisors predating descriptor identity metadata. */
2
+ export declare function legacySupervisorIdentity(role: string, name: string, temporary: boolean, generation: number): {
3
+ name: string;
4
+ cid: string;
5
+ generation: number;
6
+ proof: string;
7
+ };
@@ -0,0 +1,43 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { stateRoot } from '../paths.js';
5
+ import { binderKey } from '../agent-ours/state.js';
6
+ import { FleetError } from './errors.js';
7
+ /** Read-only compatibility proof for supervisors predating descriptor identity metadata. */
8
+ export function legacySupervisorIdentity(role, name, temporary, generation) {
9
+ const root = join(stateRoot(), 'private-ours');
10
+ try {
11
+ const matches = [];
12
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
13
+ if (!entry.isDirectory() || !/^[a-f0-9]{64}$/.test(entry.name))
14
+ continue;
15
+ const dir = join(root, entry.name);
16
+ const instance = JSON.parse(readFileSync(join(dir, 'instance.json'), 'utf8'));
17
+ if (instance.role !== role)
18
+ continue;
19
+ const state = JSON.parse(readFileSync(join(dir, 'state.json'), 'utf8'));
20
+ const pin = JSON.parse(readFileSync(join(dir, 'identity-pin.json'), 'utf8'));
21
+ if (!Number.isSafeInteger(generation) || generation < 1
22
+ || typeof state.action !== 'string' || !state.action
23
+ || instance.temporary !== temporary || typeof instance.instance !== 'string' || !instance.instance
24
+ || state.version !== 1 || state.instance !== instance.instance
25
+ || state.name !== name || state.lifetime !== (temporary ? 'temporary' : 'permanent')
26
+ || state.generation !== generation || !['READY', 'SERVING'].includes(state.phase)
27
+ || typeof state.daemon !== 'string' || !state.daemon || binderKey(state.daemon, name) !== entry.name
28
+ || !/^[a-f0-9]{64}$/i.test(state.cid ?? '')
29
+ || pin.daemon !== state.daemon || pin.name !== name || pin.cid !== state.cid)
30
+ throw new Error('mismatched legacy ownership proof');
31
+ matches.push({ name, cid: state.cid, generation, proof: createHash('sha256').update(JSON.stringify([
32
+ instance.instance, state.action, state.daemon, name, state.cid, state.lifetime, generation,
33
+ ])).digest('hex') });
34
+ }
35
+ if (matches.length !== 1)
36
+ throw new Error('missing or ambiguous legacy ownership proof');
37
+ return matches[0];
38
+ }
39
+ catch {
40
+ // The private directory also contains ownership material: never echo parse errors.
41
+ throw new FleetError('capability_unavailable', 'legacy supervisor ownership proof is missing, ambiguous, or mismatched');
42
+ }
43
+ }