@devrouter/cli 0.0.77 → 0.0.79
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/dist/devrouter-lifecycle-worker.js +1000 -172
- package/dist/devrouter.js +3302 -1001
- package/package.json +1 -1
- package/upgrade-prompts/0.0.78.md +35 -0
- package/upgrade-prompts/0.0.79.md +46 -0
|
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
|
|
25
25
|
// src/core/devpod-exec.ts
|
|
26
26
|
var import_node_child_process29 = require("child_process");
|
|
27
|
-
var
|
|
27
|
+
var import_node_crypto22 = require("crypto");
|
|
28
28
|
|
|
29
29
|
// src/core/devpod-environment.ts
|
|
30
30
|
var import_node_child_process3 = require("child_process");
|
|
@@ -350,9 +350,25 @@ function acquireFileLock(lockPath, options) {
|
|
|
350
350
|
}
|
|
351
351
|
if (options.onWait && now - lastProgressAt >= progressIntervalMs) {
|
|
352
352
|
lastProgressAt = now;
|
|
353
|
+
let holder;
|
|
354
|
+
try {
|
|
355
|
+
const bytes2 = import_node_fs2.default.readFileSync(lockPath, "utf-8");
|
|
356
|
+
const record6 = parseLockRecord(bytes2);
|
|
357
|
+
if (record6.owner && isLockOwnerLive(record6.owner, livenessCache) && import_node_fs2.default.readFileSync(lockPath, "utf-8") === bytes2) {
|
|
358
|
+
holder = {
|
|
359
|
+
kind: "live",
|
|
360
|
+
pid: record6.owner.pid,
|
|
361
|
+
acquiredAtMs: record6.acquiredAtMs
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
353
366
|
options.onWait({
|
|
354
367
|
waitingMs: now - waitStartedAt,
|
|
355
|
-
|
|
368
|
+
remainingWaitMs: Math.max(0, deadline - now),
|
|
369
|
+
queueLeaderPid: queueState.leaderPid,
|
|
370
|
+
holderPid: holder?.pid,
|
|
371
|
+
holderHeldMs: holder?.acquiredAtMs !== void 0 ? Math.max(0, now - holder.acquiredAtMs) : void 0,
|
|
356
372
|
queuePosition: queueState.position,
|
|
357
373
|
waitingOn: "queue"
|
|
358
374
|
});
|
|
@@ -390,6 +406,7 @@ function acquireFileLock(lockPath, options) {
|
|
|
390
406
|
lastProgressAt = now;
|
|
391
407
|
options.onWait({
|
|
392
408
|
waitingMs: now - waitStartedAt,
|
|
409
|
+
remainingWaitMs: Math.max(0, deadline - now),
|
|
393
410
|
holderPid: state.pid,
|
|
394
411
|
holderHeldMs: state.acquiredAtMs !== void 0 ? Math.max(0, now - state.acquiredAtMs) : void 0,
|
|
395
412
|
queuePosition: queueState?.position,
|
|
@@ -444,9 +461,11 @@ async function withFileLock(lockPath, options, operation) {
|
|
|
444
461
|
function createStderrWaitReporter(activity, target) {
|
|
445
462
|
return (progress) => {
|
|
446
463
|
const heldSeconds = progress.holderHeldMs !== void 0 ? `, held for ${Math.round(progress.holderHeldMs / 1e3)}s` : "";
|
|
447
|
-
const
|
|
464
|
+
const holder = progress.holderPid !== void 0 ? `provider lock held by PID ${progress.holderPid}${heldSeconds}` : "provider lock holder unknown";
|
|
465
|
+
const remaining = progress.remainingWaitMs !== void 0 ? `; queue acquisition timeout in ${Math.ceil(progress.remainingWaitMs / 1e3)}s (does not stop the holder)` : "";
|
|
466
|
+
const status = progress.waitingOn === "queue" ? `waiting in provider queue position ${progress.queuePosition}; first waiter PID ${progress.queueLeaderPid}; ${holder}` : progress.queuePosition !== void 0 && progress.queuePosition > 1 ? `waiting in provider queue position ${progress.queuePosition}; provider lock held by PID ${progress.holderPid}${heldSeconds}` : `waiting for the provider lock held by PID ${progress.holderPid}${heldSeconds}`;
|
|
448
467
|
process.stderr.write(
|
|
449
|
-
`${activity} for ${target}: ${status}; waited ${Math.round(progress.waitingMs / 1e3)}s so far
|
|
468
|
+
`${activity} for ${target}: ${status}; waited ${Math.round(progress.waitingMs / 1e3)}s so far${remaining}
|
|
450
469
|
`
|
|
451
470
|
);
|
|
452
471
|
};
|
|
@@ -1841,6 +1860,21 @@ function resolveWorkspaceRuntimeDetailed(repoPath, override) {
|
|
|
1841
1860
|
function resolveWorkspaceRuntimeOrDefault(repoPath) {
|
|
1842
1861
|
return resolveWorkspaceRuntimeDetailed(repoPath).runtime;
|
|
1843
1862
|
}
|
|
1863
|
+
function resolveWorkspaceRuntimeForReport(repoPath) {
|
|
1864
|
+
const snapshots = getWorkspaceRegistrySnapshots();
|
|
1865
|
+
const envRaw = process.env.DEVROUTER_WORKSPACE_RUNTIME?.trim();
|
|
1866
|
+
if (envRaw) {
|
|
1867
|
+
const runtime2 = parseWorkspaceRuntime(envRaw);
|
|
1868
|
+
return snapshots[runtime2] ? runtime2 : void 0;
|
|
1869
|
+
}
|
|
1870
|
+
if (repoPath) {
|
|
1871
|
+
const owner = pathOwnerRuntime(repoPath, snapshots);
|
|
1872
|
+
if (owner.status === "conflict" || owner.status === "unavailable") return void 0;
|
|
1873
|
+
if (owner.status === "owner") return owner.runtime;
|
|
1874
|
+
}
|
|
1875
|
+
const runtime = resolveWorkspaceRuntimeDetailed().runtime;
|
|
1876
|
+
return snapshots[runtime] ? runtime : void 0;
|
|
1877
|
+
}
|
|
1844
1878
|
function resetWorkspaceRuntimeCaches() {
|
|
1845
1879
|
cachedRuntime = void 0;
|
|
1846
1880
|
cachedSnapshots = void 0;
|
|
@@ -2048,6 +2082,16 @@ function parseVersion(output) {
|
|
|
2048
2082
|
/\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)\b/
|
|
2049
2083
|
)?.[1];
|
|
2050
2084
|
}
|
|
2085
|
+
function compareVersionCore(left, right) {
|
|
2086
|
+
const core = (value) => value.split(/[-+]/, 1)[0].split(".").map((part) => Number.parseInt(part, 10));
|
|
2087
|
+
const leftParts = core(left);
|
|
2088
|
+
const rightParts = core(right);
|
|
2089
|
+
for (let index = 0; index < 3; index += 1) {
|
|
2090
|
+
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
2091
|
+
if (difference !== 0) return difference > 0 ? 1 : -1;
|
|
2092
|
+
}
|
|
2093
|
+
return 0;
|
|
2094
|
+
}
|
|
2051
2095
|
function devsyAgentRepairSuggestion(inspection) {
|
|
2052
2096
|
if (inspection.state === "stale") {
|
|
2053
2097
|
return `Install Devsy ${SUPPORTED_DEVSY_VERSION} for a supported host, then run: ${DEVSY_AGENT_SETUP_COMMAND}`;
|
|
@@ -2119,7 +2163,7 @@ function inspectDevsyAgent(options = {}) {
|
|
|
2119
2163
|
const installedVersion = parseVersion(resolved.versionOutput);
|
|
2120
2164
|
const explicitPath = resolved.env.DEVSY_AGENT_BINARY?.trim();
|
|
2121
2165
|
const source = explicitPath ? "explicit" : "managed";
|
|
2122
|
-
if (installedVersion !== SUPPORTED_DEVSY_VERSION) {
|
|
2166
|
+
if (installedVersion === void 0 || installedVersion !== SUPPORTED_DEVSY_VERSION && compareVersionCore(installedVersion, SUPPORTED_DEVSY_VERSION) <= 0) {
|
|
2123
2167
|
return {
|
|
2124
2168
|
state: "stale",
|
|
2125
2169
|
source,
|
|
@@ -2135,6 +2179,15 @@ function inspectDevsyAgent(options = {}) {
|
|
|
2135
2179
|
installedVersion
|
|
2136
2180
|
};
|
|
2137
2181
|
}
|
|
2182
|
+
if (installedVersion !== SUPPORTED_DEVSY_VERSION) {
|
|
2183
|
+
return {
|
|
2184
|
+
state: "ready",
|
|
2185
|
+
source: "host",
|
|
2186
|
+
reason: `installed Devsy ${installedVersion} is newer than the verified ${SUPPORTED_DEVSY_VERSION} agent; the host CLI governs its own agent`,
|
|
2187
|
+
installedVersion,
|
|
2188
|
+
drift: { installed: installedVersion, supported: SUPPORTED_DEVSY_VERSION }
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2138
2191
|
const expectedName = resolved.nativeAssetName ?? nativeAssetName(resolved.platform, resolved.arch);
|
|
2139
2192
|
const asset = resolved.assets.find((candidate) => candidate.name === expectedName);
|
|
2140
2193
|
if (!asset) {
|
|
@@ -2156,9 +2209,7 @@ function inspectDevsyAgent(options = {}) {
|
|
|
2156
2209
|
}
|
|
2157
2210
|
function requireReadyDevsyAgent(options = {}) {
|
|
2158
2211
|
const inspection = inspectDevsyAgent(options);
|
|
2159
|
-
if (inspection.state !== "ready"
|
|
2160
|
-
throw new DevsyAgentReadinessError(inspection);
|
|
2161
|
-
}
|
|
2212
|
+
if (inspection.state !== "ready") throw new DevsyAgentReadinessError(inspection);
|
|
2162
2213
|
return {
|
|
2163
2214
|
binaryPath: inspection.binaryPath,
|
|
2164
2215
|
source: inspection.source,
|
|
@@ -3963,6 +4014,45 @@ function proveReplacementStopAbsence(state, baseline, replacement) {
|
|
|
3963
4014
|
);
|
|
3964
4015
|
assertEmptyPopulation();
|
|
3965
4016
|
}
|
|
4017
|
+
function proveAbsentPopulationStop(state, baseline, expected) {
|
|
4018
|
+
const observation = () => {
|
|
4019
|
+
const owner = inspectDevsyWorkspaceOwnership(
|
|
4020
|
+
listDevsyWorkspaces(),
|
|
4021
|
+
state.devpodId,
|
|
4022
|
+
state.repoPath
|
|
4023
|
+
);
|
|
4024
|
+
if (owner.status !== "owned")
|
|
4025
|
+
throw new Error("Absent stop registration changed during inspection.");
|
|
4026
|
+
const current = identityOf(owner.workspace);
|
|
4027
|
+
if (!(0, import_node_util3.isDeepStrictEqual)(current, expected))
|
|
4028
|
+
throw new Error("Absent stop registration identity changed during inspection.");
|
|
4029
|
+
if (resolveManagedStopEndpoint() !== baseline.endpoint)
|
|
4030
|
+
throw new Error("Stop endpoint changed.");
|
|
4031
|
+
if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
|
|
4032
|
+
throw new Error("Stop daemon changed.");
|
|
4033
|
+
proveLocalDockerSelection(owner.workspace);
|
|
4034
|
+
return current;
|
|
4035
|
+
};
|
|
4036
|
+
const assertAbsentPopulation = () => {
|
|
4037
|
+
assertManagedStopContainersAbsent(
|
|
4038
|
+
baseline.endpoint,
|
|
4039
|
+
baseline.containers.map((container) => container.id)
|
|
4040
|
+
);
|
|
4041
|
+
if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || inspectProviderRunnerContainers(
|
|
4042
|
+
baseline.endpoint,
|
|
4043
|
+
managedRunnerId(baseline.uid, baseline.providerId)
|
|
4044
|
+
).length !== 0)
|
|
4045
|
+
throw new Error("Absent stop observed a remaining workspace population.");
|
|
4046
|
+
};
|
|
4047
|
+
const before = observation();
|
|
4048
|
+
if (!(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
|
|
4049
|
+
throw new Error("Retained stop generation changed.");
|
|
4050
|
+
assertAbsentPopulation();
|
|
4051
|
+
const after = observation();
|
|
4052
|
+
if (!(0, import_node_util3.isDeepStrictEqual)(after, before) || !(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
|
|
4053
|
+
throw new Error("Absent stop authority changed during inspection.");
|
|
4054
|
+
assertAbsentPopulation();
|
|
4055
|
+
}
|
|
3966
4056
|
function proveManagedStop(state) {
|
|
3967
4057
|
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
3968
4058
|
const owner = inspectDevsyWorkspaceOwnership(
|
|
@@ -3974,6 +4064,10 @@ function proveManagedStop(state) {
|
|
|
3974
4064
|
const identityKeys = ["context", "providerId", "uid", "sourcePath", "sourceContainer"];
|
|
3975
4065
|
const current = identityOf(owner.workspace);
|
|
3976
4066
|
const changed = identityKeys.filter((key) => baseline[key] !== current[key]);
|
|
4067
|
+
if (changed.length === 0 && state.workspace !== void 0 && isLinkedWorktree(state.repoPath) && inspectManagedStopContainers(baseline.project, baseline.endpoint).length === 0) {
|
|
4068
|
+
proveAbsentPopulationStop(state, baseline, current);
|
|
4069
|
+
return { status: "proven-absent", containers: [] };
|
|
4070
|
+
}
|
|
3977
4071
|
if (changed.length === 1 && changed[0] === "uid" && !baseline.sourceContainer && !current.sourceContainer && state.workspace !== void 0 && isLinkedWorktree(state.repoPath)) {
|
|
3978
4072
|
proveReplacementStopAbsence(state, baseline, { uid: current.uid || void 0 });
|
|
3979
4073
|
return { status: "proven-absent", containers: [] };
|
|
@@ -4068,6 +4162,9 @@ function validOperation(value) {
|
|
|
4068
4162
|
"COMPLETION_UNKNOWN"
|
|
4069
4163
|
]) && (value.status === "COMPLETED" ? isReliabilityCounter(value.exitCode) : value.exitCode === null);
|
|
4070
4164
|
}
|
|
4165
|
+
function validIncidentUnit(value) {
|
|
4166
|
+
return record2(value) && isReliabilityId(value.key) && (value.kind === "process" || value.kind === "service") && isReliabilityCounter(value.actions) && isReliabilityCounter(value.lastActionAtMs);
|
|
4167
|
+
}
|
|
4071
4168
|
function assertReliabilityState(value) {
|
|
4072
4169
|
const intentRevision = record2(value) ? value.intentRevision : void 0;
|
|
4073
4170
|
const valid = record2(value) && value.contractVersion === RELIABILITY_CONTRACT_VERSION && oneOf(value.executionPolicy, ["manual", "capacity-managed"]) && (value.executionPolicy === "manual" ? value.admission === "not-applicable" && value.chargeHeld === false : value.admission !== "not-applicable") && boundedArray(value.operationHistory) && uniqueIds(value.operationHistory, "key") && uniqueIds(value.operationHistory, "id") && value.operationHistory.every(
|
|
@@ -4092,7 +4189,7 @@ function assertReliabilityState(value) {
|
|
|
4092
4189
|
(request) => record2(request) && oneOf(request.mode, ["start", "attach"]) && isReliabilityId(request.consumerId) && isReliabilityId(request.operationId) && isReliabilityProfile(request.profile) && isReliabilityCounter(request.intentRevision) && isReliabilityCounter(intentRevision) && request.intentRevision <= intentRevision
|
|
4093
4190
|
) && boundedArray(value.observations) && uniqueIds(value.observations, "capability") && value.observations.every(
|
|
4094
4191
|
(observation) => record2(observation) && oneOf(observation.infrastructure, ["healthy", "failed", "unknown"]) && oneOf(observation.application, ["verified", "unready", "unverified"]) && isReliabilityCounter(observation.observedAtMs) && isReliabilityCounter(observation.validForMs)
|
|
4095
|
-
) && (value.operation === null || validOperation(value.operation)) && (value.executionPolicy !== "manual" || value.desired !== "parked-for-capacity" && value.incident === null) && (value.incident === null || record2(value.incident) && isReliabilityId(value.incident.id) && isReliabilityCounter(value.incident.correctiveActionsTaken) && isReliabilityCounter(value.incident.actionLimit) && value.incident.actionLimit > 0 && value.incident.correctiveActionsTaken <= value.incident.actionLimit);
|
|
4192
|
+
) && (value.operation === null || validOperation(value.operation)) && (value.executionPolicy !== "manual" || value.desired !== "parked-for-capacity" && value.incident === null) && (value.incident === null || record2(value.incident) && isReliabilityId(value.incident.id) && isReliabilityCounter(value.incident.correctiveActionsTaken) && isReliabilityCounter(value.incident.actionLimit) && value.incident.actionLimit > 0 && value.incident.correctiveActionsTaken <= value.incident.actionLimit && (value.incident.startedAtMs === void 0 || isReliabilityCounter(value.incident.startedAtMs)) && (value.incident.units === void 0 || boundedArray(value.incident.units) && uniqueIds(value.incident.units, "key") && value.incident.units.every(validIncidentUnit)));
|
|
4096
4193
|
if (!valid) throw new Error("Invalid reliability state.");
|
|
4097
4194
|
}
|
|
4098
4195
|
function createReliabilityState(environmentId, controllerEpoch, executionPolicy = "capacity-managed") {
|
|
@@ -4195,6 +4292,15 @@ function evaluateCapacity(budgets, samples, charges, requested, nowMs, maxSample
|
|
|
4195
4292
|
|
|
4196
4293
|
// src/core/capacity-store.ts
|
|
4197
4294
|
var MAX_BYTES = 1048576;
|
|
4295
|
+
var CapacityHistoryError = class extends Error {
|
|
4296
|
+
constructor(code) {
|
|
4297
|
+
super(
|
|
4298
|
+
code === "capacity-ledger-lost" ? "Capacity ledger history is lost; restore verified capacity-reservations.json." : "Capacity history cannot be proven; inspect the private lifecycle journals."
|
|
4299
|
+
);
|
|
4300
|
+
this.code = code;
|
|
4301
|
+
this.name = "CapacityHistoryError";
|
|
4302
|
+
}
|
|
4303
|
+
};
|
|
4198
4304
|
var CapacitySnapshotChangedError = class extends Error {
|
|
4199
4305
|
constructor() {
|
|
4200
4306
|
super("Capacity snapshot changed.");
|
|
@@ -4286,18 +4392,57 @@ function serializeSnapshot(snapshot) {
|
|
|
4286
4392
|
return contents;
|
|
4287
4393
|
}
|
|
4288
4394
|
var CapacityStore = class {
|
|
4289
|
-
constructor(directory) {
|
|
4395
|
+
constructor(directory, write = writeFileAtomically, minimumRevision = () => 0) {
|
|
4290
4396
|
this.directory = directory;
|
|
4397
|
+
this.write = write;
|
|
4398
|
+
this.minimumRevision = minimumRevision;
|
|
4291
4399
|
this.file = import_node_path19.default.join(directory, "capacity-reservations.json");
|
|
4400
|
+
this.establishedFile = import_node_path19.default.join(directory, "capacity-ledger.established");
|
|
4292
4401
|
}
|
|
4293
4402
|
file;
|
|
4403
|
+
establishedFile;
|
|
4404
|
+
observedLedger = false;
|
|
4405
|
+
readEstablished() {
|
|
4406
|
+
let descriptor;
|
|
4407
|
+
try {
|
|
4408
|
+
descriptor = import_node_fs15.default.openSync(
|
|
4409
|
+
this.establishedFile,
|
|
4410
|
+
import_node_fs15.default.constants.O_RDONLY | import_node_fs15.default.constants.O_NOFOLLOW | import_node_fs15.default.constants.O_NONBLOCK
|
|
4411
|
+
);
|
|
4412
|
+
} catch (error) {
|
|
4413
|
+
if (error.code === "ENOENT") return false;
|
|
4414
|
+
throw error;
|
|
4415
|
+
}
|
|
4416
|
+
try {
|
|
4417
|
+
const stat = import_node_fs15.default.fstatSync(descriptor);
|
|
4418
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0 || stat.size > 4096)
|
|
4419
|
+
throw new Error("Unsafe capacity ledger marker.");
|
|
4420
|
+
const bytes2 = Buffer.alloc(4097);
|
|
4421
|
+
const count = import_node_fs15.default.readSync(descriptor, bytes2, 0, bytes2.length, 0);
|
|
4422
|
+
if (count > 4096) throw new Error("Capacity ledger marker exceeds byte limit.");
|
|
4423
|
+
const marker = JSON.parse(bytes2.subarray(0, count).toString("utf8"));
|
|
4424
|
+
object(marker, ["version"]);
|
|
4425
|
+
if (marker.version !== 1) throw new Error("Invalid capacity ledger marker.");
|
|
4426
|
+
return true;
|
|
4427
|
+
} finally {
|
|
4428
|
+
import_node_fs15.default.closeSync(descriptor);
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4294
4431
|
read() {
|
|
4432
|
+
const minimumRevision = this.minimumRevision();
|
|
4433
|
+
const established = this.readEstablished();
|
|
4295
4434
|
let descriptor;
|
|
4296
4435
|
try {
|
|
4297
|
-
descriptor = import_node_fs15.default.openSync(
|
|
4436
|
+
descriptor = import_node_fs15.default.openSync(
|
|
4437
|
+
this.file,
|
|
4438
|
+
import_node_fs15.default.constants.O_RDONLY | import_node_fs15.default.constants.O_NOFOLLOW | import_node_fs15.default.constants.O_NONBLOCK
|
|
4439
|
+
);
|
|
4298
4440
|
} catch (error) {
|
|
4299
|
-
if (error.code === "ENOENT")
|
|
4441
|
+
if (error.code === "ENOENT") {
|
|
4442
|
+
if (established || this.observedLedger || minimumRevision > 0)
|
|
4443
|
+
throw new CapacityHistoryError("capacity-ledger-lost");
|
|
4300
4444
|
return { version: 1, revision: 0, reservations: [] };
|
|
4445
|
+
}
|
|
4301
4446
|
throw error;
|
|
4302
4447
|
}
|
|
4303
4448
|
try {
|
|
@@ -4309,11 +4454,40 @@ var CapacityStore = class {
|
|
|
4309
4454
|
if (count > MAX_BYTES) throw new Error("Capacity reservation snapshot exceeds byte limit.");
|
|
4310
4455
|
const value = JSON.parse(buffer.subarray(0, count).toString("utf8"));
|
|
4311
4456
|
validate(value);
|
|
4457
|
+
if (value.revision < minimumRevision) throw new CapacityHistoryError("capacity-ledger-lost");
|
|
4458
|
+
this.observedLedger = true;
|
|
4312
4459
|
return value;
|
|
4313
4460
|
} finally {
|
|
4314
4461
|
import_node_fs15.default.closeSync(descriptor);
|
|
4315
4462
|
}
|
|
4316
4463
|
}
|
|
4464
|
+
establish() {
|
|
4465
|
+
if (!this.readEstablished()) this.write(this.establishedFile, '{"version":1}\n');
|
|
4466
|
+
for (const file of [this.file, this.establishedFile, this.directory]) {
|
|
4467
|
+
const descriptor = import_node_fs15.default.openSync(
|
|
4468
|
+
file,
|
|
4469
|
+
import_node_fs15.default.constants.O_RDONLY | import_node_fs15.default.constants.O_NOFOLLOW | import_node_fs15.default.constants.O_NONBLOCK
|
|
4470
|
+
);
|
|
4471
|
+
try {
|
|
4472
|
+
const stat = import_node_fs15.default.fstatSync(descriptor);
|
|
4473
|
+
if ((file === this.directory ? !stat.isDirectory() : !stat.isFile()) || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0)
|
|
4474
|
+
throw new Error("Unsafe capacity ledger durability artifact.");
|
|
4475
|
+
import_node_fs15.default.fsyncSync(descriptor);
|
|
4476
|
+
} finally {
|
|
4477
|
+
import_node_fs15.default.closeSync(descriptor);
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
readForMutation() {
|
|
4482
|
+
const snapshot = this.read();
|
|
4483
|
+
if (this.observedLedger) this.establish();
|
|
4484
|
+
return snapshot;
|
|
4485
|
+
}
|
|
4486
|
+
commit(snapshot) {
|
|
4487
|
+
this.write(this.file, serializeSnapshot(snapshot));
|
|
4488
|
+
this.observedLedger = true;
|
|
4489
|
+
this.establish();
|
|
4490
|
+
}
|
|
4317
4491
|
/**
|
|
4318
4492
|
* Caller must revoke journal authority and prove exact physical cessation first.
|
|
4319
4493
|
* An absent reservation still advances the revision to fence that proof.
|
|
@@ -4327,7 +4501,7 @@ var CapacityStore = class {
|
|
|
4327
4501
|
`${this.file}.lock`,
|
|
4328
4502
|
{ activity: "capacity settlement", waitMs: 100 },
|
|
4329
4503
|
() => {
|
|
4330
|
-
const snapshot = this.
|
|
4504
|
+
const snapshot = this.readForMutation();
|
|
4331
4505
|
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
4332
4506
|
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
4333
4507
|
throw new Error("Capacity reservation revision exhausted.");
|
|
@@ -4337,7 +4511,7 @@ var CapacityStore = class {
|
|
|
4337
4511
|
if (index >= 0) snapshot.reservations.splice(index, 1);
|
|
4338
4512
|
snapshot.revision++;
|
|
4339
4513
|
validate(snapshot);
|
|
4340
|
-
|
|
4514
|
+
this.commit(snapshot);
|
|
4341
4515
|
return { settled: index >= 0, revision: snapshot.revision };
|
|
4342
4516
|
}
|
|
4343
4517
|
);
|
|
@@ -4352,16 +4526,19 @@ var CapacityStore = class {
|
|
|
4352
4526
|
`${this.file}.lock`,
|
|
4353
4527
|
{ activity: "capacity pool observation", waitMs: 100 },
|
|
4354
4528
|
() => {
|
|
4355
|
-
const snapshot = this.
|
|
4529
|
+
const snapshot = this.readForMutation();
|
|
4356
4530
|
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
4357
4531
|
const merged = structuredClone(snapshot.pools ?? []);
|
|
4358
|
-
if (!mergePools(merged, pools))
|
|
4532
|
+
if (!mergePools(merged, pools)) {
|
|
4533
|
+
if (!this.observedLedger) this.commit(snapshot);
|
|
4534
|
+
return { changed: false, revision: snapshot.revision };
|
|
4535
|
+
}
|
|
4359
4536
|
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
4360
4537
|
throw new Error("Capacity reservation revision exhausted.");
|
|
4361
4538
|
snapshot.pools = merged;
|
|
4362
4539
|
snapshot.revision++;
|
|
4363
4540
|
validate(snapshot);
|
|
4364
|
-
|
|
4541
|
+
this.commit(snapshot);
|
|
4365
4542
|
return { changed: true, revision: snapshot.revision };
|
|
4366
4543
|
}
|
|
4367
4544
|
);
|
|
@@ -4380,7 +4557,7 @@ var CapacityStore = class {
|
|
|
4380
4557
|
`${this.file}.lock`,
|
|
4381
4558
|
{ activity: "capacity pool settlement", waitMs: 100 },
|
|
4382
4559
|
() => {
|
|
4383
|
-
const snapshot = this.
|
|
4560
|
+
const snapshot = this.readForMutation();
|
|
4384
4561
|
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
4385
4562
|
const pools = snapshot.pools ?? [];
|
|
4386
4563
|
const index = pools.findIndex((pool2) => pool2.daemonId === identity2.daemonId);
|
|
@@ -4394,7 +4571,7 @@ var CapacityStore = class {
|
|
|
4394
4571
|
if (pool) pools.splice(index, 1);
|
|
4395
4572
|
snapshot.revision++;
|
|
4396
4573
|
validate(snapshot);
|
|
4397
|
-
|
|
4574
|
+
this.commit(snapshot);
|
|
4398
4575
|
return { settled: !!pool, revision: snapshot.revision };
|
|
4399
4576
|
}
|
|
4400
4577
|
);
|
|
@@ -4409,7 +4586,7 @@ var CapacityStore = class {
|
|
|
4409
4586
|
`${this.file}.lock`,
|
|
4410
4587
|
{ activity: "capacity phase settlement", waitMs: 100 },
|
|
4411
4588
|
() => {
|
|
4412
|
-
const snapshot = this.
|
|
4589
|
+
const snapshot = this.readForMutation();
|
|
4413
4590
|
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
4414
4591
|
const index = snapshot.reservations.findIndex(
|
|
4415
4592
|
(entry) => entry.environmentId === target.environmentId
|
|
@@ -4426,7 +4603,7 @@ var CapacityStore = class {
|
|
|
4426
4603
|
snapshot.reservations[index] = structuredClone(target);
|
|
4427
4604
|
snapshot.revision++;
|
|
4428
4605
|
validate(snapshot);
|
|
4429
|
-
|
|
4606
|
+
this.commit(snapshot);
|
|
4430
4607
|
}
|
|
4431
4608
|
);
|
|
4432
4609
|
}
|
|
@@ -4444,7 +4621,7 @@ var CapacityStore = class {
|
|
|
4444
4621
|
`${this.file}.lock`,
|
|
4445
4622
|
{ activity: "capacity reservation", waitMs: 100 },
|
|
4446
4623
|
() => {
|
|
4447
|
-
const snapshot = this.
|
|
4624
|
+
const snapshot = this.readForMutation();
|
|
4448
4625
|
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
4449
4626
|
const pools = structuredClone(snapshot.pools ?? []);
|
|
4450
4627
|
const poolChanged = mergePools(pools, requestedPool === void 0 ? [] : [requestedPool]);
|
|
@@ -4489,7 +4666,7 @@ var CapacityStore = class {
|
|
|
4489
4666
|
snapshot.reservations[snapshot.reservations.indexOf(existing)] = structuredClone(request);
|
|
4490
4667
|
else snapshot.reservations.push(structuredClone(request));
|
|
4491
4668
|
validate(snapshot);
|
|
4492
|
-
|
|
4669
|
+
this.commit(snapshot);
|
|
4493
4670
|
return { admitted: true, revision: snapshot.revision, joined: false };
|
|
4494
4671
|
}
|
|
4495
4672
|
);
|
|
@@ -4501,6 +4678,28 @@ var import_node_crypto7 = require("crypto");
|
|
|
4501
4678
|
var import_node_fs16 = __toESM(require("fs"));
|
|
4502
4679
|
var import_node_path20 = __toESM(require("path"));
|
|
4503
4680
|
var CONTROLLER_SNAPSHOT_BYTES = 1048576;
|
|
4681
|
+
var EVENT_KINDS = [
|
|
4682
|
+
"acquired",
|
|
4683
|
+
"renewed",
|
|
4684
|
+
"released",
|
|
4685
|
+
"expired",
|
|
4686
|
+
"invalidated",
|
|
4687
|
+
"observed",
|
|
4688
|
+
"consent",
|
|
4689
|
+
"reconnected"
|
|
4690
|
+
];
|
|
4691
|
+
var LEGACY_EVENT_KINDS = EVENT_KINDS.slice(0, 6);
|
|
4692
|
+
var PROJECTION_STATUSES = [
|
|
4693
|
+
"READY",
|
|
4694
|
+
"APP_ERROR",
|
|
4695
|
+
"BLOCKED",
|
|
4696
|
+
"STOPPED",
|
|
4697
|
+
"STARTING",
|
|
4698
|
+
"RECOVERING",
|
|
4699
|
+
"WAITING_CAPACITY",
|
|
4700
|
+
"PARKED_CAPACITY",
|
|
4701
|
+
"UNKNOWN"
|
|
4702
|
+
];
|
|
4504
4703
|
function fail() {
|
|
4505
4704
|
throw new Error("Invalid controller snapshot.");
|
|
4506
4705
|
}
|
|
@@ -4518,38 +4717,150 @@ function id3(value) {
|
|
|
4518
4717
|
function counter(value) {
|
|
4519
4718
|
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
4520
4719
|
}
|
|
4720
|
+
function requirements(value) {
|
|
4721
|
+
return Array.isArray(value) && value.length >= 1 && value.length <= 16 && new Set(value).size === value.length && value.every(
|
|
4722
|
+
(entry) => text2(entry, 132) && /^(runtime|app:[a-z0-9][a-z0-9-]*)$/.test(entry)
|
|
4723
|
+
);
|
|
4724
|
+
}
|
|
4725
|
+
function parkingConsent(value) {
|
|
4726
|
+
return value === "protected" || value === "allow-unusable";
|
|
4727
|
+
}
|
|
4728
|
+
function retainedReason(value) {
|
|
4729
|
+
return value === "expired" || value === "restart" || value === "discontinuity" || value === "binding-changed";
|
|
4730
|
+
}
|
|
4731
|
+
function checkEnvironment(value, ids2, paths) {
|
|
4732
|
+
fields(value, [
|
|
4733
|
+
"id",
|
|
4734
|
+
"repoPath",
|
|
4735
|
+
"workspace",
|
|
4736
|
+
"provider",
|
|
4737
|
+
"providerId",
|
|
4738
|
+
"profile",
|
|
4739
|
+
"fingerprint"
|
|
4740
|
+
]);
|
|
4741
|
+
if (!id3(value.id) || !text2(value.repoPath, 4096) || !import_node_path20.default.isAbsolute(value.repoPath) || import_node_path20.default.resolve(value.repoPath) !== value.repoPath || !id3(value.workspace) || !id3(value.providerId) || !text2(value.profile) || !/^[a-z0-9-]+(?:,[a-z0-9-]+)*$/.test(value.profile) || !text2(value.fingerprint, 128) || !/^[a-f0-9]{64}$/.test(value.fingerprint) || !["devpod", "devsy"].includes(String(value.provider)))
|
|
4742
|
+
fail();
|
|
4743
|
+
const knownPath = ids2.get(value.id);
|
|
4744
|
+
if (knownPath !== void 0 && knownPath !== value.repoPath) fail();
|
|
4745
|
+
const knownId = paths.get(value.repoPath);
|
|
4746
|
+
if (knownId !== void 0 && knownId !== value.id) fail();
|
|
4747
|
+
ids2.set(value.id, value.repoPath);
|
|
4748
|
+
paths.set(value.repoPath, value.id);
|
|
4749
|
+
}
|
|
4750
|
+
function checkProjection(session) {
|
|
4751
|
+
if (session.observation === void 0) return;
|
|
4752
|
+
const observation = session.observation;
|
|
4753
|
+
fields(observation, [
|
|
4754
|
+
"status",
|
|
4755
|
+
"sampledAtMs",
|
|
4756
|
+
"validUntilMs",
|
|
4757
|
+
"journalRevision",
|
|
4758
|
+
"runtimeFingerprint"
|
|
4759
|
+
]);
|
|
4760
|
+
if (!PROJECTION_STATUSES.includes(String(observation.status)) || !counter(observation.sampledAtMs) || !counter(observation.validUntilMs) || observation.validUntilMs < observation.sampledAtMs || !counter(observation.journalRevision) || !text2(observation.runtimeFingerprint, 128) || !/^[a-f0-9]{64}$/.test(observation.runtimeFingerprint))
|
|
4761
|
+
fail();
|
|
4762
|
+
}
|
|
4763
|
+
function checkEvents(events, nextSequence, kinds) {
|
|
4764
|
+
let previous = 0;
|
|
4765
|
+
for (const event of events) {
|
|
4766
|
+
fields(event, ["sequence", "session", "generation", "kind"]);
|
|
4767
|
+
if (!counter(event.sequence) || event.sequence <= previous || event.sequence >= nextSequence || !id3(event.session) || !id3(event.generation) || typeof event.kind !== "string" || !kinds.includes(event.kind))
|
|
4768
|
+
fail();
|
|
4769
|
+
previous = event.sequence;
|
|
4770
|
+
}
|
|
4771
|
+
if (Buffer.byteLength(JSON.stringify(events)) > 262144) fail();
|
|
4772
|
+
}
|
|
4521
4773
|
function validateControllerSnapshot(value) {
|
|
4522
4774
|
fields(value, [
|
|
4523
4775
|
"version",
|
|
4524
4776
|
"store",
|
|
4525
4777
|
"epoch",
|
|
4526
4778
|
"revision",
|
|
4779
|
+
"parkingRevision",
|
|
4780
|
+
"history",
|
|
4527
4781
|
"nextSequence",
|
|
4528
4782
|
"environments",
|
|
4529
4783
|
"sessions",
|
|
4530
|
-
"
|
|
4784
|
+
"retainedSessions",
|
|
4785
|
+
"events",
|
|
4786
|
+
...value?.version === 3 ? ["fingerprint"] : []
|
|
4531
4787
|
]);
|
|
4532
|
-
if (value.version !==
|
|
4788
|
+
if (value.version !== 2 && value.version !== 3 || !id3(value.store) || !counter(value.epoch) || !counter(value.revision) || !counter(value.parkingRevision) || value.history !== "complete" && value.history !== "legacy-unknown" || !counter(value.nextSequence) || value.nextSequence < 1 || !Array.isArray(value.environments) || !Array.isArray(value.sessions) || !Array.isArray(value.retainedSessions) || !Array.isArray(value.events) || value.events.length > 256 || value.sessions.length + value.retainedSessions.length > 128)
|
|
4533
4789
|
fail();
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4790
|
+
if (value.version === 3) {
|
|
4791
|
+
fields(value.fingerprint, ["digest", "enrolledEpoch"]);
|
|
4792
|
+
if (typeof value.fingerprint.digest !== "string" || !/^[a-f0-9]{64}$/.test(value.fingerprint.digest) || !counter(value.fingerprint.enrolledEpoch) || value.fingerprint.enrolledEpoch < 1 || value.fingerprint.enrolledEpoch > Number(value.epoch))
|
|
4793
|
+
fail();
|
|
4794
|
+
}
|
|
4795
|
+
const ids2 = /* @__PURE__ */ new Map();
|
|
4796
|
+
const paths = /* @__PURE__ */ new Map();
|
|
4797
|
+
for (const environment of value.environments) checkEnvironment(environment, ids2, paths);
|
|
4798
|
+
if (ids2.size !== value.environments.length) fail();
|
|
4799
|
+
const activeEnvironments = new Set(ids2.keys());
|
|
4800
|
+
const usedEnvironments = /* @__PURE__ */ new Set();
|
|
4801
|
+
const activeSessions = /* @__PURE__ */ new Set();
|
|
4802
|
+
const tuples = /* @__PURE__ */ new Set();
|
|
4803
|
+
for (const session of value.sessions) {
|
|
4804
|
+
fields(session, [
|
|
4538
4805
|
"id",
|
|
4539
|
-
"
|
|
4540
|
-
"
|
|
4541
|
-
"
|
|
4542
|
-
"
|
|
4543
|
-
"
|
|
4544
|
-
"
|
|
4806
|
+
"environmentId",
|
|
4807
|
+
"generation",
|
|
4808
|
+
"requirements",
|
|
4809
|
+
"renewedAtMs",
|
|
4810
|
+
"parkingConsent",
|
|
4811
|
+
"consentRevision",
|
|
4812
|
+
...Object.hasOwn(session, "observation") ? ["observation"] : []
|
|
4545
4813
|
]);
|
|
4546
|
-
|
|
4814
|
+
const tuple = `${session.id}\0${value.epoch}\0${session.generation}`;
|
|
4815
|
+
if (!id3(session.id) || activeSessions.has(session.id) || !id3(session.environmentId) || !activeEnvironments.has(session.environmentId) || !id3(session.generation) || !counter(session.renewedAtMs) || !requirements(session.requirements) || !parkingConsent(session.parkingConsent) || !counter(session.consentRevision) || tuples.has(tuple))
|
|
4547
4816
|
fail();
|
|
4548
|
-
|
|
4549
|
-
|
|
4817
|
+
activeSessions.add(session.id);
|
|
4818
|
+
usedEnvironments.add(session.environmentId);
|
|
4819
|
+
tuples.add(tuple);
|
|
4820
|
+
checkProjection(session);
|
|
4550
4821
|
}
|
|
4551
|
-
|
|
4822
|
+
if (usedEnvironments.size !== value.environments.length) fail();
|
|
4823
|
+
for (const retainedSession of value.retainedSessions) {
|
|
4824
|
+
fields(retainedSession, [
|
|
4825
|
+
"id",
|
|
4826
|
+
"generation",
|
|
4827
|
+
"epoch",
|
|
4828
|
+
"requirements",
|
|
4829
|
+
"parkingConsent",
|
|
4830
|
+
"consentRevision",
|
|
4831
|
+
"reason",
|
|
4832
|
+
"environment"
|
|
4833
|
+
]);
|
|
4834
|
+
const tuple = `${retainedSession.id}\0${retainedSession.epoch}\0${retainedSession.generation}`;
|
|
4835
|
+
if (!id3(retainedSession.id) || !id3(retainedSession.generation) || !counter(retainedSession.epoch) || retainedSession.epoch > value.epoch || !requirements(retainedSession.requirements) || !parkingConsent(retainedSession.parkingConsent) || !counter(retainedSession.consentRevision) || !retainedReason(retainedSession.reason) || tuples.has(tuple))
|
|
4836
|
+
fail();
|
|
4837
|
+
tuples.add(tuple);
|
|
4838
|
+
checkEnvironment(retainedSession.environment, ids2, paths);
|
|
4839
|
+
}
|
|
4840
|
+
if (ids2.size > 32) fail();
|
|
4841
|
+
checkEvents(value.events, value.nextSequence, EVENT_KINDS);
|
|
4842
|
+
if (Buffer.byteLength(JSON.stringify(value)) + 1 > CONTROLLER_SNAPSHOT_BYTES) fail();
|
|
4843
|
+
}
|
|
4844
|
+
function validateLegacySnapshot(value) {
|
|
4845
|
+
fields(value, [
|
|
4846
|
+
"version",
|
|
4847
|
+
"store",
|
|
4848
|
+
"epoch",
|
|
4849
|
+
"revision",
|
|
4850
|
+
"nextSequence",
|
|
4851
|
+
"environments",
|
|
4852
|
+
"sessions",
|
|
4853
|
+
"events"
|
|
4854
|
+
]);
|
|
4855
|
+
if (value.version !== 1 || !id3(value.store) || !counter(value.epoch) || !counter(value.revision) || !counter(value.nextSequence) || value.nextSequence < 1 || !Array.isArray(value.environments) || value.environments.length > 32 || !Array.isArray(value.sessions) || value.sessions.length > 128 || !Array.isArray(value.events) || value.events.length > 256)
|
|
4856
|
+
fail();
|
|
4857
|
+
const ids2 = /* @__PURE__ */ new Map();
|
|
4858
|
+
const paths = /* @__PURE__ */ new Map();
|
|
4859
|
+
for (const environment of value.environments) checkEnvironment(environment, ids2, paths);
|
|
4860
|
+
if (ids2.size !== value.environments.length) fail();
|
|
4861
|
+
const activeEnvironments = new Set(ids2.keys());
|
|
4552
4862
|
const usedEnvironments = /* @__PURE__ */ new Set();
|
|
4863
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
4553
4864
|
for (const session of value.sessions) {
|
|
4554
4865
|
fields(session, [
|
|
4555
4866
|
"id",
|
|
@@ -4559,60 +4870,66 @@ function validateControllerSnapshot(value) {
|
|
|
4559
4870
|
"renewedAtMs",
|
|
4560
4871
|
...Object.hasOwn(session, "observation") ? ["observation"] : []
|
|
4561
4872
|
]);
|
|
4562
|
-
if (!id3(session.id) || sessions.has(session.id) || !id3(session.environmentId) || !
|
|
4563
|
-
(r) => !text2(r, 132) || !/^(runtime|app:[a-z0-9][a-z0-9-]*)$/.test(r)
|
|
4564
|
-
))
|
|
4873
|
+
if (!id3(session.id) || sessions.has(session.id) || !id3(session.environmentId) || !activeEnvironments.has(session.environmentId) || !id3(session.generation) || !counter(session.renewedAtMs) || !requirements(session.requirements))
|
|
4565
4874
|
fail();
|
|
4566
|
-
if (session.observation !== void 0) {
|
|
4567
|
-
const observation = session.observation;
|
|
4568
|
-
fields(observation, [
|
|
4569
|
-
"status",
|
|
4570
|
-
"sampledAtMs",
|
|
4571
|
-
"validUntilMs",
|
|
4572
|
-
"journalRevision",
|
|
4573
|
-
"runtimeFingerprint"
|
|
4574
|
-
]);
|
|
4575
|
-
if (![
|
|
4576
|
-
"READY",
|
|
4577
|
-
"APP_ERROR",
|
|
4578
|
-
"BLOCKED",
|
|
4579
|
-
"STOPPED",
|
|
4580
|
-
"STARTING",
|
|
4581
|
-
"RECOVERING",
|
|
4582
|
-
"WAITING_CAPACITY",
|
|
4583
|
-
"PARKED_CAPACITY",
|
|
4584
|
-
"UNKNOWN"
|
|
4585
|
-
].includes(String(observation.status)) || !counter(observation.sampledAtMs) || !counter(observation.validUntilMs) || observation.validUntilMs < observation.sampledAtMs || !counter(observation.journalRevision) || !text2(observation.runtimeFingerprint, 128) || !/^[a-f0-9]{64}$/.test(observation.runtimeFingerprint))
|
|
4586
|
-
fail();
|
|
4587
|
-
}
|
|
4588
4875
|
sessions.add(session.id);
|
|
4589
4876
|
usedEnvironments.add(session.environmentId);
|
|
4877
|
+
checkProjection(session);
|
|
4590
4878
|
}
|
|
4591
|
-
if (usedEnvironments.size !== environments.
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4879
|
+
if (usedEnvironments.size !== value.environments.length) fail();
|
|
4880
|
+
checkEvents(value.events, value.nextSequence, LEGACY_EVENT_KINDS);
|
|
4881
|
+
if (Buffer.byteLength(JSON.stringify(value)) + 1 > CONTROLLER_SNAPSHOT_BYTES) fail();
|
|
4882
|
+
}
|
|
4883
|
+
function normalizeLegacySnapshot(value) {
|
|
4884
|
+
return {
|
|
4885
|
+
version: 2,
|
|
4886
|
+
store: value.store,
|
|
4887
|
+
epoch: value.epoch,
|
|
4888
|
+
revision: value.revision,
|
|
4889
|
+
parkingRevision: 0,
|
|
4890
|
+
history: "legacy-unknown",
|
|
4891
|
+
nextSequence: value.nextSequence,
|
|
4892
|
+
environments: value.environments.map((environment) => ({ ...environment })),
|
|
4893
|
+
sessions: value.sessions.map((session) => ({
|
|
4894
|
+
id: session.id,
|
|
4895
|
+
environmentId: session.environmentId,
|
|
4896
|
+
generation: session.generation,
|
|
4897
|
+
requirements: [...session.requirements],
|
|
4898
|
+
renewedAtMs: session.renewedAtMs,
|
|
4899
|
+
parkingConsent: "protected",
|
|
4900
|
+
consentRevision: 0,
|
|
4901
|
+
...session.observation ? { observation: { ...session.observation } } : {}
|
|
4902
|
+
})),
|
|
4903
|
+
retainedSessions: [],
|
|
4904
|
+
events: value.events.map((event) => ({ ...event }))
|
|
4905
|
+
};
|
|
4906
|
+
}
|
|
4907
|
+
function parseStoredSnapshot(value) {
|
|
4908
|
+
if (value && typeof value === "object" && !Array.isArray(value) && value.version === 1) {
|
|
4909
|
+
validateLegacySnapshot(value);
|
|
4910
|
+
return normalizeLegacySnapshot(value);
|
|
4600
4911
|
}
|
|
4601
|
-
|
|
4602
|
-
|
|
4912
|
+
validateControllerSnapshot(value);
|
|
4913
|
+
return value;
|
|
4603
4914
|
}
|
|
4604
4915
|
var ControllerStore = class {
|
|
4605
|
-
constructor(directory, write = writeFileAtomically) {
|
|
4916
|
+
constructor(directory, write = writeFileAtomically, ownsLifetimeLock = false) {
|
|
4606
4917
|
this.directory = directory;
|
|
4607
4918
|
this.write = write;
|
|
4919
|
+
this.ownsLifetimeLock = ownsLifetimeLock;
|
|
4608
4920
|
this.file = import_node_path20.default.join(directory, "snapshot.json");
|
|
4921
|
+
this.identityFile = import_node_path20.default.join(directory, "store-identity.json");
|
|
4609
4922
|
}
|
|
4610
4923
|
file;
|
|
4611
|
-
|
|
4924
|
+
identityFile;
|
|
4925
|
+
identity;
|
|
4926
|
+
observedStore;
|
|
4927
|
+
observedFingerprint;
|
|
4928
|
+
readBytes(file = this.file, limit = CONTROLLER_SNAPSHOT_BYTES) {
|
|
4612
4929
|
let fd;
|
|
4613
4930
|
try {
|
|
4614
4931
|
fd = import_node_fs16.default.openSync(
|
|
4615
|
-
|
|
4932
|
+
file,
|
|
4616
4933
|
import_node_fs16.default.constants.O_RDONLY | import_node_fs16.default.constants.O_NOFOLLOW | import_node_fs16.default.constants.O_NONBLOCK
|
|
4617
4934
|
);
|
|
4618
4935
|
} catch (error) {
|
|
@@ -4621,39 +4938,150 @@ var ControllerStore = class {
|
|
|
4621
4938
|
}
|
|
4622
4939
|
try {
|
|
4623
4940
|
const stat = import_node_fs16.default.fstatSync(fd);
|
|
4624
|
-
if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0 || stat.size >
|
|
4941
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0 || stat.size > limit)
|
|
4625
4942
|
fail();
|
|
4626
|
-
const bytes2 = Buffer.alloc(
|
|
4943
|
+
const bytes2 = Buffer.alloc(limit + 1);
|
|
4627
4944
|
let length = 0;
|
|
4628
4945
|
while (length < bytes2.length) {
|
|
4629
4946
|
const count = import_node_fs16.default.readSync(fd, bytes2, length, bytes2.length - length, null);
|
|
4630
4947
|
if (!count) break;
|
|
4631
4948
|
length += count;
|
|
4632
4949
|
}
|
|
4633
|
-
if (length >
|
|
4634
|
-
|
|
4635
|
-
validateControllerSnapshot(value);
|
|
4636
|
-
return value;
|
|
4950
|
+
if (length > limit) fail();
|
|
4951
|
+
return bytes2.subarray(0, length);
|
|
4637
4952
|
} catch {
|
|
4638
4953
|
throw new Error("Controller snapshot is unreadable or invalid.");
|
|
4639
4954
|
} finally {
|
|
4640
4955
|
import_node_fs16.default.closeSync(fd);
|
|
4641
4956
|
}
|
|
4642
4957
|
}
|
|
4958
|
+
readIdentity() {
|
|
4959
|
+
try {
|
|
4960
|
+
const bytes2 = this.readBytes(this.identityFile, 4096);
|
|
4961
|
+
if (!bytes2) return void 0;
|
|
4962
|
+
const value = JSON.parse(bytes2.toString("utf8"));
|
|
4963
|
+
fields(value, [
|
|
4964
|
+
"version",
|
|
4965
|
+
"store",
|
|
4966
|
+
...value?.version === 2 ? ["key", "enrolledEpoch"] : []
|
|
4967
|
+
]);
|
|
4968
|
+
if (value.version !== 1 && value.version !== 2 || !id3(value.store)) fail();
|
|
4969
|
+
if (value.version === 2 && (typeof value.key !== "string" || !/^[a-f0-9]{64}$/.test(value.key) || !counter(value.enrolledEpoch) || Number(value.enrolledEpoch) < 1))
|
|
4970
|
+
fail();
|
|
4971
|
+
return value;
|
|
4972
|
+
} catch {
|
|
4973
|
+
throw new Error("Controller store-identity.json is unreadable or invalid.");
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
4976
|
+
provenance(identity2) {
|
|
4977
|
+
if (identity2.version !== 2) throw new Error("Controller binding provenance is unavailable.");
|
|
4978
|
+
return {
|
|
4979
|
+
digest: (0, import_node_crypto7.createHash)("sha256").update(JSON.stringify(["devrouter-controller-binding-v1", identity2.store, identity2.key])).digest("hex"),
|
|
4980
|
+
enrolledEpoch: identity2.enrolledEpoch
|
|
4981
|
+
};
|
|
4982
|
+
}
|
|
4983
|
+
checkProvenance(snapshot, identity2) {
|
|
4984
|
+
if (snapshot.version === 3) {
|
|
4985
|
+
if (!identity2 || identity2.store !== snapshot.store || JSON.stringify(this.provenance(identity2)) !== JSON.stringify(snapshot.fingerprint))
|
|
4986
|
+
throw new Error("Controller binding provenance changed.");
|
|
4987
|
+
} else if (identity2?.version === 2 && identity2.enrolledEpoch !== snapshot.epoch + 1) {
|
|
4988
|
+
throw new Error("Controller binding enrollment epoch changed.");
|
|
4989
|
+
}
|
|
4990
|
+
if (this.observedFingerprint && (snapshot.version !== 3 || JSON.stringify(snapshot.fingerprint) !== this.observedFingerprint))
|
|
4991
|
+
throw new Error("Controller binding provenance disappeared or changed.");
|
|
4992
|
+
}
|
|
4993
|
+
/** Return an incarnation-fenced capability; private key bytes never leave this store. */
|
|
4994
|
+
bindingFingerprint() {
|
|
4995
|
+
const captured = this.read();
|
|
4996
|
+
if (captured?.version !== 3) throw new Error("Controller binding provenance is unavailable.");
|
|
4997
|
+
return (owner, config) => {
|
|
4998
|
+
const current = this.read();
|
|
4999
|
+
const identity2 = this.readIdentity();
|
|
5000
|
+
if (!current || current.store !== captured.store || current.epoch !== captured.epoch || JSON.stringify(current.fingerprint) !== JSON.stringify(captured.fingerprint) || identity2?.version !== 2)
|
|
5001
|
+
throw new Error("Controller binding incarnation changed.");
|
|
5002
|
+
this.checkProvenance(current, identity2);
|
|
5003
|
+
return (0, import_node_crypto7.createHmac)("sha256", Buffer.from(identity2.key, "hex")).update(JSON.stringify({ owner, config })).digest("hex");
|
|
5004
|
+
};
|
|
5005
|
+
}
|
|
5006
|
+
read() {
|
|
5007
|
+
const identity2 = this.readIdentity();
|
|
5008
|
+
if (this.identity && JSON.stringify(identity2) !== this.identity)
|
|
5009
|
+
throw new Error("Controller store-identity.json disappeared or changed.");
|
|
5010
|
+
const bytes2 = this.readBytes();
|
|
5011
|
+
if (!bytes2) {
|
|
5012
|
+
if (identity2 || this.observedStore)
|
|
5013
|
+
throw new Error(
|
|
5014
|
+
"Controller snapshot.json is missing; restore verified history matching store-identity.json."
|
|
5015
|
+
);
|
|
5016
|
+
return void 0;
|
|
5017
|
+
}
|
|
5018
|
+
try {
|
|
5019
|
+
const snapshot = parseStoredSnapshot(JSON.parse(bytes2.toString("utf8")));
|
|
5020
|
+
if (identity2 && identity2.store !== snapshot.store || this.observedStore && this.observedStore !== snapshot.store)
|
|
5021
|
+
fail();
|
|
5022
|
+
this.checkProvenance(snapshot, identity2);
|
|
5023
|
+
this.observedStore = snapshot.store;
|
|
5024
|
+
this.identity = identity2 ? JSON.stringify(identity2) : void 0;
|
|
5025
|
+
if (snapshot.version === 3) this.observedFingerprint = JSON.stringify(snapshot.fingerprint);
|
|
5026
|
+
return snapshot;
|
|
5027
|
+
} catch {
|
|
5028
|
+
throw new Error("Controller snapshot is unreadable or invalid.");
|
|
5029
|
+
}
|
|
5030
|
+
}
|
|
5031
|
+
assertEmptyDirectory(ignoreOwnerLock) {
|
|
5032
|
+
let entries;
|
|
5033
|
+
try {
|
|
5034
|
+
entries = import_node_fs16.default.readdirSync(this.directory);
|
|
5035
|
+
} catch (error) {
|
|
5036
|
+
if (error.code === "ENOENT") return;
|
|
5037
|
+
throw error;
|
|
5038
|
+
}
|
|
5039
|
+
if (entries.some(
|
|
5040
|
+
(entry) => entry !== "capacity-policy.json" && !(ignoreOwnerLock && entry === "owner.lock")
|
|
5041
|
+
))
|
|
5042
|
+
throw new Error(
|
|
5043
|
+
"Controller history is missing with surviving artifacts; restore verified history."
|
|
5044
|
+
);
|
|
5045
|
+
}
|
|
5046
|
+
/** Refuse before acquiring a lock could consume the last surviving history evidence. */
|
|
5047
|
+
assertStartup() {
|
|
5048
|
+
if (!this.read()) this.assertEmptyDirectory(false);
|
|
5049
|
+
}
|
|
5050
|
+
enroll(store, epoch) {
|
|
5051
|
+
const existing = this.readIdentity();
|
|
5052
|
+
if (existing && existing.store !== store) throw new Error("Controller store identity changed.");
|
|
5053
|
+
const identity2 = existing?.version === 2 ? existing : {
|
|
5054
|
+
version: 2,
|
|
5055
|
+
store,
|
|
5056
|
+
key: (0, import_node_crypto7.randomBytes)(32).toString("hex"),
|
|
5057
|
+
enrolledEpoch: epoch
|
|
5058
|
+
};
|
|
5059
|
+
if (existing?.version !== 2)
|
|
5060
|
+
this.commitBytes(this.identityFile, `${JSON.stringify(identity2)}
|
|
5061
|
+
`, 4096);
|
|
5062
|
+
this.identity = JSON.stringify(identity2);
|
|
5063
|
+
return this.provenance(identity2);
|
|
5064
|
+
}
|
|
4643
5065
|
persist(snapshot) {
|
|
4644
5066
|
validateControllerSnapshot(snapshot);
|
|
4645
|
-
this.read();
|
|
5067
|
+
const previous = this.read();
|
|
5068
|
+
if (previous && previous.store !== snapshot.store)
|
|
5069
|
+
throw new Error("Controller store identity changed.");
|
|
5070
|
+
this.checkProvenance(snapshot, this.readIdentity());
|
|
4646
5071
|
const contents = `${JSON.stringify(snapshot)}
|
|
4647
5072
|
`;
|
|
5073
|
+
this.commitBytes(this.file, contents, CONTROLLER_SNAPSHOT_BYTES);
|
|
5074
|
+
if (snapshot.version === 3) this.observedFingerprint = JSON.stringify(snapshot.fingerprint);
|
|
5075
|
+
}
|
|
5076
|
+
commitBytes(file, contents, limit) {
|
|
4648
5077
|
try {
|
|
4649
|
-
this.write(
|
|
5078
|
+
this.write(file, contents);
|
|
4650
5079
|
} catch {
|
|
4651
|
-
const
|
|
4652
|
-
if (!
|
|
4653
|
-
` !== contents)
|
|
5080
|
+
const stored = this.readBytes(file, limit);
|
|
5081
|
+
if (!stored || stored.toString("utf8") !== contents)
|
|
4654
5082
|
throw new Error("Controller snapshot commit failed.");
|
|
4655
|
-
for (const
|
|
4656
|
-
const fd = import_node_fs16.default.openSync(
|
|
5083
|
+
for (const target of [file, this.directory]) {
|
|
5084
|
+
const fd = import_node_fs16.default.openSync(target, import_node_fs16.default.constants.O_RDONLY | import_node_fs16.default.constants.O_NOFOLLOW);
|
|
4657
5085
|
try {
|
|
4658
5086
|
import_node_fs16.default.fsyncSync(fd);
|
|
4659
5087
|
} finally {
|
|
@@ -4663,27 +5091,66 @@ var ControllerStore = class {
|
|
|
4663
5091
|
}
|
|
4664
5092
|
}
|
|
4665
5093
|
startIncarnation() {
|
|
4666
|
-
|
|
4667
|
-
|
|
5094
|
+
let previous = this.read();
|
|
5095
|
+
const pristine = !previous;
|
|
5096
|
+
if (!previous) this.assertEmptyDirectory(this.ownsLifetimeLock);
|
|
5097
|
+
if (previous && (previous.epoch === Number.MAX_SAFE_INTEGER || previous.revision === Number.MAX_SAFE_INTEGER || previous.parkingRevision === Number.MAX_SAFE_INTEGER))
|
|
4668
5098
|
throw new Error("Controller snapshot counters exhausted.");
|
|
5099
|
+
if (!previous) {
|
|
5100
|
+
previous = {
|
|
5101
|
+
version: 2,
|
|
5102
|
+
store: (0, import_node_crypto7.randomUUID)(),
|
|
5103
|
+
epoch: 0,
|
|
5104
|
+
revision: 0,
|
|
5105
|
+
parkingRevision: 0,
|
|
5106
|
+
history: "complete",
|
|
5107
|
+
nextSequence: 1,
|
|
5108
|
+
environments: [],
|
|
5109
|
+
sessions: [],
|
|
5110
|
+
retainedSessions: [],
|
|
5111
|
+
events: []
|
|
5112
|
+
};
|
|
5113
|
+
this.persist(previous);
|
|
5114
|
+
}
|
|
5115
|
+
const fingerprint = this.enroll(previous.store, previous.epoch + 1);
|
|
4669
5116
|
const snapshot = {
|
|
4670
|
-
version:
|
|
4671
|
-
|
|
5117
|
+
version: 3,
|
|
5118
|
+
fingerprint,
|
|
5119
|
+
store: previous.store,
|
|
4672
5120
|
epoch: (previous?.epoch ?? 0) + 1,
|
|
4673
5121
|
revision: (previous?.revision ?? 0) + 1,
|
|
5122
|
+
parkingRevision: pristine ? 0 : previous.parkingRevision + 1,
|
|
5123
|
+
history: previous?.history ?? "complete",
|
|
4674
5124
|
nextSequence: 1,
|
|
4675
5125
|
environments: [],
|
|
4676
5126
|
sessions: [],
|
|
5127
|
+
retainedSessions: previous ? [...previous.retainedSessions, ...previous.sessions.map((s) => retain(s, previous))] : [],
|
|
4677
5128
|
events: []
|
|
4678
5129
|
};
|
|
4679
5130
|
this.persist(snapshot);
|
|
4680
5131
|
return snapshot;
|
|
4681
5132
|
}
|
|
4682
5133
|
};
|
|
5134
|
+
function retain(session, previous) {
|
|
5135
|
+
const environment = previous.environments.find(
|
|
5136
|
+
(candidate) => candidate.id === session.environmentId
|
|
5137
|
+
);
|
|
5138
|
+
if (!environment) fail();
|
|
5139
|
+
return {
|
|
5140
|
+
id: session.id,
|
|
5141
|
+
generation: session.generation,
|
|
5142
|
+
epoch: previous.epoch,
|
|
5143
|
+
requirements: [...session.requirements],
|
|
5144
|
+
parkingConsent: session.parkingConsent,
|
|
5145
|
+
consentRevision: session.consentRevision,
|
|
5146
|
+
reason: "restart",
|
|
5147
|
+
environment: { ...environment }
|
|
5148
|
+
};
|
|
5149
|
+
}
|
|
4683
5150
|
|
|
4684
5151
|
// src/core/reliability-operation-store.ts
|
|
4685
5152
|
function installedCliVersion() {
|
|
4686
|
-
return true ? "0.0.
|
|
5153
|
+
return true ? "0.0.79" : "0.0.0-dev";
|
|
4687
5154
|
}
|
|
4688
5155
|
function compareCliVersions(left, right) {
|
|
4689
5156
|
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -4698,6 +5165,10 @@ function compareCliVersions(left, right) {
|
|
|
4698
5165
|
var MAX_RECORD_BYTES = 1048576;
|
|
4699
5166
|
var MAX_RELIABILITY_JOURNALS = 256;
|
|
4700
5167
|
var MAX_RELIABILITY_DIRECTORY_ENTRIES = MAX_RELIABILITY_JOURNALS * 2;
|
|
5168
|
+
var MAX_RELIABILITY_ENUMERATION_MS = 2e3;
|
|
5169
|
+
var RELIABILITY_JOURNAL_NAME_RE = /^([0-9a-f]{64})\.json$/;
|
|
5170
|
+
var RELIABILITY_LOCK_NAME_RE = /^[0-9a-f]{64}\.json\.lock(?:\.[0-9]+\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.candidate(?:\.stale)?|\.queue\.[0-9]+\.[0-9]+\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}(?:\.candidate)?)?$/;
|
|
5171
|
+
var RELIABILITY_ATOMIC_TEMP_NAME_RE = /^\.[0-9a-f]{64}\.json\.[0-9]+\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.tmp$/;
|
|
4701
5172
|
function identityKey(identity2) {
|
|
4702
5173
|
if (!import_node_path21.default.isAbsolute(identity2.repoPath) || import_node_path21.default.resolve(identity2.repoPath) !== identity2.repoPath || identity2.repoPath.includes("\0") || identity2.workspace !== null && !isReliabilityId(identity2.workspace) || !["devpod", "devsy"].includes(identity2.provider)) {
|
|
4703
5174
|
throw new Error("Invalid reliability workspace identity.");
|
|
@@ -4717,6 +5188,17 @@ function exactKeys(value, expected, message) {
|
|
|
4717
5188
|
throw new Error(message);
|
|
4718
5189
|
}
|
|
4719
5190
|
}
|
|
5191
|
+
function validateConsumerProtection(record5) {
|
|
5192
|
+
const protection = record5.consumerProtection;
|
|
5193
|
+
if (protection === void 0) return;
|
|
5194
|
+
exactKeys(
|
|
5195
|
+
protection,
|
|
5196
|
+
["version", "revision", "humanPinned"],
|
|
5197
|
+
"Invalid consumer protection fields."
|
|
5198
|
+
);
|
|
5199
|
+
if (protection.version !== 1 || !isReliabilityCounter(protection.revision) || typeof protection.humanPinned !== "boolean")
|
|
5200
|
+
throw new Error("Invalid consumer protection.");
|
|
5201
|
+
}
|
|
4720
5202
|
function validateCapacityPhaseSettlement(record5, state, enrollment) {
|
|
4721
5203
|
const settlement = record5.phaseSettlement;
|
|
4722
5204
|
if (settlement === void 0 || settlement === null) return;
|
|
@@ -4812,6 +5294,7 @@ function validate2(record5, identity2) {
|
|
|
4812
5294
|
"outcome",
|
|
4813
5295
|
"result",
|
|
4814
5296
|
"writtenByVersion",
|
|
5297
|
+
"consumerProtection",
|
|
4815
5298
|
...record5.version === 2 ? [
|
|
4816
5299
|
"capacity",
|
|
4817
5300
|
"enrollment",
|
|
@@ -4821,6 +5304,7 @@ function validate2(record5, identity2) {
|
|
|
4821
5304
|
"startupWitness"
|
|
4822
5305
|
] : []
|
|
4823
5306
|
]);
|
|
5307
|
+
validateConsumerProtection(record5);
|
|
4824
5308
|
if (record5.writtenByVersion !== void 0 && compareCliVersions(record5.writtenByVersion, installedCliVersion()) > 0) {
|
|
4825
5309
|
throw new Error(
|
|
4826
5310
|
`Reliability record was last written by devrouter ${record5.writtenByVersion}, which is newer than the installed ${installedCliVersion()}; upgrade devrouter (devrouter upgrade) before running lifecycle commands.`
|
|
@@ -4988,6 +5472,19 @@ function validate2(record5, identity2) {
|
|
|
4988
5472
|
}
|
|
4989
5473
|
}
|
|
4990
5474
|
}
|
|
5475
|
+
function createLifecycleCapacityStore(directory = import_node_path21.default.join(DEVROUTER_HOME, "controller")) {
|
|
5476
|
+
return new CapacityStore(directory, void 0, () => {
|
|
5477
|
+
try {
|
|
5478
|
+
let revision = 0;
|
|
5479
|
+
for (const record5 of listReliabilityOperations()) {
|
|
5480
|
+
if (record5.capacity) revision = Math.max(revision, record5.capacity.snapshotRevision ?? 1);
|
|
5481
|
+
}
|
|
5482
|
+
return revision;
|
|
5483
|
+
} catch {
|
|
5484
|
+
throw new CapacityHistoryError("capacity-history-unprovable");
|
|
5485
|
+
}
|
|
5486
|
+
});
|
|
5487
|
+
}
|
|
4991
5488
|
function assertCapacityEffect(record5, workerId, nowMs, directory = import_node_path21.default.join(DEVROUTER_HOME, "controller")) {
|
|
4992
5489
|
if (record5.version === 1) return;
|
|
4993
5490
|
const binding = record5.capacity;
|
|
@@ -4998,7 +5495,7 @@ function assertCapacityEffect(record5, workerId, nowMs, directory = import_node_
|
|
|
4998
5495
|
if (!binding.controller || !controller || controller.store !== binding.controller.store || controller.epoch !== binding.controller.epoch)
|
|
4999
5496
|
throw new Error("Capacity controller incarnation changed.");
|
|
5000
5497
|
}
|
|
5001
|
-
const snapshot =
|
|
5498
|
+
const snapshot = createLifecycleCapacityStore(directory).read();
|
|
5002
5499
|
if (binding.snapshotRevision === void 0 || snapshot.revision !== binding.snapshotRevision)
|
|
5003
5500
|
throw new Error("Capacity snapshot authority is absent or stale.");
|
|
5004
5501
|
const reservation = snapshot.reservations.find(
|
|
@@ -5039,6 +5536,79 @@ function readBoundedPrivateJson(file, label) {
|
|
|
5039
5536
|
import_node_fs17.default.closeSync(descriptor);
|
|
5040
5537
|
}
|
|
5041
5538
|
}
|
|
5539
|
+
function assertReliabilityEnumerationTime(startedAtMs) {
|
|
5540
|
+
if (performance.now() - startedAtMs > MAX_RELIABILITY_ENUMERATION_MS)
|
|
5541
|
+
throw new Error("Reliability journal enumeration exceeded its time limit.");
|
|
5542
|
+
}
|
|
5543
|
+
function reliabilityDirectory() {
|
|
5544
|
+
return import_node_path21.default.join(DEVROUTER_HOME, "reliability");
|
|
5545
|
+
}
|
|
5546
|
+
function journalIdentity(value) {
|
|
5547
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5548
|
+
throw new Error("Reliability journal identity is invalid.");
|
|
5549
|
+
const identity2 = value.identity;
|
|
5550
|
+
if (!identity2 || typeof identity2 !== "object" || Array.isArray(identity2))
|
|
5551
|
+
throw new Error("Reliability journal identity is invalid.");
|
|
5552
|
+
return identity2;
|
|
5553
|
+
}
|
|
5554
|
+
function listReliabilityOperations() {
|
|
5555
|
+
const startedAtMs = performance.now();
|
|
5556
|
+
const directory = reliabilityDirectory();
|
|
5557
|
+
let stat;
|
|
5558
|
+
try {
|
|
5559
|
+
stat = import_node_fs17.default.lstatSync(directory);
|
|
5560
|
+
} catch (error) {
|
|
5561
|
+
if (error.code === "ENOENT") return [];
|
|
5562
|
+
throw error;
|
|
5563
|
+
}
|
|
5564
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
5565
|
+
throw new Error("Reliability journal directory is unsafe.");
|
|
5566
|
+
if (stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0)
|
|
5567
|
+
throw new Error("Reliability journal directory is not private.");
|
|
5568
|
+
const entries = [];
|
|
5569
|
+
const handle = import_node_fs17.default.opendirSync(directory);
|
|
5570
|
+
try {
|
|
5571
|
+
for (; ; ) {
|
|
5572
|
+
assertReliabilityEnumerationTime(startedAtMs);
|
|
5573
|
+
const entry = handle.readSync();
|
|
5574
|
+
if (!entry) break;
|
|
5575
|
+
entries.push(entry);
|
|
5576
|
+
if (entries.length > MAX_RELIABILITY_DIRECTORY_ENTRIES)
|
|
5577
|
+
throw new Error("Reliability journal directory exceeds its entry limit.");
|
|
5578
|
+
}
|
|
5579
|
+
} finally {
|
|
5580
|
+
handle.closeSync();
|
|
5581
|
+
}
|
|
5582
|
+
const journals = [];
|
|
5583
|
+
for (const entry of entries) {
|
|
5584
|
+
assertReliabilityEnumerationTime(startedAtMs);
|
|
5585
|
+
if (entry.isSymbolicLink())
|
|
5586
|
+
throw new Error("Reliability journal directory contains a symlink.");
|
|
5587
|
+
if (!entry.isFile())
|
|
5588
|
+
throw new Error("Reliability journal directory contains an unsupported entry.");
|
|
5589
|
+
if (RELIABILITY_LOCK_NAME_RE.test(entry.name) || RELIABILITY_ATOMIC_TEMP_NAME_RE.test(entry.name))
|
|
5590
|
+
continue;
|
|
5591
|
+
const match = RELIABILITY_JOURNAL_NAME_RE.exec(entry.name);
|
|
5592
|
+
if (!match) throw new Error("Reliability journal directory contains an unsupported entry.");
|
|
5593
|
+
journals.push({ name: entry.name, key: match[1] });
|
|
5594
|
+
}
|
|
5595
|
+
if (journals.length > MAX_RELIABILITY_JOURNALS)
|
|
5596
|
+
throw new Error("Reliability journal directory exceeds its journal limit.");
|
|
5597
|
+
journals.sort((left, right) => left.name.localeCompare(right.name));
|
|
5598
|
+
const records = [];
|
|
5599
|
+
for (const journal of journals) {
|
|
5600
|
+
assertReliabilityEnumerationTime(startedAtMs);
|
|
5601
|
+
const file = import_node_path21.default.join(directory, journal.name);
|
|
5602
|
+
const identity2 = journalIdentity(readBoundedPrivateJson(file, "Reliability journal"));
|
|
5603
|
+
if (identityKey(identity2) !== journal.key)
|
|
5604
|
+
throw new Error("Reliability journal filename does not match its identity.");
|
|
5605
|
+
const record5 = readReliabilityOperation(identity2);
|
|
5606
|
+
if (!record5) throw new Error("Reliability journal disappeared during enumeration.");
|
|
5607
|
+
records.push(record5);
|
|
5608
|
+
assertReliabilityEnumerationTime(startedAtMs);
|
|
5609
|
+
}
|
|
5610
|
+
return records;
|
|
5611
|
+
}
|
|
5042
5612
|
function persist(record5) {
|
|
5043
5613
|
validate2(record5, record5.identity);
|
|
5044
5614
|
const contents = `${JSON.stringify(record5)}
|
|
@@ -5082,12 +5652,15 @@ function updateReliabilityOperation(identity2, operation) {
|
|
|
5082
5652
|
throw new Error("Reliability record revision is exhausted.");
|
|
5083
5653
|
const previousVersion = record5.version;
|
|
5084
5654
|
const previousEnrollment = record5.enrollment && { ...record5.enrollment };
|
|
5655
|
+
const previousProtection = structuredClone(record5.consumerProtection);
|
|
5085
5656
|
const result = operation(record5);
|
|
5086
5657
|
if (result && typeof result.then === "function") {
|
|
5087
5658
|
throw new Error("Reliability record updates must be synchronous.");
|
|
5088
5659
|
}
|
|
5089
5660
|
if (record5.version < previousVersion || previousEnrollment && !(0, import_node_util4.isDeepStrictEqual)(record5.enrollment, previousEnrollment))
|
|
5090
5661
|
throw new Error("Durable capacity enrollment cannot be downgraded or replaced.");
|
|
5662
|
+
if (!(0, import_node_util4.isDeepStrictEqual)(record5.consumerProtection, previousProtection))
|
|
5663
|
+
throw new Error("Consumer protection is only writable through the dedicated pin authority.");
|
|
5091
5664
|
record5.writtenByVersion = installedCliVersion();
|
|
5092
5665
|
validate2(record5, identity2);
|
|
5093
5666
|
record5.revision += 1;
|
|
@@ -6383,7 +6956,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs18.default.rea
|
|
|
6383
6956
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
6384
6957
|
const requiredVersion = config.devrouter?.version;
|
|
6385
6958
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
6386
|
-
const cliVersion = true ? "0.0.
|
|
6959
|
+
const cliVersion = true ? "0.0.79" : "0.0.0-dev";
|
|
6387
6960
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
6388
6961
|
hasWarnedVersionMismatch = true;
|
|
6389
6962
|
process.stderr.write(
|
|
@@ -9315,7 +9888,8 @@ async function startDevsyWorkspace(options) {
|
|
|
9315
9888
|
}
|
|
9316
9889
|
if (options.recreate) args.push("--recreate");
|
|
9317
9890
|
const env = network ? networkProviderEnvironment(network.binding, process.env) : { ...process.env };
|
|
9318
|
-
env.DEVSY_AGENT_BINARY = agent.binaryPath;
|
|
9891
|
+
if (agent.binaryPath) env.DEVSY_AGENT_BINARY = agent.binaryPath;
|
|
9892
|
+
else delete env.DEVSY_AGENT_BINARY;
|
|
9319
9893
|
if (options.workspace) {
|
|
9320
9894
|
env.WORKSPACE = options.workspace.token;
|
|
9321
9895
|
env.DEVROUTER_WORKSPACE = options.workspace.token;
|
|
@@ -9339,7 +9913,7 @@ async function startDevsyWorkspace(options) {
|
|
|
9339
9913
|
let message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
|
|
9340
9914
|
if (result.error?.message) message += ` ${result.error.message}`;
|
|
9341
9915
|
if (AGENT_ACQUISITION_RE.test(result.stderrTail)) {
|
|
9342
|
-
message += ` Devsy rejected the verified agent source. Run: ${DEVSY_AGENT_SETUP_COMMAND}
|
|
9916
|
+
message += agent.binaryPath ? ` Devsy rejected the verified agent source. Run: ${DEVSY_AGENT_SETUP_COMMAND}` : " Devsy could not acquire an agent binary for this host CLI.";
|
|
9343
9917
|
}
|
|
9344
9918
|
if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
|
|
9345
9919
|
throw new DevsyStartPostconditionError(message);
|
|
@@ -9379,9 +9953,9 @@ var ExecutionOutcomeError = class extends Error {
|
|
|
9379
9953
|
};
|
|
9380
9954
|
|
|
9381
9955
|
// src/core/reliability-lifecycle.ts
|
|
9382
|
-
var
|
|
9956
|
+
var import_node_crypto21 = require("crypto");
|
|
9383
9957
|
var import_node_path40 = __toESM(require("path"));
|
|
9384
|
-
var
|
|
9958
|
+
var import_node_util7 = require("util");
|
|
9385
9959
|
|
|
9386
9960
|
// src/core/capacity-policy.ts
|
|
9387
9961
|
var import_node_fs26 = __toESM(require("fs"));
|
|
@@ -9402,6 +9976,76 @@ var CONTROLLER_PROFILE_RE = new RegExp(
|
|
|
9402
9976
|
`^(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)})(?:,(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)}))*$`
|
|
9403
9977
|
);
|
|
9404
9978
|
|
|
9979
|
+
// src/core/controller-monitor.ts
|
|
9980
|
+
var import_node_crypto15 = require("crypto");
|
|
9981
|
+
var import_node_util6 = require("util");
|
|
9982
|
+
|
|
9983
|
+
// src/core/reliability-output.ts
|
|
9984
|
+
function reliabilityAttention(state) {
|
|
9985
|
+
assertReliabilityState(state);
|
|
9986
|
+
const completeStop = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
|
|
9987
|
+
if (state.desired === "stopped-by-user") return completeStop ? void 0 : "stop-incomplete";
|
|
9988
|
+
if (state.operation?.status === "COMPLETION_UNKNOWN" || state.operation?.status === "INTERRUPTED")
|
|
9989
|
+
return "operation-unknown";
|
|
9990
|
+
if (state.desired === "parked-for-capacity")
|
|
9991
|
+
return completeStop ? "capacity-parked" : "stop-incomplete";
|
|
9992
|
+
if (state.admission === "denied-unadmittable") return "unadmittable";
|
|
9993
|
+
if (state.admission === "waiting") return "capacity-waiting";
|
|
9994
|
+
if (state.phase === "recovering") return "recovering";
|
|
9995
|
+
if (state.phase === "queued" || state.phase === "starting" || state.phase === "verifying")
|
|
9996
|
+
return "starting";
|
|
9997
|
+
return void 0;
|
|
9998
|
+
}
|
|
9999
|
+
|
|
10000
|
+
// src/core/recovery-budget.ts
|
|
10001
|
+
function findRecoveryUnit(incident, unit) {
|
|
10002
|
+
return (incident.units ?? []).find((record5) => record5.key === unit.key);
|
|
10003
|
+
}
|
|
10004
|
+
function restartsFor(kind, limits) {
|
|
10005
|
+
return kind === "service" ? limits.maxServiceRestarts : limits.maxProcessRestarts;
|
|
10006
|
+
}
|
|
10007
|
+
function recoveryBudgetRefusal(input) {
|
|
10008
|
+
const startedAtMs = input.incident.startedAtMs ?? input.nowMs;
|
|
10009
|
+
const elapsed = input.activeElapsedMs ?? Math.max(0, input.nowMs - startedAtMs);
|
|
10010
|
+
if (elapsed > input.limits.windowSeconds * 1e3) return "window-closed";
|
|
10011
|
+
if (input.unit !== void 0) {
|
|
10012
|
+
const record5 = findRecoveryUnit(input.incident, input.unit);
|
|
10013
|
+
if (record5 !== void 0 && record5.actions >= restartsFor(input.unit.kind, input.limits))
|
|
10014
|
+
return "unit-exhausted";
|
|
10015
|
+
}
|
|
10016
|
+
if (input.incident.correctiveActionsTaken >= input.incident.actionLimit)
|
|
10017
|
+
return "budget-exhausted";
|
|
10018
|
+
return void 0;
|
|
10019
|
+
}
|
|
10020
|
+
function claimRecoveryUnit(input) {
|
|
10021
|
+
const reason2 = recoveryBudgetRefusal(input);
|
|
10022
|
+
if (reason2 !== void 0) return { ok: false, reason: reason2 };
|
|
10023
|
+
const units = (input.incident.units ?? []).map((record5) => ({ ...record5 }));
|
|
10024
|
+
if (input.unit !== void 0) {
|
|
10025
|
+
const index = units.findIndex((record5) => record5.key === input.unit?.key);
|
|
10026
|
+
if (index === -1) {
|
|
10027
|
+
if (units.length >= RELIABILITY_MAX_ITEMS) return { ok: false, reason: "unit-exhausted" };
|
|
10028
|
+
units.push({ ...input.unit, actions: 1, lastActionAtMs: input.nowMs });
|
|
10029
|
+
} else {
|
|
10030
|
+
const record5 = units[index];
|
|
10031
|
+
units[index] = {
|
|
10032
|
+
...record5,
|
|
10033
|
+
actions: record5.actions + 1,
|
|
10034
|
+
lastActionAtMs: input.nowMs
|
|
10035
|
+
};
|
|
10036
|
+
}
|
|
10037
|
+
}
|
|
10038
|
+
return {
|
|
10039
|
+
ok: true,
|
|
10040
|
+
incident: {
|
|
10041
|
+
...input.incident,
|
|
10042
|
+
startedAtMs: input.incident.startedAtMs ?? input.nowMs,
|
|
10043
|
+
correctiveActionsTaken: input.incident.correctiveActionsTaken + 1,
|
|
10044
|
+
units
|
|
10045
|
+
}
|
|
10046
|
+
};
|
|
10047
|
+
}
|
|
10048
|
+
|
|
9405
10049
|
// src/core/reliability-model.ts
|
|
9406
10050
|
var INFRASTRUCTURE_VALUES = ["healthy", "failed", "unknown"];
|
|
9407
10051
|
var APPLICATION_VALUES = ["verified", "unready", "unverified"];
|
|
@@ -9474,7 +10118,7 @@ function assertReliabilityEvent(value) {
|
|
|
9474
10118
|
valid = isReliabilityCounter(value.type === "epoch" ? value.nextEpoch : value.nextGeneration);
|
|
9475
10119
|
break;
|
|
9476
10120
|
case "recover":
|
|
9477
|
-
valid = isReliabilityId(value.incidentId) && isReliabilityCounter(value.actionLimit) && value.actionLimit > 0 && isReliabilityId(value.operationId);
|
|
10121
|
+
valid = isReliabilityId(value.incidentId) && isReliabilityCounter(value.actionLimit) && value.actionLimit > 0 && isReliabilityId(value.operationId) && (value.unit === null || record3(value.unit) && isReliabilityId(value.unit.key) && (value.unit.kind === "process" || value.unit.kind === "service")) && record3(value.recoveryLimits) && isReliabilityCounter(value.recoveryLimits.maxProcessRestarts) && isReliabilityCounter(value.recoveryLimits.maxServiceRestarts) && isReliabilityCounter(value.recoveryLimits.windowSeconds) && value.recoveryLimits.windowSeconds > 0 && (value.activeElapsedMs === null || isReliabilityCounter(value.activeElapsedMs));
|
|
9478
10122
|
break;
|
|
9479
10123
|
case "rearm":
|
|
9480
10124
|
valid = isReliabilityId(value.incidentId);
|
|
@@ -9494,7 +10138,10 @@ function cloneOperation(operation) {
|
|
|
9494
10138
|
return { ...operation };
|
|
9495
10139
|
}
|
|
9496
10140
|
function cloneIncident(incident) {
|
|
9497
|
-
return {
|
|
10141
|
+
return {
|
|
10142
|
+
...incident,
|
|
10143
|
+
...incident.units ? { units: incident.units.map((unit) => ({ ...unit })) } : {}
|
|
10144
|
+
};
|
|
9498
10145
|
}
|
|
9499
10146
|
function cloneState(state) {
|
|
9500
10147
|
return {
|
|
@@ -9546,6 +10193,12 @@ function possibleDispatch(operation) {
|
|
|
9546
10193
|
function pendingCommand(operation) {
|
|
9547
10194
|
return operation !== null && !["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(operation.status);
|
|
9548
10195
|
}
|
|
10196
|
+
function retirableEntryIndex(state) {
|
|
10197
|
+
const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
|
|
10198
|
+
return state.operationHistory.findIndex(
|
|
10199
|
+
(entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(entry.status)
|
|
10200
|
+
);
|
|
10201
|
+
}
|
|
9549
10202
|
function setUnknown(operation) {
|
|
9550
10203
|
if (!operation || !possibleDispatch(operation)) return operation;
|
|
9551
10204
|
return { ...operation, status: "COMPLETION_UNKNOWN", exitCode: null };
|
|
@@ -9665,10 +10318,7 @@ function handleOperationRequest(state, event) {
|
|
|
9665
10318
|
const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
|
|
9666
10319
|
let retired = -1;
|
|
9667
10320
|
if (rollover) {
|
|
9668
|
-
|
|
9669
|
-
retired = state.operationHistory.findIndex(
|
|
9670
|
-
(entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(entry.status)
|
|
9671
|
-
);
|
|
10321
|
+
retired = retirableEntryIndex(state);
|
|
9672
10322
|
if (retired === -1)
|
|
9673
10323
|
return blocked(
|
|
9674
10324
|
state,
|
|
@@ -9721,7 +10371,7 @@ function handleAdmission(state, event) {
|
|
|
9721
10371
|
return unchanged(state, "blocked");
|
|
9722
10372
|
}
|
|
9723
10373
|
if (state.admission === event.result) return unchanged(state, "joined");
|
|
9724
|
-
if (state.desired === "parked-for-capacity" && event.result === "admitted"
|
|
10374
|
+
if (state.desired === "parked-for-capacity" && event.result === "admitted")
|
|
9725
10375
|
return unchanged(state, "blocked");
|
|
9726
10376
|
state.admission = event.result;
|
|
9727
10377
|
if (event.result === "admitted") {
|
|
@@ -9756,16 +10406,7 @@ function handleDispatchPersisted(state, event) {
|
|
|
9756
10406
|
`dispatch persistence requires desired 'running' (desired='${state.desired}'); the operation outcome is unknown.`
|
|
9757
10407
|
);
|
|
9758
10408
|
}
|
|
9759
|
-
if (state.phase === "recovering" && state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
|
|
9760
|
-
return unchanged(state, "blocked");
|
|
9761
|
-
}
|
|
9762
10409
|
state.operation = { ...state.operation, status: "DISPATCH_RECORDED", exitCode: null };
|
|
9763
|
-
if (state.phase === "recovering" && state.incident !== null) {
|
|
9764
|
-
state.incident = {
|
|
9765
|
-
...state.incident,
|
|
9766
|
-
correctiveActionsTaken: state.incident.correctiveActionsTaken + 1
|
|
9767
|
-
};
|
|
9768
|
-
}
|
|
9769
10410
|
return transition(state, "accepted", [effect(state, "launch", event.operationId)]);
|
|
9770
10411
|
}
|
|
9771
10412
|
function handleLaunched(state, event) {
|
|
@@ -9898,9 +10539,18 @@ function handlePark(state) {
|
|
|
9898
10539
|
return transition(state, "accepted", [effect(state, "stop", operationId)]);
|
|
9899
10540
|
}
|
|
9900
10541
|
function handleResume(state, event) {
|
|
9901
|
-
if (state.executionPolicy === "manual" || state.desired !== "parked-for-capacity" || !event.pressureDwellSatisfied || !state.stopProof.workloadsStopped || !state.stopProof.routesRemoved || state.consumers.length === 0 || state.
|
|
10542
|
+
if (state.executionPolicy === "manual" || state.desired !== "parked-for-capacity" || !event.pressureDwellSatisfied || !state.stopProof.workloadsStopped || !state.stopProof.routesRemoved || state.consumers.length === 0 || state.profile === null || state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit || state.operation === null || !["NOT_STARTED", "COMPLETED", "INTERRUPTED"].includes(state.operation.status) || ["COMPLETED", "INTERRUPTED"].includes(state.operation.status) && event.operationId === state.operation.id) {
|
|
9902
10543
|
return unchanged(state, "blocked");
|
|
9903
10544
|
}
|
|
10545
|
+
if (state.operationHistory.some((entry) => entry.id === event.operationId))
|
|
10546
|
+
return unchanged(state, "conflict");
|
|
10547
|
+
const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
|
|
10548
|
+
const retired = rollover ? retirableEntryIndex(state) : -1;
|
|
10549
|
+
if (rollover && retired === -1)
|
|
10550
|
+
return blocked(
|
|
10551
|
+
state,
|
|
10552
|
+
`journal holds ${state.operationHistory.length} entries and every retirable entry is the current operation or the latest ensure.`
|
|
10553
|
+
);
|
|
9904
10554
|
const intentRevision = advance(state.intentRevision);
|
|
9905
10555
|
const runtimeGeneration = advance(state.runtimeGeneration);
|
|
9906
10556
|
if (intentRevision === null || runtimeGeneration === null) return unchanged(state, "blocked");
|
|
@@ -9908,9 +10558,10 @@ function handleResume(state, event) {
|
|
|
9908
10558
|
state.runtimeGeneration = runtimeGeneration;
|
|
9909
10559
|
state.desired = "running";
|
|
9910
10560
|
state.phase = "queued";
|
|
10561
|
+
state.admission = "waiting";
|
|
9911
10562
|
state.stopProof = { workloadsStopped: false, routesRemoved: false };
|
|
9912
10563
|
state.observations = [];
|
|
9913
|
-
state.chargeHeld =
|
|
10564
|
+
state.chargeHeld = false;
|
|
9914
10565
|
state.operation = {
|
|
9915
10566
|
id: event.operationId,
|
|
9916
10567
|
kind: "ensure",
|
|
@@ -9923,6 +10574,13 @@ function handleResume(state, event) {
|
|
|
9923
10574
|
operationId: event.operationId,
|
|
9924
10575
|
intentRevision
|
|
9925
10576
|
}));
|
|
10577
|
+
if (retired !== -1) state.operationHistory.splice(retired, 1);
|
|
10578
|
+
state.operationHistory.push({
|
|
10579
|
+
...state.operation,
|
|
10580
|
+
key: event.operationId,
|
|
10581
|
+
profile: state.profile,
|
|
10582
|
+
consumer: cloneConsumer(state.consumers[0])
|
|
10583
|
+
});
|
|
9926
10584
|
return transition(state, "accepted");
|
|
9927
10585
|
}
|
|
9928
10586
|
function handleGenerationChange(state, kind, nextValue) {
|
|
@@ -9941,9 +10599,10 @@ function handleGenerationChange(state, kind, nextValue) {
|
|
|
9941
10599
|
}
|
|
9942
10600
|
return transition(state, "accepted");
|
|
9943
10601
|
}
|
|
9944
|
-
function handleRecover(state, event) {
|
|
10602
|
+
function handleRecover(state, event, nowMs) {
|
|
9945
10603
|
if (state.executionPolicy === "manual" || state.desired !== "running")
|
|
9946
10604
|
return unchanged(state, "blocked");
|
|
10605
|
+
if (state.phase === "stopping") return unchanged(state, "blocked");
|
|
9947
10606
|
if (possibleDispatch(state.operation)) return unchanged(state, "blocked");
|
|
9948
10607
|
const stranded = state.operation?.drained === true && ["NOT_STARTED", "NOT_LAUNCHED"].includes(state.operation.status);
|
|
9949
10608
|
const operation = stranded ? null : state.operation;
|
|
@@ -9961,37 +10620,58 @@ function handleRecover(state, event) {
|
|
|
9961
10620
|
return unchanged(state, "blocked");
|
|
9962
10621
|
}
|
|
9963
10622
|
}
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
10623
|
+
const createsIncident = state.incident === null;
|
|
10624
|
+
const replaceable = operation === null || operation.drained && (operation.status === "COMPLETED" || operation.status === "INTERRUPTED" && operation.kind === "ensure");
|
|
10625
|
+
if (!replaceable) return unchanged(state, "blocked");
|
|
10626
|
+
const consumer = state.consumers[0];
|
|
10627
|
+
if (state.profile === null || consumer === void 0) return unchanged(state, "blocked");
|
|
10628
|
+
if (state.operation?.id === event.operationId || state.operationHistory.some(
|
|
10629
|
+
(entry) => entry.id === event.operationId || entry.key === event.operationId
|
|
10630
|
+
))
|
|
10631
|
+
return unchanged(state, "conflict");
|
|
10632
|
+
const runtimeGeneration = advance(state.runtimeGeneration);
|
|
10633
|
+
if (runtimeGeneration === null) return unchanged(state, "blocked");
|
|
10634
|
+
const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
|
|
10635
|
+
const retired = rollover ? retirableEntryIndex(state) : -1;
|
|
10636
|
+
if (rollover && retired === -1)
|
|
10637
|
+
return blocked(
|
|
10638
|
+
state,
|
|
10639
|
+
`journal holds ${state.operationHistory.length} entries and every retirable entry is the current operation or the latest ensure.`
|
|
10640
|
+
);
|
|
10641
|
+
const incident = createsIncident ? {
|
|
10642
|
+
id: event.incidentId,
|
|
10643
|
+
correctiveActionsTaken: 0,
|
|
10644
|
+
actionLimit: event.actionLimit
|
|
10645
|
+
} : state.incident;
|
|
10646
|
+
const claim2 = claimRecoveryUnit({
|
|
10647
|
+
incident,
|
|
10648
|
+
limits: event.recoveryLimits,
|
|
10649
|
+
unit: event.unit ?? void 0,
|
|
10650
|
+
nowMs,
|
|
10651
|
+
activeElapsedMs: event.activeElapsedMs
|
|
10652
|
+
});
|
|
10653
|
+
if (!claim2.ok)
|
|
10654
|
+
return blocked(
|
|
10655
|
+
state,
|
|
10656
|
+
`recovery budget refused the action (${claim2.reason})${event.unit ? ` for ${event.unit.kind} unit ${event.unit.key}` : ""}.`
|
|
10657
|
+
);
|
|
10658
|
+
state.incident = claim2.incident;
|
|
10659
|
+
if (retired !== -1) state.operationHistory.splice(retired, 1);
|
|
10660
|
+
state.runtimeGeneration = runtimeGeneration;
|
|
10661
|
+
state.observations = [];
|
|
10662
|
+
state.operation = {
|
|
10663
|
+
id: event.operationId,
|
|
10664
|
+
kind: "ensure",
|
|
10665
|
+
drained: false,
|
|
10666
|
+
status: "NOT_STARTED",
|
|
10667
|
+
exitCode: null
|
|
10668
|
+
};
|
|
10669
|
+
state.operationHistory.push({
|
|
10670
|
+
...state.operation,
|
|
10671
|
+
key: event.operationId,
|
|
10672
|
+
profile: state.profile,
|
|
10673
|
+
consumer: cloneConsumer(consumer)
|
|
10674
|
+
});
|
|
9995
10675
|
state.phase = "recovering";
|
|
9996
10676
|
return transition(state, "accepted");
|
|
9997
10677
|
}
|
|
@@ -10062,7 +10742,7 @@ function applyReliabilityEvent(state, event, nowMs) {
|
|
|
10062
10742
|
case "runtime":
|
|
10063
10743
|
return handleGenerationChange(next, "runtime", event.nextGeneration);
|
|
10064
10744
|
case "recover":
|
|
10065
|
-
return handleRecover(next, event);
|
|
10745
|
+
return handleRecover(next, event, nowMs);
|
|
10066
10746
|
case "rearm":
|
|
10067
10747
|
return handleRearm(next, event);
|
|
10068
10748
|
}
|
|
@@ -10086,9 +10766,64 @@ function stepReliability(state, event, nowMs) {
|
|
|
10086
10766
|
|
|
10087
10767
|
// src/core/reliability-worker.ts
|
|
10088
10768
|
var import_node_child_process17 = require("child_process");
|
|
10089
|
-
var
|
|
10769
|
+
var import_node_crypto16 = require("crypto");
|
|
10090
10770
|
var import_node_fs28 = __toESM(require("fs"));
|
|
10091
10771
|
var import_node_path33 = __toESM(require("path"));
|
|
10772
|
+
|
|
10773
|
+
// src/core/lifecycle-progress.ts
|
|
10774
|
+
var ROLES = {
|
|
10775
|
+
validation: "lifecycle-worker",
|
|
10776
|
+
preparation: "repository-preparation",
|
|
10777
|
+
provider: "provider-operation",
|
|
10778
|
+
"service-start": "service-reconciliation",
|
|
10779
|
+
"service-stop": "service-reconciliation",
|
|
10780
|
+
"process-start": "repository-process-adapter",
|
|
10781
|
+
"process-stop": "repository-process-adapter",
|
|
10782
|
+
"route-publication": "lifecycle-worker",
|
|
10783
|
+
"route-removal": "lifecycle-worker",
|
|
10784
|
+
readiness: "lifecycle-worker",
|
|
10785
|
+
rollback: "lifecycle-worker",
|
|
10786
|
+
stop: "lifecycle-stop"
|
|
10787
|
+
};
|
|
10788
|
+
function isLifecycleProgressPhase(value) {
|
|
10789
|
+
return typeof value === "string" && Object.hasOwn(ROLES, value);
|
|
10790
|
+
}
|
|
10791
|
+
var sender;
|
|
10792
|
+
function installLifecycleProgressSender(send) {
|
|
10793
|
+
let active = true;
|
|
10794
|
+
let pending = false;
|
|
10795
|
+
let latest;
|
|
10796
|
+
const report = (phase) => {
|
|
10797
|
+
if (!active || !isLifecycleProgressPhase(phase)) return;
|
|
10798
|
+
if (pending) {
|
|
10799
|
+
latest = phase;
|
|
10800
|
+
return;
|
|
10801
|
+
}
|
|
10802
|
+
pending = true;
|
|
10803
|
+
try {
|
|
10804
|
+
send({ lifecycleProgress: phase }, () => {
|
|
10805
|
+
pending = false;
|
|
10806
|
+
const next = latest;
|
|
10807
|
+
latest = void 0;
|
|
10808
|
+
if (active && next) report(next);
|
|
10809
|
+
});
|
|
10810
|
+
} catch {
|
|
10811
|
+
pending = false;
|
|
10812
|
+
latest = void 0;
|
|
10813
|
+
}
|
|
10814
|
+
};
|
|
10815
|
+
sender = report;
|
|
10816
|
+
return () => {
|
|
10817
|
+
active = false;
|
|
10818
|
+
latest = void 0;
|
|
10819
|
+
if (sender === report) sender = void 0;
|
|
10820
|
+
};
|
|
10821
|
+
}
|
|
10822
|
+
function reportLifecycleProgress(phase) {
|
|
10823
|
+
sender?.(phase);
|
|
10824
|
+
}
|
|
10825
|
+
|
|
10826
|
+
// src/core/reliability-worker.ts
|
|
10092
10827
|
var DEFAULT_OUTPUT_PAGE_BYTES = 48 * 1024;
|
|
10093
10828
|
function workerGroupAbsent(pid) {
|
|
10094
10829
|
try {
|
|
@@ -10796,7 +11531,7 @@ async function runManagedHostPreparation(repoPath, config) {
|
|
|
10796
11531
|
|
|
10797
11532
|
// src/core/managed-post-start.ts
|
|
10798
11533
|
var import_node_child_process22 = require("child_process");
|
|
10799
|
-
var
|
|
11534
|
+
var import_node_crypto17 = require("crypto");
|
|
10800
11535
|
var import_node_fs31 = __toESM(require("fs"));
|
|
10801
11536
|
var import_node_path36 = __toESM(require("path"));
|
|
10802
11537
|
var MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
@@ -10853,7 +11588,7 @@ function readRegularFile(filePath) {
|
|
|
10853
11588
|
return readRegularFileBytes(filePath)?.toString("utf-8");
|
|
10854
11589
|
}
|
|
10855
11590
|
function adapterFingerprint(adapter) {
|
|
10856
|
-
return (0,
|
|
11591
|
+
return (0, import_node_crypto17.createHash)("sha256").update(adapter).digest("hex");
|
|
10857
11592
|
}
|
|
10858
11593
|
function resolveProcessHelperPath() {
|
|
10859
11594
|
const candidates = [
|
|
@@ -11006,6 +11741,56 @@ function runManagedProcessAction(options) {
|
|
|
11006
11741
|
}
|
|
11007
11742
|
|
|
11008
11743
|
// src/core/managed-runtime-status.ts
|
|
11744
|
+
function reliabilityRecovery(reason2, repoPath) {
|
|
11745
|
+
switch (reason2) {
|
|
11746
|
+
case "stop-incomplete":
|
|
11747
|
+
case "operation-unknown":
|
|
11748
|
+
return [`Run: devrouter stop ${repoPath}`, `Run: devrouter ensure ${repoPath} --repair`];
|
|
11749
|
+
case "unadmittable":
|
|
11750
|
+
return [
|
|
11751
|
+
`Run: devrouter stop ${repoPath}`,
|
|
11752
|
+
`Lower the requested capacity in .devrouter.yml, then run: devrouter ensure ${repoPath}`
|
|
11753
|
+
];
|
|
11754
|
+
case "capacity-parked":
|
|
11755
|
+
return [`Run: devrouter ensure ${repoPath} (waits for capacity to free up)`];
|
|
11756
|
+
case "capacity-waiting":
|
|
11757
|
+
return [`Run: devrouter ensure ${repoPath} (still queued until capacity frees up)`];
|
|
11758
|
+
default:
|
|
11759
|
+
return [];
|
|
11760
|
+
}
|
|
11761
|
+
}
|
|
11762
|
+
function readReliabilityStatus(options) {
|
|
11763
|
+
let provider;
|
|
11764
|
+
try {
|
|
11765
|
+
provider = resolveWorkspaceRuntimeForReport(options.repoPath);
|
|
11766
|
+
} catch {
|
|
11767
|
+
return void 0;
|
|
11768
|
+
}
|
|
11769
|
+
if (!provider) return void 0;
|
|
11770
|
+
let record5;
|
|
11771
|
+
try {
|
|
11772
|
+
record5 = readReliabilityOperation({
|
|
11773
|
+
repoPath: options.repoPath,
|
|
11774
|
+
workspace: options.workspace ?? null,
|
|
11775
|
+
provider
|
|
11776
|
+
});
|
|
11777
|
+
} catch {
|
|
11778
|
+
return void 0;
|
|
11779
|
+
}
|
|
11780
|
+
if (!record5) return void 0;
|
|
11781
|
+
const reason2 = reliabilityAttention(record5.state);
|
|
11782
|
+
return {
|
|
11783
|
+
desired: record5.state.desired,
|
|
11784
|
+
phase: record5.state.phase,
|
|
11785
|
+
admission: record5.state.admission,
|
|
11786
|
+
chargeHeld: record5.state.chargeHeld,
|
|
11787
|
+
incident: record5.state.incident ? {
|
|
11788
|
+
correctiveActionsTaken: record5.state.incident.correctiveActionsTaken,
|
|
11789
|
+
actionLimit: record5.state.incident.actionLimit
|
|
11790
|
+
} : null,
|
|
11791
|
+
...reason2 ? { attention: { reason: reason2, recovery: reliabilityRecovery(reason2, options.repoPath) } } : {}
|
|
11792
|
+
};
|
|
11793
|
+
}
|
|
11009
11794
|
var EMPTY_RESOURCES = {
|
|
11010
11795
|
apps: [],
|
|
11011
11796
|
services: [],
|
|
@@ -11405,10 +12190,15 @@ function collectManagedRuntimeStatus(options) {
|
|
|
11405
12190
|
services: inspection.activeServices,
|
|
11406
12191
|
processes: inspection.activeProcesses
|
|
11407
12192
|
};
|
|
12193
|
+
const reliability = readReliabilityStatus({
|
|
12194
|
+
repoPath: options.repoPath,
|
|
12195
|
+
...options.workspace !== void 0 ? { workspace: options.workspace } : {}
|
|
12196
|
+
});
|
|
11408
12197
|
return {
|
|
11409
12198
|
mode: "managed",
|
|
11410
12199
|
status,
|
|
11411
12200
|
profile: options.profile,
|
|
12201
|
+
...reliability ? { reliability } : {},
|
|
11412
12202
|
...state?.profile ? { activeProfile: state.profile } : {},
|
|
11413
12203
|
...options.workspace !== void 0 ? { workspace: options.workspace } : {},
|
|
11414
12204
|
...state?.devpodId ? { devpodId: state.devpodId } : {},
|
|
@@ -11706,10 +12496,10 @@ function inspectLegacyNetworkCapacity(input) {
|
|
|
11706
12496
|
}
|
|
11707
12497
|
|
|
11708
12498
|
// src/core/network-managed.ts
|
|
11709
|
-
var
|
|
12499
|
+
var import_node_crypto20 = require("crypto");
|
|
11710
12500
|
|
|
11711
12501
|
// src/core/network-compose.ts
|
|
11712
|
-
var
|
|
12502
|
+
var import_node_crypto18 = require("crypto");
|
|
11713
12503
|
var SAFE_ENDPOINT_UPPER_BOUND_SEMANTICS = "full-lifecycle-demand";
|
|
11714
12504
|
function isRecord5(value) {
|
|
11715
12505
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -12515,12 +13305,12 @@ function stableValue(value) {
|
|
|
12515
13305
|
}
|
|
12516
13306
|
function fingerprintComposeNetworkModel(input) {
|
|
12517
13307
|
const projection = normalizedFingerprintProjection(input);
|
|
12518
|
-
return (0,
|
|
13308
|
+
return (0, import_node_crypto18.createHash)("sha256").update(stableValue(projection), "utf8").digest("hex");
|
|
12519
13309
|
}
|
|
12520
13310
|
|
|
12521
13311
|
// src/core/network-compose-files.ts
|
|
12522
13312
|
var import_node_child_process25 = require("child_process");
|
|
12523
|
-
var
|
|
13313
|
+
var import_node_crypto19 = require("crypto");
|
|
12524
13314
|
var import_node_fs33 = __toESM(require("fs"));
|
|
12525
13315
|
var import_node_path38 = __toESM(require("path"));
|
|
12526
13316
|
var import_yaml4 = __toESM(require("yaml"));
|
|
@@ -12621,7 +13411,7 @@ ${JSON.stringify(extended.config, null, 2)}
|
|
|
12621
13411
|
...plan,
|
|
12622
13412
|
contents,
|
|
12623
13413
|
composeFiles: [...plan.composeFiles, overlayPath],
|
|
12624
|
-
effectiveConfigSha256: (0,
|
|
13414
|
+
effectiveConfigSha256: (0, import_node_crypto19.createHash)("sha256").update(contents).digest("hex")
|
|
12625
13415
|
};
|
|
12626
13416
|
return {
|
|
12627
13417
|
plan: result,
|
|
@@ -12939,7 +13729,7 @@ function createManagedNetworkSession(input) {
|
|
|
12939
13729
|
"Insufficient endpoint headroom; request an allowed larger network before first allocation."
|
|
12940
13730
|
);
|
|
12941
13731
|
const authority = readNetworkOperationAuthority(identity2);
|
|
12942
|
-
const fingerprint = (0,
|
|
13732
|
+
const fingerprint = (0, import_node_crypto20.createHash)("sha256").update(input.plan().sourceConfigSha256).update(fingerprintComposeNetworkModel(model)).digest("hex");
|
|
12943
13733
|
if (!saved) {
|
|
12944
13734
|
if (policy.status !== "valid")
|
|
12945
13735
|
throw new Error("New network allocation requires valid machine policy.");
|
|
@@ -13595,6 +14385,15 @@ function resolveRepairTarget(repoPath, linked) {
|
|
|
13595
14385
|
}
|
|
13596
14386
|
return { kind: "primary", devpodId: state.devpodId, hadExactDevpod: true };
|
|
13597
14387
|
}
|
|
14388
|
+
var RepairBaselineMismatchError = class extends Error {
|
|
14389
|
+
constructor(reasons) {
|
|
14390
|
+
super(
|
|
14391
|
+
`Repair requires the recorded profile, resource sets, and unchanged managed configuration. [MANAGED_REPAIR_BASELINE_MISMATCH: ${reasons.join(", ")}] Inspect the retained baseline before choosing recovery; missing routes do not prove configuration ownership.`
|
|
14392
|
+
);
|
|
14393
|
+
this.reasons = reasons;
|
|
14394
|
+
}
|
|
14395
|
+
code = "MANAGED_REPAIR_BASELINE_MISMATCH";
|
|
14396
|
+
};
|
|
13598
14397
|
function assertRepairBaseline(options) {
|
|
13599
14398
|
const { repoPath, target, state, plan, config, profile, processes } = options;
|
|
13600
14399
|
const desired = {
|
|
@@ -13602,14 +14401,23 @@ function assertRepairBaseline(options) {
|
|
|
13602
14401
|
services: plan.desiredProfileServices,
|
|
13603
14402
|
processes
|
|
13604
14403
|
};
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
13608
|
-
|
|
13609
|
-
|
|
13610
|
-
|
|
13611
|
-
|
|
14404
|
+
const reasons = [];
|
|
14405
|
+
if (state.profile !== profile) reasons.push("profile");
|
|
14406
|
+
if (state.sourceConfigSha256 !== plan.sourceConfigSha256) reasons.push("source-config");
|
|
14407
|
+
if (state.effectiveConfigSha256 !== plan.effectiveConfigSha256) reasons.push("effective-config");
|
|
14408
|
+
for (const resource of ["apps", "services", "processes"]) {
|
|
14409
|
+
if (JSON.stringify([...desired[resource]].sort()) !== JSON.stringify([...state.desired[resource]].sort())) {
|
|
14410
|
+
reasons.push(resource);
|
|
14411
|
+
}
|
|
13612
14412
|
}
|
|
14413
|
+
if (reasons.length > 0) throw new RepairBaselineMismatchError(reasons);
|
|
14414
|
+
let generated;
|
|
14415
|
+
try {
|
|
14416
|
+
generated = inspectManagedDevcontainerGeneratedConfig(plan).status;
|
|
14417
|
+
} catch {
|
|
14418
|
+
throw new RepairBaselineMismatchError(["generated-unavailable"]);
|
|
14419
|
+
}
|
|
14420
|
+
if (generated !== "valid") throw new RepairBaselineMismatchError([`generated-${generated}`]);
|
|
13613
14421
|
if (options.routes.some(
|
|
13614
14422
|
(route) => route.workspace !== target.workspace || !desired.apps.includes(route.name)
|
|
13615
14423
|
)) {
|
|
@@ -13766,6 +14574,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
13766
14574
|
...options.repair ? { allowRestart: false } : {}
|
|
13767
14575
|
};
|
|
13768
14576
|
try {
|
|
14577
|
+
reportLifecycleProgress("validation");
|
|
13769
14578
|
const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
13770
14579
|
let devpodId = target.devpodId;
|
|
13771
14580
|
if (target.kind === "linked") {
|
|
@@ -13826,6 +14635,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
13826
14635
|
});
|
|
13827
14636
|
managedWorkspaceEnv = target.kind === "linked" ? { token: target.workspace, gitCommonDir: target.gitCommonDir } : void 0;
|
|
13828
14637
|
if (previousManagedState && isWarmWorkspaceActive(repoPath) && previousManagedState.sourceConfigSha256 !== managedPlan.sourceConfigSha256) {
|
|
14638
|
+
if (options.repair) throw new RepairBaselineMismatchError(["source-config"]);
|
|
13829
14639
|
throw new Error(
|
|
13830
14640
|
`Managed Dev Container source configuration changed since the last successful transition; a warm ensure cannot apply it to the existing runtime for '${repoPath}'. Stop and delete the exact runtime, then run ensure again to recreate it from the current source configuration.`
|
|
13831
14641
|
);
|
|
@@ -13936,6 +14746,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
13936
14746
|
const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
|
|
13937
14747
|
const currentTarget = () => target.kind === "linked" ? target : { ...target, devpodId };
|
|
13938
14748
|
const startAndProveAttachment = async (recreate = false) => {
|
|
14749
|
+
reportLifecycleProgress("provider");
|
|
13939
14750
|
const requestedTarget = currentTarget();
|
|
13940
14751
|
try {
|
|
13941
14752
|
devpodId = await startDevpodWorkspace({
|
|
@@ -13991,6 +14802,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
13991
14802
|
if (stopped.length > 0) {
|
|
13992
14803
|
repairMutationStarted = true;
|
|
13993
14804
|
transitionPhase = "service-start";
|
|
14805
|
+
reportLifecycleProgress("service-start");
|
|
13994
14806
|
claimLifecycleEffect();
|
|
13995
14807
|
const result = (0, import_node_child_process27.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
|
|
13996
14808
|
...networkDockerOptions(),
|
|
@@ -14028,6 +14840,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14028
14840
|
managedContainer = container;
|
|
14029
14841
|
if (managedPlan) {
|
|
14030
14842
|
transitionPhase = "service-start";
|
|
14843
|
+
reportLifecycleProgress("service-start");
|
|
14031
14844
|
const initialContainers = inspectWorkspaceContainers();
|
|
14032
14845
|
managedComposeProject = resolveComposeProject(
|
|
14033
14846
|
initialContainers,
|
|
@@ -14163,6 +14976,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14163
14976
|
if (options.repair && managedPlan)
|
|
14164
14977
|
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
14165
14978
|
transitionPhase = "process-start";
|
|
14979
|
+
reportLifecycleProgress("process-start");
|
|
14166
14980
|
claimLifecycleEffect();
|
|
14167
14981
|
runManagedPostStart({
|
|
14168
14982
|
plan: managedPostStart,
|
|
@@ -14187,6 +15001,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14187
15001
|
}
|
|
14188
15002
|
continue;
|
|
14189
15003
|
}
|
|
15004
|
+
reportLifecycleProgress("process-stop");
|
|
14190
15005
|
claimLifecycleEffect();
|
|
14191
15006
|
runManagedProcessAction({
|
|
14192
15007
|
container,
|
|
@@ -14207,6 +15022,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14207
15022
|
}
|
|
14208
15023
|
}
|
|
14209
15024
|
transitionPhase = "service-stop";
|
|
15025
|
+
reportLifecycleProgress("service-stop");
|
|
14210
15026
|
if (!options.repair)
|
|
14211
15027
|
stopDroppedManagedServices(managedPlan, repoPath, managedComposeProject);
|
|
14212
15028
|
assertDroppedManagedServicesStopped(managedPlan, repoPath, managedComposeProject);
|
|
@@ -14220,6 +15036,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14220
15036
|
);
|
|
14221
15037
|
}
|
|
14222
15038
|
transitionPhase = "route-publication";
|
|
15039
|
+
reportLifecycleProgress("route-publication");
|
|
14223
15040
|
claimLifecycleEffect();
|
|
14224
15041
|
const publication = await replacePublishedProxyRoutes(
|
|
14225
15042
|
repoPath,
|
|
@@ -14237,6 +15054,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14237
15054
|
);
|
|
14238
15055
|
let applicationReadiness;
|
|
14239
15056
|
try {
|
|
15057
|
+
reportLifecycleProgress("readiness");
|
|
14240
15058
|
applicationReadiness = await waitForHttpRoutes(
|
|
14241
15059
|
repoPath,
|
|
14242
15060
|
apps,
|
|
@@ -14464,6 +15282,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14464
15282
|
const rollbackErrors = [];
|
|
14465
15283
|
const failedPhase = transitionPhase;
|
|
14466
15284
|
transitionPhase = "rollback";
|
|
15285
|
+
reportLifecycleProgress("rollback");
|
|
14467
15286
|
const previousServices = previousManagedState?.desired.services ?? firstTransitionBaseline?.services ?? [];
|
|
14468
15287
|
const previousProcesses = previousManagedState?.desired.processes ?? firstTransitionBaseline?.processes ?? [];
|
|
14469
15288
|
const previousServiceSet = new Set(previousServices);
|
|
@@ -14695,6 +15514,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14695
15514
|
repair: Boolean(options.repair)
|
|
14696
15515
|
});
|
|
14697
15516
|
if (preparationConfig.managedRuntime?.devcontainer.prepareCommand) {
|
|
15517
|
+
reportLifecycleProgress("preparation");
|
|
14698
15518
|
claimLifecycleEffect();
|
|
14699
15519
|
await runManagedHostPreparation(repoPath, preparationConfig);
|
|
14700
15520
|
claimLifecycleEffect();
|
|
@@ -15003,7 +15823,7 @@ function proveLifecycleStopped() {
|
|
|
15003
15823
|
throw new Error("Full stop proof requires an explicit stop worker.");
|
|
15004
15824
|
const settle = () => {
|
|
15005
15825
|
claimLifecycleEffect();
|
|
15006
|
-
if (initialStopAbsence && !(0,
|
|
15826
|
+
if (initialStopAbsence && !(0, import_node_util7.isDeepStrictEqual)(
|
|
15007
15827
|
proveInitialManagedDevsyAbsence(request.repoPath, initialStopAbsence.record.devpodId),
|
|
15008
15828
|
initialStopAbsence
|
|
15009
15829
|
))
|
|
@@ -15052,7 +15872,7 @@ function proveLifecycleStopped() {
|
|
|
15052
15872
|
return void 0;
|
|
15053
15873
|
});
|
|
15054
15874
|
if (settlement) {
|
|
15055
|
-
const capacity =
|
|
15875
|
+
const capacity = createLifecycleCapacityStore(import_node_path40.default.join(DEVROUTER_HOME, "controller"));
|
|
15056
15876
|
for (let attempt = 0; ; attempt++) {
|
|
15057
15877
|
try {
|
|
15058
15878
|
capacity.settleEnvironmentAfterStop(settlement, capacity.read().revision);
|
|
@@ -15213,7 +16033,7 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
15213
16033
|
}
|
|
15214
16034
|
assertRunningDevpod(devpod.id, repoPath);
|
|
15215
16035
|
const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
|
|
15216
|
-
const statusMarker = `__DEVROUTER_EXIT_${(0,
|
|
16036
|
+
const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto22.randomUUID)()}__:`;
|
|
15217
16037
|
const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
|
|
15218
16038
|
const literalCommand = command.map(quotePosixArg).join(" ");
|
|
15219
16039
|
const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
|
|
@@ -15529,6 +16349,7 @@ async function workspaceDeleteOwnedPath(worktreePath, opts = {}) {
|
|
|
15529
16349
|
|
|
15530
16350
|
// src/core/environment-stop.ts
|
|
15531
16351
|
async function environmentStop(repoPath, options = {}) {
|
|
16352
|
+
reportLifecycleProgress("stop");
|
|
15532
16353
|
const linked = isLinkedWorktree(repoPath);
|
|
15533
16354
|
const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
|
|
15534
16355
|
if (linked && !workspace) {
|
|
@@ -15579,6 +16400,7 @@ async function environmentStop(repoPath, options = {}) {
|
|
|
15579
16400
|
const removedRoutes = removeHostRoutesWhere(
|
|
15580
16401
|
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
15581
16402
|
);
|
|
16403
|
+
reportLifecycleProgress("route-removal");
|
|
15582
16404
|
await ensureTraefikRoutesRemoved(removedRoutes);
|
|
15583
16405
|
return {
|
|
15584
16406
|
kind: linked ? "linked" : "primary",
|
|
@@ -15604,6 +16426,10 @@ process.on("disconnect", cancel);
|
|
|
15604
16426
|
process.once("message", async (message) => {
|
|
15605
16427
|
started = true;
|
|
15606
16428
|
let result;
|
|
16429
|
+
const stopProgress = message.request.kind === "exec" ? void 0 : installLifecycleProgressSender((progress, done) => {
|
|
16430
|
+
if (process.connected && process.send) process.send(progress, () => done());
|
|
16431
|
+
else done();
|
|
16432
|
+
});
|
|
15607
16433
|
try {
|
|
15608
16434
|
const request = message.request;
|
|
15609
16435
|
const value = await executeLifecycleWorker(request, async () => {
|
|
@@ -15641,6 +16467,8 @@ process.once("message", async (message) => {
|
|
|
15641
16467
|
} catch {
|
|
15642
16468
|
}
|
|
15643
16469
|
result = { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
16470
|
+
} finally {
|
|
16471
|
+
stopProgress?.();
|
|
15644
16472
|
}
|
|
15645
16473
|
if (process.connected && process.send) process.send(result, () => process.disconnect());
|
|
15646
16474
|
});
|