@devrouter/cli 0.0.78 → 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 +978 -169
- package/dist/devrouter.js +3280 -998
- package/package.json +1 -1
- 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");
|
|
@@ -1860,6 +1860,21 @@ function resolveWorkspaceRuntimeDetailed(repoPath, override) {
|
|
|
1860
1860
|
function resolveWorkspaceRuntimeOrDefault(repoPath) {
|
|
1861
1861
|
return resolveWorkspaceRuntimeDetailed(repoPath).runtime;
|
|
1862
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
|
+
}
|
|
1863
1878
|
function resetWorkspaceRuntimeCaches() {
|
|
1864
1879
|
cachedRuntime = void 0;
|
|
1865
1880
|
cachedSnapshots = void 0;
|
|
@@ -2067,6 +2082,16 @@ function parseVersion(output) {
|
|
|
2067
2082
|
/\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)\b/
|
|
2068
2083
|
)?.[1];
|
|
2069
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
|
+
}
|
|
2070
2095
|
function devsyAgentRepairSuggestion(inspection) {
|
|
2071
2096
|
if (inspection.state === "stale") {
|
|
2072
2097
|
return `Install Devsy ${SUPPORTED_DEVSY_VERSION} for a supported host, then run: ${DEVSY_AGENT_SETUP_COMMAND}`;
|
|
@@ -2138,7 +2163,7 @@ function inspectDevsyAgent(options = {}) {
|
|
|
2138
2163
|
const installedVersion = parseVersion(resolved.versionOutput);
|
|
2139
2164
|
const explicitPath = resolved.env.DEVSY_AGENT_BINARY?.trim();
|
|
2140
2165
|
const source = explicitPath ? "explicit" : "managed";
|
|
2141
|
-
if (installedVersion !== SUPPORTED_DEVSY_VERSION) {
|
|
2166
|
+
if (installedVersion === void 0 || installedVersion !== SUPPORTED_DEVSY_VERSION && compareVersionCore(installedVersion, SUPPORTED_DEVSY_VERSION) <= 0) {
|
|
2142
2167
|
return {
|
|
2143
2168
|
state: "stale",
|
|
2144
2169
|
source,
|
|
@@ -2154,6 +2179,15 @@ function inspectDevsyAgent(options = {}) {
|
|
|
2154
2179
|
installedVersion
|
|
2155
2180
|
};
|
|
2156
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
|
+
}
|
|
2157
2191
|
const expectedName = resolved.nativeAssetName ?? nativeAssetName(resolved.platform, resolved.arch);
|
|
2158
2192
|
const asset = resolved.assets.find((candidate) => candidate.name === expectedName);
|
|
2159
2193
|
if (!asset) {
|
|
@@ -2175,9 +2209,7 @@ function inspectDevsyAgent(options = {}) {
|
|
|
2175
2209
|
}
|
|
2176
2210
|
function requireReadyDevsyAgent(options = {}) {
|
|
2177
2211
|
const inspection = inspectDevsyAgent(options);
|
|
2178
|
-
if (inspection.state !== "ready"
|
|
2179
|
-
throw new DevsyAgentReadinessError(inspection);
|
|
2180
|
-
}
|
|
2212
|
+
if (inspection.state !== "ready") throw new DevsyAgentReadinessError(inspection);
|
|
2181
2213
|
return {
|
|
2182
2214
|
binaryPath: inspection.binaryPath,
|
|
2183
2215
|
source: inspection.source,
|
|
@@ -3982,6 +4014,45 @@ function proveReplacementStopAbsence(state, baseline, replacement) {
|
|
|
3982
4014
|
);
|
|
3983
4015
|
assertEmptyPopulation();
|
|
3984
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
|
+
}
|
|
3985
4056
|
function proveManagedStop(state) {
|
|
3986
4057
|
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
3987
4058
|
const owner = inspectDevsyWorkspaceOwnership(
|
|
@@ -3993,6 +4064,10 @@ function proveManagedStop(state) {
|
|
|
3993
4064
|
const identityKeys = ["context", "providerId", "uid", "sourcePath", "sourceContainer"];
|
|
3994
4065
|
const current = identityOf(owner.workspace);
|
|
3995
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
|
+
}
|
|
3996
4071
|
if (changed.length === 1 && changed[0] === "uid" && !baseline.sourceContainer && !current.sourceContainer && state.workspace !== void 0 && isLinkedWorktree(state.repoPath)) {
|
|
3997
4072
|
proveReplacementStopAbsence(state, baseline, { uid: current.uid || void 0 });
|
|
3998
4073
|
return { status: "proven-absent", containers: [] };
|
|
@@ -4087,6 +4162,9 @@ function validOperation(value) {
|
|
|
4087
4162
|
"COMPLETION_UNKNOWN"
|
|
4088
4163
|
]) && (value.status === "COMPLETED" ? isReliabilityCounter(value.exitCode) : value.exitCode === null);
|
|
4089
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
|
+
}
|
|
4090
4168
|
function assertReliabilityState(value) {
|
|
4091
4169
|
const intentRevision = record2(value) ? value.intentRevision : void 0;
|
|
4092
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(
|
|
@@ -4111,7 +4189,7 @@ function assertReliabilityState(value) {
|
|
|
4111
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
|
|
4112
4190
|
) && boundedArray(value.observations) && uniqueIds(value.observations, "capability") && value.observations.every(
|
|
4113
4191
|
(observation) => record2(observation) && oneOf(observation.infrastructure, ["healthy", "failed", "unknown"]) && oneOf(observation.application, ["verified", "unready", "unverified"]) && isReliabilityCounter(observation.observedAtMs) && isReliabilityCounter(observation.validForMs)
|
|
4114
|
-
) && (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)));
|
|
4115
4193
|
if (!valid) throw new Error("Invalid reliability state.");
|
|
4116
4194
|
}
|
|
4117
4195
|
function createReliabilityState(environmentId, controllerEpoch, executionPolicy = "capacity-managed") {
|
|
@@ -4214,6 +4292,15 @@ function evaluateCapacity(budgets, samples, charges, requested, nowMs, maxSample
|
|
|
4214
4292
|
|
|
4215
4293
|
// src/core/capacity-store.ts
|
|
4216
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
|
+
};
|
|
4217
4304
|
var CapacitySnapshotChangedError = class extends Error {
|
|
4218
4305
|
constructor() {
|
|
4219
4306
|
super("Capacity snapshot changed.");
|
|
@@ -4305,18 +4392,57 @@ function serializeSnapshot(snapshot) {
|
|
|
4305
4392
|
return contents;
|
|
4306
4393
|
}
|
|
4307
4394
|
var CapacityStore = class {
|
|
4308
|
-
constructor(directory) {
|
|
4395
|
+
constructor(directory, write = writeFileAtomically, minimumRevision = () => 0) {
|
|
4309
4396
|
this.directory = directory;
|
|
4397
|
+
this.write = write;
|
|
4398
|
+
this.minimumRevision = minimumRevision;
|
|
4310
4399
|
this.file = import_node_path19.default.join(directory, "capacity-reservations.json");
|
|
4400
|
+
this.establishedFile = import_node_path19.default.join(directory, "capacity-ledger.established");
|
|
4311
4401
|
}
|
|
4312
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
|
+
}
|
|
4313
4431
|
read() {
|
|
4432
|
+
const minimumRevision = this.minimumRevision();
|
|
4433
|
+
const established = this.readEstablished();
|
|
4314
4434
|
let descriptor;
|
|
4315
4435
|
try {
|
|
4316
|
-
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
|
+
);
|
|
4317
4440
|
} catch (error) {
|
|
4318
|
-
if (error.code === "ENOENT")
|
|
4441
|
+
if (error.code === "ENOENT") {
|
|
4442
|
+
if (established || this.observedLedger || minimumRevision > 0)
|
|
4443
|
+
throw new CapacityHistoryError("capacity-ledger-lost");
|
|
4319
4444
|
return { version: 1, revision: 0, reservations: [] };
|
|
4445
|
+
}
|
|
4320
4446
|
throw error;
|
|
4321
4447
|
}
|
|
4322
4448
|
try {
|
|
@@ -4328,11 +4454,40 @@ var CapacityStore = class {
|
|
|
4328
4454
|
if (count > MAX_BYTES) throw new Error("Capacity reservation snapshot exceeds byte limit.");
|
|
4329
4455
|
const value = JSON.parse(buffer.subarray(0, count).toString("utf8"));
|
|
4330
4456
|
validate(value);
|
|
4457
|
+
if (value.revision < minimumRevision) throw new CapacityHistoryError("capacity-ledger-lost");
|
|
4458
|
+
this.observedLedger = true;
|
|
4331
4459
|
return value;
|
|
4332
4460
|
} finally {
|
|
4333
4461
|
import_node_fs15.default.closeSync(descriptor);
|
|
4334
4462
|
}
|
|
4335
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
|
+
}
|
|
4336
4491
|
/**
|
|
4337
4492
|
* Caller must revoke journal authority and prove exact physical cessation first.
|
|
4338
4493
|
* An absent reservation still advances the revision to fence that proof.
|
|
@@ -4346,7 +4501,7 @@ var CapacityStore = class {
|
|
|
4346
4501
|
`${this.file}.lock`,
|
|
4347
4502
|
{ activity: "capacity settlement", waitMs: 100 },
|
|
4348
4503
|
() => {
|
|
4349
|
-
const snapshot = this.
|
|
4504
|
+
const snapshot = this.readForMutation();
|
|
4350
4505
|
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
4351
4506
|
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
4352
4507
|
throw new Error("Capacity reservation revision exhausted.");
|
|
@@ -4356,7 +4511,7 @@ var CapacityStore = class {
|
|
|
4356
4511
|
if (index >= 0) snapshot.reservations.splice(index, 1);
|
|
4357
4512
|
snapshot.revision++;
|
|
4358
4513
|
validate(snapshot);
|
|
4359
|
-
|
|
4514
|
+
this.commit(snapshot);
|
|
4360
4515
|
return { settled: index >= 0, revision: snapshot.revision };
|
|
4361
4516
|
}
|
|
4362
4517
|
);
|
|
@@ -4371,16 +4526,19 @@ var CapacityStore = class {
|
|
|
4371
4526
|
`${this.file}.lock`,
|
|
4372
4527
|
{ activity: "capacity pool observation", waitMs: 100 },
|
|
4373
4528
|
() => {
|
|
4374
|
-
const snapshot = this.
|
|
4529
|
+
const snapshot = this.readForMutation();
|
|
4375
4530
|
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
4376
4531
|
const merged = structuredClone(snapshot.pools ?? []);
|
|
4377
|
-
if (!mergePools(merged, pools))
|
|
4532
|
+
if (!mergePools(merged, pools)) {
|
|
4533
|
+
if (!this.observedLedger) this.commit(snapshot);
|
|
4534
|
+
return { changed: false, revision: snapshot.revision };
|
|
4535
|
+
}
|
|
4378
4536
|
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
4379
4537
|
throw new Error("Capacity reservation revision exhausted.");
|
|
4380
4538
|
snapshot.pools = merged;
|
|
4381
4539
|
snapshot.revision++;
|
|
4382
4540
|
validate(snapshot);
|
|
4383
|
-
|
|
4541
|
+
this.commit(snapshot);
|
|
4384
4542
|
return { changed: true, revision: snapshot.revision };
|
|
4385
4543
|
}
|
|
4386
4544
|
);
|
|
@@ -4399,7 +4557,7 @@ var CapacityStore = class {
|
|
|
4399
4557
|
`${this.file}.lock`,
|
|
4400
4558
|
{ activity: "capacity pool settlement", waitMs: 100 },
|
|
4401
4559
|
() => {
|
|
4402
|
-
const snapshot = this.
|
|
4560
|
+
const snapshot = this.readForMutation();
|
|
4403
4561
|
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
4404
4562
|
const pools = snapshot.pools ?? [];
|
|
4405
4563
|
const index = pools.findIndex((pool2) => pool2.daemonId === identity2.daemonId);
|
|
@@ -4413,7 +4571,7 @@ var CapacityStore = class {
|
|
|
4413
4571
|
if (pool) pools.splice(index, 1);
|
|
4414
4572
|
snapshot.revision++;
|
|
4415
4573
|
validate(snapshot);
|
|
4416
|
-
|
|
4574
|
+
this.commit(snapshot);
|
|
4417
4575
|
return { settled: !!pool, revision: snapshot.revision };
|
|
4418
4576
|
}
|
|
4419
4577
|
);
|
|
@@ -4428,7 +4586,7 @@ var CapacityStore = class {
|
|
|
4428
4586
|
`${this.file}.lock`,
|
|
4429
4587
|
{ activity: "capacity phase settlement", waitMs: 100 },
|
|
4430
4588
|
() => {
|
|
4431
|
-
const snapshot = this.
|
|
4589
|
+
const snapshot = this.readForMutation();
|
|
4432
4590
|
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
4433
4591
|
const index = snapshot.reservations.findIndex(
|
|
4434
4592
|
(entry) => entry.environmentId === target.environmentId
|
|
@@ -4445,7 +4603,7 @@ var CapacityStore = class {
|
|
|
4445
4603
|
snapshot.reservations[index] = structuredClone(target);
|
|
4446
4604
|
snapshot.revision++;
|
|
4447
4605
|
validate(snapshot);
|
|
4448
|
-
|
|
4606
|
+
this.commit(snapshot);
|
|
4449
4607
|
}
|
|
4450
4608
|
);
|
|
4451
4609
|
}
|
|
@@ -4463,7 +4621,7 @@ var CapacityStore = class {
|
|
|
4463
4621
|
`${this.file}.lock`,
|
|
4464
4622
|
{ activity: "capacity reservation", waitMs: 100 },
|
|
4465
4623
|
() => {
|
|
4466
|
-
const snapshot = this.
|
|
4624
|
+
const snapshot = this.readForMutation();
|
|
4467
4625
|
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
4468
4626
|
const pools = structuredClone(snapshot.pools ?? []);
|
|
4469
4627
|
const poolChanged = mergePools(pools, requestedPool === void 0 ? [] : [requestedPool]);
|
|
@@ -4508,7 +4666,7 @@ var CapacityStore = class {
|
|
|
4508
4666
|
snapshot.reservations[snapshot.reservations.indexOf(existing)] = structuredClone(request);
|
|
4509
4667
|
else snapshot.reservations.push(structuredClone(request));
|
|
4510
4668
|
validate(snapshot);
|
|
4511
|
-
|
|
4669
|
+
this.commit(snapshot);
|
|
4512
4670
|
return { admitted: true, revision: snapshot.revision, joined: false };
|
|
4513
4671
|
}
|
|
4514
4672
|
);
|
|
@@ -4520,6 +4678,28 @@ var import_node_crypto7 = require("crypto");
|
|
|
4520
4678
|
var import_node_fs16 = __toESM(require("fs"));
|
|
4521
4679
|
var import_node_path20 = __toESM(require("path"));
|
|
4522
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
|
+
];
|
|
4523
4703
|
function fail() {
|
|
4524
4704
|
throw new Error("Invalid controller snapshot.");
|
|
4525
4705
|
}
|
|
@@ -4537,38 +4717,150 @@ function id3(value) {
|
|
|
4537
4717
|
function counter(value) {
|
|
4538
4718
|
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
4539
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
|
+
}
|
|
4540
4773
|
function validateControllerSnapshot(value) {
|
|
4541
4774
|
fields(value, [
|
|
4542
4775
|
"version",
|
|
4543
4776
|
"store",
|
|
4544
4777
|
"epoch",
|
|
4545
4778
|
"revision",
|
|
4779
|
+
"parkingRevision",
|
|
4780
|
+
"history",
|
|
4546
4781
|
"nextSequence",
|
|
4547
4782
|
"environments",
|
|
4548
4783
|
"sessions",
|
|
4549
|
-
"
|
|
4784
|
+
"retainedSessions",
|
|
4785
|
+
"events",
|
|
4786
|
+
...value?.version === 3 ? ["fingerprint"] : []
|
|
4550
4787
|
]);
|
|
4551
|
-
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)
|
|
4552
4789
|
fail();
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
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, [
|
|
4557
4805
|
"id",
|
|
4558
|
-
"
|
|
4559
|
-
"
|
|
4560
|
-
"
|
|
4561
|
-
"
|
|
4562
|
-
"
|
|
4563
|
-
"
|
|
4806
|
+
"environmentId",
|
|
4807
|
+
"generation",
|
|
4808
|
+
"requirements",
|
|
4809
|
+
"renewedAtMs",
|
|
4810
|
+
"parkingConsent",
|
|
4811
|
+
"consentRevision",
|
|
4812
|
+
...Object.hasOwn(session, "observation") ? ["observation"] : []
|
|
4564
4813
|
]);
|
|
4565
|
-
|
|
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))
|
|
4566
4816
|
fail();
|
|
4567
|
-
|
|
4568
|
-
|
|
4817
|
+
activeSessions.add(session.id);
|
|
4818
|
+
usedEnvironments.add(session.environmentId);
|
|
4819
|
+
tuples.add(tuple);
|
|
4820
|
+
checkProjection(session);
|
|
4569
4821
|
}
|
|
4570
|
-
|
|
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());
|
|
4571
4862
|
const usedEnvironments = /* @__PURE__ */ new Set();
|
|
4863
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
4572
4864
|
for (const session of value.sessions) {
|
|
4573
4865
|
fields(session, [
|
|
4574
4866
|
"id",
|
|
@@ -4578,60 +4870,66 @@ function validateControllerSnapshot(value) {
|
|
|
4578
4870
|
"renewedAtMs",
|
|
4579
4871
|
...Object.hasOwn(session, "observation") ? ["observation"] : []
|
|
4580
4872
|
]);
|
|
4581
|
-
if (!id3(session.id) || sessions.has(session.id) || !id3(session.environmentId) || !
|
|
4582
|
-
(r) => !text2(r, 132) || !/^(runtime|app:[a-z0-9][a-z0-9-]*)$/.test(r)
|
|
4583
|
-
))
|
|
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))
|
|
4584
4874
|
fail();
|
|
4585
|
-
if (session.observation !== void 0) {
|
|
4586
|
-
const observation = session.observation;
|
|
4587
|
-
fields(observation, [
|
|
4588
|
-
"status",
|
|
4589
|
-
"sampledAtMs",
|
|
4590
|
-
"validUntilMs",
|
|
4591
|
-
"journalRevision",
|
|
4592
|
-
"runtimeFingerprint"
|
|
4593
|
-
]);
|
|
4594
|
-
if (![
|
|
4595
|
-
"READY",
|
|
4596
|
-
"APP_ERROR",
|
|
4597
|
-
"BLOCKED",
|
|
4598
|
-
"STOPPED",
|
|
4599
|
-
"STARTING",
|
|
4600
|
-
"RECOVERING",
|
|
4601
|
-
"WAITING_CAPACITY",
|
|
4602
|
-
"PARKED_CAPACITY",
|
|
4603
|
-
"UNKNOWN"
|
|
4604
|
-
].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))
|
|
4605
|
-
fail();
|
|
4606
|
-
}
|
|
4607
4875
|
sessions.add(session.id);
|
|
4608
4876
|
usedEnvironments.add(session.environmentId);
|
|
4877
|
+
checkProjection(session);
|
|
4609
4878
|
}
|
|
4610
|
-
if (usedEnvironments.size !== environments.
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
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);
|
|
4619
4911
|
}
|
|
4620
|
-
|
|
4621
|
-
|
|
4912
|
+
validateControllerSnapshot(value);
|
|
4913
|
+
return value;
|
|
4622
4914
|
}
|
|
4623
4915
|
var ControllerStore = class {
|
|
4624
|
-
constructor(directory, write = writeFileAtomically) {
|
|
4916
|
+
constructor(directory, write = writeFileAtomically, ownsLifetimeLock = false) {
|
|
4625
4917
|
this.directory = directory;
|
|
4626
4918
|
this.write = write;
|
|
4919
|
+
this.ownsLifetimeLock = ownsLifetimeLock;
|
|
4627
4920
|
this.file = import_node_path20.default.join(directory, "snapshot.json");
|
|
4921
|
+
this.identityFile = import_node_path20.default.join(directory, "store-identity.json");
|
|
4628
4922
|
}
|
|
4629
4923
|
file;
|
|
4630
|
-
|
|
4924
|
+
identityFile;
|
|
4925
|
+
identity;
|
|
4926
|
+
observedStore;
|
|
4927
|
+
observedFingerprint;
|
|
4928
|
+
readBytes(file = this.file, limit = CONTROLLER_SNAPSHOT_BYTES) {
|
|
4631
4929
|
let fd;
|
|
4632
4930
|
try {
|
|
4633
4931
|
fd = import_node_fs16.default.openSync(
|
|
4634
|
-
|
|
4932
|
+
file,
|
|
4635
4933
|
import_node_fs16.default.constants.O_RDONLY | import_node_fs16.default.constants.O_NOFOLLOW | import_node_fs16.default.constants.O_NONBLOCK
|
|
4636
4934
|
);
|
|
4637
4935
|
} catch (error) {
|
|
@@ -4640,39 +4938,150 @@ var ControllerStore = class {
|
|
|
4640
4938
|
}
|
|
4641
4939
|
try {
|
|
4642
4940
|
const stat = import_node_fs16.default.fstatSync(fd);
|
|
4643
|
-
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)
|
|
4644
4942
|
fail();
|
|
4645
|
-
const bytes2 = Buffer.alloc(
|
|
4943
|
+
const bytes2 = Buffer.alloc(limit + 1);
|
|
4646
4944
|
let length = 0;
|
|
4647
4945
|
while (length < bytes2.length) {
|
|
4648
4946
|
const count = import_node_fs16.default.readSync(fd, bytes2, length, bytes2.length - length, null);
|
|
4649
4947
|
if (!count) break;
|
|
4650
4948
|
length += count;
|
|
4651
4949
|
}
|
|
4652
|
-
if (length >
|
|
4653
|
-
|
|
4654
|
-
validateControllerSnapshot(value);
|
|
4655
|
-
return value;
|
|
4950
|
+
if (length > limit) fail();
|
|
4951
|
+
return bytes2.subarray(0, length);
|
|
4656
4952
|
} catch {
|
|
4657
4953
|
throw new Error("Controller snapshot is unreadable or invalid.");
|
|
4658
4954
|
} finally {
|
|
4659
4955
|
import_node_fs16.default.closeSync(fd);
|
|
4660
4956
|
}
|
|
4661
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
|
+
}
|
|
4662
5065
|
persist(snapshot) {
|
|
4663
5066
|
validateControllerSnapshot(snapshot);
|
|
4664
|
-
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());
|
|
4665
5071
|
const contents = `${JSON.stringify(snapshot)}
|
|
4666
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) {
|
|
4667
5077
|
try {
|
|
4668
|
-
this.write(
|
|
5078
|
+
this.write(file, contents);
|
|
4669
5079
|
} catch {
|
|
4670
|
-
const
|
|
4671
|
-
if (!
|
|
4672
|
-
` !== contents)
|
|
5080
|
+
const stored = this.readBytes(file, limit);
|
|
5081
|
+
if (!stored || stored.toString("utf8") !== contents)
|
|
4673
5082
|
throw new Error("Controller snapshot commit failed.");
|
|
4674
|
-
for (const
|
|
4675
|
-
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);
|
|
4676
5085
|
try {
|
|
4677
5086
|
import_node_fs16.default.fsyncSync(fd);
|
|
4678
5087
|
} finally {
|
|
@@ -4682,27 +5091,66 @@ var ControllerStore = class {
|
|
|
4682
5091
|
}
|
|
4683
5092
|
}
|
|
4684
5093
|
startIncarnation() {
|
|
4685
|
-
|
|
4686
|
-
|
|
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))
|
|
4687
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);
|
|
4688
5116
|
const snapshot = {
|
|
4689
|
-
version:
|
|
4690
|
-
|
|
5117
|
+
version: 3,
|
|
5118
|
+
fingerprint,
|
|
5119
|
+
store: previous.store,
|
|
4691
5120
|
epoch: (previous?.epoch ?? 0) + 1,
|
|
4692
5121
|
revision: (previous?.revision ?? 0) + 1,
|
|
5122
|
+
parkingRevision: pristine ? 0 : previous.parkingRevision + 1,
|
|
5123
|
+
history: previous?.history ?? "complete",
|
|
4693
5124
|
nextSequence: 1,
|
|
4694
5125
|
environments: [],
|
|
4695
5126
|
sessions: [],
|
|
5127
|
+
retainedSessions: previous ? [...previous.retainedSessions, ...previous.sessions.map((s) => retain(s, previous))] : [],
|
|
4696
5128
|
events: []
|
|
4697
5129
|
};
|
|
4698
5130
|
this.persist(snapshot);
|
|
4699
5131
|
return snapshot;
|
|
4700
5132
|
}
|
|
4701
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
|
+
}
|
|
4702
5150
|
|
|
4703
5151
|
// src/core/reliability-operation-store.ts
|
|
4704
5152
|
function installedCliVersion() {
|
|
4705
|
-
return true ? "0.0.
|
|
5153
|
+
return true ? "0.0.79" : "0.0.0-dev";
|
|
4706
5154
|
}
|
|
4707
5155
|
function compareCliVersions(left, right) {
|
|
4708
5156
|
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -4717,6 +5165,10 @@ function compareCliVersions(left, right) {
|
|
|
4717
5165
|
var MAX_RECORD_BYTES = 1048576;
|
|
4718
5166
|
var MAX_RELIABILITY_JOURNALS = 256;
|
|
4719
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$/;
|
|
4720
5172
|
function identityKey(identity2) {
|
|
4721
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)) {
|
|
4722
5174
|
throw new Error("Invalid reliability workspace identity.");
|
|
@@ -4736,6 +5188,17 @@ function exactKeys(value, expected, message) {
|
|
|
4736
5188
|
throw new Error(message);
|
|
4737
5189
|
}
|
|
4738
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
|
+
}
|
|
4739
5202
|
function validateCapacityPhaseSettlement(record5, state, enrollment) {
|
|
4740
5203
|
const settlement = record5.phaseSettlement;
|
|
4741
5204
|
if (settlement === void 0 || settlement === null) return;
|
|
@@ -4831,6 +5294,7 @@ function validate2(record5, identity2) {
|
|
|
4831
5294
|
"outcome",
|
|
4832
5295
|
"result",
|
|
4833
5296
|
"writtenByVersion",
|
|
5297
|
+
"consumerProtection",
|
|
4834
5298
|
...record5.version === 2 ? [
|
|
4835
5299
|
"capacity",
|
|
4836
5300
|
"enrollment",
|
|
@@ -4840,6 +5304,7 @@ function validate2(record5, identity2) {
|
|
|
4840
5304
|
"startupWitness"
|
|
4841
5305
|
] : []
|
|
4842
5306
|
]);
|
|
5307
|
+
validateConsumerProtection(record5);
|
|
4843
5308
|
if (record5.writtenByVersion !== void 0 && compareCliVersions(record5.writtenByVersion, installedCliVersion()) > 0) {
|
|
4844
5309
|
throw new Error(
|
|
4845
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.`
|
|
@@ -5007,6 +5472,19 @@ function validate2(record5, identity2) {
|
|
|
5007
5472
|
}
|
|
5008
5473
|
}
|
|
5009
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
|
+
}
|
|
5010
5488
|
function assertCapacityEffect(record5, workerId, nowMs, directory = import_node_path21.default.join(DEVROUTER_HOME, "controller")) {
|
|
5011
5489
|
if (record5.version === 1) return;
|
|
5012
5490
|
const binding = record5.capacity;
|
|
@@ -5017,7 +5495,7 @@ function assertCapacityEffect(record5, workerId, nowMs, directory = import_node_
|
|
|
5017
5495
|
if (!binding.controller || !controller || controller.store !== binding.controller.store || controller.epoch !== binding.controller.epoch)
|
|
5018
5496
|
throw new Error("Capacity controller incarnation changed.");
|
|
5019
5497
|
}
|
|
5020
|
-
const snapshot =
|
|
5498
|
+
const snapshot = createLifecycleCapacityStore(directory).read();
|
|
5021
5499
|
if (binding.snapshotRevision === void 0 || snapshot.revision !== binding.snapshotRevision)
|
|
5022
5500
|
throw new Error("Capacity snapshot authority is absent or stale.");
|
|
5023
5501
|
const reservation = snapshot.reservations.find(
|
|
@@ -5058,6 +5536,79 @@ function readBoundedPrivateJson(file, label) {
|
|
|
5058
5536
|
import_node_fs17.default.closeSync(descriptor);
|
|
5059
5537
|
}
|
|
5060
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
|
+
}
|
|
5061
5612
|
function persist(record5) {
|
|
5062
5613
|
validate2(record5, record5.identity);
|
|
5063
5614
|
const contents = `${JSON.stringify(record5)}
|
|
@@ -5101,12 +5652,15 @@ function updateReliabilityOperation(identity2, operation) {
|
|
|
5101
5652
|
throw new Error("Reliability record revision is exhausted.");
|
|
5102
5653
|
const previousVersion = record5.version;
|
|
5103
5654
|
const previousEnrollment = record5.enrollment && { ...record5.enrollment };
|
|
5655
|
+
const previousProtection = structuredClone(record5.consumerProtection);
|
|
5104
5656
|
const result = operation(record5);
|
|
5105
5657
|
if (result && typeof result.then === "function") {
|
|
5106
5658
|
throw new Error("Reliability record updates must be synchronous.");
|
|
5107
5659
|
}
|
|
5108
5660
|
if (record5.version < previousVersion || previousEnrollment && !(0, import_node_util4.isDeepStrictEqual)(record5.enrollment, previousEnrollment))
|
|
5109
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.");
|
|
5110
5664
|
record5.writtenByVersion = installedCliVersion();
|
|
5111
5665
|
validate2(record5, identity2);
|
|
5112
5666
|
record5.revision += 1;
|
|
@@ -6402,7 +6956,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs18.default.rea
|
|
|
6402
6956
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
6403
6957
|
const requiredVersion = config.devrouter?.version;
|
|
6404
6958
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
6405
|
-
const cliVersion = true ? "0.0.
|
|
6959
|
+
const cliVersion = true ? "0.0.79" : "0.0.0-dev";
|
|
6406
6960
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
6407
6961
|
hasWarnedVersionMismatch = true;
|
|
6408
6962
|
process.stderr.write(
|
|
@@ -9334,7 +9888,8 @@ async function startDevsyWorkspace(options) {
|
|
|
9334
9888
|
}
|
|
9335
9889
|
if (options.recreate) args.push("--recreate");
|
|
9336
9890
|
const env = network ? networkProviderEnvironment(network.binding, process.env) : { ...process.env };
|
|
9337
|
-
env.DEVSY_AGENT_BINARY = agent.binaryPath;
|
|
9891
|
+
if (agent.binaryPath) env.DEVSY_AGENT_BINARY = agent.binaryPath;
|
|
9892
|
+
else delete env.DEVSY_AGENT_BINARY;
|
|
9338
9893
|
if (options.workspace) {
|
|
9339
9894
|
env.WORKSPACE = options.workspace.token;
|
|
9340
9895
|
env.DEVROUTER_WORKSPACE = options.workspace.token;
|
|
@@ -9358,7 +9913,7 @@ async function startDevsyWorkspace(options) {
|
|
|
9358
9913
|
let message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
|
|
9359
9914
|
if (result.error?.message) message += ` ${result.error.message}`;
|
|
9360
9915
|
if (AGENT_ACQUISITION_RE.test(result.stderrTail)) {
|
|
9361
|
-
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.";
|
|
9362
9917
|
}
|
|
9363
9918
|
if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
|
|
9364
9919
|
throw new DevsyStartPostconditionError(message);
|
|
@@ -9398,9 +9953,9 @@ var ExecutionOutcomeError = class extends Error {
|
|
|
9398
9953
|
};
|
|
9399
9954
|
|
|
9400
9955
|
// src/core/reliability-lifecycle.ts
|
|
9401
|
-
var
|
|
9956
|
+
var import_node_crypto21 = require("crypto");
|
|
9402
9957
|
var import_node_path40 = __toESM(require("path"));
|
|
9403
|
-
var
|
|
9958
|
+
var import_node_util7 = require("util");
|
|
9404
9959
|
|
|
9405
9960
|
// src/core/capacity-policy.ts
|
|
9406
9961
|
var import_node_fs26 = __toESM(require("fs"));
|
|
@@ -9421,6 +9976,76 @@ var CONTROLLER_PROFILE_RE = new RegExp(
|
|
|
9421
9976
|
`^(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)})(?:,(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)}))*$`
|
|
9422
9977
|
);
|
|
9423
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
|
+
|
|
9424
10049
|
// src/core/reliability-model.ts
|
|
9425
10050
|
var INFRASTRUCTURE_VALUES = ["healthy", "failed", "unknown"];
|
|
9426
10051
|
var APPLICATION_VALUES = ["verified", "unready", "unverified"];
|
|
@@ -9493,7 +10118,7 @@ function assertReliabilityEvent(value) {
|
|
|
9493
10118
|
valid = isReliabilityCounter(value.type === "epoch" ? value.nextEpoch : value.nextGeneration);
|
|
9494
10119
|
break;
|
|
9495
10120
|
case "recover":
|
|
9496
|
-
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));
|
|
9497
10122
|
break;
|
|
9498
10123
|
case "rearm":
|
|
9499
10124
|
valid = isReliabilityId(value.incidentId);
|
|
@@ -9513,7 +10138,10 @@ function cloneOperation(operation) {
|
|
|
9513
10138
|
return { ...operation };
|
|
9514
10139
|
}
|
|
9515
10140
|
function cloneIncident(incident) {
|
|
9516
|
-
return {
|
|
10141
|
+
return {
|
|
10142
|
+
...incident,
|
|
10143
|
+
...incident.units ? { units: incident.units.map((unit) => ({ ...unit })) } : {}
|
|
10144
|
+
};
|
|
9517
10145
|
}
|
|
9518
10146
|
function cloneState(state) {
|
|
9519
10147
|
return {
|
|
@@ -9565,6 +10193,12 @@ function possibleDispatch(operation) {
|
|
|
9565
10193
|
function pendingCommand(operation) {
|
|
9566
10194
|
return operation !== null && !["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(operation.status);
|
|
9567
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
|
+
}
|
|
9568
10202
|
function setUnknown(operation) {
|
|
9569
10203
|
if (!operation || !possibleDispatch(operation)) return operation;
|
|
9570
10204
|
return { ...operation, status: "COMPLETION_UNKNOWN", exitCode: null };
|
|
@@ -9684,10 +10318,7 @@ function handleOperationRequest(state, event) {
|
|
|
9684
10318
|
const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
|
|
9685
10319
|
let retired = -1;
|
|
9686
10320
|
if (rollover) {
|
|
9687
|
-
|
|
9688
|
-
retired = state.operationHistory.findIndex(
|
|
9689
|
-
(entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(entry.status)
|
|
9690
|
-
);
|
|
10321
|
+
retired = retirableEntryIndex(state);
|
|
9691
10322
|
if (retired === -1)
|
|
9692
10323
|
return blocked(
|
|
9693
10324
|
state,
|
|
@@ -9740,7 +10371,7 @@ function handleAdmission(state, event) {
|
|
|
9740
10371
|
return unchanged(state, "blocked");
|
|
9741
10372
|
}
|
|
9742
10373
|
if (state.admission === event.result) return unchanged(state, "joined");
|
|
9743
|
-
if (state.desired === "parked-for-capacity" && event.result === "admitted"
|
|
10374
|
+
if (state.desired === "parked-for-capacity" && event.result === "admitted")
|
|
9744
10375
|
return unchanged(state, "blocked");
|
|
9745
10376
|
state.admission = event.result;
|
|
9746
10377
|
if (event.result === "admitted") {
|
|
@@ -9775,16 +10406,7 @@ function handleDispatchPersisted(state, event) {
|
|
|
9775
10406
|
`dispatch persistence requires desired 'running' (desired='${state.desired}'); the operation outcome is unknown.`
|
|
9776
10407
|
);
|
|
9777
10408
|
}
|
|
9778
|
-
if (state.phase === "recovering" && state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
|
|
9779
|
-
return unchanged(state, "blocked");
|
|
9780
|
-
}
|
|
9781
10409
|
state.operation = { ...state.operation, status: "DISPATCH_RECORDED", exitCode: null };
|
|
9782
|
-
if (state.phase === "recovering" && state.incident !== null) {
|
|
9783
|
-
state.incident = {
|
|
9784
|
-
...state.incident,
|
|
9785
|
-
correctiveActionsTaken: state.incident.correctiveActionsTaken + 1
|
|
9786
|
-
};
|
|
9787
|
-
}
|
|
9788
10410
|
return transition(state, "accepted", [effect(state, "launch", event.operationId)]);
|
|
9789
10411
|
}
|
|
9790
10412
|
function handleLaunched(state, event) {
|
|
@@ -9917,9 +10539,18 @@ function handlePark(state) {
|
|
|
9917
10539
|
return transition(state, "accepted", [effect(state, "stop", operationId)]);
|
|
9918
10540
|
}
|
|
9919
10541
|
function handleResume(state, event) {
|
|
9920
|
-
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) {
|
|
9921
10543
|
return unchanged(state, "blocked");
|
|
9922
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
|
+
);
|
|
9923
10554
|
const intentRevision = advance(state.intentRevision);
|
|
9924
10555
|
const runtimeGeneration = advance(state.runtimeGeneration);
|
|
9925
10556
|
if (intentRevision === null || runtimeGeneration === null) return unchanged(state, "blocked");
|
|
@@ -9927,9 +10558,10 @@ function handleResume(state, event) {
|
|
|
9927
10558
|
state.runtimeGeneration = runtimeGeneration;
|
|
9928
10559
|
state.desired = "running";
|
|
9929
10560
|
state.phase = "queued";
|
|
10561
|
+
state.admission = "waiting";
|
|
9930
10562
|
state.stopProof = { workloadsStopped: false, routesRemoved: false };
|
|
9931
10563
|
state.observations = [];
|
|
9932
|
-
state.chargeHeld =
|
|
10564
|
+
state.chargeHeld = false;
|
|
9933
10565
|
state.operation = {
|
|
9934
10566
|
id: event.operationId,
|
|
9935
10567
|
kind: "ensure",
|
|
@@ -9942,6 +10574,13 @@ function handleResume(state, event) {
|
|
|
9942
10574
|
operationId: event.operationId,
|
|
9943
10575
|
intentRevision
|
|
9944
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
|
+
});
|
|
9945
10584
|
return transition(state, "accepted");
|
|
9946
10585
|
}
|
|
9947
10586
|
function handleGenerationChange(state, kind, nextValue) {
|
|
@@ -9960,9 +10599,10 @@ function handleGenerationChange(state, kind, nextValue) {
|
|
|
9960
10599
|
}
|
|
9961
10600
|
return transition(state, "accepted");
|
|
9962
10601
|
}
|
|
9963
|
-
function handleRecover(state, event) {
|
|
10602
|
+
function handleRecover(state, event, nowMs) {
|
|
9964
10603
|
if (state.executionPolicy === "manual" || state.desired !== "running")
|
|
9965
10604
|
return unchanged(state, "blocked");
|
|
10605
|
+
if (state.phase === "stopping") return unchanged(state, "blocked");
|
|
9966
10606
|
if (possibleDispatch(state.operation)) return unchanged(state, "blocked");
|
|
9967
10607
|
const stranded = state.operation?.drained === true && ["NOT_STARTED", "NOT_LAUNCHED"].includes(state.operation.status);
|
|
9968
10608
|
const operation = stranded ? null : state.operation;
|
|
@@ -9980,37 +10620,58 @@ function handleRecover(state, event) {
|
|
|
9980
10620
|
return unchanged(state, "blocked");
|
|
9981
10621
|
}
|
|
9982
10622
|
}
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
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
|
+
});
|
|
10014
10675
|
state.phase = "recovering";
|
|
10015
10676
|
return transition(state, "accepted");
|
|
10016
10677
|
}
|
|
@@ -10081,7 +10742,7 @@ function applyReliabilityEvent(state, event, nowMs) {
|
|
|
10081
10742
|
case "runtime":
|
|
10082
10743
|
return handleGenerationChange(next, "runtime", event.nextGeneration);
|
|
10083
10744
|
case "recover":
|
|
10084
|
-
return handleRecover(next, event);
|
|
10745
|
+
return handleRecover(next, event, nowMs);
|
|
10085
10746
|
case "rearm":
|
|
10086
10747
|
return handleRearm(next, event);
|
|
10087
10748
|
}
|
|
@@ -10105,9 +10766,64 @@ function stepReliability(state, event, nowMs) {
|
|
|
10105
10766
|
|
|
10106
10767
|
// src/core/reliability-worker.ts
|
|
10107
10768
|
var import_node_child_process17 = require("child_process");
|
|
10108
|
-
var
|
|
10769
|
+
var import_node_crypto16 = require("crypto");
|
|
10109
10770
|
var import_node_fs28 = __toESM(require("fs"));
|
|
10110
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
|
|
10111
10827
|
var DEFAULT_OUTPUT_PAGE_BYTES = 48 * 1024;
|
|
10112
10828
|
function workerGroupAbsent(pid) {
|
|
10113
10829
|
try {
|
|
@@ -10815,7 +11531,7 @@ async function runManagedHostPreparation(repoPath, config) {
|
|
|
10815
11531
|
|
|
10816
11532
|
// src/core/managed-post-start.ts
|
|
10817
11533
|
var import_node_child_process22 = require("child_process");
|
|
10818
|
-
var
|
|
11534
|
+
var import_node_crypto17 = require("crypto");
|
|
10819
11535
|
var import_node_fs31 = __toESM(require("fs"));
|
|
10820
11536
|
var import_node_path36 = __toESM(require("path"));
|
|
10821
11537
|
var MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
@@ -10872,7 +11588,7 @@ function readRegularFile(filePath) {
|
|
|
10872
11588
|
return readRegularFileBytes(filePath)?.toString("utf-8");
|
|
10873
11589
|
}
|
|
10874
11590
|
function adapterFingerprint(adapter) {
|
|
10875
|
-
return (0,
|
|
11591
|
+
return (0, import_node_crypto17.createHash)("sha256").update(adapter).digest("hex");
|
|
10876
11592
|
}
|
|
10877
11593
|
function resolveProcessHelperPath() {
|
|
10878
11594
|
const candidates = [
|
|
@@ -11025,6 +11741,56 @@ function runManagedProcessAction(options) {
|
|
|
11025
11741
|
}
|
|
11026
11742
|
|
|
11027
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
|
+
}
|
|
11028
11794
|
var EMPTY_RESOURCES = {
|
|
11029
11795
|
apps: [],
|
|
11030
11796
|
services: [],
|
|
@@ -11424,10 +12190,15 @@ function collectManagedRuntimeStatus(options) {
|
|
|
11424
12190
|
services: inspection.activeServices,
|
|
11425
12191
|
processes: inspection.activeProcesses
|
|
11426
12192
|
};
|
|
12193
|
+
const reliability = readReliabilityStatus({
|
|
12194
|
+
repoPath: options.repoPath,
|
|
12195
|
+
...options.workspace !== void 0 ? { workspace: options.workspace } : {}
|
|
12196
|
+
});
|
|
11427
12197
|
return {
|
|
11428
12198
|
mode: "managed",
|
|
11429
12199
|
status,
|
|
11430
12200
|
profile: options.profile,
|
|
12201
|
+
...reliability ? { reliability } : {},
|
|
11431
12202
|
...state?.profile ? { activeProfile: state.profile } : {},
|
|
11432
12203
|
...options.workspace !== void 0 ? { workspace: options.workspace } : {},
|
|
11433
12204
|
...state?.devpodId ? { devpodId: state.devpodId } : {},
|
|
@@ -11725,10 +12496,10 @@ function inspectLegacyNetworkCapacity(input) {
|
|
|
11725
12496
|
}
|
|
11726
12497
|
|
|
11727
12498
|
// src/core/network-managed.ts
|
|
11728
|
-
var
|
|
12499
|
+
var import_node_crypto20 = require("crypto");
|
|
11729
12500
|
|
|
11730
12501
|
// src/core/network-compose.ts
|
|
11731
|
-
var
|
|
12502
|
+
var import_node_crypto18 = require("crypto");
|
|
11732
12503
|
var SAFE_ENDPOINT_UPPER_BOUND_SEMANTICS = "full-lifecycle-demand";
|
|
11733
12504
|
function isRecord5(value) {
|
|
11734
12505
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -12534,12 +13305,12 @@ function stableValue(value) {
|
|
|
12534
13305
|
}
|
|
12535
13306
|
function fingerprintComposeNetworkModel(input) {
|
|
12536
13307
|
const projection = normalizedFingerprintProjection(input);
|
|
12537
|
-
return (0,
|
|
13308
|
+
return (0, import_node_crypto18.createHash)("sha256").update(stableValue(projection), "utf8").digest("hex");
|
|
12538
13309
|
}
|
|
12539
13310
|
|
|
12540
13311
|
// src/core/network-compose-files.ts
|
|
12541
13312
|
var import_node_child_process25 = require("child_process");
|
|
12542
|
-
var
|
|
13313
|
+
var import_node_crypto19 = require("crypto");
|
|
12543
13314
|
var import_node_fs33 = __toESM(require("fs"));
|
|
12544
13315
|
var import_node_path38 = __toESM(require("path"));
|
|
12545
13316
|
var import_yaml4 = __toESM(require("yaml"));
|
|
@@ -12640,7 +13411,7 @@ ${JSON.stringify(extended.config, null, 2)}
|
|
|
12640
13411
|
...plan,
|
|
12641
13412
|
contents,
|
|
12642
13413
|
composeFiles: [...plan.composeFiles, overlayPath],
|
|
12643
|
-
effectiveConfigSha256: (0,
|
|
13414
|
+
effectiveConfigSha256: (0, import_node_crypto19.createHash)("sha256").update(contents).digest("hex")
|
|
12644
13415
|
};
|
|
12645
13416
|
return {
|
|
12646
13417
|
plan: result,
|
|
@@ -12958,7 +13729,7 @@ function createManagedNetworkSession(input) {
|
|
|
12958
13729
|
"Insufficient endpoint headroom; request an allowed larger network before first allocation."
|
|
12959
13730
|
);
|
|
12960
13731
|
const authority = readNetworkOperationAuthority(identity2);
|
|
12961
|
-
const fingerprint = (0,
|
|
13732
|
+
const fingerprint = (0, import_node_crypto20.createHash)("sha256").update(input.plan().sourceConfigSha256).update(fingerprintComposeNetworkModel(model)).digest("hex");
|
|
12962
13733
|
if (!saved) {
|
|
12963
13734
|
if (policy.status !== "valid")
|
|
12964
13735
|
throw new Error("New network allocation requires valid machine policy.");
|
|
@@ -13614,6 +14385,15 @@ function resolveRepairTarget(repoPath, linked) {
|
|
|
13614
14385
|
}
|
|
13615
14386
|
return { kind: "primary", devpodId: state.devpodId, hadExactDevpod: true };
|
|
13616
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
|
+
};
|
|
13617
14397
|
function assertRepairBaseline(options) {
|
|
13618
14398
|
const { repoPath, target, state, plan, config, profile, processes } = options;
|
|
13619
14399
|
const desired = {
|
|
@@ -13621,14 +14401,23 @@ function assertRepairBaseline(options) {
|
|
|
13621
14401
|
services: plan.desiredProfileServices,
|
|
13622
14402
|
processes
|
|
13623
14403
|
};
|
|
13624
|
-
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13629
|
-
|
|
13630
|
-
|
|
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
|
+
}
|
|
13631
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}`]);
|
|
13632
14421
|
if (options.routes.some(
|
|
13633
14422
|
(route) => route.workspace !== target.workspace || !desired.apps.includes(route.name)
|
|
13634
14423
|
)) {
|
|
@@ -13785,6 +14574,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
13785
14574
|
...options.repair ? { allowRestart: false } : {}
|
|
13786
14575
|
};
|
|
13787
14576
|
try {
|
|
14577
|
+
reportLifecycleProgress("validation");
|
|
13788
14578
|
const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
13789
14579
|
let devpodId = target.devpodId;
|
|
13790
14580
|
if (target.kind === "linked") {
|
|
@@ -13845,6 +14635,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
13845
14635
|
});
|
|
13846
14636
|
managedWorkspaceEnv = target.kind === "linked" ? { token: target.workspace, gitCommonDir: target.gitCommonDir } : void 0;
|
|
13847
14637
|
if (previousManagedState && isWarmWorkspaceActive(repoPath) && previousManagedState.sourceConfigSha256 !== managedPlan.sourceConfigSha256) {
|
|
14638
|
+
if (options.repair) throw new RepairBaselineMismatchError(["source-config"]);
|
|
13848
14639
|
throw new Error(
|
|
13849
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.`
|
|
13850
14641
|
);
|
|
@@ -13955,6 +14746,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
13955
14746
|
const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
|
|
13956
14747
|
const currentTarget = () => target.kind === "linked" ? target : { ...target, devpodId };
|
|
13957
14748
|
const startAndProveAttachment = async (recreate = false) => {
|
|
14749
|
+
reportLifecycleProgress("provider");
|
|
13958
14750
|
const requestedTarget = currentTarget();
|
|
13959
14751
|
try {
|
|
13960
14752
|
devpodId = await startDevpodWorkspace({
|
|
@@ -14010,6 +14802,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14010
14802
|
if (stopped.length > 0) {
|
|
14011
14803
|
repairMutationStarted = true;
|
|
14012
14804
|
transitionPhase = "service-start";
|
|
14805
|
+
reportLifecycleProgress("service-start");
|
|
14013
14806
|
claimLifecycleEffect();
|
|
14014
14807
|
const result = (0, import_node_child_process27.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
|
|
14015
14808
|
...networkDockerOptions(),
|
|
@@ -14047,6 +14840,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14047
14840
|
managedContainer = container;
|
|
14048
14841
|
if (managedPlan) {
|
|
14049
14842
|
transitionPhase = "service-start";
|
|
14843
|
+
reportLifecycleProgress("service-start");
|
|
14050
14844
|
const initialContainers = inspectWorkspaceContainers();
|
|
14051
14845
|
managedComposeProject = resolveComposeProject(
|
|
14052
14846
|
initialContainers,
|
|
@@ -14182,6 +14976,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14182
14976
|
if (options.repair && managedPlan)
|
|
14183
14977
|
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
14184
14978
|
transitionPhase = "process-start";
|
|
14979
|
+
reportLifecycleProgress("process-start");
|
|
14185
14980
|
claimLifecycleEffect();
|
|
14186
14981
|
runManagedPostStart({
|
|
14187
14982
|
plan: managedPostStart,
|
|
@@ -14206,6 +15001,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14206
15001
|
}
|
|
14207
15002
|
continue;
|
|
14208
15003
|
}
|
|
15004
|
+
reportLifecycleProgress("process-stop");
|
|
14209
15005
|
claimLifecycleEffect();
|
|
14210
15006
|
runManagedProcessAction({
|
|
14211
15007
|
container,
|
|
@@ -14226,6 +15022,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14226
15022
|
}
|
|
14227
15023
|
}
|
|
14228
15024
|
transitionPhase = "service-stop";
|
|
15025
|
+
reportLifecycleProgress("service-stop");
|
|
14229
15026
|
if (!options.repair)
|
|
14230
15027
|
stopDroppedManagedServices(managedPlan, repoPath, managedComposeProject);
|
|
14231
15028
|
assertDroppedManagedServicesStopped(managedPlan, repoPath, managedComposeProject);
|
|
@@ -14239,6 +15036,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14239
15036
|
);
|
|
14240
15037
|
}
|
|
14241
15038
|
transitionPhase = "route-publication";
|
|
15039
|
+
reportLifecycleProgress("route-publication");
|
|
14242
15040
|
claimLifecycleEffect();
|
|
14243
15041
|
const publication = await replacePublishedProxyRoutes(
|
|
14244
15042
|
repoPath,
|
|
@@ -14256,6 +15054,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14256
15054
|
);
|
|
14257
15055
|
let applicationReadiness;
|
|
14258
15056
|
try {
|
|
15057
|
+
reportLifecycleProgress("readiness");
|
|
14259
15058
|
applicationReadiness = await waitForHttpRoutes(
|
|
14260
15059
|
repoPath,
|
|
14261
15060
|
apps,
|
|
@@ -14483,6 +15282,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14483
15282
|
const rollbackErrors = [];
|
|
14484
15283
|
const failedPhase = transitionPhase;
|
|
14485
15284
|
transitionPhase = "rollback";
|
|
15285
|
+
reportLifecycleProgress("rollback");
|
|
14486
15286
|
const previousServices = previousManagedState?.desired.services ?? firstTransitionBaseline?.services ?? [];
|
|
14487
15287
|
const previousProcesses = previousManagedState?.desired.processes ?? firstTransitionBaseline?.processes ?? [];
|
|
14488
15288
|
const previousServiceSet = new Set(previousServices);
|
|
@@ -14714,6 +15514,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
14714
15514
|
repair: Boolean(options.repair)
|
|
14715
15515
|
});
|
|
14716
15516
|
if (preparationConfig.managedRuntime?.devcontainer.prepareCommand) {
|
|
15517
|
+
reportLifecycleProgress("preparation");
|
|
14717
15518
|
claimLifecycleEffect();
|
|
14718
15519
|
await runManagedHostPreparation(repoPath, preparationConfig);
|
|
14719
15520
|
claimLifecycleEffect();
|
|
@@ -15022,7 +15823,7 @@ function proveLifecycleStopped() {
|
|
|
15022
15823
|
throw new Error("Full stop proof requires an explicit stop worker.");
|
|
15023
15824
|
const settle = () => {
|
|
15024
15825
|
claimLifecycleEffect();
|
|
15025
|
-
if (initialStopAbsence && !(0,
|
|
15826
|
+
if (initialStopAbsence && !(0, import_node_util7.isDeepStrictEqual)(
|
|
15026
15827
|
proveInitialManagedDevsyAbsence(request.repoPath, initialStopAbsence.record.devpodId),
|
|
15027
15828
|
initialStopAbsence
|
|
15028
15829
|
))
|
|
@@ -15071,7 +15872,7 @@ function proveLifecycleStopped() {
|
|
|
15071
15872
|
return void 0;
|
|
15072
15873
|
});
|
|
15073
15874
|
if (settlement) {
|
|
15074
|
-
const capacity =
|
|
15875
|
+
const capacity = createLifecycleCapacityStore(import_node_path40.default.join(DEVROUTER_HOME, "controller"));
|
|
15075
15876
|
for (let attempt = 0; ; attempt++) {
|
|
15076
15877
|
try {
|
|
15077
15878
|
capacity.settleEnvironmentAfterStop(settlement, capacity.read().revision);
|
|
@@ -15232,7 +16033,7 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
15232
16033
|
}
|
|
15233
16034
|
assertRunningDevpod(devpod.id, repoPath);
|
|
15234
16035
|
const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
|
|
15235
|
-
const statusMarker = `__DEVROUTER_EXIT_${(0,
|
|
16036
|
+
const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto22.randomUUID)()}__:`;
|
|
15236
16037
|
const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
|
|
15237
16038
|
const literalCommand = command.map(quotePosixArg).join(" ");
|
|
15238
16039
|
const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
|
|
@@ -15548,6 +16349,7 @@ async function workspaceDeleteOwnedPath(worktreePath, opts = {}) {
|
|
|
15548
16349
|
|
|
15549
16350
|
// src/core/environment-stop.ts
|
|
15550
16351
|
async function environmentStop(repoPath, options = {}) {
|
|
16352
|
+
reportLifecycleProgress("stop");
|
|
15551
16353
|
const linked = isLinkedWorktree(repoPath);
|
|
15552
16354
|
const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
|
|
15553
16355
|
if (linked && !workspace) {
|
|
@@ -15598,6 +16400,7 @@ async function environmentStop(repoPath, options = {}) {
|
|
|
15598
16400
|
const removedRoutes = removeHostRoutesWhere(
|
|
15599
16401
|
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
15600
16402
|
);
|
|
16403
|
+
reportLifecycleProgress("route-removal");
|
|
15601
16404
|
await ensureTraefikRoutesRemoved(removedRoutes);
|
|
15602
16405
|
return {
|
|
15603
16406
|
kind: linked ? "linked" : "primary",
|
|
@@ -15623,6 +16426,10 @@ process.on("disconnect", cancel);
|
|
|
15623
16426
|
process.once("message", async (message) => {
|
|
15624
16427
|
started = true;
|
|
15625
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
|
+
});
|
|
15626
16433
|
try {
|
|
15627
16434
|
const request = message.request;
|
|
15628
16435
|
const value = await executeLifecycleWorker(request, async () => {
|
|
@@ -15660,6 +16467,8 @@ process.once("message", async (message) => {
|
|
|
15660
16467
|
} catch {
|
|
15661
16468
|
}
|
|
15662
16469
|
result = { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
16470
|
+
} finally {
|
|
16471
|
+
stopProgress?.();
|
|
15663
16472
|
}
|
|
15664
16473
|
if (process.connected && process.send) process.send(result, () => process.disconnect());
|
|
15665
16474
|
});
|