@ours.network/fleet 1.2.0-nightly.54 → 1.2.0-nightly.56
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 +20 -12
- package/dist/agent-ours/service.d.ts +4 -1
- package/dist/agent-ours/service.js +31 -7
- package/dist/application/log-service.js +8 -8
- package/dist/application/role-creation-service.js +2 -1
- package/dist/application/role-removal-service.js +7 -1
- package/dist/application/role-repository.js +17 -0
- package/dist/application/task-layouts.js +12 -2
- package/dist/application/task-room-service.js +19 -17
- package/dist/application/types.d.ts +2 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +31 -3
- package/dist/config.d.ts +2 -0
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +2 -2
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/codex-session.js +2 -2
- package/dist/managed-recovery.d.ts +13 -0
- package/dist/managed-recovery.js +31 -0
- package/dist/ops.d.ts +2 -0
- package/dist/ops.js +15 -2
- package/dist/rooms-tasks/cli.js +21 -0
- package/dist/rooms-tasks/close.d.ts +2 -0
- package/dist/rooms-tasks/close.js +66 -1
- package/dist/rooms-tasks/deletion.js +10 -0
- package/dist/rooms-tasks/erasure.js +4 -2
- package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
- package/dist/rooms-tasks/layout-member-retirement.js +90 -0
- package/dist/rooms-tasks/layout-service.d.ts +1 -0
- package/dist/rooms-tasks/layout-service.js +31 -13
- package/dist/rooms-tasks/layout.d.ts +7 -0
- package/dist/rooms-tasks/layout.js +35 -0
- package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
- package/dist/rooms-tasks/legacy-task-member.js +116 -0
- package/dist/rooms-tasks/member-ownership.d.ts +5 -0
- package/dist/rooms-tasks/member-ownership.js +40 -0
- package/dist/rooms-tasks/provision.js +26 -1
- package/dist/rooms-tasks/recovery.d.ts +19 -0
- package/dist/rooms-tasks/recovery.js +110 -0
- package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
- package/dist/rooms-tasks/task-service-retirement.js +21 -0
- package/dist/rooms-tasks/task-state.d.ts +1 -0
- package/dist/rooms-tasks/task-state.js +16 -1
- package/dist/rooms-tasks/types.d.ts +3 -0
- package/dist/rooms-tasks/workspace.d.ts +2 -0
- package/dist/rooms-tasks/workspace.js +5 -0
- package/dist/runner.d.ts +6 -0
- package/dist/runner.js +160 -22
- package/dist/session/acp.d.ts +1 -0
- package/dist/session/acp.js +7 -0
- package/dist/session/arbiter.d.ts +5 -0
- package/dist/session/arbiter.js +16 -2
- package/dist/session/codex-app-server.d.ts +1 -0
- package/dist/session/codex-app-server.js +5 -0
- package/dist/session/types.d.ts +2 -0
- package/dist/spawn.d.ts +5 -1
- package/dist/spawn.js +32 -3
- package/dist/startup-readiness.d.ts +1 -1
- package/dist/startup-readiness.js +3 -3
- package/dist/supervisor/adoption.d.ts +8 -0
- package/dist/supervisor/adoption.js +78 -0
- package/dist/supervisor/catalog.d.ts +62 -0
- package/dist/supervisor/catalog.js +254 -0
- package/dist/supervisor/fleet.d.ts +8 -0
- package/dist/supervisor/fleet.js +89 -0
- package/dist/supervisor/index.js +3 -6
- package/dist/supervisor/launchd.d.ts +1 -1
- package/dist/supervisor/launchd.js +24 -12
- package/dist/supervisor/legacy.d.ts +35 -0
- package/dist/supervisor/legacy.js +524 -0
- package/dist/supervisor/manager.d.ts +15 -0
- package/dist/supervisor/manager.js +258 -0
- package/dist/supervisor/scope.d.ts +4 -0
- package/dist/supervisor/scope.js +10 -0
- package/dist/supervisor/systemd.d.ts +2 -2
- package/dist/supervisor/systemd.js +14 -14
- package/dist/task-supervision.d.ts +9 -0
- package/dist/task-supervision.js +145 -0
- package/dist/temp-lifecycle.d.ts +17 -7
- package/dist/temp-lifecycle.js +85 -53
- package/dist/temp-supervisor-recovery.d.ts +1 -1
- package/dist/temp-supervisor-recovery.js +2 -2
- package/docs/fleet-migration.md +54 -0
- package/docs/task-workspaces.md +26 -0
- package/package.json +4 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Exec } from '../exec.js';
|
|
2
|
+
import type { ResolvedRole } from '../config.js';
|
|
3
|
+
type Desired = 'running' | 'stopped';
|
|
4
|
+
export interface LegacyReadinessGate {
|
|
5
|
+
path: string;
|
|
6
|
+
argv: string[];
|
|
7
|
+
timeoutStartSec: number;
|
|
8
|
+
restartSec: number;
|
|
9
|
+
}
|
|
10
|
+
export interface LegacyPermanentMigrationInput {
|
|
11
|
+
roles: Pick<ResolvedRole, 'name' | 'identity'>[];
|
|
12
|
+
binPath: string;
|
|
13
|
+
platform: NodeJS.Platform;
|
|
14
|
+
configPath?: string;
|
|
15
|
+
exec?: Exec;
|
|
16
|
+
/** All-role preflight precedes this hook. Ensure the singleton is running and
|
|
17
|
+
* preserves every gate before returning; failure prevents native retirement. */
|
|
18
|
+
prepareParent(gates: LegacyReadinessGate[]): Promise<void>;
|
|
19
|
+
/** Isolated process-environment seam; production reads only OURS_FLEET_HOME. */
|
|
20
|
+
processHome?(pid: number): string;
|
|
21
|
+
/** Create if absent; an existing registration's desired state must survive retry. */
|
|
22
|
+
register(name: string, desired: Desired, configPath?: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface LegacyPermanentMigrationResult {
|
|
25
|
+
name: string;
|
|
26
|
+
status: 'migrated' | 'already-migrated' | 'not-installed' | 'disabled';
|
|
27
|
+
}
|
|
28
|
+
/** Start the common parent after all-role preflight and before retirement. Only configured,
|
|
29
|
+
* identity-proven legacy registrations transfer; no identity/session writes.
|
|
30
|
+
* A receipt survives removal of the old native job and preserves its original
|
|
31
|
+
* running/stopped intent across retries. Native services are never inferred
|
|
32
|
+
* from role names alone. Uncertain or conflicting proof fails closed.
|
|
33
|
+
*/
|
|
34
|
+
export declare function migrateLegacyPermanentMembers(input: LegacyPermanentMigrationInput): Promise<LegacyPermanentMigrationResult[]>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync, readdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
|
|
6
|
+
import { agentDir, home, stateRoot } from '../paths.js';
|
|
7
|
+
import { realExec } from '../exec.js';
|
|
8
|
+
import { assertSafeAncestors } from '../rooms-tasks/workspace.js';
|
|
9
|
+
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
function proof(path) {
|
|
11
|
+
assertSafeAncestors(dirname(path));
|
|
12
|
+
const stat = lstatSync(path);
|
|
13
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024)
|
|
14
|
+
throw Error('LEGACY_PERMANENT_UNSAFE_PROOF');
|
|
15
|
+
return readFileSync(path, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
function assertReceiptGates(receipt) {
|
|
18
|
+
if (receipt.readinessGates !== undefined && !Array.isArray(receipt.readinessGates))
|
|
19
|
+
throw Error('LEGACY_PERMANENT_RECEIPT_MISMATCH');
|
|
20
|
+
for (const gate of receipt.readinessGates ?? []) {
|
|
21
|
+
if (gate.path !== resolve(gate.path) || !Array.isArray(gate.argv) || gate.argv[0] !== gate.path
|
|
22
|
+
|| gate.argv.some(arg => typeof arg !== 'string' || /[\r\n\0]/.test(arg))
|
|
23
|
+
|| !Number.isSafeInteger(gate.timeoutStartSec) || gate.timeoutStartSec < 1
|
|
24
|
+
|| !Number.isSafeInteger(gate.restartSec) || gate.restartSec < 0)
|
|
25
|
+
throw Error('LEGACY_PERMANENT_RECEIPT_MISMATCH');
|
|
26
|
+
}
|
|
27
|
+
if (receipt.operatorFiles !== undefined && !Array.isArray(receipt.operatorFiles))
|
|
28
|
+
throw Error('LEGACY_PERMANENT_RECEIPT_MISMATCH');
|
|
29
|
+
for (const file of receipt.operatorFiles ?? []) {
|
|
30
|
+
if (dirname(file.path) !== join(home(), '.config/systemd/user/ours-fleet.service.d')
|
|
31
|
+
|| !/^[a-f0-9]{64}$/.test(file.hash) || digest(proof(file.path)) !== file.hash)
|
|
32
|
+
throw Error('LEGACY_PERMANENT_OPERATOR_DROPIN_CHANGED');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function xml(value) {
|
|
36
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
|
|
37
|
+
.replaceAll('"', '"').replaceAll("'", ''');
|
|
38
|
+
}
|
|
39
|
+
function assertLaunchdFile(bytes, name, binPath) {
|
|
40
|
+
const label = `<key>Label</key><string>network.ours.fleet.${name}</string>`;
|
|
41
|
+
const argumentsBlock = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/g;
|
|
42
|
+
const matches = [...bytes.matchAll(argumentsBlock)];
|
|
43
|
+
const expected = `<string>${xml(binPath)}</string><string>_run</string><string>${name}</string>`;
|
|
44
|
+
if (!bytes.includes(label) || (bytes.match(/<key>Label<\/key>/g)?.length ?? 0) !== 1
|
|
45
|
+
|| matches.length !== 1 || matches[0][1].replace(/\s+(?=<)/g, '').trim() !== expected
|
|
46
|
+
|| /<key>(?:Program|WorkingDirectory)<\/key>/.test(bytes))
|
|
47
|
+
throw Error('LEGACY_PERMANENT_NATIVE_OWNER_MISMATCH');
|
|
48
|
+
}
|
|
49
|
+
function properties(text) {
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const line of text.trim().split('\n')) {
|
|
52
|
+
const index = line.indexOf('=');
|
|
53
|
+
if (index < 1 || Object.hasOwn(out, line.slice(0, index)))
|
|
54
|
+
throw Error('LEGACY_PERMANENT_NATIVE_PROBE_UNKNOWN');
|
|
55
|
+
out[line.slice(0, index)] = line.slice(index + 1);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
const runningStates = ['active', 'activating', 'reloading', 'deactivating'];
|
|
60
|
+
const stoppedStates = ['inactive', 'failed'];
|
|
61
|
+
function argv(value) {
|
|
62
|
+
// systemctl's structured command property is not shell syntax. Reject
|
|
63
|
+
// ambiguous escaped/quoted paths rather than guessing token boundaries.
|
|
64
|
+
const match = /^\{ path=([^;]+?) ; argv\[\]=(.*?) ; ignore_errors=no ; [^{}]*\}$/.exec(value);
|
|
65
|
+
if (!match || /["'\\\n\r]/.test(match[2]))
|
|
66
|
+
throw Error('LEGACY_PERMANENT_NATIVE_OWNER_MISMATCH');
|
|
67
|
+
const args = match[2].trim().split(/\s+/);
|
|
68
|
+
if (args[0] !== match[1].trim())
|
|
69
|
+
throw Error('LEGACY_PERMANENT_NATIVE_OWNER_MISMATCH');
|
|
70
|
+
return args;
|
|
71
|
+
}
|
|
72
|
+
function legacyCli(args, name, inputBin) {
|
|
73
|
+
let command = args;
|
|
74
|
+
if (args.length === 4) {
|
|
75
|
+
if (args[0] !== resolve(args[0]) || !/^node(?:js)?$/.test(basename(args[0])))
|
|
76
|
+
throw Error('LEGACY_PERMANENT_NODE_PROOF_REQUIRED: use the native unit Node or review its executable');
|
|
77
|
+
const node = lstatSync(args[0]);
|
|
78
|
+
if (!node.isFile() || node.isSymbolicLink() || !(node.mode & 0o111))
|
|
79
|
+
throw Error('LEGACY_PERMANENT_NODE_PROOF_REQUIRED');
|
|
80
|
+
command = args.slice(1);
|
|
81
|
+
}
|
|
82
|
+
if (command.length !== 3 || command[1] !== '_run' || command[2] !== name
|
|
83
|
+
|| command[0] !== resolve(command[0]))
|
|
84
|
+
throw Error('LEGACY_PERMANENT_NATIVE_OWNER_MISMATCH');
|
|
85
|
+
let cliPath = command[0];
|
|
86
|
+
if (existsSync(cliPath) && lstatSync(cliPath).isSymbolicLink())
|
|
87
|
+
cliPath = resolve(dirname(cliPath), readlinkSync(cliPath));
|
|
88
|
+
if (command[0] !== inputBin || cliPath !== command[0]) {
|
|
89
|
+
const packageDir = dirname(dirname(cliPath));
|
|
90
|
+
const pkg = JSON.parse(proof(join(packageDir, 'package.json')));
|
|
91
|
+
if (pkg.name !== '@ours.network/fleet' || pkg.bin?.['ours-fleet'] !== 'dist/cli.js'
|
|
92
|
+
|| cliPath !== join(packageDir, 'dist/cli.js'))
|
|
93
|
+
throw Error('LEGACY_PERMANENT_NATIVE_OWNER_MISMATCH');
|
|
94
|
+
// The installed CLI can exceed the small proof limit. Only its regular
|
|
95
|
+
// non-symlink shape is needed; package metadata binds the entry point.
|
|
96
|
+
assertSafeAncestors(dirname(cliPath));
|
|
97
|
+
const stat = lstatSync(cliPath);
|
|
98
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
99
|
+
throw Error('LEGACY_PERMANENT_UNSAFE_PROOF');
|
|
100
|
+
}
|
|
101
|
+
return command[0];
|
|
102
|
+
}
|
|
103
|
+
function seconds(value) {
|
|
104
|
+
if (value === '0')
|
|
105
|
+
return 0;
|
|
106
|
+
if (/^\d+s$/.test(value))
|
|
107
|
+
return Number(value.slice(0, -1));
|
|
108
|
+
const parts = /^(?:(\d+)min )?(\d+)s$/.exec(value);
|
|
109
|
+
if (!parts)
|
|
110
|
+
throw Error('LEGACY_PERMANENT_NATIVE_PROBE_UNKNOWN');
|
|
111
|
+
return Number(parts[1] ?? 0) * 60 + Number(parts[2]);
|
|
112
|
+
}
|
|
113
|
+
function directives(bytes) {
|
|
114
|
+
const result = new Map();
|
|
115
|
+
let section = '';
|
|
116
|
+
for (const raw of bytes.split('\n')) {
|
|
117
|
+
const line = raw.trim();
|
|
118
|
+
if (!line || /^[#;]/.test(line))
|
|
119
|
+
continue;
|
|
120
|
+
if (/^\[[A-Za-z]+\]$/.test(line)) {
|
|
121
|
+
section = line;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const match = /^([A-Za-z]+)\s*=(.*)$/.exec(line);
|
|
125
|
+
if (!match || !section || /\\$/.test(line))
|
|
126
|
+
throw Error('LEGACY_PERMANENT_UNSUPPORTED_DROPIN');
|
|
127
|
+
const key = `${section}${match[1]}`;
|
|
128
|
+
result.set(key, [...result.get(key) ?? [], match[2].trim()]);
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
/** We never copy or execute operator hooks. Explicit parent drop-ins must
|
|
133
|
+
* preserve non-ExecStart settings; generated parent init leaves them alone. */
|
|
134
|
+
function assertOperatorDropins(paths) {
|
|
135
|
+
const required = new Map();
|
|
136
|
+
for (const path of paths) {
|
|
137
|
+
for (const [key, values] of directives(proof(path))) {
|
|
138
|
+
if (key === '[Service]ExecStart')
|
|
139
|
+
continue;
|
|
140
|
+
if (!['[Service]ExecStartPre', '[Service]TimeoutStartSec', '[Service]RestartSec', '[Service]Environment',
|
|
141
|
+
'[Unit]StartLimitIntervalSec', '[Service]Restart'].includes(key))
|
|
142
|
+
throw Error(`LEGACY_PERMANENT_UNSUPPORTED_DROPIN: operator review required for ${key}`);
|
|
143
|
+
if (key === '[Service]Environment' && values.some(value => !value || /["'\\]/.test(value)
|
|
144
|
+
|| value.split(/\s+/).some(field => !/^[A-Za-z_][A-Za-z0-9_]*=\S+$/.test(field))))
|
|
145
|
+
throw Error('LEGACY_PERMANENT_UNSUPPORTED_DROPIN');
|
|
146
|
+
if (['[Service]TimeoutStartSec', '[Service]RestartSec'].includes(key)
|
|
147
|
+
&& values.some(value => !/^\d+$/.test(value)))
|
|
148
|
+
throw Error('LEGACY_PERMANENT_UNSUPPORTED_DROPIN');
|
|
149
|
+
const previous = required.get(key);
|
|
150
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(values))
|
|
151
|
+
throw Error('LEGACY_PERMANENT_OPERATOR_DROPIN_CONFLICT');
|
|
152
|
+
required.set(key, values);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!required.size)
|
|
156
|
+
return [];
|
|
157
|
+
const dir = join(home(), '.config/systemd/user/ours-fleet.service.d');
|
|
158
|
+
if (!existsSync(dir))
|
|
159
|
+
throw Error('LEGACY_PERMANENT_OPERATOR_DROPIN_REQUIRED: preserve legacy directives in ours-fleet.service.d before retry');
|
|
160
|
+
assertSafeAncestors(dir);
|
|
161
|
+
const files = readdirSync(dir).filter(name => name.endsWith('.conf')).sort();
|
|
162
|
+
if (!files.length || files.length > 32)
|
|
163
|
+
throw Error('LEGACY_PERMANENT_OPERATOR_DROPIN_REQUIRED');
|
|
164
|
+
const actual = new Map(), proofs = [];
|
|
165
|
+
for (const name of files) {
|
|
166
|
+
const path = join(dir, name), bytes = proof(path);
|
|
167
|
+
proofs.push({ path, hash: digest(bytes) });
|
|
168
|
+
for (const [key, values] of directives(bytes))
|
|
169
|
+
actual.set(key, [...actual.get(key) ?? [], ...values]);
|
|
170
|
+
}
|
|
171
|
+
for (const [key, values] of required) {
|
|
172
|
+
if (JSON.stringify(actual.get(key)) !== JSON.stringify(values))
|
|
173
|
+
throw Error(`LEGACY_PERMANENT_OPERATOR_DROPIN_REQUIRED: preserve ${key} in ours-fleet.service.d before retry`);
|
|
174
|
+
}
|
|
175
|
+
return proofs;
|
|
176
|
+
}
|
|
177
|
+
async function assertParentSettings(nativePaths, exec) {
|
|
178
|
+
const required = new Map();
|
|
179
|
+
for (const path of nativePaths)
|
|
180
|
+
for (const [key, values] of directives(proof(path)))
|
|
181
|
+
if (key !== '[Service]ExecStart')
|
|
182
|
+
required.set(key, values);
|
|
183
|
+
if (!required.size)
|
|
184
|
+
return;
|
|
185
|
+
const result = await exec('systemctl', ['--user', 'show', '-p', 'LoadState', '-p', 'ActiveState',
|
|
186
|
+
'-p', 'ExecStartPre', '-p', 'TimeoutStartUSec', '-p', 'RestartUSec', '-p', 'Environment',
|
|
187
|
+
'-p', 'StartLimitIntervalUSec', '-p', 'Restart', 'ours-fleet.service']);
|
|
188
|
+
if (result.code !== 0)
|
|
189
|
+
throw Error('LEGACY_PERMANENT_PARENT_SETTINGS_UNPROVEN');
|
|
190
|
+
const value = properties(result.stdout);
|
|
191
|
+
value.ExecStartPre ??= '';
|
|
192
|
+
value.Environment ??= '';
|
|
193
|
+
if (value.LoadState !== 'loaded' || value.ActiveState !== 'active')
|
|
194
|
+
throw Error('LEGACY_PERMANENT_PARENT_NOT_READY');
|
|
195
|
+
const pre = required.get('[Service]ExecStartPre');
|
|
196
|
+
if (pre) {
|
|
197
|
+
const commands = [];
|
|
198
|
+
for (const command of pre) {
|
|
199
|
+
if (!command)
|
|
200
|
+
commands.length = 0;
|
|
201
|
+
else
|
|
202
|
+
commands.push(command);
|
|
203
|
+
}
|
|
204
|
+
if (commands.length !== 1 || !value.ExecStartPre
|
|
205
|
+
|| JSON.stringify(argv(value.ExecStartPre)) !== JSON.stringify(commands[0].split(/\s+/)))
|
|
206
|
+
throw Error('LEGACY_PERMANENT_PARENT_SETTINGS_UNPROVEN');
|
|
207
|
+
}
|
|
208
|
+
for (const [key, property] of [['[Service]TimeoutStartSec', 'TimeoutStartUSec'], ['[Service]RestartSec', 'RestartUSec']]) {
|
|
209
|
+
const wanted = required.get(key)?.at(-1);
|
|
210
|
+
if (wanted && seconds(value[property]) !== Number(wanted))
|
|
211
|
+
throw Error('LEGACY_PERMANENT_PARENT_SETTINGS_UNPROVEN');
|
|
212
|
+
}
|
|
213
|
+
for (const line of required.get('[Service]Environment') ?? []) {
|
|
214
|
+
if (!line || /["'\\]/.test(line))
|
|
215
|
+
throw Error('LEGACY_PERMANENT_UNSUPPORTED_DROPIN');
|
|
216
|
+
for (const variable of line.split(/\s+/))
|
|
217
|
+
if (!(value.Environment ?? '').split(/\s+/).includes(variable))
|
|
218
|
+
throw Error('LEGACY_PERMANENT_PARENT_SETTINGS_UNPROVEN');
|
|
219
|
+
}
|
|
220
|
+
if (required.has('[Unit]StartLimitIntervalSec')
|
|
221
|
+
&& seconds(value.StartLimitIntervalUSec) !== Number(required.get('[Unit]StartLimitIntervalSec')?.at(-1)))
|
|
222
|
+
throw Error('LEGACY_PERMANENT_PARENT_SETTINGS_UNPROVEN');
|
|
223
|
+
if (required.has('[Service]Restart') && value.Restart !== required.get('[Service]Restart')?.at(-1))
|
|
224
|
+
throw Error('LEGACY_PERMANENT_PARENT_SETTINGS_UNPROVEN');
|
|
225
|
+
}
|
|
226
|
+
async function systemdState(name, path, exec) {
|
|
227
|
+
const result = await exec('systemctl', ['--user', 'show', '-p', 'LoadState', '-p', 'FragmentPath',
|
|
228
|
+
'-p', 'DropInPaths', '-p', 'UnitFileState', '-p', 'ActiveState', '-p', 'ExecStart', '-p', 'ExecStartPre',
|
|
229
|
+
'-p', 'ExecStartPost', '-p', 'ExecStop', '-p', 'ExecStopPost', '-p', 'ExecReload', '-p', 'ExecCondition',
|
|
230
|
+
'-p', 'Environment', '-p', 'MainPID', '-p', 'ControlGroup', '-p', 'TimeoutStartUSec', '-p', 'RestartUSec',
|
|
231
|
+
`ours-fleet-agent@${name}.service`]);
|
|
232
|
+
if (result.code !== 0)
|
|
233
|
+
throw Error('LEGACY_PERMANENT_NATIVE_PROBE_UNKNOWN');
|
|
234
|
+
const value = properties(result.stdout);
|
|
235
|
+
// systemctl omits unset properties unless --all is used. Required state
|
|
236
|
+
// fields stay mandatory; omitted optional hooks/environment mean empty.
|
|
237
|
+
for (const key of ['DropInPaths', 'ExecStartPre', 'ExecStartPost', 'ExecStop', 'ExecStopPost', 'ExecReload',
|
|
238
|
+
'ExecCondition', 'Environment', 'ControlGroup'])
|
|
239
|
+
value[key] ??= '';
|
|
240
|
+
if (value.LoadState === 'not-found' && !value.FragmentPath && !value.DropInPaths && value.MainPID === '0')
|
|
241
|
+
return { enabled: false, live: false, absent: true, value };
|
|
242
|
+
if (value.LoadState !== 'loaded' || value.FragmentPath !== path)
|
|
243
|
+
throw Error('LEGACY_PERMANENT_NATIVE_OWNER_MISMATCH');
|
|
244
|
+
if (!['enabled', 'disabled'].includes(value.UnitFileState)
|
|
245
|
+
|| ![...runningStates, ...stoppedStates].includes(value.ActiveState))
|
|
246
|
+
throw Error('LEGACY_PERMANENT_NATIVE_PROBE_UNKNOWN');
|
|
247
|
+
return { enabled: value.UnitFileState === 'enabled', live: runningStates.includes(value.ActiveState), absent: false, value };
|
|
248
|
+
}
|
|
249
|
+
function systemdProof(name, path, value, inputBin) {
|
|
250
|
+
const paths = [path, ...(value.DropInPaths ? value.DropInPaths.split(' ') : [])];
|
|
251
|
+
for (const drop of paths.slice(1)) {
|
|
252
|
+
if (![`${path}.d`, join(dirname(path), `ours-fleet-agent@${name}.service.d`)].includes(dirname(drop))
|
|
253
|
+
|| !/^[A-Za-z0-9_.-]+\.conf$/.test(drop.slice(dirname(drop).length + 1)))
|
|
254
|
+
throw Error('LEGACY_PERMANENT_NATIVE_OWNER_MISMATCH');
|
|
255
|
+
}
|
|
256
|
+
const files = paths.map(file => ({ path: file, hash: digest(proof(file)) }));
|
|
257
|
+
const operatorFiles = assertOperatorDropins(paths.slice(1));
|
|
258
|
+
// Validate the effective command, not an overridden template ExecStart.
|
|
259
|
+
const legacyBinPath = legacyCli(argv(value.ExecStart), name, inputBin);
|
|
260
|
+
const cliLinkTarget = existsSync(legacyBinPath) && lstatSync(legacyBinPath).isSymbolicLink() ? readlinkSync(legacyBinPath) : undefined;
|
|
261
|
+
const cliRealPath = cliLinkTarget === undefined ? legacyBinPath : resolve(dirname(legacyBinPath), cliLinkTarget);
|
|
262
|
+
for (const hook of ['ExecStartPost', 'ExecStop', 'ExecStopPost', 'ExecReload', 'ExecCondition'])
|
|
263
|
+
if (value[hook] !== '')
|
|
264
|
+
throw Error('LEGACY_PERMANENT_UNSUPPORTED_HOOK');
|
|
265
|
+
if (value.Environment === undefined || /(?:^|\s)OURS_FLEET_HOME=/.test(value.Environment)
|
|
266
|
+
&& !value.Environment.split(' ').includes(`OURS_FLEET_HOME=${home()}`))
|
|
267
|
+
throw Error('LEGACY_PERMANENT_CONFIG_MISMATCH');
|
|
268
|
+
if (!/^(0|[1-9]\d*)$/.test(value.MainPID) || value.ControlGroup === undefined)
|
|
269
|
+
throw Error('LEGACY_PERMANENT_NATIVE_PROBE_UNKNOWN');
|
|
270
|
+
const readinessGates = [];
|
|
271
|
+
if (value.ExecStartPre) {
|
|
272
|
+
const pre = argv(value.ExecStartPre);
|
|
273
|
+
const internal = pre.length === 4 && pre[1] === legacyBinPath
|
|
274
|
+
&& pre[2] === '_wait-daemon' && pre[3] === name;
|
|
275
|
+
if (!internal) {
|
|
276
|
+
if (pre[0] !== resolve(pre[0]))
|
|
277
|
+
throw Error('LEGACY_PERMANENT_UNSUPPORTED_READINESS_GATE');
|
|
278
|
+
readinessGates.push({ path: pre[0], argv: pre,
|
|
279
|
+
timeoutStartSec: seconds(value.TimeoutStartUSec), restartSec: seconds(value.RestartUSec) });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else if (value.ExecStartPre !== '')
|
|
283
|
+
throw Error('LEGACY_PERMANENT_NATIVE_PROBE_UNKNOWN');
|
|
284
|
+
return { files, legacyBinPath, cliLinkTarget, cliRealPath, readinessGates, operatorFiles };
|
|
285
|
+
}
|
|
286
|
+
async function launchdState(name, path, binPath, exec) {
|
|
287
|
+
const result = await exec('launchctl', ['print', `gui/${process.getuid?.() ?? 501}/network.ours.fleet.${name}`]);
|
|
288
|
+
if (result.code !== 0) {
|
|
289
|
+
if (/could not find service|no such process/i.test(`${result.stdout}\n${result.stderr}`))
|
|
290
|
+
return { loaded: false, live: false };
|
|
291
|
+
throw Error('LEGACY_PERMANENT_NATIVE_PROBE_UNKNOWN');
|
|
292
|
+
}
|
|
293
|
+
const pathValue = /^\s*path\s*=\s*(.+)\s*$/m.exec(result.stdout)?.[1]?.trim();
|
|
294
|
+
const program = /^\s*program\s*=\s*(.+)\s*$/m.exec(result.stdout)?.[1]?.trim();
|
|
295
|
+
const args = /^\s*arguments\s*=\s*\{([\s\S]*?)^\s*\}/m.exec(result.stdout)?.[1]
|
|
296
|
+
.split('\n').map(value => value.trim()).filter(Boolean);
|
|
297
|
+
if (pathValue !== path || program !== binPath || JSON.stringify(args) !== JSON.stringify([binPath, '_run', name]))
|
|
298
|
+
throw Error('LEGACY_PERMANENT_NATIVE_OWNER_MISMATCH');
|
|
299
|
+
const state = /^\s*state\s*=\s*(.+)\s*$/m.exec(result.stdout)?.[1]?.trim();
|
|
300
|
+
if (!state || !['running', 'waiting', 'not running'].includes(state))
|
|
301
|
+
throw Error('LEGACY_PERMANENT_NATIVE_PROBE_UNKNOWN');
|
|
302
|
+
return { loaded: true, live: state !== 'not running' };
|
|
303
|
+
}
|
|
304
|
+
async function assertLegacyProcessAbsent(name, binPath, input) {
|
|
305
|
+
const exec = input.exec ?? realExec;
|
|
306
|
+
const result = await exec('ps', ['-ax', '-o', 'pid=', '-o', 'command=']);
|
|
307
|
+
if (result.code !== 0)
|
|
308
|
+
throw Error('LEGACY_PERMANENT_PROCESS_ABSENCE_UNKNOWN');
|
|
309
|
+
const exactRole = new RegExp(`(?:^|\\s)_run\\s+${name}(?:\\s|$)`);
|
|
310
|
+
if (result.stdout.split('\n').some(line => {
|
|
311
|
+
const row = /^\s*(\d+)\s+(.*)$/.exec(line);
|
|
312
|
+
if (!row || !exactRole.test(row[2]) || !row[2].split(/\s+/).includes(binPath))
|
|
313
|
+
return false;
|
|
314
|
+
if (input.platform !== 'linux')
|
|
315
|
+
return true;
|
|
316
|
+
let fleetHome;
|
|
317
|
+
try {
|
|
318
|
+
fleetHome = input.processHome ? input.processHome(Number(row[1]))
|
|
319
|
+
: readFileSync(`/proc/${row[1]}/environ`, 'utf8').split('\0')
|
|
320
|
+
.find(field => field.startsWith('OURS_FLEET_HOME='))?.slice('OURS_FLEET_HOME='.length) ?? homedir();
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
throw Error('LEGACY_PERMANENT_PROCESS_ABSENCE_UNKNOWN');
|
|
324
|
+
}
|
|
325
|
+
return resolve(fleetHome) === resolve(home());
|
|
326
|
+
}))
|
|
327
|
+
throw Error('LEGACY_PERMANENT_PROCESS_STILL_RUNNING');
|
|
328
|
+
}
|
|
329
|
+
/** Start the common parent after all-role preflight and before retirement. Only configured,
|
|
330
|
+
* identity-proven legacy registrations transfer; no identity/session writes.
|
|
331
|
+
* A receipt survives removal of the old native job and preserves its original
|
|
332
|
+
* running/stopped intent across retries. Native services are never inferred
|
|
333
|
+
* from role names alone. Uncertain or conflicting proof fails closed.
|
|
334
|
+
*/
|
|
335
|
+
export async function migrateLegacyPermanentMembers(input) {
|
|
336
|
+
if (!['linux', 'darwin'].includes(input.platform))
|
|
337
|
+
return [];
|
|
338
|
+
if (!input.binPath || /[\r\n\0]/.test(input.binPath) || input.binPath !== resolve(input.binPath))
|
|
339
|
+
throw Error('LEGACY_PERMANENT_INVALID_INPUT');
|
|
340
|
+
const names = new Set(), results = [];
|
|
341
|
+
for (const role of input.roles) {
|
|
342
|
+
if (!/^[A-Za-z0-9_-]{1,120}$/.test(role.name) || !/^[A-Za-z0-9_-]{1,120}$/.test(role.identity) || names.has(role.name))
|
|
343
|
+
throw Error('LEGACY_PERMANENT_INVALID_INPUT');
|
|
344
|
+
names.add(role.name);
|
|
345
|
+
}
|
|
346
|
+
const exec = input.exec ?? realExec;
|
|
347
|
+
const gates = new Map();
|
|
348
|
+
const execute = async (role, dryRun) => withFileLock(join(stateRoot(), 'locks', 'legacy-permanent', role.name), async () => {
|
|
349
|
+
const receiptPath = join(stateRoot(), 'supervisor', 'legacy-permanent', `${role.name}.json`);
|
|
350
|
+
const platform = input.platform;
|
|
351
|
+
const nativePath = platform === 'linux'
|
|
352
|
+
? join(home(), '.config', 'systemd', 'user', 'ours-fleet-agent@.service')
|
|
353
|
+
: join(home(), 'Library', 'LaunchAgents', `network.ours.fleet.${role.name}.plist`);
|
|
354
|
+
let receipt = existsSync(receiptPath) ? JSON.parse(proof(receiptPath)) : undefined;
|
|
355
|
+
if (!receipt && !existsSync(nativePath))
|
|
356
|
+
return { name: role.name, status: 'not-installed' };
|
|
357
|
+
if (!receipt && platform === 'linux') {
|
|
358
|
+
const state = await systemdState(role.name, nativePath, exec);
|
|
359
|
+
if (state.absent)
|
|
360
|
+
return { name: role.name, status: 'not-installed' };
|
|
361
|
+
if (!state.enabled && !state.live)
|
|
362
|
+
return { name: role.name, status: 'disabled' };
|
|
363
|
+
}
|
|
364
|
+
if (proof(join(agentDir(role.name), '.identity')).trim() !== role.identity)
|
|
365
|
+
throw Error('LEGACY_PERMANENT_IDENTITY_MISMATCH');
|
|
366
|
+
const recordedConfig = proof(join(agentDir(role.name), '.config-path')).trim() || undefined;
|
|
367
|
+
if (recordedConfig && input.configPath && resolve(recordedConfig) !== resolve(input.configPath))
|
|
368
|
+
throw Error('LEGACY_PERMANENT_CONFIG_MISMATCH');
|
|
369
|
+
const configPath = recordedConfig ?? input.configPath;
|
|
370
|
+
if (receipt) {
|
|
371
|
+
if (receipt.version !== 1 || receipt.name !== role.name || receipt.identity !== role.identity
|
|
372
|
+
|| receipt.platform !== platform || receipt.nativePath !== nativePath || !/^[a-f0-9]{64}$/.test(receipt.fileHash)
|
|
373
|
+
|| !['running', 'stopped'].includes(receipt.desired)
|
|
374
|
+
|| !['prepared', 'native-retired', 'registered'].includes(receipt.phase))
|
|
375
|
+
throw Error('LEGACY_PERMANENT_RECEIPT_MISMATCH');
|
|
376
|
+
if (receipt.phase === 'registered') {
|
|
377
|
+
if (platform === 'linux') {
|
|
378
|
+
const state = await systemdState(role.name, nativePath, exec);
|
|
379
|
+
if (state.enabled || state.live)
|
|
380
|
+
throw Error('LEGACY_PERMANENT_NATIVE_REAPPEARED');
|
|
381
|
+
}
|
|
382
|
+
else if (existsSync(nativePath) || (await launchdState(role.name, nativePath, receipt.binPath, exec)).loaded)
|
|
383
|
+
throw Error('LEGACY_PERMANENT_NATIVE_REAPPEARED');
|
|
384
|
+
for (const gate of receipt.readinessGates ?? [])
|
|
385
|
+
gates.set(gate.path, gate);
|
|
386
|
+
return { name: role.name, status: 'already-migrated' };
|
|
387
|
+
}
|
|
388
|
+
assertReceiptGates(receipt);
|
|
389
|
+
// A new CLI may finish an interrupted adoption of the same role. The
|
|
390
|
+
// receipt pins old native artifacts, not the invoking release path.
|
|
391
|
+
if (receipt.configPath !== configPath)
|
|
392
|
+
throw Error('LEGACY_PERMANENT_RECEIPT_MISMATCH');
|
|
393
|
+
}
|
|
394
|
+
let linuxProof;
|
|
395
|
+
if (existsSync(nativePath)) {
|
|
396
|
+
const bytes = proof(nativePath);
|
|
397
|
+
if (platform === 'linux')
|
|
398
|
+
linuxProof = systemdProof(role.name, nativePath, (await systemdState(role.name, nativePath, exec)).value, receipt?.legacyBinPath ?? input.binPath);
|
|
399
|
+
else
|
|
400
|
+
assertLaunchdFile(bytes, role.name, receipt?.legacyBinPath ?? receipt?.binPath ?? input.binPath);
|
|
401
|
+
if (receipt && digest(bytes) !== receipt.fileHash)
|
|
402
|
+
throw Error('LEGACY_PERMANENT_NATIVE_FILE_CHANGED');
|
|
403
|
+
if (receipt?.files && JSON.stringify(linuxProof?.files) !== JSON.stringify(receipt.files))
|
|
404
|
+
throw Error('LEGACY_PERMANENT_NATIVE_FILE_CHANGED');
|
|
405
|
+
if (receipt?.operatorFiles && JSON.stringify(linuxProof?.operatorFiles) !== JSON.stringify(receipt.operatorFiles))
|
|
406
|
+
throw Error('LEGACY_PERMANENT_OPERATOR_DROPIN_CHANGED');
|
|
407
|
+
if (receipt && (receipt.cliRealPath !== undefined && receipt.cliRealPath !== linuxProof?.cliRealPath
|
|
408
|
+
|| receipt.cliLinkTarget !== undefined && receipt.cliLinkTarget !== linuxProof?.cliLinkTarget))
|
|
409
|
+
throw Error('LEGACY_PERMANENT_NATIVE_FILE_CHANGED');
|
|
410
|
+
}
|
|
411
|
+
else if (!receipt || receipt.phase === 'prepared')
|
|
412
|
+
throw Error('LEGACY_PERMANENT_NATIVE_PROOF_MISSING');
|
|
413
|
+
if (!receipt) {
|
|
414
|
+
let desired;
|
|
415
|
+
if (platform === 'linux') {
|
|
416
|
+
const state = await systemdState(role.name, nativePath, exec);
|
|
417
|
+
if (state.absent || (!state.enabled && !state.live))
|
|
418
|
+
return { name: role.name, status: 'disabled' };
|
|
419
|
+
if (!state.enabled)
|
|
420
|
+
throw Error('LEGACY_PERMANENT_DISABLED_BUT_RUNNING');
|
|
421
|
+
// Enabled units express boot intent, including failed/inactive units.
|
|
422
|
+
desired = 'running';
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
const state = await launchdState(role.name, nativePath, input.binPath, exec);
|
|
426
|
+
desired = state.live ? 'running' : 'stopped';
|
|
427
|
+
}
|
|
428
|
+
receipt = { version: 1, name: role.name, identity: role.identity, platform, binPath: input.binPath,
|
|
429
|
+
configPath, nativePath, fileHash: digest(proof(nativePath)), desired, phase: 'prepared',
|
|
430
|
+
...linuxProof, legacyBinPath: linuxProof?.legacyBinPath ?? input.binPath };
|
|
431
|
+
}
|
|
432
|
+
for (const gate of receipt.readinessGates ?? []) {
|
|
433
|
+
const existing = gates.get(gate.path);
|
|
434
|
+
if (existing && JSON.stringify(existing) !== JSON.stringify(gate))
|
|
435
|
+
throw Error('LEGACY_PERMANENT_READINESS_GATE_CONFLICT');
|
|
436
|
+
gates.set(gate.path, gate);
|
|
437
|
+
}
|
|
438
|
+
if (dryRun)
|
|
439
|
+
return { name: role.name, status: 'migrated' };
|
|
440
|
+
if (platform === 'linux' && receipt.operatorFiles?.length)
|
|
441
|
+
await assertParentSettings((receipt.files ?? []).slice(1).map(file => file.path), exec);
|
|
442
|
+
if (!existsSync(receiptPath))
|
|
443
|
+
replaceFileAtomically(receiptPath, JSON.stringify(receipt));
|
|
444
|
+
if (receipt.phase === 'prepared') {
|
|
445
|
+
if (platform === 'linux') {
|
|
446
|
+
const state = await systemdState(role.name, nativePath, exec);
|
|
447
|
+
if (state.enabled || state.live) {
|
|
448
|
+
const result = await exec('systemctl', ['--user', 'disable', '--now', `ours-fleet-agent@${role.name}.service`]);
|
|
449
|
+
if (result.code !== 0)
|
|
450
|
+
throw Error('LEGACY_PERMANENT_NATIVE_RETIRE_FAILED');
|
|
451
|
+
}
|
|
452
|
+
const after = await systemdState(role.name, nativePath, exec);
|
|
453
|
+
if (after.enabled || after.live)
|
|
454
|
+
throw Error('LEGACY_PERMANENT_NATIVE_STOP_UNPROVEN');
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
const state = await launchdState(role.name, nativePath, receipt.legacyBinPath ?? receipt.binPath, exec);
|
|
458
|
+
if (state.loaded) {
|
|
459
|
+
const result = await exec('launchctl', ['bootout', `gui/${process.getuid?.() ?? 501}/network.ours.fleet.${role.name}`]);
|
|
460
|
+
if (result.code !== 0)
|
|
461
|
+
throw Error('LEGACY_PERMANENT_NATIVE_RETIRE_FAILED');
|
|
462
|
+
}
|
|
463
|
+
if ((await launchdState(role.name, nativePath, receipt.legacyBinPath ?? receipt.binPath, exec)).loaded)
|
|
464
|
+
throw Error('LEGACY_PERMANENT_NATIVE_STOP_UNPROVEN');
|
|
465
|
+
if (digest(proof(nativePath)) !== receipt.fileHash)
|
|
466
|
+
throw Error('LEGACY_PERMANENT_NATIVE_FILE_CHANGED');
|
|
467
|
+
// Mark native retirement before removing the file so a crash after
|
|
468
|
+
// unlink still resumes from proof outside the old service path.
|
|
469
|
+
}
|
|
470
|
+
receipt.phase = 'native-retired';
|
|
471
|
+
replaceFileAtomically(receiptPath, JSON.stringify(receipt));
|
|
472
|
+
}
|
|
473
|
+
if (platform === 'linux') {
|
|
474
|
+
if (digest(proof(nativePath)) !== receipt.fileHash)
|
|
475
|
+
throw Error('LEGACY_PERMANENT_NATIVE_FILE_CHANGED');
|
|
476
|
+
const after = await systemdState(role.name, nativePath, exec);
|
|
477
|
+
if (after.enabled || after.live || after.value.MainPID !== '0')
|
|
478
|
+
throw Error('LEGACY_PERMANENT_NATIVE_STOP_UNPROVEN');
|
|
479
|
+
const postProof = systemdProof(role.name, nativePath, after.value, receipt.legacyBinPath ?? receipt.binPath);
|
|
480
|
+
if (receipt.files && JSON.stringify(postProof.files) !== JSON.stringify(receipt.files))
|
|
481
|
+
throw Error('LEGACY_PERMANENT_NATIVE_FILE_CHANGED');
|
|
482
|
+
assertReceiptGates(receipt);
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
if ((await launchdState(role.name, nativePath, receipt.legacyBinPath ?? receipt.binPath, exec)).loaded)
|
|
486
|
+
throw Error('LEGACY_PERMANENT_NATIVE_STOP_UNPROVEN');
|
|
487
|
+
if (existsSync(nativePath)) {
|
|
488
|
+
if (digest(proof(nativePath)) !== receipt.fileHash)
|
|
489
|
+
throw Error('LEGACY_PERMANENT_NATIVE_FILE_CHANGED');
|
|
490
|
+
rmSync(nativePath);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (proof(join(agentDir(role.name), '.identity')).trim() !== role.identity
|
|
494
|
+
|| (proof(join(agentDir(role.name), '.config-path')).trim() || input.configPath) !== configPath)
|
|
495
|
+
throw Error('LEGACY_PERMANENT_STATE_CHANGED');
|
|
496
|
+
// Native job absence alone does not prove that a late exiting runner or
|
|
497
|
+
// an escaped process is gone. Block adoption rather than overlap it.
|
|
498
|
+
await assertLegacyProcessAbsent(role.name, receipt.legacyBinPath ?? receipt.binPath, input);
|
|
499
|
+
await input.register(role.name, receipt.desired, configPath);
|
|
500
|
+
receipt.phase = 'registered';
|
|
501
|
+
replaceFileAtomically(receiptPath, JSON.stringify(receipt));
|
|
502
|
+
return { name: role.name, status: 'migrated' };
|
|
503
|
+
});
|
|
504
|
+
// No receipt writes, native changes or registrations until every requested
|
|
505
|
+
// role passes inspection. Revalidate under its lock immediately before use.
|
|
506
|
+
const checkedExecute = async (role, dryRun) => {
|
|
507
|
+
try {
|
|
508
|
+
return await execute(role, dryRun);
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
if (error.code)
|
|
512
|
+
throw Error(`LEGACY_PERMANENT_PROOF_UNAVAILABLE: ${role.name}: ${error.code}`);
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
for (const role of input.roles)
|
|
517
|
+
await checkedExecute(role, true);
|
|
518
|
+
if (!input.prepareParent)
|
|
519
|
+
throw Error('LEGACY_PERMANENT_PARENT_PREPARATION_REQUIRED');
|
|
520
|
+
await input.prepareParent([...gates.values()]);
|
|
521
|
+
for (const role of input.roles)
|
|
522
|
+
results.push(await checkedExecute(role, false));
|
|
523
|
+
return results;
|
|
524
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { type Exec } from '../exec.js';
|
|
3
|
+
import { type FleetMember } from './catalog.js';
|
|
4
|
+
export interface ManagerDeps {
|
|
5
|
+
exec?: Exec;
|
|
6
|
+
now?(): number;
|
|
7
|
+
sleep?(ms: number): Promise<void>;
|
|
8
|
+
shouldStop?(): boolean;
|
|
9
|
+
log?(message: string): void;
|
|
10
|
+
spawnChild?(member: FleetMember, generation: string): ChildProcess;
|
|
11
|
+
}
|
|
12
|
+
/** One parent owns process creation for every kind of Fleet member. */
|
|
13
|
+
export declare function runFleetManager(entrypoint: string, deps?: ManagerDeps): Promise<void>;
|
|
14
|
+
/** Workers cannot outlive their parent or overlap another generation of this member. */
|
|
15
|
+
export declare function runManagedMember(key: string, generation: string): Promise<void>;
|