@devrouter/cli 0.0.66 → 0.0.68
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 +1517 -213
- package/dist/devrouter.js +18715 -14268
- package/package.json +2 -1
- package/upgrade-prompts/0.0.67.md +41 -0
- package/upgrade-prompts/0.0.68.md +51 -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_process28 = require("child_process");
|
|
27
|
-
var
|
|
27
|
+
var import_node_crypto21 = require("crypto");
|
|
28
28
|
|
|
29
29
|
// src/core/devpod-environment.ts
|
|
30
30
|
var import_node_child_process3 = require("child_process");
|
|
@@ -141,35 +141,56 @@ function isProcessAlive(pid) {
|
|
|
141
141
|
return error.code === "EPERM";
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
-
function
|
|
144
|
+
function processBirthIdentityWithCause(pid) {
|
|
145
|
+
const causeParts = [];
|
|
145
146
|
try {
|
|
146
147
|
const stat = import_node_fs2.default.readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
147
148
|
const commandEnd = stat.lastIndexOf(")");
|
|
148
149
|
if (commandEnd >= 0) {
|
|
149
|
-
const
|
|
150
|
-
const startTime =
|
|
151
|
-
if (startTime) return `proc:${startTime}
|
|
150
|
+
const fields2 = stat.slice(commandEnd + 1).trim().split(/\s+/);
|
|
151
|
+
const startTime = fields2[19];
|
|
152
|
+
if (startTime) return { identity: `proc:${startTime}` };
|
|
153
|
+
causeParts.push(`procfs /proc/${pid}/stat had no start-time field`);
|
|
152
154
|
}
|
|
153
|
-
} catch {
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const code = error.code;
|
|
157
|
+
if (code !== "ENOENT")
|
|
158
|
+
causeParts.push(`procfs /proc/${pid}/stat read failed (${code ?? "unknown"})`);
|
|
154
159
|
}
|
|
155
160
|
const result = (0, import_node_child_process.spawnSync)("ps", ["-o", "lstart=", "-o", "command=", "-p", String(pid)], {
|
|
156
161
|
encoding: "utf-8",
|
|
157
162
|
env: { ...process.env, LC_ALL: "C" }
|
|
158
163
|
});
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
if (result.error) {
|
|
165
|
+
causeParts.push(
|
|
166
|
+
`ps spawn failed (${result.error.code ?? result.error.message})`
|
|
167
|
+
);
|
|
168
|
+
} else if (result.status !== 0) {
|
|
169
|
+
causeParts.push(
|
|
170
|
+
result.status === null && result.signal ? `ps was killed by signal ${result.signal}` : `ps exited with status ${result.status}` + (result.signal ? ` on ${result.signal}` : "")
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
const startedAt = result.stdout.trim().replace(/\s+/g, " ");
|
|
174
|
+
if (startedAt)
|
|
175
|
+
return { identity: `ps:${(0, import_node_crypto2.createHash)("sha256").update(startedAt).digest("hex")}` };
|
|
176
|
+
causeParts.push(`ps produced no start time for pid ${pid}`);
|
|
177
|
+
}
|
|
178
|
+
return { cause: causeParts.join("; ") };
|
|
179
|
+
}
|
|
180
|
+
function processBirthIdentity(pid) {
|
|
181
|
+
return processBirthIdentityWithCause(pid).identity;
|
|
161
182
|
}
|
|
162
183
|
function parseLockOwner(value) {
|
|
163
|
-
const
|
|
164
|
-
const pid = Number(
|
|
184
|
+
const fields2 = value.split(":");
|
|
185
|
+
const pid = Number(fields2[0]);
|
|
165
186
|
if (!Number.isInteger(pid) || pid <= 0) return void 0;
|
|
166
|
-
if (
|
|
187
|
+
if (fields2.length !== 3 || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(fields2[2])) {
|
|
167
188
|
return { pid };
|
|
168
189
|
}
|
|
169
190
|
try {
|
|
170
|
-
const processBirth = Buffer.from(
|
|
191
|
+
const processBirth = Buffer.from(fields2[1], "base64url").toString("utf-8");
|
|
171
192
|
const canonical = Buffer.from(processBirth).toString("base64url");
|
|
172
|
-
return canonical ===
|
|
193
|
+
return canonical === fields2[1] && /^(proc|ps):/.test(processBirth) ? { pid, processBirth } : { pid };
|
|
173
194
|
} catch {
|
|
174
195
|
return { pid };
|
|
175
196
|
}
|
|
@@ -282,14 +303,15 @@ function tryReclaimStaleLock(lockPath, staleLinkPath, livenessCache) {
|
|
|
282
303
|
}
|
|
283
304
|
}
|
|
284
305
|
function acquireFileLock(lockPath, options) {
|
|
285
|
-
const processBirth =
|
|
286
|
-
|
|
306
|
+
const processBirth = processBirthIdentityWithCause(process.pid);
|
|
307
|
+
const birthIdentity = processBirth.identity;
|
|
308
|
+
if (!birthIdentity) {
|
|
287
309
|
throw new Error(
|
|
288
|
-
`could not determine process identity for ${options.activity} lock: process inspection may be unavailable or sandbox
|
|
310
|
+
`could not determine process identity for ${options.activity} lock at ${lockPath}: ${processBirth.cause ?? "process inspection returned no result"}. process inspection may be unavailable or denied by the execution sandbox; verify with LC_ALL=C ps -o lstart= -o command= -p <any-live-pid> and, if that fails with an operation-not-permitted error, run the canonical command in a permitted host context outside the sandbox. the lock stays fail-closed; no identity fallback was attempted.`
|
|
289
311
|
);
|
|
290
312
|
}
|
|
291
313
|
const ownerId = (0, import_node_crypto2.randomUUID)();
|
|
292
|
-
const owner = `${process.pid}:${Buffer.from(
|
|
314
|
+
const owner = `${process.pid}:${Buffer.from(birthIdentity).toString("base64url")}:${ownerId}`;
|
|
293
315
|
const candidatePath = `${lockPath}.${process.pid}.${ownerId}.candidate`;
|
|
294
316
|
const staleLinkPath = `${candidatePath}.stale`;
|
|
295
317
|
const enqueuedAtMs = Date.now();
|
|
@@ -811,7 +833,7 @@ function requireSameContainerPopulation(expected, actual) {
|
|
|
811
833
|
throw new Error("Managed stop Docker inspection observed a changed population.");
|
|
812
834
|
}
|
|
813
835
|
const expectedSet = new Set(expected);
|
|
814
|
-
if (actual.some((
|
|
836
|
+
if (actual.some((id5) => !expectedSet.has(id5))) {
|
|
815
837
|
throw new Error("Managed stop Docker inspection observed a changed population.");
|
|
816
838
|
}
|
|
817
839
|
}
|
|
@@ -849,7 +871,7 @@ function inspectWorkspaceContainers(options) {
|
|
|
849
871
|
`docker ps failed: ${(listed.stderr || listed.stdout || listed.error?.message || "unknown error").trim()}`
|
|
850
872
|
);
|
|
851
873
|
}
|
|
852
|
-
ids2 = listed.stdout.split(/\r?\n/).map((
|
|
874
|
+
ids2 = listed.stdout.split(/\r?\n/).map((id5) => id5.trim()).filter(Boolean);
|
|
853
875
|
}
|
|
854
876
|
if (ids2.length === 0) return [];
|
|
855
877
|
const template = options?.withSize ? SIZE_INSPECT_TEMPLATE : SAFE_INSPECT_TEMPLATE;
|
|
@@ -1024,10 +1046,10 @@ function inspectManagedStopWorkspaceIds(endpoint, composeDirectory) {
|
|
|
1024
1046
|
assertUniqueContainerIds(ids2);
|
|
1025
1047
|
return ids2;
|
|
1026
1048
|
}
|
|
1027
|
-
function hasExactManagedStopMissingObjectError(stderr,
|
|
1049
|
+
function hasExactManagedStopMissingObjectError(stderr, id5) {
|
|
1028
1050
|
if (typeof stderr !== "string") return false;
|
|
1029
1051
|
const line = stderr.endsWith("\r\n") ? stderr.slice(0, -2) : stderr.endsWith("\n") ? stderr.slice(0, -1) : stderr;
|
|
1030
|
-
return line.toLowerCase() === `error: no such object: ${
|
|
1052
|
+
return line.toLowerCase() === `error: no such object: ${id5}`;
|
|
1031
1053
|
}
|
|
1032
1054
|
function assertManagedStopContainersAbsent(endpoint, ids2) {
|
|
1033
1055
|
assertManagedStopEndpoint(endpoint);
|
|
@@ -1039,14 +1061,14 @@ function assertManagedStopContainersAbsent(endpoint, ids2) {
|
|
|
1039
1061
|
const env = { ...process.env };
|
|
1040
1062
|
delete env.DOCKER_CONTEXT;
|
|
1041
1063
|
delete env.DOCKER_HOST;
|
|
1042
|
-
for (const
|
|
1043
|
-
const result = (0, import_node_child_process3.spawnSync)("docker", ["--host", endpoint, "inspect", "--format", "{{.Id}}",
|
|
1064
|
+
for (const id5 of ids2) {
|
|
1065
|
+
const result = (0, import_node_child_process3.spawnSync)("docker", ["--host", endpoint, "inspect", "--format", "{{.Id}}", id5], {
|
|
1044
1066
|
env,
|
|
1045
1067
|
encoding: "utf-8",
|
|
1046
1068
|
timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
|
|
1047
1069
|
maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
|
|
1048
1070
|
});
|
|
1049
|
-
if (result.status !== 1 || result.error || result.signal || result.stdout !== "" && result.stdout !== "\n" && result.stdout !== "\r\n" || !hasExactManagedStopMissingObjectError(result.stderr,
|
|
1071
|
+
if (result.status !== 1 || result.error || result.signal || result.stdout !== "" && result.stdout !== "\n" && result.stdout !== "\r\n" || !hasExactManagedStopMissingObjectError(result.stderr, id5)) {
|
|
1050
1072
|
throw new Error("Managed stop Docker inspection did not prove container absence.");
|
|
1051
1073
|
}
|
|
1052
1074
|
}
|
|
@@ -1188,6 +1210,17 @@ function inspectDevsyRuntimeStatus(devsyId) {
|
|
|
1188
1210
|
if (result.status !== 0 || result.error) return "unknown";
|
|
1189
1211
|
return parseDevsyRuntimeStatus(result.stdout, devsyId);
|
|
1190
1212
|
}
|
|
1213
|
+
function inspectDevsyRuntimeAbsence(devsyId) {
|
|
1214
|
+
const result = (0, import_node_child_process5.spawnSync)("devsy", ["workspace", "status", devsyId, "--result-format", "json"], {
|
|
1215
|
+
encoding: "utf-8",
|
|
1216
|
+
timeout: 1e4
|
|
1217
|
+
});
|
|
1218
|
+
if (!result.error && result.status === 0) {
|
|
1219
|
+
return parseDevsyRuntimeStatus(result.stdout, devsyId) === "not-found";
|
|
1220
|
+
}
|
|
1221
|
+
const output = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
1222
|
+
return /workspace not found/i.test(output);
|
|
1223
|
+
}
|
|
1191
1224
|
function inspectDevsyWorkspaceOwnership(workspaces, devsyId, worktreePath) {
|
|
1192
1225
|
const idOwners = workspaces.filter((workspace) => workspace.id === devsyId);
|
|
1193
1226
|
const pathOwners = workspaces.filter(
|
|
@@ -1987,6 +2020,7 @@ function requireReadyDevsyAgent(options = {}) {
|
|
|
1987
2020
|
// src/core/managed-devsy-stop.ts
|
|
1988
2021
|
var import_node_os4 = __toESM(require("os"));
|
|
1989
2022
|
var import_node_path20 = __toESM(require("path"));
|
|
2023
|
+
var import_node_util4 = require("util");
|
|
1990
2024
|
|
|
1991
2025
|
// src/core/devcontainer-profile.ts
|
|
1992
2026
|
var import_node_child_process10 = require("child_process");
|
|
@@ -2461,7 +2495,7 @@ function stopExactManagedService(containerId, service, options) {
|
|
|
2461
2495
|
var import_node_fs10 = __toESM(require("fs"));
|
|
2462
2496
|
var import_node_path12 = __toESM(require("path"));
|
|
2463
2497
|
function proveManagedComposePopulation(options) {
|
|
2464
|
-
const { plan, repoPath, composeProject,
|
|
2498
|
+
const { plan, repoPath, composeProject, providerRoot, featureDirectory, containers } = options;
|
|
2465
2499
|
const services = /* @__PURE__ */ new Set();
|
|
2466
2500
|
for (const container of containers) {
|
|
2467
2501
|
const service = container.labels["com.docker.compose.service"] ?? "";
|
|
@@ -2479,7 +2513,7 @@ function proveManagedComposePopulation(options) {
|
|
|
2479
2513
|
for (const file of files.slice(plan.composeFiles.length)) {
|
|
2480
2514
|
if (import_node_path12.default.dirname(file) !== featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
|
|
2481
2515
|
import_node_path12.default.basename(file)
|
|
2482
|
-
) || import_node_fs10.default.realpathSync(file) !== import_node_path12.default.join(import_node_fs10.default.realpathSync(
|
|
2516
|
+
) || import_node_fs10.default.realpathSync(file) !== import_node_path12.default.join(import_node_fs10.default.realpathSync(providerRoot), import_node_path12.default.relative(providerRoot, file))) {
|
|
2483
2517
|
throw new Error("Managed stop refuses a foreign or escaped provider Compose file.");
|
|
2484
2518
|
}
|
|
2485
2519
|
}
|
|
@@ -3208,8 +3242,8 @@ function managedRunnerId(uid, providerId) {
|
|
|
3208
3242
|
if (uid === void 0) {
|
|
3209
3243
|
return providerId;
|
|
3210
3244
|
}
|
|
3211
|
-
const
|
|
3212
|
-
return
|
|
3245
|
+
const bytes2 = Buffer.byteLength(uid);
|
|
3246
|
+
return bytes2 === 16 || bytes2 === 40 ? uid : providerId;
|
|
3213
3247
|
}
|
|
3214
3248
|
function identityOf(value) {
|
|
3215
3249
|
return {
|
|
@@ -3297,7 +3331,7 @@ function captureManagedStopBaseline(state, plan, primaryId, expectedEndpoint) {
|
|
|
3297
3331
|
repoPath: state.repoPath,
|
|
3298
3332
|
composeProject: state.composeProject,
|
|
3299
3333
|
featureDirectory,
|
|
3300
|
-
devsyRoot,
|
|
3334
|
+
providerRoot: devsyRoot,
|
|
3301
3335
|
containers
|
|
3302
3336
|
});
|
|
3303
3337
|
if (primary.id !== primaryId)
|
|
@@ -3401,7 +3435,7 @@ function proveReplacementStopAbsence(state, baseline, replacement) {
|
|
|
3401
3435
|
const newRunner = managedRunnerId(before.uid, baseline.providerId);
|
|
3402
3436
|
const runners = oldRunner === newRunner ? [oldRunner] : [oldRunner, newRunner];
|
|
3403
3437
|
const assertEmptyPopulation = () => {
|
|
3404
|
-
if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || runners.some((
|
|
3438
|
+
if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || runners.some((id5) => inspectProviderRunnerContainers(baseline.endpoint, id5).length !== 0))
|
|
3405
3439
|
throw new Error("Replacement stop observed a remaining workspace population.");
|
|
3406
3440
|
};
|
|
3407
3441
|
assertEmptyPopulation();
|
|
@@ -3484,6 +3518,7 @@ function managedStopRouteReferences(state) {
|
|
|
3484
3518
|
}
|
|
3485
3519
|
|
|
3486
3520
|
// src/core/repo-config.ts
|
|
3521
|
+
var import_node_crypto7 = require("crypto");
|
|
3487
3522
|
var import_node_fs14 = __toESM(require("fs"));
|
|
3488
3523
|
var import_node_path18 = __toESM(require("path"));
|
|
3489
3524
|
var import_yaml3 = __toESM(require("yaml"));
|
|
@@ -3846,10 +3881,10 @@ function replaceHostRoutesForRepo(repoPath, inputs) {
|
|
|
3846
3881
|
}
|
|
3847
3882
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3848
3883
|
const replacements = inputs.map((input) => {
|
|
3849
|
-
const
|
|
3884
|
+
const id5 = buildHostRouteId(input.repoPath, input.name);
|
|
3850
3885
|
return hostRouteFromInput(
|
|
3851
3886
|
input,
|
|
3852
|
-
routes.find((route) => route.id ===
|
|
3887
|
+
routes.find((route) => route.id === id5),
|
|
3853
3888
|
now
|
|
3854
3889
|
);
|
|
3855
3890
|
});
|
|
@@ -3891,6 +3926,10 @@ var VALID_HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-
|
|
|
3891
3926
|
var DEVROUTER_VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
3892
3927
|
var VALID_ENV_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/i;
|
|
3893
3928
|
var VALID_ENV_VAR_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3929
|
+
var MAX_CAPACITY_PROFILES = 256;
|
|
3930
|
+
var MAX_CAPACITY_OPERATIONS = 64;
|
|
3931
|
+
var MAX_CAPACITY_TRANSITIONS = 1024;
|
|
3932
|
+
var MAX_CAPACITY_NAME_LENGTH = 64;
|
|
3894
3933
|
var UPSTREAM_TEMPLATE_RE = /^(?:\$\{WORKSPACE\}|[a-zA-Z0-9._-])+:\d{1,5}$/;
|
|
3895
3934
|
function assertUpstreamSpec(upstream, label) {
|
|
3896
3935
|
if (upstream.includes(WORKSPACE_PLACEHOLDER)) {
|
|
@@ -4446,11 +4485,265 @@ function parseManagedRuntime(value, configPath) {
|
|
|
4446
4485
|
processes
|
|
4447
4486
|
};
|
|
4448
4487
|
}
|
|
4488
|
+
function parseCapacitySafeInteger(value, pathLabel, minimum) {
|
|
4489
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || Object.is(value, -0)) {
|
|
4490
|
+
throw new Error(`${pathLabel} must be a safe integer.`);
|
|
4491
|
+
}
|
|
4492
|
+
if (value < minimum) {
|
|
4493
|
+
throw new Error(`${pathLabel} must be at least ${minimum}.`);
|
|
4494
|
+
}
|
|
4495
|
+
return value;
|
|
4496
|
+
}
|
|
4497
|
+
function parseCapacityName(value, pathLabel) {
|
|
4498
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
4499
|
+
throw new Error(`${pathLabel} must be a non-empty string.`);
|
|
4500
|
+
}
|
|
4501
|
+
if (value !== value.trim()) {
|
|
4502
|
+
throw new Error(`${pathLabel} must not have leading or trailing whitespace.`);
|
|
4503
|
+
}
|
|
4504
|
+
if (value.length > MAX_CAPACITY_NAME_LENGTH) {
|
|
4505
|
+
throw new Error(
|
|
4506
|
+
`${pathLabel} exceeds the maximum length of ${MAX_CAPACITY_NAME_LENGTH} characters.`
|
|
4507
|
+
);
|
|
4508
|
+
}
|
|
4509
|
+
if ([...value].some(
|
|
4510
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127
|
|
4511
|
+
)) {
|
|
4512
|
+
throw new Error(`${pathLabel} must not contain control characters.`);
|
|
4513
|
+
}
|
|
4514
|
+
return value;
|
|
4515
|
+
}
|
|
4516
|
+
function parseCapacityProfileKey(value, pathLabel, declaredProfiles) {
|
|
4517
|
+
const key = parseCapacityName(value, pathLabel);
|
|
4518
|
+
if (key === "full") return key;
|
|
4519
|
+
const names = key.split(",");
|
|
4520
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4521
|
+
for (const [index, name] of names.entries()) {
|
|
4522
|
+
if (!PROFILE_NAME_RE.test(name)) {
|
|
4523
|
+
throw new Error(`${pathLabel}[${index}] is not a valid profile name.`);
|
|
4524
|
+
}
|
|
4525
|
+
if (seen.has(name)) {
|
|
4526
|
+
throw new Error(`${pathLabel} contains duplicate profile '${name}'.`);
|
|
4527
|
+
}
|
|
4528
|
+
if (!declaredProfiles || !Object.hasOwn(declaredProfiles, name)) {
|
|
4529
|
+
throw new Error(`${pathLabel} references undefined profile '${name}'.`);
|
|
4530
|
+
}
|
|
4531
|
+
seen.add(name);
|
|
4532
|
+
}
|
|
4533
|
+
return [...names].sort().join(",");
|
|
4534
|
+
}
|
|
4535
|
+
function parseCapacityDimension(value, pathLabel, runtime) {
|
|
4536
|
+
const dimension = ensureObject(value, pathLabel);
|
|
4537
|
+
ensureAllowedKeys(dimension, ["steadyBytes", "startupTotalBytes"], pathLabel);
|
|
4538
|
+
const minimum = runtime ? 1 : 0;
|
|
4539
|
+
const steadyBytes = parseCapacitySafeInteger(
|
|
4540
|
+
dimension.steadyBytes,
|
|
4541
|
+
`${pathLabel}.steadyBytes`,
|
|
4542
|
+
minimum
|
|
4543
|
+
);
|
|
4544
|
+
const startupTotalBytes = parseCapacitySafeInteger(
|
|
4545
|
+
dimension.startupTotalBytes,
|
|
4546
|
+
`${pathLabel}.startupTotalBytes`,
|
|
4547
|
+
minimum
|
|
4548
|
+
);
|
|
4549
|
+
if (startupTotalBytes < steadyBytes) {
|
|
4550
|
+
throw new Error(`${pathLabel}.startupTotalBytes must be at least steadyBytes.`);
|
|
4551
|
+
}
|
|
4552
|
+
return { steadyBytes, startupTotalBytes };
|
|
4553
|
+
}
|
|
4554
|
+
function parseCapacityOperation(value, pathLabel) {
|
|
4555
|
+
const operation = ensureObject(value, pathLabel);
|
|
4556
|
+
ensureAllowedKeys(operation, ["hostIncrementBytes", "runtimeIncrementBytes"], pathLabel);
|
|
4557
|
+
return {
|
|
4558
|
+
hostIncrementBytes: parseCapacitySafeInteger(
|
|
4559
|
+
operation.hostIncrementBytes,
|
|
4560
|
+
`${pathLabel}.hostIncrementBytes`,
|
|
4561
|
+
0
|
|
4562
|
+
),
|
|
4563
|
+
runtimeIncrementBytes: parseCapacitySafeInteger(
|
|
4564
|
+
operation.runtimeIncrementBytes,
|
|
4565
|
+
`${pathLabel}.runtimeIncrementBytes`,
|
|
4566
|
+
0
|
|
4567
|
+
)
|
|
4568
|
+
};
|
|
4569
|
+
}
|
|
4570
|
+
function parseCapacityOperations(value, pathLabel) {
|
|
4571
|
+
const rawOperations = ensureObject(value, pathLabel);
|
|
4572
|
+
const operationEntries = Object.entries(rawOperations);
|
|
4573
|
+
if (operationEntries.length === 0) {
|
|
4574
|
+
throw new Error(`${pathLabel} must define at least one named operation.`);
|
|
4575
|
+
}
|
|
4576
|
+
if (operationEntries.length > MAX_CAPACITY_OPERATIONS) {
|
|
4577
|
+
throw new Error(`${pathLabel} exceeds the maximum of ${MAX_CAPACITY_OPERATIONS} operations.`);
|
|
4578
|
+
}
|
|
4579
|
+
const operations = {};
|
|
4580
|
+
for (const [operationName, operationValue] of operationEntries) {
|
|
4581
|
+
const name = parseCapacityName(operationName, `${pathLabel} key`);
|
|
4582
|
+
if (!/^[a-z][a-z0-9._-]*$/.test(name)) {
|
|
4583
|
+
throw new Error(`${pathLabel}.${operationName} is not a valid operation name.`);
|
|
4584
|
+
}
|
|
4585
|
+
operations[name] = parseCapacityOperation(operationValue, `${pathLabel}.${operationName}`);
|
|
4586
|
+
}
|
|
4587
|
+
return operations;
|
|
4588
|
+
}
|
|
4589
|
+
function parseCapacityTransition(value, pathLabel) {
|
|
4590
|
+
const transition2 = ensureObject(value, pathLabel);
|
|
4591
|
+
ensureAllowedKeys(transition2, ["hostTotalBytes", "runtimeTotalBytes"], pathLabel);
|
|
4592
|
+
return {
|
|
4593
|
+
hostTotalBytes: parseCapacitySafeInteger(
|
|
4594
|
+
transition2.hostTotalBytes,
|
|
4595
|
+
`${pathLabel}.hostTotalBytes`,
|
|
4596
|
+
0
|
|
4597
|
+
),
|
|
4598
|
+
runtimeTotalBytes: parseCapacitySafeInteger(
|
|
4599
|
+
transition2.runtimeTotalBytes,
|
|
4600
|
+
`${pathLabel}.runtimeTotalBytes`,
|
|
4601
|
+
1
|
|
4602
|
+
)
|
|
4603
|
+
};
|
|
4604
|
+
}
|
|
4605
|
+
function parseCapacityEstimates(value, configPath, declaredProfiles) {
|
|
4606
|
+
if (value === void 0) return void 0;
|
|
4607
|
+
const pathLabel = `${configPath}.capacity`;
|
|
4608
|
+
const capacity = ensureObject(value, pathLabel);
|
|
4609
|
+
ensureAllowedKeys(capacity, ["version", "profiles", "transitions"], pathLabel);
|
|
4610
|
+
const version = parseCapacitySafeInteger(capacity.version, `${pathLabel}.version`, 1);
|
|
4611
|
+
if (version !== 1) {
|
|
4612
|
+
throw new Error(`${pathLabel}.version must be 1.`);
|
|
4613
|
+
}
|
|
4614
|
+
const rawProfiles = ensureObject(capacity.profiles, `${pathLabel}.profiles`);
|
|
4615
|
+
const profileEntries = Object.entries(rawProfiles);
|
|
4616
|
+
if (profileEntries.length === 0) {
|
|
4617
|
+
throw new Error(`${pathLabel}.profiles must define at least one exact profile combination.`);
|
|
4618
|
+
}
|
|
4619
|
+
if (profileEntries.length > MAX_CAPACITY_PROFILES) {
|
|
4620
|
+
throw new Error(
|
|
4621
|
+
`${pathLabel}.profiles exceeds the maximum of ${MAX_CAPACITY_PROFILES} combinations.`
|
|
4622
|
+
);
|
|
4623
|
+
}
|
|
4624
|
+
const profiles = {};
|
|
4625
|
+
for (const [rawKey, profileValue] of profileEntries) {
|
|
4626
|
+
const profileKey = parseCapacityProfileKey(
|
|
4627
|
+
rawKey,
|
|
4628
|
+
`${pathLabel}.profiles key`,
|
|
4629
|
+
declaredProfiles
|
|
4630
|
+
);
|
|
4631
|
+
if (Object.hasOwn(profiles, profileKey)) {
|
|
4632
|
+
throw new Error(
|
|
4633
|
+
`${pathLabel}.profiles contains aliases for the same exact combination '${profileKey}'.`
|
|
4634
|
+
);
|
|
4635
|
+
}
|
|
4636
|
+
const profile = ensureObject(profileValue, `${pathLabel}.profiles.${rawKey}`);
|
|
4637
|
+
ensureAllowedKeys(
|
|
4638
|
+
profile,
|
|
4639
|
+
["host", "runtime", "operations"],
|
|
4640
|
+
`${pathLabel}.profiles.${rawKey}`
|
|
4641
|
+
);
|
|
4642
|
+
profiles[profileKey] = {
|
|
4643
|
+
host: parseCapacityDimension(profile.host, `${pathLabel}.profiles.${rawKey}.host`, false),
|
|
4644
|
+
runtime: parseCapacityDimension(
|
|
4645
|
+
profile.runtime,
|
|
4646
|
+
`${pathLabel}.profiles.${rawKey}.runtime`,
|
|
4647
|
+
true
|
|
4648
|
+
),
|
|
4649
|
+
operations: parseCapacityOperations(
|
|
4650
|
+
profile.operations,
|
|
4651
|
+
`${pathLabel}.profiles.${rawKey}.operations`
|
|
4652
|
+
)
|
|
4653
|
+
};
|
|
4654
|
+
}
|
|
4655
|
+
let transitions;
|
|
4656
|
+
if (capacity.transitions !== void 0) {
|
|
4657
|
+
const rawTransitions = ensureObject(capacity.transitions, `${pathLabel}.transitions`);
|
|
4658
|
+
const transitionEntries = Object.entries(rawTransitions);
|
|
4659
|
+
if (transitionEntries.length === 0) {
|
|
4660
|
+
throw new Error(`${pathLabel}.transitions must define at least one transition.`);
|
|
4661
|
+
}
|
|
4662
|
+
transitions = {};
|
|
4663
|
+
let transitionCount = 0;
|
|
4664
|
+
for (const [rawSource, targetValue] of transitionEntries) {
|
|
4665
|
+
const source = parseCapacityProfileKey(
|
|
4666
|
+
rawSource,
|
|
4667
|
+
`${pathLabel}.transitions source key`,
|
|
4668
|
+
declaredProfiles
|
|
4669
|
+
);
|
|
4670
|
+
if (!Object.hasOwn(profiles, source)) {
|
|
4671
|
+
throw new Error(
|
|
4672
|
+
`${pathLabel}.transitions source '${source}' must have a profile estimate.`
|
|
4673
|
+
);
|
|
4674
|
+
}
|
|
4675
|
+
if (Object.hasOwn(transitions, source)) {
|
|
4676
|
+
throw new Error(`${pathLabel}.transitions contains aliases for source '${source}'.`);
|
|
4677
|
+
}
|
|
4678
|
+
const rawTargets = ensureObject(targetValue, `${pathLabel}.transitions.${rawSource}`);
|
|
4679
|
+
const targetEntries = Object.entries(rawTargets);
|
|
4680
|
+
if (targetEntries.length === 0) {
|
|
4681
|
+
throw new Error(`${pathLabel}.transitions.${rawSource} must define a target.`);
|
|
4682
|
+
}
|
|
4683
|
+
const targets = {};
|
|
4684
|
+
for (const [rawTarget, transitionValue] of targetEntries) {
|
|
4685
|
+
transitionCount += 1;
|
|
4686
|
+
if (transitionCount > MAX_CAPACITY_TRANSITIONS) {
|
|
4687
|
+
throw new Error(
|
|
4688
|
+
`${pathLabel}.transitions exceeds the maximum of ${MAX_CAPACITY_TRANSITIONS} entries.`
|
|
4689
|
+
);
|
|
4690
|
+
}
|
|
4691
|
+
const target = parseCapacityProfileKey(
|
|
4692
|
+
rawTarget,
|
|
4693
|
+
`${pathLabel}.transitions.${rawSource} target key`,
|
|
4694
|
+
declaredProfiles
|
|
4695
|
+
);
|
|
4696
|
+
if (!Object.hasOwn(profiles, target)) {
|
|
4697
|
+
throw new Error(
|
|
4698
|
+
`${pathLabel}.transitions target '${target}' must have a profile estimate.`
|
|
4699
|
+
);
|
|
4700
|
+
}
|
|
4701
|
+
if (Object.hasOwn(targets, target)) {
|
|
4702
|
+
throw new Error(
|
|
4703
|
+
`${pathLabel}.transitions.${rawSource} contains aliases for '${target}'.`
|
|
4704
|
+
);
|
|
4705
|
+
}
|
|
4706
|
+
const transition2 = parseCapacityTransition(
|
|
4707
|
+
transitionValue,
|
|
4708
|
+
`${pathLabel}.transitions.${rawSource}.${rawTarget}`
|
|
4709
|
+
);
|
|
4710
|
+
const sourceProfile = profiles[source];
|
|
4711
|
+
const targetProfile = profiles[target];
|
|
4712
|
+
if (transition2.hostTotalBytes < Math.max(sourceProfile.host.steadyBytes, targetProfile.host.steadyBytes)) {
|
|
4713
|
+
throw new Error(
|
|
4714
|
+
`${pathLabel}.transitions.${rawSource}.${rawTarget}.hostTotalBytes must cover both steady host estimates.`
|
|
4715
|
+
);
|
|
4716
|
+
}
|
|
4717
|
+
if (transition2.runtimeTotalBytes < Math.max(sourceProfile.runtime.steadyBytes, targetProfile.runtime.steadyBytes)) {
|
|
4718
|
+
throw new Error(
|
|
4719
|
+
`${pathLabel}.transitions.${rawSource}.${rawTarget}.runtimeTotalBytes must cover both steady runtime estimates.`
|
|
4720
|
+
);
|
|
4721
|
+
}
|
|
4722
|
+
targets[target] = transition2;
|
|
4723
|
+
}
|
|
4724
|
+
transitions[source] = targets;
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
return {
|
|
4728
|
+
version: 1,
|
|
4729
|
+
profiles,
|
|
4730
|
+
...transitions ? { transitions } : {}
|
|
4731
|
+
};
|
|
4732
|
+
}
|
|
4449
4733
|
function parseConfig(raw, configPath) {
|
|
4450
4734
|
const root = ensureObject(raw, configPath);
|
|
4451
4735
|
ensureAllowedKeys(
|
|
4452
4736
|
root,
|
|
4453
|
-
[
|
|
4737
|
+
[
|
|
4738
|
+
"version",
|
|
4739
|
+
"devrouter",
|
|
4740
|
+
"project",
|
|
4741
|
+
"secretManager",
|
|
4742
|
+
"managedRuntime",
|
|
4743
|
+
"profiles",
|
|
4744
|
+
"capacity",
|
|
4745
|
+
"apps"
|
|
4746
|
+
],
|
|
4454
4747
|
configPath
|
|
4455
4748
|
);
|
|
4456
4749
|
const version = toIntegerOrThrow(root.version, `${configPath}.version`);
|
|
@@ -4522,6 +4815,7 @@ function parseConfig(raw, configPath) {
|
|
|
4522
4815
|
}
|
|
4523
4816
|
const managedRuntime = parseManagedRuntime(root.managedRuntime, configPath);
|
|
4524
4817
|
const profiles = parseProfiles(root.profiles, configPath, apps, managedRuntime);
|
|
4818
|
+
const capacity = parseCapacityEstimates(root.capacity, configPath, profiles);
|
|
4525
4819
|
return {
|
|
4526
4820
|
version: 1,
|
|
4527
4821
|
...devrouter ? { devrouter } : {},
|
|
@@ -4529,6 +4823,7 @@ function parseConfig(raw, configPath) {
|
|
|
4529
4823
|
...secretManager ? { secretManager } : {},
|
|
4530
4824
|
...managedRuntime ? { managedRuntime } : {},
|
|
4531
4825
|
...profiles ? { profiles } : {},
|
|
4826
|
+
...capacity ? { capacity } : {},
|
|
4532
4827
|
apps
|
|
4533
4828
|
};
|
|
4534
4829
|
}
|
|
@@ -4863,7 +5158,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs14.default.rea
|
|
|
4863
5158
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
4864
5159
|
const requiredVersion = config.devrouter?.version;
|
|
4865
5160
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
4866
|
-
const cliVersion = true ? "0.0.
|
|
5161
|
+
const cliVersion = true ? "0.0.68" : "0.0.0-dev";
|
|
4867
5162
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
4868
5163
|
hasWarnedVersionMismatch = true;
|
|
4869
5164
|
process.stderr.write(
|
|
@@ -5042,7 +5337,7 @@ async function ensureNetwork(name) {
|
|
|
5042
5337
|
|
|
5043
5338
|
// src/core/tls.ts
|
|
5044
5339
|
var import_node_child_process13 = require("child_process");
|
|
5045
|
-
var
|
|
5340
|
+
var import_node_crypto8 = require("crypto");
|
|
5046
5341
|
var import_node_fs15 = __toESM(require("fs"));
|
|
5047
5342
|
var import_node_path19 = __toESM(require("path"));
|
|
5048
5343
|
var DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
|
|
@@ -5112,7 +5407,7 @@ function parseDnsHostsFromSubjectAltName(subjectAltName) {
|
|
|
5112
5407
|
return normalizeUniqueHosts(names);
|
|
5113
5408
|
}
|
|
5114
5409
|
function parseCertificateDnsHosts(pem) {
|
|
5115
|
-
const certificate = new
|
|
5410
|
+
const certificate = new import_node_crypto8.X509Certificate(pem);
|
|
5116
5411
|
const subjectAltName = certificate.subjectAltName ?? "";
|
|
5117
5412
|
if (subjectAltName.length === 0) {
|
|
5118
5413
|
return [];
|
|
@@ -5418,6 +5713,66 @@ function stopRetainedManagedDevsyWorkspace(options) {
|
|
|
5418
5713
|
}
|
|
5419
5714
|
return owner.workspace;
|
|
5420
5715
|
}
|
|
5716
|
+
function registrationStatus() {
|
|
5717
|
+
resetWorkspaceRuntimeCaches();
|
|
5718
|
+
if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy") {
|
|
5719
|
+
throw new Error("Managed stop provider selection changed.");
|
|
5720
|
+
}
|
|
5721
|
+
return inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
|
|
5722
|
+
}
|
|
5723
|
+
function proveAbsentRegistrationStop() {
|
|
5724
|
+
const observe = () => {
|
|
5725
|
+
const owner = registrationStatus();
|
|
5726
|
+
if (owner.status === "conflict") throw new Error(owner.reason);
|
|
5727
|
+
if (owner.status !== "absent") {
|
|
5728
|
+
throw new Error("Managed stop absence proof requires the registration to remain absent.");
|
|
5729
|
+
}
|
|
5730
|
+
if (listDevpodWorkspacesRaw({ allowMissingExecutable: true }).some(
|
|
5731
|
+
(entry) => entry.id === devsyId || sameWorkspacePath(entry.source.localFolder, repoPath)
|
|
5732
|
+
)) {
|
|
5733
|
+
throw new Error(
|
|
5734
|
+
"Managed stop absence proof requires both provider registrations to remain absent."
|
|
5735
|
+
);
|
|
5736
|
+
}
|
|
5737
|
+
if (!inspectDevsyRuntimeAbsence(devsyId)) {
|
|
5738
|
+
throw new Error(
|
|
5739
|
+
"Managed stop absence proof requires Devsy to report the runtime not-found."
|
|
5740
|
+
);
|
|
5741
|
+
}
|
|
5742
|
+
if (inspectManagedStopContainers(state.composeProject).length !== 0) {
|
|
5743
|
+
throw new Error("Managed stop absence proof observed a remaining compose population.");
|
|
5744
|
+
}
|
|
5745
|
+
if (inspectProviderRunnerContainers(resolveManagedStopEndpoint(), devsyId).length !== 0) {
|
|
5746
|
+
throw new Error("Managed stop absence proof observed a remaining runner population.");
|
|
5747
|
+
}
|
|
5748
|
+
const owned = inspectWorkspaceContainers().filter(
|
|
5749
|
+
(container) => sameWorkspacePath(
|
|
5750
|
+
container.labels["com.docker.compose.project.working_dir"] ?? "",
|
|
5751
|
+
import_node_path20.default.join(repoPath, ".devcontainer")
|
|
5752
|
+
)
|
|
5753
|
+
);
|
|
5754
|
+
if (owned.length !== 0) {
|
|
5755
|
+
throw new Error("Managed stop absence proof observed a remaining workspace container.");
|
|
5756
|
+
}
|
|
5757
|
+
if (workspace) {
|
|
5758
|
+
const record5 = readWorkspaceOwnership(repoPath, workspace);
|
|
5759
|
+
if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== workspaceEnv?.gitCommonDir || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present") {
|
|
5760
|
+
throw new Error("Managed stop workspace ownership changed during absence proof.");
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
if (!(0, import_node_util4.isDeepStrictEqual)(readManagedRuntimeState(repoPath, workspace), state)) {
|
|
5764
|
+
throw new Error("Managed stop retained state changed during absence proof.");
|
|
5765
|
+
}
|
|
5766
|
+
};
|
|
5767
|
+
observe();
|
|
5768
|
+
observe();
|
|
5769
|
+
}
|
|
5770
|
+
const initialOwner = registrationStatus();
|
|
5771
|
+
if (initialOwner.status === "conflict") throw new Error(initialOwner.reason);
|
|
5772
|
+
if (initialOwner.status === "absent") {
|
|
5773
|
+
proveAbsentRegistrationStop();
|
|
5774
|
+
return "proven-absent";
|
|
5775
|
+
}
|
|
5421
5776
|
const context = registration2().context;
|
|
5422
5777
|
if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
|
|
5423
5778
|
throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
|
|
@@ -5461,7 +5816,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
|
|
|
5461
5816
|
repoPath,
|
|
5462
5817
|
composeProject: state.composeProject,
|
|
5463
5818
|
featureDirectory,
|
|
5464
|
-
devsyRoot,
|
|
5819
|
+
providerRoot: devsyRoot,
|
|
5465
5820
|
containers
|
|
5466
5821
|
});
|
|
5467
5822
|
if (previous && (!sameSet(
|
|
@@ -5561,12 +5916,12 @@ function networkProviderEnvironment(binding, source) {
|
|
|
5561
5916
|
}
|
|
5562
5917
|
|
|
5563
5918
|
// src/core/network-claim-lookup.ts
|
|
5564
|
-
var
|
|
5919
|
+
var import_node_crypto10 = require("crypto");
|
|
5565
5920
|
var import_node_fs18 = __toESM(require("fs"));
|
|
5566
5921
|
var import_node_path23 = __toESM(require("path"));
|
|
5567
5922
|
|
|
5568
5923
|
// src/core/network-claims.ts
|
|
5569
|
-
var
|
|
5924
|
+
var import_node_crypto9 = require("crypto");
|
|
5570
5925
|
var import_node_fs17 = __toESM(require("fs"));
|
|
5571
5926
|
var import_node_path22 = __toESM(require("path"));
|
|
5572
5927
|
|
|
@@ -6756,7 +7111,7 @@ function assertDirectory(directory) {
|
|
|
6756
7111
|
}
|
|
6757
7112
|
}
|
|
6758
7113
|
function hashKey(value) {
|
|
6759
|
-
return (0,
|
|
7114
|
+
return (0, import_node_crypto9.createHash)("sha256").update(value).digest("hex");
|
|
6760
7115
|
}
|
|
6761
7116
|
function cloneFence(fence) {
|
|
6762
7117
|
return { ...fence };
|
|
@@ -6784,7 +7139,7 @@ function claimError(code, message) {
|
|
|
6784
7139
|
function networkOwnerKey(repoPath) {
|
|
6785
7140
|
if (!import_node_path23.default.isAbsolute(repoPath) || import_node_path23.default.resolve(repoPath) !== repoPath)
|
|
6786
7141
|
throw new Error("Invalid network owner path.");
|
|
6787
|
-
return (0,
|
|
7142
|
+
return (0, import_node_crypto10.createHash)("sha256").update(repoPath).digest("hex");
|
|
6788
7143
|
}
|
|
6789
7144
|
function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
|
|
6790
7145
|
let names;
|
|
@@ -6809,22 +7164,22 @@ function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
|
|
|
6809
7164
|
);
|
|
6810
7165
|
try {
|
|
6811
7166
|
if (!import_node_fs18.default.fstatSync(descriptor).isFile()) throw new Error();
|
|
6812
|
-
const
|
|
7167
|
+
const bytes2 = Buffer.alloc(NETWORK_CLAIMS_MAX_FILE_BYTES + 1);
|
|
6813
7168
|
let size = 0;
|
|
6814
|
-
while (size <
|
|
6815
|
-
const count = import_node_fs18.default.readSync(descriptor,
|
|
7169
|
+
while (size < bytes2.length) {
|
|
7170
|
+
const count = import_node_fs18.default.readSync(descriptor, bytes2, size, bytes2.length - size, null);
|
|
6816
7171
|
if (!count) break;
|
|
6817
7172
|
size += count;
|
|
6818
7173
|
}
|
|
6819
7174
|
if (size > NETWORK_CLAIMS_MAX_FILE_BYTES) throw new Error();
|
|
6820
|
-
value = JSON.parse(
|
|
7175
|
+
value = JSON.parse(bytes2.subarray(0, size).toString("utf8"));
|
|
6821
7176
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error();
|
|
6822
7177
|
} catch {
|
|
6823
7178
|
throw new Error("Retained network claim could not be read safely.");
|
|
6824
7179
|
} finally {
|
|
6825
7180
|
import_node_fs18.default.closeSync(descriptor);
|
|
6826
7181
|
}
|
|
6827
|
-
if (typeof value.daemonId !== "string" || `${(0,
|
|
7182
|
+
if (typeof value.daemonId !== "string" || `${(0, import_node_crypto10.createHash)("sha256").update(value.daemonId).digest("hex")}.json` !== file)
|
|
6828
7183
|
throw new Error("Retained network daemon identity is invalid.");
|
|
6829
7184
|
const claim2 = readNetworkClaim({
|
|
6830
7185
|
daemonId: value.daemonId,
|
|
@@ -6841,7 +7196,7 @@ function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
|
|
|
6841
7196
|
|
|
6842
7197
|
// src/core/network-provider-inspect.ts
|
|
6843
7198
|
var import_node_child_process14 = require("child_process");
|
|
6844
|
-
var
|
|
7199
|
+
var import_node_crypto11 = require("crypto");
|
|
6845
7200
|
function object(value) {
|
|
6846
7201
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6847
7202
|
throw new Error("Invalid provider evidence.");
|
|
@@ -6861,7 +7216,7 @@ function qualifyNetworkProviderDefinition(value, provider) {
|
|
|
6861
7216
|
if (definition.name !== "docker" || ![true, "true"].includes(agent.local) || agent.driver !== void 0 && agent.driver !== "docker" && agent.driver !== "" || docker.path !== "${DOCKER_PATH}" || docker.builder !== "${DOCKER_BUILDER}" || ![false, "false"].includes(docker.install) || (provider === "devsy" ? docker.elevation !== "${DOCKER_ELEVATION}" : docker.elevation !== void 0 && docker.elevation !== "") || env.DOCKER_HOST !== "${DOCKER_HOST}" || Object.keys(env).some((key) => key !== "DOCKER_HOST") || host.global !== true || dockerPath.default !== "docker" || actualCommand.length !== 1 || typeof actualCommand[0] !== "string" || actualCommand[0].trim() !== command || Object.keys(exec).some((key) => key !== "command") || agent.path !== void 0 && agent.path !== "" || agent.downloadURL !== void 0 && agent.downloadURL !== "") {
|
|
6862
7217
|
throw new Error("Docker provider definition is not qualified for network allocation.");
|
|
6863
7218
|
}
|
|
6864
|
-
return (0,
|
|
7219
|
+
return (0, import_node_crypto11.createHash)("sha256").update(JSON.stringify({ name: definition.name, agent, options, exec })).digest("hex");
|
|
6865
7220
|
}
|
|
6866
7221
|
function read(provider, args) {
|
|
6867
7222
|
const result = (0, import_node_child_process14.spawnSync)(provider, args, {
|
|
@@ -6923,9 +7278,9 @@ function inspectNetworkProviderBinding(input) {
|
|
|
6923
7278
|
const option = (key, localOnly = false) => {
|
|
6924
7279
|
const value = localOnly ? stored[key] : stored[key] ?? settings[key];
|
|
6925
7280
|
if (value === void 0) return null;
|
|
6926
|
-
const
|
|
6927
|
-
if (typeof
|
|
6928
|
-
return
|
|
7281
|
+
const text4 = object(value).value;
|
|
7282
|
+
if (typeof text4 !== "string") throw new Error();
|
|
7283
|
+
return text4 || null;
|
|
6929
7284
|
};
|
|
6930
7285
|
if (workspaceProvider && workspaceProvider.name !== "docker") throw new Error();
|
|
6931
7286
|
if (option("DOCKER_ELEVATION") && option("DOCKER_ELEVATION") !== "none") throw new Error();
|
|
@@ -7125,12 +7480,12 @@ function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
|
|
|
7125
7480
|
function assertDevsyTarget(devsyId, repoPath) {
|
|
7126
7481
|
const workspaces = listDevsyWorkspaces();
|
|
7127
7482
|
const existing = selectDevsyWorkspace(workspaces, repoPath);
|
|
7128
|
-
const
|
|
7129
|
-
if (
|
|
7130
|
-
const before = inspectDevsyWorkspaceOwnership(workspaces,
|
|
7483
|
+
const id5 = devsyId ?? existing?.id;
|
|
7484
|
+
if (id5) {
|
|
7485
|
+
const before = inspectDevsyWorkspaceOwnership(workspaces, id5, repoPath);
|
|
7131
7486
|
if (before.status === "conflict") throw new Error(before.reason);
|
|
7132
7487
|
}
|
|
7133
|
-
return
|
|
7488
|
+
return id5;
|
|
7134
7489
|
}
|
|
7135
7490
|
async function startDevsyWorkspace(options) {
|
|
7136
7491
|
const activity = options.recreate ? "Devsy recreate" : "Devsy start";
|
|
@@ -7242,7 +7597,572 @@ var ExecutionOutcomeError = class extends Error {
|
|
|
7242
7597
|
};
|
|
7243
7598
|
|
|
7244
7599
|
// src/core/reliability-lifecycle.ts
|
|
7245
|
-
var
|
|
7600
|
+
var import_node_crypto20 = require("crypto");
|
|
7601
|
+
var import_node_path38 = __toESM(require("path"));
|
|
7602
|
+
var import_node_util6 = require("util");
|
|
7603
|
+
|
|
7604
|
+
// src/core/capacity-accounting.ts
|
|
7605
|
+
function bytes(value) {
|
|
7606
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
7607
|
+
}
|
|
7608
|
+
function evaluateCapacity(budgets, samples, charges, requested, nowMs, maxSampleAgeMs, pools = []) {
|
|
7609
|
+
if (!bytes(nowMs) || !bytes(maxSampleAgeMs)) throw new Error("Invalid capacity sample clock.");
|
|
7610
|
+
for (const domain of Object.keys(requested.totals).sort()) {
|
|
7611
|
+
const fail2 = (reason2) => ({ admitted: false, domain, reason: reason2 });
|
|
7612
|
+
const budget = budgets[domain];
|
|
7613
|
+
const sample = samples[domain];
|
|
7614
|
+
if (!budget || !sample || !bytes(budget.capacityBytes) || !bytes(budget.protectedHeadroomBytes) || budget.protectedHeadroomBytes >= budget.capacityBytes || !Number.isSafeInteger(budget.startupSlots) || budget.startupSlots < 1 || !Number.isSafeInteger(budget.heavySlots) || budget.heavySlots < 1 || !bytes(sample.unmanagedBytes) || !bytes(sample.sharedBytes) || !bytes(sample.sampledAtMs) || Object.values(sample.ownedBytes).some((value) => !bytes(value)) || !bytes(requested.totals[domain]))
|
|
7615
|
+
return fail2("unknown");
|
|
7616
|
+
if (sample.sampledAtMs > nowMs || nowMs - sample.sampledAtMs > maxSampleAgeMs)
|
|
7617
|
+
return fail2("stale");
|
|
7618
|
+
if (sample.pressure !== "normal")
|
|
7619
|
+
return fail2(sample.pressure === "pressured" ? "pressure" : "unknown");
|
|
7620
|
+
const totals = /* @__PURE__ */ new Map();
|
|
7621
|
+
let startups = 0;
|
|
7622
|
+
let heavy = 0;
|
|
7623
|
+
for (const charge of charges) {
|
|
7624
|
+
if (!Object.hasOwn(charge.totals, domain)) continue;
|
|
7625
|
+
if (!bytes(charge.totals[domain]) || totals.has(charge.environmentId)) return fail2("unknown");
|
|
7626
|
+
totals.set(charge.environmentId, charge.totals[domain]);
|
|
7627
|
+
if (charge.environmentId !== requested.environmentId) {
|
|
7628
|
+
if (charge.startup) startups++;
|
|
7629
|
+
if (charge.heavy) heavy++;
|
|
7630
|
+
}
|
|
7631
|
+
}
|
|
7632
|
+
totals.set(
|
|
7633
|
+
requested.environmentId,
|
|
7634
|
+
Math.max(totals.get(requested.environmentId) ?? 0, requested.totals[domain])
|
|
7635
|
+
);
|
|
7636
|
+
for (const [environment, observed] of Object.entries(sample.ownedBytes))
|
|
7637
|
+
totals.set(environment, Math.max(totals.get(environment) ?? 0, observed));
|
|
7638
|
+
let used = BigInt(sample.unmanagedBytes) + BigInt(sample.sharedBytes);
|
|
7639
|
+
for (const total of totals.values()) used += BigInt(total);
|
|
7640
|
+
for (const pool of pools) {
|
|
7641
|
+
if (pool.hostDomain !== domain) continue;
|
|
7642
|
+
if (!bytes(pool.hostChargeCeilingBytes)) return fail2("unknown");
|
|
7643
|
+
used += BigInt(pool.hostChargeCeilingBytes);
|
|
7644
|
+
}
|
|
7645
|
+
if (used > BigInt(budget.capacityBytes) - BigInt(budget.protectedHeadroomBytes))
|
|
7646
|
+
return fail2("memory");
|
|
7647
|
+
const existing = charges.find((charge) => charge.environmentId === requested.environmentId);
|
|
7648
|
+
if ((requested.startup || existing?.startup) && startups >= budget.startupSlots)
|
|
7649
|
+
return fail2("startup-slot");
|
|
7650
|
+
if ((requested.heavy || existing?.heavy) && heavy >= budget.heavySlots)
|
|
7651
|
+
return fail2("heavy-slot");
|
|
7652
|
+
}
|
|
7653
|
+
if (Object.keys(requested.totals).length === 0)
|
|
7654
|
+
throw new Error("Capacity request has no domains.");
|
|
7655
|
+
return { admitted: true };
|
|
7656
|
+
}
|
|
7657
|
+
|
|
7658
|
+
// src/core/capacity-policy.ts
|
|
7659
|
+
var import_node_fs20 = __toESM(require("fs"));
|
|
7660
|
+
var import_node_path25 = __toESM(require("path"));
|
|
7661
|
+
|
|
7662
|
+
// src/core/capacity-store.ts
|
|
7663
|
+
var import_node_fs21 = __toESM(require("fs"));
|
|
7664
|
+
var import_node_path26 = __toESM(require("path"));
|
|
7665
|
+
var MAX_BYTES = 1048576;
|
|
7666
|
+
var CapacitySnapshotChangedError = class extends Error {
|
|
7667
|
+
constructor() {
|
|
7668
|
+
super("Capacity snapshot changed.");
|
|
7669
|
+
this.name = "CapacitySnapshotChangedError";
|
|
7670
|
+
}
|
|
7671
|
+
};
|
|
7672
|
+
function assertPrivateDirectory(directoryPath) {
|
|
7673
|
+
import_node_fs21.default.mkdirSync(directoryPath, { recursive: true, mode: 448 });
|
|
7674
|
+
const directory = import_node_fs21.default.lstatSync(directoryPath);
|
|
7675
|
+
if (!directory.isDirectory() || directory.isSymbolicLink() || directory.uid !== process.getuid?.() || (directory.mode & 63) !== 0)
|
|
7676
|
+
throw new Error("Capacity reservation directory is not private.");
|
|
7677
|
+
}
|
|
7678
|
+
function object2(value, fields2) {
|
|
7679
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length !== fields2.length || Object.keys(value).some((key) => !fields2.includes(key)))
|
|
7680
|
+
throw new Error("Invalid capacity reservation fields.");
|
|
7681
|
+
}
|
|
7682
|
+
function id2(value) {
|
|
7683
|
+
return typeof value === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(value);
|
|
7684
|
+
}
|
|
7685
|
+
function validatePools(value) {
|
|
7686
|
+
if (!Array.isArray(value) || value.length > 256) throw new Error("Invalid capacity pools.");
|
|
7687
|
+
const daemons = /* @__PURE__ */ new Set();
|
|
7688
|
+
const runtimes = /* @__PURE__ */ new Set();
|
|
7689
|
+
for (const pool of value) {
|
|
7690
|
+
object2(pool, ["daemonId", "runtimeDomain", "hostDomain", "hostChargeCeilingBytes"]);
|
|
7691
|
+
if (typeof pool.daemonId !== "string" || pool.daemonId.length === 0 || pool.daemonId.length > 256 || pool.daemonId !== pool.daemonId.trim() || [...pool.daemonId].some(
|
|
7692
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127
|
|
7693
|
+
) || !id2(pool.runtimeDomain) || !id2(pool.hostDomain) || pool.runtimeDomain === pool.hostDomain || !Number.isSafeInteger(pool.hostChargeCeilingBytes) || Number(pool.hostChargeCeilingBytes) <= 0 || daemons.has(pool.daemonId) || runtimes.has(pool.runtimeDomain))
|
|
7694
|
+
throw new Error("Invalid capacity pool identity or ceiling.");
|
|
7695
|
+
daemons.add(pool.daemonId);
|
|
7696
|
+
runtimes.add(pool.runtimeDomain);
|
|
7697
|
+
}
|
|
7698
|
+
}
|
|
7699
|
+
function validate(value) {
|
|
7700
|
+
const hasPools = !!value && typeof value === "object" && Object.hasOwn(value, "pools");
|
|
7701
|
+
object2(value, ["version", "revision", "reservations", ...hasPools ? ["pools"] : []]);
|
|
7702
|
+
if (hasPools) validatePools(value.pools);
|
|
7703
|
+
if (value.version !== 1 || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0 || !Array.isArray(value.reservations) || value.reservations.length > 256)
|
|
7704
|
+
throw new Error("Invalid capacity reservation snapshot.");
|
|
7705
|
+
const environments = /* @__PURE__ */ new Set();
|
|
7706
|
+
const operations = /* @__PURE__ */ new Set();
|
|
7707
|
+
const reservations = /* @__PURE__ */ new Set();
|
|
7708
|
+
for (const reservation of value.reservations) {
|
|
7709
|
+
object2(reservation, [
|
|
7710
|
+
"environmentId",
|
|
7711
|
+
"operationId",
|
|
7712
|
+
"reservationId",
|
|
7713
|
+
"policyRevision",
|
|
7714
|
+
"totals",
|
|
7715
|
+
"startup",
|
|
7716
|
+
"heavy"
|
|
7717
|
+
]);
|
|
7718
|
+
if (!id2(reservation.environmentId) || !id2(reservation.operationId) || !id2(reservation.reservationId) || !Number.isSafeInteger(reservation.policyRevision) || Number(reservation.policyRevision) < 1 || typeof reservation.startup !== "boolean" || typeof reservation.heavy !== "boolean" || environments.has(reservation.environmentId) || operations.has(reservation.operationId) || reservations.has(reservation.reservationId))
|
|
7719
|
+
throw new Error("Invalid capacity reservation identity.");
|
|
7720
|
+
environments.add(reservation.environmentId);
|
|
7721
|
+
operations.add(reservation.operationId);
|
|
7722
|
+
reservations.add(reservation.reservationId);
|
|
7723
|
+
const totals = reservation.totals;
|
|
7724
|
+
if (!totals || typeof totals !== "object" || Array.isArray(totals) || Object.keys(totals).length < 1 || Object.keys(totals).length > 256 || Object.entries(totals).some(
|
|
7725
|
+
([key, bytes2]) => !id2(key) || !Number.isSafeInteger(bytes2) || Number(bytes2) < 0
|
|
7726
|
+
))
|
|
7727
|
+
throw new Error("Invalid capacity reservation totals.");
|
|
7728
|
+
}
|
|
7729
|
+
}
|
|
7730
|
+
function mergePools(pools, incoming) {
|
|
7731
|
+
let changed = false;
|
|
7732
|
+
for (const requested of incoming) {
|
|
7733
|
+
const retained = pools.find((pool) => pool.daemonId === requested.daemonId);
|
|
7734
|
+
if (retained) {
|
|
7735
|
+
if (retained.hostDomain !== requested.hostDomain || retained.runtimeDomain !== requested.runtimeDomain)
|
|
7736
|
+
throw new Error("Capacity pool binding changed.");
|
|
7737
|
+
if (requested.hostChargeCeilingBytes > retained.hostChargeCeilingBytes) {
|
|
7738
|
+
retained.hostChargeCeilingBytes = requested.hostChargeCeilingBytes;
|
|
7739
|
+
changed = true;
|
|
7740
|
+
}
|
|
7741
|
+
} else {
|
|
7742
|
+
pools.push(structuredClone(requested));
|
|
7743
|
+
changed = true;
|
|
7744
|
+
}
|
|
7745
|
+
}
|
|
7746
|
+
validatePools(pools);
|
|
7747
|
+
return changed;
|
|
7748
|
+
}
|
|
7749
|
+
function serializeSnapshot(snapshot) {
|
|
7750
|
+
const contents = `${JSON.stringify(snapshot)}
|
|
7751
|
+
`;
|
|
7752
|
+
if (Buffer.byteLength(contents) > MAX_BYTES)
|
|
7753
|
+
throw new Error("Capacity reservation snapshot exceeds byte limit.");
|
|
7754
|
+
return contents;
|
|
7755
|
+
}
|
|
7756
|
+
var CapacityStore = class {
|
|
7757
|
+
constructor(directory) {
|
|
7758
|
+
this.directory = directory;
|
|
7759
|
+
this.file = import_node_path26.default.join(directory, "capacity-reservations.json");
|
|
7760
|
+
}
|
|
7761
|
+
file;
|
|
7762
|
+
read() {
|
|
7763
|
+
let descriptor;
|
|
7764
|
+
try {
|
|
7765
|
+
descriptor = import_node_fs21.default.openSync(this.file, import_node_fs21.default.constants.O_RDONLY | import_node_fs21.default.constants.O_NOFOLLOW);
|
|
7766
|
+
} catch (error) {
|
|
7767
|
+
if (error.code === "ENOENT")
|
|
7768
|
+
return { version: 1, revision: 0, reservations: [] };
|
|
7769
|
+
throw error;
|
|
7770
|
+
}
|
|
7771
|
+
try {
|
|
7772
|
+
const stat = import_node_fs21.default.fstatSync(descriptor);
|
|
7773
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0 || stat.size > MAX_BYTES)
|
|
7774
|
+
throw new Error("Unsafe capacity reservation snapshot.");
|
|
7775
|
+
const buffer = Buffer.alloc(MAX_BYTES + 1);
|
|
7776
|
+
const count = import_node_fs21.default.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
7777
|
+
if (count > MAX_BYTES) throw new Error("Capacity reservation snapshot exceeds byte limit.");
|
|
7778
|
+
const value = JSON.parse(buffer.subarray(0, count).toString("utf8"));
|
|
7779
|
+
validate(value);
|
|
7780
|
+
return value;
|
|
7781
|
+
} finally {
|
|
7782
|
+
import_node_fs21.default.closeSync(descriptor);
|
|
7783
|
+
}
|
|
7784
|
+
}
|
|
7785
|
+
/**
|
|
7786
|
+
* Caller must revoke journal authority and prove exact physical cessation first.
|
|
7787
|
+
* An absent reservation still advances the revision to fence that proof.
|
|
7788
|
+
*/
|
|
7789
|
+
settleEnvironmentAfterStop(environmentId, expectedRevision) {
|
|
7790
|
+
if (!id2(environmentId)) throw new Error("Invalid capacity environment ID.");
|
|
7791
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
|
|
7792
|
+
throw new Error("Invalid capacity settlement revision.");
|
|
7793
|
+
assertPrivateDirectory(this.directory);
|
|
7794
|
+
return withFileLockSync(
|
|
7795
|
+
`${this.file}.lock`,
|
|
7796
|
+
{ activity: "capacity settlement", waitMs: 100 },
|
|
7797
|
+
() => {
|
|
7798
|
+
const snapshot = this.read();
|
|
7799
|
+
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
7800
|
+
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
7801
|
+
throw new Error("Capacity reservation revision exhausted.");
|
|
7802
|
+
const index = snapshot.reservations.findIndex(
|
|
7803
|
+
(entry) => entry.environmentId === environmentId
|
|
7804
|
+
);
|
|
7805
|
+
if (index >= 0) snapshot.reservations.splice(index, 1);
|
|
7806
|
+
snapshot.revision++;
|
|
7807
|
+
validate(snapshot);
|
|
7808
|
+
writeFileAtomically(this.file, serializeSnapshot(snapshot));
|
|
7809
|
+
return { settled: index >= 0, revision: snapshot.revision };
|
|
7810
|
+
}
|
|
7811
|
+
);
|
|
7812
|
+
}
|
|
7813
|
+
/** Retain positively observed pools even when their charge already exceeds admission budgets. */
|
|
7814
|
+
mergeObservedPools(pools, expectedRevision) {
|
|
7815
|
+
validatePools(pools);
|
|
7816
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
|
|
7817
|
+
throw new Error("Invalid capacity expected revision.");
|
|
7818
|
+
assertPrivateDirectory(this.directory);
|
|
7819
|
+
return withFileLockSync(
|
|
7820
|
+
`${this.file}.lock`,
|
|
7821
|
+
{ activity: "capacity pool observation", waitMs: 100 },
|
|
7822
|
+
() => {
|
|
7823
|
+
const snapshot = this.read();
|
|
7824
|
+
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
7825
|
+
const merged = structuredClone(snapshot.pools ?? []);
|
|
7826
|
+
if (!mergePools(merged, pools)) return { changed: false, revision: snapshot.revision };
|
|
7827
|
+
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
7828
|
+
throw new Error("Capacity reservation revision exhausted.");
|
|
7829
|
+
snapshot.pools = merged;
|
|
7830
|
+
snapshot.revision++;
|
|
7831
|
+
validate(snapshot);
|
|
7832
|
+
writeFileAtomically(this.file, serializeSnapshot(snapshot));
|
|
7833
|
+
return { changed: true, revision: snapshot.revision };
|
|
7834
|
+
}
|
|
7835
|
+
);
|
|
7836
|
+
}
|
|
7837
|
+
/**
|
|
7838
|
+
* Caller must revoke pool launch authority and prove exact VM cessation first.
|
|
7839
|
+
* Container or environment cessation alone never permits releasing this ceiling.
|
|
7840
|
+
*/
|
|
7841
|
+
settlePoolAfterCessation(identity2, expectedRevision) {
|
|
7842
|
+
object2(identity2, ["daemonId", "runtimeDomain", "hostDomain"]);
|
|
7843
|
+
validatePools([{ ...identity2, hostChargeCeilingBytes: 1 }]);
|
|
7844
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
|
|
7845
|
+
throw new Error("Invalid capacity settlement revision.");
|
|
7846
|
+
assertPrivateDirectory(this.directory);
|
|
7847
|
+
return withFileLockSync(
|
|
7848
|
+
`${this.file}.lock`,
|
|
7849
|
+
{ activity: "capacity pool settlement", waitMs: 100 },
|
|
7850
|
+
() => {
|
|
7851
|
+
const snapshot = this.read();
|
|
7852
|
+
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
7853
|
+
const pools = snapshot.pools ?? [];
|
|
7854
|
+
const index = pools.findIndex((pool2) => pool2.daemonId === identity2.daemonId);
|
|
7855
|
+
const pool = pools[index];
|
|
7856
|
+
if (pool && (pool.hostDomain !== identity2.hostDomain || pool.runtimeDomain !== identity2.runtimeDomain) || pools.some(
|
|
7857
|
+
(entry) => entry.runtimeDomain === identity2.runtimeDomain && entry.daemonId !== identity2.daemonId
|
|
7858
|
+
) || snapshot.reservations.some((entry) => Object.hasOwn(entry.totals, identity2.runtimeDomain)))
|
|
7859
|
+
throw new Error("Capacity pool cessation identity remains bound.");
|
|
7860
|
+
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
7861
|
+
throw new Error("Capacity reservation revision exhausted.");
|
|
7862
|
+
if (pool) pools.splice(index, 1);
|
|
7863
|
+
snapshot.revision++;
|
|
7864
|
+
validate(snapshot);
|
|
7865
|
+
writeFileAtomically(this.file, serializeSnapshot(snapshot));
|
|
7866
|
+
return { settled: !!pool, revision: snapshot.revision };
|
|
7867
|
+
}
|
|
7868
|
+
);
|
|
7869
|
+
}
|
|
7870
|
+
/** Caller persists phase proof and revokes authority before reducing this exact row. */
|
|
7871
|
+
reduceAfterPhase(target, expectedRevision) {
|
|
7872
|
+
validate({ version: 1, revision: 0, reservations: [target] });
|
|
7873
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
|
|
7874
|
+
throw new Error("Invalid capacity settlement revision.");
|
|
7875
|
+
assertPrivateDirectory(this.directory);
|
|
7876
|
+
withFileLockSync(
|
|
7877
|
+
`${this.file}.lock`,
|
|
7878
|
+
{ activity: "capacity phase settlement", waitMs: 100 },
|
|
7879
|
+
() => {
|
|
7880
|
+
const snapshot = this.read();
|
|
7881
|
+
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
7882
|
+
const index = snapshot.reservations.findIndex(
|
|
7883
|
+
(entry) => entry.environmentId === target.environmentId
|
|
7884
|
+
);
|
|
7885
|
+
const current = snapshot.reservations[index];
|
|
7886
|
+
if (!current || current.reservationId !== target.reservationId || current.operationId !== target.operationId || current.policyRevision !== target.policyRevision)
|
|
7887
|
+
throw new Error("Capacity phase settlement predecessor changed.");
|
|
7888
|
+
if (Object.keys(current.totals).length !== Object.keys(target.totals).length || Object.entries(target.totals).some(
|
|
7889
|
+
([domain, bytes2]) => !Object.hasOwn(current.totals, domain) || bytes2 > current.totals[domain]
|
|
7890
|
+
) || target.startup && !current.startup || target.heavy && !current.heavy)
|
|
7891
|
+
throw new Error("Capacity phase settlement cannot acquire resources.");
|
|
7892
|
+
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
7893
|
+
throw new Error("Capacity reservation revision exhausted.");
|
|
7894
|
+
snapshot.reservations[index] = structuredClone(target);
|
|
7895
|
+
snapshot.revision++;
|
|
7896
|
+
validate(snapshot);
|
|
7897
|
+
writeFileAtomically(this.file, serializeSnapshot(snapshot));
|
|
7898
|
+
}
|
|
7899
|
+
);
|
|
7900
|
+
}
|
|
7901
|
+
reserve(request, budgets, samples, nowMs, maxSampleAgeMs, previous, expectedRevision, requestedPool) {
|
|
7902
|
+
validate({ version: 1, revision: 0, reservations: [request] });
|
|
7903
|
+
if (requestedPool !== void 0) {
|
|
7904
|
+
validatePools([requestedPool]);
|
|
7905
|
+
if (!Object.hasOwn(request.totals, requestedPool.hostDomain) || !Object.hasOwn(request.totals, requestedPool.runtimeDomain))
|
|
7906
|
+
throw new Error("Capacity pool request lacks its bound domains.");
|
|
7907
|
+
}
|
|
7908
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
|
|
7909
|
+
throw new Error("Invalid capacity expected revision.");
|
|
7910
|
+
assertPrivateDirectory(this.directory);
|
|
7911
|
+
return withFileLockSync(
|
|
7912
|
+
`${this.file}.lock`,
|
|
7913
|
+
{ activity: "capacity reservation", waitMs: 100 },
|
|
7914
|
+
() => {
|
|
7915
|
+
const snapshot = this.read();
|
|
7916
|
+
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
7917
|
+
const pools = structuredClone(snapshot.pools ?? []);
|
|
7918
|
+
const poolChanged = mergePools(pools, requestedPool === void 0 ? [] : [requestedPool]);
|
|
7919
|
+
const existing = snapshot.reservations.find(
|
|
7920
|
+
(entry) => entry.environmentId === request.environmentId
|
|
7921
|
+
);
|
|
7922
|
+
if (previous) {
|
|
7923
|
+
if (snapshot.revision !== previous.revision || !existing || existing.reservationId !== previous.reservationId || existing.operationId !== previous.operationId || existing.policyRevision !== request.policyRevision)
|
|
7924
|
+
throw new Error("Capacity expansion predecessor changed.");
|
|
7925
|
+
const totals = { ...existing.totals };
|
|
7926
|
+
for (const [domain, bytes2] of Object.entries(request.totals))
|
|
7927
|
+
totals[domain] = Math.max(totals[domain] ?? 0, bytes2);
|
|
7928
|
+
request = {
|
|
7929
|
+
...request,
|
|
7930
|
+
totals,
|
|
7931
|
+
startup: existing.startup || request.startup,
|
|
7932
|
+
heavy: existing.heavy || request.heavy
|
|
7933
|
+
};
|
|
7934
|
+
} else if (existing) {
|
|
7935
|
+
if (existing.reservationId !== request.reservationId || existing.operationId !== request.operationId || existing.policyRevision !== request.policyRevision || existing.startup !== request.startup || existing.heavy !== request.heavy || Object.keys(existing.totals).length !== Object.keys(request.totals).length || Object.entries(existing.totals).some(
|
|
7936
|
+
([domain, bytes2]) => request.totals[domain] !== bytes2
|
|
7937
|
+
))
|
|
7938
|
+
throw new Error("Environment retains an earlier capacity reservation.");
|
|
7939
|
+
}
|
|
7940
|
+
const decision = evaluateCapacity(
|
|
7941
|
+
budgets,
|
|
7942
|
+
samples,
|
|
7943
|
+
snapshot.reservations,
|
|
7944
|
+
request,
|
|
7945
|
+
nowMs,
|
|
7946
|
+
maxSampleAgeMs,
|
|
7947
|
+
pools
|
|
7948
|
+
);
|
|
7949
|
+
if (!decision.admitted) return decision;
|
|
7950
|
+
if (!previous && existing && !poolChanged)
|
|
7951
|
+
return { admitted: true, revision: snapshot.revision, joined: true };
|
|
7952
|
+
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
7953
|
+
throw new Error("Capacity reservation revision exhausted.");
|
|
7954
|
+
snapshot.revision++;
|
|
7955
|
+
if (poolChanged) snapshot.pools = pools;
|
|
7956
|
+
if (existing)
|
|
7957
|
+
snapshot.reservations[snapshot.reservations.indexOf(existing)] = structuredClone(request);
|
|
7958
|
+
else snapshot.reservations.push(structuredClone(request));
|
|
7959
|
+
validate(snapshot);
|
|
7960
|
+
writeFileAtomically(this.file, serializeSnapshot(snapshot));
|
|
7961
|
+
return { admitted: true, revision: snapshot.revision, joined: false };
|
|
7962
|
+
}
|
|
7963
|
+
);
|
|
7964
|
+
}
|
|
7965
|
+
};
|
|
7966
|
+
|
|
7967
|
+
// src/core/controller-client.ts
|
|
7968
|
+
var import_node_crypto12 = require("crypto");
|
|
7969
|
+
var import_node_fs22 = __toESM(require("fs"));
|
|
7970
|
+
var import_node_net2 = __toESM(require("net"));
|
|
7971
|
+
var import_node_path28 = __toESM(require("path"));
|
|
7972
|
+
|
|
7973
|
+
// src/core/controller-protocol.ts
|
|
7974
|
+
var import_node_path27 = __toESM(require("path"));
|
|
7975
|
+
var CONTROLLER_FRAME_BYTES = 64 * 1024;
|
|
7976
|
+
var CONTROLLER_MAX_OPERATION_REQUEST_BYTES = 32 * 1024;
|
|
7977
|
+
var CONTROLLER_PROFILE_TOKEN_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7978
|
+
var CONTROLLER_PROFILE_RE = new RegExp(
|
|
7979
|
+
`^(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)})(?:,(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)}))*$`
|
|
7980
|
+
);
|
|
7981
|
+
|
|
7982
|
+
// src/core/controller-store.ts
|
|
7983
|
+
var import_node_crypto13 = require("crypto");
|
|
7984
|
+
var import_node_fs23 = __toESM(require("fs"));
|
|
7985
|
+
var import_node_path29 = __toESM(require("path"));
|
|
7986
|
+
var CONTROLLER_SNAPSHOT_BYTES = 1048576;
|
|
7987
|
+
function fail() {
|
|
7988
|
+
throw new Error("Invalid controller snapshot.");
|
|
7989
|
+
}
|
|
7990
|
+
function fields(value, expected) {
|
|
7991
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail();
|
|
7992
|
+
const actual = Object.keys(value);
|
|
7993
|
+
if (actual.length !== expected.length || actual.some((key) => !expected.includes(key))) fail();
|
|
7994
|
+
}
|
|
7995
|
+
function text2(value, max = 256) {
|
|
7996
|
+
return typeof value === "string" && value.length > 0 && value.length <= max && ![...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
|
|
7997
|
+
}
|
|
7998
|
+
function id3(value) {
|
|
7999
|
+
return text2(value, 128) && /^[a-zA-Z0-9_-]+$/.test(value);
|
|
8000
|
+
}
|
|
8001
|
+
function counter(value) {
|
|
8002
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
8003
|
+
}
|
|
8004
|
+
function validateControllerSnapshot(value) {
|
|
8005
|
+
fields(value, [
|
|
8006
|
+
"version",
|
|
8007
|
+
"store",
|
|
8008
|
+
"epoch",
|
|
8009
|
+
"revision",
|
|
8010
|
+
"nextSequence",
|
|
8011
|
+
"environments",
|
|
8012
|
+
"sessions",
|
|
8013
|
+
"events"
|
|
8014
|
+
]);
|
|
8015
|
+
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)
|
|
8016
|
+
fail();
|
|
8017
|
+
const environments = /* @__PURE__ */ new Set();
|
|
8018
|
+
const checkouts = /* @__PURE__ */ new Set();
|
|
8019
|
+
for (const env of value.environments) {
|
|
8020
|
+
fields(env, [
|
|
8021
|
+
"id",
|
|
8022
|
+
"repoPath",
|
|
8023
|
+
"workspace",
|
|
8024
|
+
"provider",
|
|
8025
|
+
"providerId",
|
|
8026
|
+
"profile",
|
|
8027
|
+
"fingerprint"
|
|
8028
|
+
]);
|
|
8029
|
+
if (!id3(env.id) || environments.has(env.id) || !text2(env.repoPath, 4096) || !import_node_path29.default.isAbsolute(env.repoPath) || import_node_path29.default.resolve(env.repoPath) !== env.repoPath || checkouts.has(env.repoPath) || !id3(env.workspace) || !id3(env.providerId) || !text2(env.profile) || !/^[a-z0-9-]+(?:,[a-z0-9-]+)*$/.test(env.profile) || !text2(env.fingerprint, 128) || !/^[a-f0-9]{64}$/.test(env.fingerprint) || !["devpod", "devsy"].includes(String(env.provider)))
|
|
8030
|
+
fail();
|
|
8031
|
+
environments.add(env.id);
|
|
8032
|
+
checkouts.add(env.repoPath);
|
|
8033
|
+
}
|
|
8034
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
8035
|
+
const usedEnvironments = /* @__PURE__ */ new Set();
|
|
8036
|
+
for (const session of value.sessions) {
|
|
8037
|
+
fields(session, [
|
|
8038
|
+
"id",
|
|
8039
|
+
"environmentId",
|
|
8040
|
+
"generation",
|
|
8041
|
+
"requirements",
|
|
8042
|
+
"renewedAtMs",
|
|
8043
|
+
...Object.hasOwn(session, "observation") ? ["observation"] : []
|
|
8044
|
+
]);
|
|
8045
|
+
if (!id3(session.id) || sessions.has(session.id) || !id3(session.environmentId) || !environments.has(session.environmentId) || !id3(session.generation) || !counter(session.renewedAtMs) || !Array.isArray(session.requirements) || session.requirements.length < 1 || session.requirements.length > 16 || new Set(session.requirements).size !== session.requirements.length || session.requirements.some(
|
|
8046
|
+
(r) => !text2(r, 132) || !/^(runtime|app:[a-z0-9][a-z0-9-]*)$/.test(r)
|
|
8047
|
+
))
|
|
8048
|
+
fail();
|
|
8049
|
+
if (session.observation !== void 0) {
|
|
8050
|
+
const observation = session.observation;
|
|
8051
|
+
fields(observation, [
|
|
8052
|
+
"status",
|
|
8053
|
+
"sampledAtMs",
|
|
8054
|
+
"validUntilMs",
|
|
8055
|
+
"journalRevision",
|
|
8056
|
+
"runtimeFingerprint"
|
|
8057
|
+
]);
|
|
8058
|
+
if (![
|
|
8059
|
+
"READY",
|
|
8060
|
+
"APP_ERROR",
|
|
8061
|
+
"BLOCKED",
|
|
8062
|
+
"STOPPED",
|
|
8063
|
+
"STARTING",
|
|
8064
|
+
"RECOVERING",
|
|
8065
|
+
"WAITING_CAPACITY",
|
|
8066
|
+
"PARKED_CAPACITY",
|
|
8067
|
+
"UNKNOWN"
|
|
8068
|
+
].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))
|
|
8069
|
+
fail();
|
|
8070
|
+
}
|
|
8071
|
+
sessions.add(session.id);
|
|
8072
|
+
usedEnvironments.add(session.environmentId);
|
|
8073
|
+
}
|
|
8074
|
+
if (usedEnvironments.size !== environments.size) fail();
|
|
8075
|
+
let previous = 0;
|
|
8076
|
+
for (const event of value.events) {
|
|
8077
|
+
fields(event, ["sequence", "session", "generation", "kind"]);
|
|
8078
|
+
if (!counter(event.sequence) || event.sequence <= previous || event.sequence >= value.nextSequence || !id3(event.session) || !id3(event.generation) || !["acquired", "renewed", "released", "expired", "invalidated", "observed"].includes(
|
|
8079
|
+
String(event.kind)
|
|
8080
|
+
))
|
|
8081
|
+
fail();
|
|
8082
|
+
previous = event.sequence;
|
|
8083
|
+
}
|
|
8084
|
+
if (Buffer.byteLength(JSON.stringify(value.events)) > 262144 || Buffer.byteLength(JSON.stringify(value)) + 1 > CONTROLLER_SNAPSHOT_BYTES)
|
|
8085
|
+
fail();
|
|
8086
|
+
}
|
|
8087
|
+
var ControllerStore = class {
|
|
8088
|
+
constructor(directory, write = writeFileAtomically) {
|
|
8089
|
+
this.directory = directory;
|
|
8090
|
+
this.write = write;
|
|
8091
|
+
this.file = import_node_path29.default.join(directory, "snapshot.json");
|
|
8092
|
+
}
|
|
8093
|
+
file;
|
|
8094
|
+
read() {
|
|
8095
|
+
let fd;
|
|
8096
|
+
try {
|
|
8097
|
+
fd = import_node_fs23.default.openSync(
|
|
8098
|
+
this.file,
|
|
8099
|
+
import_node_fs23.default.constants.O_RDONLY | import_node_fs23.default.constants.O_NOFOLLOW | import_node_fs23.default.constants.O_NONBLOCK
|
|
8100
|
+
);
|
|
8101
|
+
} catch (error) {
|
|
8102
|
+
if (error.code === "ENOENT") return void 0;
|
|
8103
|
+
throw new Error("Controller snapshot cannot be opened.");
|
|
8104
|
+
}
|
|
8105
|
+
try {
|
|
8106
|
+
const stat = import_node_fs23.default.fstatSync(fd);
|
|
8107
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0 || stat.size > CONTROLLER_SNAPSHOT_BYTES)
|
|
8108
|
+
fail();
|
|
8109
|
+
const bytes2 = Buffer.alloc(CONTROLLER_SNAPSHOT_BYTES + 1);
|
|
8110
|
+
let length = 0;
|
|
8111
|
+
while (length < bytes2.length) {
|
|
8112
|
+
const count = import_node_fs23.default.readSync(fd, bytes2, length, bytes2.length - length, null);
|
|
8113
|
+
if (!count) break;
|
|
8114
|
+
length += count;
|
|
8115
|
+
}
|
|
8116
|
+
if (length > CONTROLLER_SNAPSHOT_BYTES) fail();
|
|
8117
|
+
const value = JSON.parse(bytes2.subarray(0, length).toString("utf8"));
|
|
8118
|
+
validateControllerSnapshot(value);
|
|
8119
|
+
return value;
|
|
8120
|
+
} catch {
|
|
8121
|
+
throw new Error("Controller snapshot is unreadable or invalid.");
|
|
8122
|
+
} finally {
|
|
8123
|
+
import_node_fs23.default.closeSync(fd);
|
|
8124
|
+
}
|
|
8125
|
+
}
|
|
8126
|
+
persist(snapshot) {
|
|
8127
|
+
validateControllerSnapshot(snapshot);
|
|
8128
|
+
this.read();
|
|
8129
|
+
const contents = `${JSON.stringify(snapshot)}
|
|
8130
|
+
`;
|
|
8131
|
+
try {
|
|
8132
|
+
this.write(this.file, contents);
|
|
8133
|
+
} catch {
|
|
8134
|
+
const current = this.read();
|
|
8135
|
+
if (!current || `${JSON.stringify(current)}
|
|
8136
|
+
` !== contents)
|
|
8137
|
+
throw new Error("Controller snapshot commit failed.");
|
|
8138
|
+
for (const file of [this.file, this.directory]) {
|
|
8139
|
+
const fd = import_node_fs23.default.openSync(file, import_node_fs23.default.constants.O_RDONLY | import_node_fs23.default.constants.O_NOFOLLOW);
|
|
8140
|
+
try {
|
|
8141
|
+
import_node_fs23.default.fsyncSync(fd);
|
|
8142
|
+
} finally {
|
|
8143
|
+
import_node_fs23.default.closeSync(fd);
|
|
8144
|
+
}
|
|
8145
|
+
}
|
|
8146
|
+
}
|
|
8147
|
+
}
|
|
8148
|
+
startIncarnation() {
|
|
8149
|
+
const previous = this.read();
|
|
8150
|
+
if (previous && (previous.epoch === Number.MAX_SAFE_INTEGER || previous.revision === Number.MAX_SAFE_INTEGER))
|
|
8151
|
+
throw new Error("Controller snapshot counters exhausted.");
|
|
8152
|
+
const snapshot = {
|
|
8153
|
+
version: 1,
|
|
8154
|
+
store: previous?.store ?? (0, import_node_crypto13.randomUUID)(),
|
|
8155
|
+
epoch: (previous?.epoch ?? 0) + 1,
|
|
8156
|
+
revision: (previous?.revision ?? 0) + 1,
|
|
8157
|
+
nextSequence: 1,
|
|
8158
|
+
environments: [],
|
|
8159
|
+
sessions: [],
|
|
8160
|
+
events: []
|
|
8161
|
+
};
|
|
8162
|
+
this.persist(snapshot);
|
|
8163
|
+
return snapshot;
|
|
8164
|
+
}
|
|
8165
|
+
};
|
|
7246
8166
|
|
|
7247
8167
|
// src/core/reliability-contract.ts
|
|
7248
8168
|
var RELIABILITY_CONTRACT_VERSION = 2;
|
|
@@ -7396,6 +8316,7 @@ function assertReliabilityEvent(value) {
|
|
|
7396
8316
|
case "launched":
|
|
7397
8317
|
case "interrupted":
|
|
7398
8318
|
case "not-started":
|
|
8319
|
+
case "settle":
|
|
7399
8320
|
valid = isReliabilityId(value.operationId);
|
|
7400
8321
|
break;
|
|
7401
8322
|
case "completion":
|
|
@@ -7465,8 +8386,13 @@ function transition(state, outcome, effects = []) {
|
|
|
7465
8386
|
assertReliabilityState(state);
|
|
7466
8387
|
return { state, effects, outcome };
|
|
7467
8388
|
}
|
|
7468
|
-
function unchanged(state, outcome) {
|
|
7469
|
-
|
|
8389
|
+
function unchanged(state, outcome, reason2) {
|
|
8390
|
+
const result = transition(cloneState(state), outcome);
|
|
8391
|
+
if (reason2) result.reason = reason2;
|
|
8392
|
+
return result;
|
|
8393
|
+
}
|
|
8394
|
+
function blocked(state, reason2) {
|
|
8395
|
+
return unchanged(state, "blocked", reason2);
|
|
7470
8396
|
}
|
|
7471
8397
|
function advance(value) {
|
|
7472
8398
|
return value === Number.MAX_SAFE_INTEGER ? null : value + 1;
|
|
@@ -7491,7 +8417,10 @@ function updateRunningPhase(state) {
|
|
|
7491
8417
|
}
|
|
7492
8418
|
function handleRequest(state, event) {
|
|
7493
8419
|
if (state.executionPolicy === "manual" && event.mode === "start")
|
|
7494
|
-
return
|
|
8420
|
+
return blocked(
|
|
8421
|
+
state,
|
|
8422
|
+
"start requests are not admitted under manual execution policy; use the ensure/stop commands."
|
|
8423
|
+
);
|
|
7495
8424
|
const existing = state.requests.find((request) => request.key === event.key);
|
|
7496
8425
|
if (existing) {
|
|
7497
8426
|
const consumer = state.consumers.find((candidate) => candidate.id === existing.consumerId);
|
|
@@ -7558,10 +8487,10 @@ function handleRequest(state, event) {
|
|
|
7558
8487
|
}
|
|
7559
8488
|
function canExecAfterInterruptedEnsure(state) {
|
|
7560
8489
|
const latestEnsure = [...state.operationHistory].reverse().find((operation) => operation.kind === "ensure");
|
|
7561
|
-
|
|
8490
|
+
const quiescent = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
|
|
8491
|
+
return state.executionPolicy === "manual" && (state.desired === "running" || quiescent && state.desired === "stopped-by-user") && latestEnsure?.status === "INTERRUPTED" && latestEnsure.drained && state.operation?.drained === true && (state.operation.kind === "ensure" && (state.phase === "recovering" || quiescent) || state.operation.kind === "exec" && ["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status));
|
|
7562
8492
|
}
|
|
7563
8493
|
function handleOperationRequest(state, event) {
|
|
7564
|
-
if (state.executionPolicy !== "manual") return unchanged(state, "blocked");
|
|
7565
8494
|
const previous = state.operationHistory.find((entry) => entry.key === event.key);
|
|
7566
8495
|
if (previous)
|
|
7567
8496
|
return unchanged(
|
|
@@ -7570,26 +8499,42 @@ function handleOperationRequest(state, event) {
|
|
|
7570
8499
|
);
|
|
7571
8500
|
if (state.operationHistory.some((entry) => entry.id === event.operationId))
|
|
7572
8501
|
return unchanged(state, "conflict");
|
|
7573
|
-
if (
|
|
8502
|
+
if (state.executionPolicy !== "manual" && state.operationHistory.length >= RELIABILITY_MAX_ITEMS)
|
|
7574
8503
|
return unchanged(state, "blocked");
|
|
8504
|
+
if (event.kind === "exec" && canExecAfterInterruptedEnsure(state) && event.recoverInterruptedEnsure !== true)
|
|
8505
|
+
return blocked(
|
|
8506
|
+
state,
|
|
8507
|
+
"the latest ensure is INTERRUPTED and drained; exec requires recovering it first (run ensure, or exec with recovery proof)."
|
|
8508
|
+
);
|
|
7575
8509
|
const fullyStopped = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
|
|
7576
8510
|
const reconcileEnsure = event.kind === "ensure" && state.operation?.kind === "ensure" && ["NOT_STARTED", "INTERRUPTED"].includes(state.operation.status);
|
|
7577
8511
|
if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status) && !fullyStopped && !reconcileEnsure && !(event.kind === "exec" && event.recoverInterruptedEnsure === true && canExecAfterInterruptedEnsure(state))))
|
|
7578
|
-
return
|
|
8512
|
+
return blocked(
|
|
8513
|
+
state,
|
|
8514
|
+
`operation ${state.operation.id} is ${state.operation.status} (drained=${state.operation.drained}) and no stop proof, ensure reconciliation, or exec recovery supersedes it.`
|
|
8515
|
+
);
|
|
7579
8516
|
if (state.phase === "stopping" || state.desired === "parked-for-capacity")
|
|
7580
|
-
return
|
|
8517
|
+
return blocked(
|
|
8518
|
+
state,
|
|
8519
|
+
`phase is '${state.phase}' with desired '${state.desired}'; wait for the running stop to finish.`
|
|
8520
|
+
);
|
|
7581
8521
|
if (event.kind === "exec" && (!event.runtimeRunning || state.desired !== "running" && state.intentRevision !== 0))
|
|
7582
|
-
return
|
|
7583
|
-
|
|
8522
|
+
return blocked(
|
|
8523
|
+
state,
|
|
8524
|
+
`exec requires a running workspace (runtimeRunning=${event.runtimeRunning}, desired='${state.desired}').`
|
|
8525
|
+
);
|
|
8526
|
+
const rollover = state.executionPolicy === "manual" && state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
|
|
7584
8527
|
let retired = -1;
|
|
7585
8528
|
if (rollover) {
|
|
7586
|
-
if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED"].includes(state.operation.status)))
|
|
7587
|
-
return unchanged(state, "blocked");
|
|
7588
8529
|
const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
|
|
7589
8530
|
retired = state.operationHistory.findIndex(
|
|
7590
|
-
(entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED"].includes(entry.status)
|
|
8531
|
+
(entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(entry.status)
|
|
7591
8532
|
);
|
|
7592
|
-
if (retired === -1)
|
|
8533
|
+
if (retired === -1)
|
|
8534
|
+
return blocked(
|
|
8535
|
+
state,
|
|
8536
|
+
`journal holds ${state.operationHistory.length} entries and every retirable entry is the current operation or the latest ensure.`
|
|
8537
|
+
);
|
|
7593
8538
|
}
|
|
7594
8539
|
if (state.desired !== "running" || rollover) {
|
|
7595
8540
|
const revision = advance(state.intentRevision);
|
|
@@ -7600,6 +8545,7 @@ function handleOperationRequest(state, event) {
|
|
|
7600
8545
|
state.desired = "running";
|
|
7601
8546
|
state.phase = "queued";
|
|
7602
8547
|
state.profile = event.profile;
|
|
8548
|
+
if (state.executionPolicy === "capacity-managed") state.admission = "waiting";
|
|
7603
8549
|
state.stopProof = { workloadsStopped: false, routesRemoved: false };
|
|
7604
8550
|
state.consumers = [cloneConsumer(event.consumer)];
|
|
7605
8551
|
state.requests = [
|
|
@@ -7625,7 +8571,11 @@ function handleOperationRequest(state, event) {
|
|
|
7625
8571
|
profile: event.profile,
|
|
7626
8572
|
consumer: cloneConsumer(event.consumer)
|
|
7627
8573
|
});
|
|
7628
|
-
return transition(
|
|
8574
|
+
return transition(
|
|
8575
|
+
state,
|
|
8576
|
+
"accepted",
|
|
8577
|
+
state.executionPolicy === "capacity-managed" ? [effect(state, "request-admission", event.operationId)] : []
|
|
8578
|
+
);
|
|
7629
8579
|
}
|
|
7630
8580
|
function handleAdmission(state, event) {
|
|
7631
8581
|
if (state.executionPolicy === "manual" || state.desired !== "running" && state.desired !== "parked-for-capacity" || state.requests.length === 0) {
|
|
@@ -7644,7 +8594,10 @@ function handleDispatch(state) {
|
|
|
7644
8594
|
if (state.operation?.drained || state.operation?.status !== "NOT_STARTED")
|
|
7645
8595
|
return unchanged(state, "stale");
|
|
7646
8596
|
if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && state.admission !== "admitted" || state.consumers.length === 0) {
|
|
7647
|
-
return
|
|
8597
|
+
return blocked(
|
|
8598
|
+
state,
|
|
8599
|
+
`dispatch requires desired 'running' with a registered consumer (desired='${state.desired}', consumers=${state.consumers.length}).`
|
|
8600
|
+
);
|
|
7648
8601
|
}
|
|
7649
8602
|
const corrective = state.phase === "recovering" && state.incident !== null;
|
|
7650
8603
|
state.operation = { ...state.operation, status: "DISPATCH_PENDING", exitCode: null };
|
|
@@ -7659,7 +8612,10 @@ function handleDispatchPersisted(state, event) {
|
|
|
7659
8612
|
if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && (state.admission !== "admitted" || !state.chargeHeld)) {
|
|
7660
8613
|
state.operation = setUnknown(state.operation);
|
|
7661
8614
|
state.chargeHeld = state.executionPolicy === "capacity-managed";
|
|
7662
|
-
return
|
|
8615
|
+
return blocked(
|
|
8616
|
+
state,
|
|
8617
|
+
`dispatch persistence requires desired 'running' (desired='${state.desired}'); the operation outcome is unknown.`
|
|
8618
|
+
);
|
|
7663
8619
|
}
|
|
7664
8620
|
if (state.phase === "recovering" && state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
|
|
7665
8621
|
return unchanged(state, "blocked");
|
|
@@ -7707,6 +8663,22 @@ function handleInterrupted(state, event) {
|
|
|
7707
8663
|
if (state.desired === "running") state.phase = "recovering";
|
|
7708
8664
|
return transition(state, "accepted");
|
|
7709
8665
|
}
|
|
8666
|
+
function handleSettle(state, event) {
|
|
8667
|
+
if (state.executionPolicy !== "manual") return unchanged(state, "stale");
|
|
8668
|
+
if (!state.operation || state.operation.id !== event.operationId)
|
|
8669
|
+
return unchanged(state, "stale");
|
|
8670
|
+
if (state.operation.drained && state.operation.status === "INTERRUPTED")
|
|
8671
|
+
return unchanged(state, "joined");
|
|
8672
|
+
if (state.operation.status === "COMPLETED") return unchanged(state, "joined");
|
|
8673
|
+
state.operation = {
|
|
8674
|
+
...state.operation,
|
|
8675
|
+
status: "INTERRUPTED",
|
|
8676
|
+
drained: true,
|
|
8677
|
+
exitCode: null
|
|
8678
|
+
};
|
|
8679
|
+
if (state.desired === "running") state.phase = "recovering";
|
|
8680
|
+
return transition(state, "accepted");
|
|
8681
|
+
}
|
|
7710
8682
|
function handleObservation(state, event, nowMs) {
|
|
7711
8683
|
if (state.desired !== "running") return unchanged(state, "stale");
|
|
7712
8684
|
const previous = state.observations.find(
|
|
@@ -7716,7 +8688,10 @@ function handleObservation(state, event, nowMs) {
|
|
|
7716
8688
|
return unchanged(state, "stale");
|
|
7717
8689
|
}
|
|
7718
8690
|
if (!previous && state.observations.length >= RELIABILITY_MAX_ITEMS)
|
|
7719
|
-
return
|
|
8691
|
+
return blocked(
|
|
8692
|
+
state,
|
|
8693
|
+
`observation set holds ${state.observations.length} distinct capabilities; observations repeat per capability and never retire.`
|
|
8694
|
+
);
|
|
7720
8695
|
state.observations = state.observations.filter(
|
|
7721
8696
|
(observation) => observation.capability !== event.observation.capability
|
|
7722
8697
|
);
|
|
@@ -7725,7 +8700,7 @@ function handleObservation(state, event, nowMs) {
|
|
|
7725
8700
|
return transition(state, "accepted");
|
|
7726
8701
|
}
|
|
7727
8702
|
function handleStop(state) {
|
|
7728
|
-
if (state.desired === "stopped-by-user" && state.
|
|
8703
|
+
if (state.desired === "stopped-by-user" && state.phase === "stopping")
|
|
7729
8704
|
return unchanged(state, "joined");
|
|
7730
8705
|
const intentRevision = advance(state.intentRevision);
|
|
7731
8706
|
if (intentRevision === null) return unchanged(state, "blocked");
|
|
@@ -7744,7 +8719,10 @@ function handleStop(state) {
|
|
|
7744
8719
|
function handleStopProof(state, event) {
|
|
7745
8720
|
if (state.desired === "running") return unchanged(state, "stale");
|
|
7746
8721
|
if (state.executionPolicy === "manual" && state.operation && !state.operation.drained && event.workloadsStopped && event.routesRemoved)
|
|
7747
|
-
return
|
|
8722
|
+
return blocked(
|
|
8723
|
+
state,
|
|
8724
|
+
`stop proof cannot settle while operation ${state.operation.id} is not drained; wait for its worker or settle the journal.`
|
|
8725
|
+
);
|
|
7748
8726
|
const previous = { ...state.stopProof };
|
|
7749
8727
|
if (previous.workloadsStopped === event.workloadsStopped && previous.routesRemoved === event.routesRemoved)
|
|
7750
8728
|
return unchanged(state, "joined");
|
|
@@ -7923,6 +8901,8 @@ function applyReliabilityEvent(state, event, nowMs) {
|
|
|
7923
8901
|
return handleCompletion(next, event);
|
|
7924
8902
|
case "interrupted":
|
|
7925
8903
|
return handleInterrupted(next, event);
|
|
8904
|
+
case "settle":
|
|
8905
|
+
return handleSettle(next, event);
|
|
7926
8906
|
case "observation":
|
|
7927
8907
|
return handleObservation(next, event, nowMs);
|
|
7928
8908
|
case "stop-proof":
|
|
@@ -7942,7 +8922,7 @@ function stepReliability(state, event, nowMs) {
|
|
|
7942
8922
|
if (!isReliabilityCounter(nowMs) || nowMs < state.observationsAfterMs)
|
|
7943
8923
|
throw new Error("Invalid reliability clock.");
|
|
7944
8924
|
const result = applyReliabilityEvent(state, event, nowMs);
|
|
7945
|
-
if (result.outcome === "accepted" && result.state.
|
|
8925
|
+
if (result.outcome === "accepted" && result.state.operation) {
|
|
7946
8926
|
const operation = result.state.operation;
|
|
7947
8927
|
result.state.operationHistory = result.state.operationHistory.map(
|
|
7948
8928
|
(entry) => entry.id === operation.id ? { ...entry, ...operation } : entry
|
|
@@ -7955,34 +8935,206 @@ function stepReliability(state, event, nowMs) {
|
|
|
7955
8935
|
}
|
|
7956
8936
|
|
|
7957
8937
|
// src/core/reliability-operation-store.ts
|
|
7958
|
-
var
|
|
7959
|
-
var
|
|
7960
|
-
var
|
|
8938
|
+
var import_node_crypto14 = require("crypto");
|
|
8939
|
+
var import_node_fs24 = __toESM(require("fs"));
|
|
8940
|
+
var import_node_path30 = __toESM(require("path"));
|
|
8941
|
+
var import_node_util5 = require("util");
|
|
8942
|
+
function installedCliVersion() {
|
|
8943
|
+
return true ? "0.0.68" : "0.0.0-dev";
|
|
8944
|
+
}
|
|
8945
|
+
function compareCliVersions(left, right) {
|
|
8946
|
+
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
8947
|
+
const leftParts = parts(left);
|
|
8948
|
+
const rightParts = parts(right);
|
|
8949
|
+
for (let index = 0; index < 3; index += 1) {
|
|
8950
|
+
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
8951
|
+
if (difference !== 0) return difference;
|
|
8952
|
+
}
|
|
8953
|
+
return 0;
|
|
8954
|
+
}
|
|
7961
8955
|
var MAX_RECORD_BYTES = 1048576;
|
|
8956
|
+
var MAX_RELIABILITY_JOURNALS = 256;
|
|
8957
|
+
var MAX_RELIABILITY_DIRECTORY_ENTRIES = MAX_RELIABILITY_JOURNALS * 2;
|
|
7962
8958
|
function identityKey(identity2) {
|
|
7963
|
-
if (!
|
|
8959
|
+
if (!import_node_path30.default.isAbsolute(identity2.repoPath) || import_node_path30.default.resolve(identity2.repoPath) !== identity2.repoPath || identity2.repoPath.includes("\0") || identity2.workspace !== null && !isReliabilityId(identity2.workspace) || !["devpod", "devsy"].includes(identity2.provider)) {
|
|
7964
8960
|
throw new Error("Invalid reliability workspace identity.");
|
|
7965
8961
|
}
|
|
7966
|
-
return (0,
|
|
8962
|
+
return (0, import_node_crypto14.createHash)("sha256").update(identity2.repoPath).digest("hex");
|
|
7967
8963
|
}
|
|
7968
8964
|
function reliabilityOperationPath(identity2) {
|
|
7969
|
-
return
|
|
8965
|
+
return import_node_path30.default.join(DEVROUTER_HOME, "reliability", `${identityKey(identity2)}.json`);
|
|
7970
8966
|
}
|
|
7971
8967
|
function keys(value, expected) {
|
|
7972
8968
|
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !expected.includes(key))) {
|
|
7973
8969
|
throw new Error("Reliability record contains unsupported fields.");
|
|
7974
8970
|
}
|
|
7975
8971
|
}
|
|
7976
|
-
function
|
|
7977
|
-
|
|
8972
|
+
function exactKeys(value, expected, message) {
|
|
8973
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length !== expected.length || Object.keys(value).some((key) => !expected.includes(key))) {
|
|
8974
|
+
throw new Error(message);
|
|
8975
|
+
}
|
|
8976
|
+
}
|
|
8977
|
+
function validateCapacityPhaseSettlement(record5, state, enrollment) {
|
|
8978
|
+
const settlement = record5.phaseSettlement;
|
|
8979
|
+
if (settlement === void 0 || settlement === null) return;
|
|
8980
|
+
if (record5.version !== 2 || !enrollment || state.executionPolicy !== "capacity-managed")
|
|
8981
|
+
throw new Error("Phase settlement requires durable capacity enrollment.");
|
|
8982
|
+
exactKeys(
|
|
8983
|
+
settlement,
|
|
8984
|
+
["id", "fence", "target", "estimatesDigest"],
|
|
8985
|
+
"Invalid capacity phase settlement fields."
|
|
8986
|
+
);
|
|
8987
|
+
exactKeys(
|
|
8988
|
+
settlement.fence,
|
|
8989
|
+
["environmentId", "intentRevision", "runtimeGeneration", "controllerEpoch"],
|
|
8990
|
+
"Invalid capacity phase settlement fence."
|
|
8991
|
+
);
|
|
8992
|
+
exactKeys(
|
|
8993
|
+
settlement.target,
|
|
8994
|
+
[
|
|
8995
|
+
"environmentId",
|
|
8996
|
+
"operationId",
|
|
8997
|
+
"reservationId",
|
|
8998
|
+
"policyRevision",
|
|
8999
|
+
"totals",
|
|
9000
|
+
"startup",
|
|
9001
|
+
"heavy"
|
|
9002
|
+
],
|
|
9003
|
+
"Invalid capacity phase settlement target fields."
|
|
9004
|
+
);
|
|
9005
|
+
const target = settlement.target;
|
|
9006
|
+
const totals = target.totals;
|
|
9007
|
+
if (!isReliabilityId(settlement.id) || !isReliabilityId(settlement.fence.environmentId) || settlement.fence.environmentId !== state.environmentId || !isReliabilityCounter(settlement.fence.intentRevision) || !isReliabilityCounter(settlement.fence.runtimeGeneration) || !isReliabilityCounter(settlement.fence.controllerEpoch) || !isReliabilityId(target.environmentId) || target.environmentId !== state.environmentId || !isReliabilityId(target.operationId) || !isReliabilityId(target.reservationId) || !Number.isSafeInteger(target.policyRevision) || target.policyRevision < 1 || target.startup !== false || target.heavy !== false || typeof settlement.estimatesDigest !== "string" || !/^[0-9a-f]{64}$/.test(settlement.estimatesDigest))
|
|
9008
|
+
throw new Error("Invalid capacity phase settlement.");
|
|
9009
|
+
if (!totals || typeof totals !== "object" || Array.isArray(totals) || Object.keys(totals).length < 1 || Object.keys(totals).length > 256 || Object.entries(totals).some(
|
|
9010
|
+
([domain, bytes2]) => !isReliabilityId(domain) || !Number.isSafeInteger(bytes2) || bytes2 < 0
|
|
9011
|
+
))
|
|
9012
|
+
throw new Error("Invalid capacity phase settlement totals.");
|
|
9013
|
+
const capacity = record5.capacity;
|
|
9014
|
+
if (capacity?.validUntilMs !== 0 || target.operationId !== capacity?.operationId || target.reservationId !== capacity?.reservationId || target.policyRevision !== capacity?.policyRevision || settlement.estimatesDigest !== enrollment.estimatesDigest)
|
|
9015
|
+
throw new Error("Capacity phase settlement does not match durable capacity authority.");
|
|
9016
|
+
const targetDomains = Object.keys(totals);
|
|
9017
|
+
if (targetDomains.length !== 2 || !targetDomains.includes(enrollment.hostDomain) || !targetDomains.includes(enrollment.runtimeDomain))
|
|
9018
|
+
throw new Error("Capacity phase settlement domains do not match enrollment.");
|
|
9019
|
+
}
|
|
9020
|
+
function validateStartupWitness(record5, state, enrollment) {
|
|
9021
|
+
const witness = record5.startupWitness;
|
|
9022
|
+
if (witness === void 0 || witness === null) return;
|
|
9023
|
+
if (record5.version !== 2 || !enrollment || state.executionPolicy !== "capacity-managed")
|
|
9024
|
+
throw new Error("Startup witness requires current durable managed intent.");
|
|
9025
|
+
exactKeys(
|
|
9026
|
+
witness,
|
|
9027
|
+
[
|
|
9028
|
+
"operationId",
|
|
9029
|
+
"fence",
|
|
9030
|
+
"provider",
|
|
9031
|
+
"profile",
|
|
9032
|
+
"sourceConfigSha256",
|
|
9033
|
+
"effectiveConfigSha256",
|
|
9034
|
+
"composeFiles",
|
|
9035
|
+
"primaryService",
|
|
9036
|
+
"startupServices",
|
|
9037
|
+
"retainedContainerIds"
|
|
9038
|
+
],
|
|
9039
|
+
"Invalid capacity startup witness fields."
|
|
9040
|
+
);
|
|
9041
|
+
exactKeys(
|
|
9042
|
+
witness.fence,
|
|
9043
|
+
["environmentId", "intentRevision", "runtimeGeneration", "controllerEpoch"],
|
|
9044
|
+
"Invalid capacity startup witness fence."
|
|
9045
|
+
);
|
|
9046
|
+
exactKeys(
|
|
9047
|
+
witness.provider,
|
|
9048
|
+
["id", "context", "uid", "sourceContainer"],
|
|
9049
|
+
"Invalid capacity startup witness provider fields."
|
|
9050
|
+
);
|
|
9051
|
+
const digest = (value) => typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
|
|
9052
|
+
const boundedText = (value) => typeof value === "string" && value.length <= 256 && ![...value].some(
|
|
9053
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127
|
|
9054
|
+
);
|
|
9055
|
+
const absolutePath = (value) => typeof value === "string" && value.length <= 4096 && !value.includes("\0") && import_node_path30.default.isAbsolute(value) && import_node_path30.default.resolve(value) === value;
|
|
9056
|
+
const unique = (values) => new Set(values).size === values.length;
|
|
9057
|
+
const fence = witness.fence;
|
|
9058
|
+
if (!isReliabilityId(witness.operationId) || !isReliabilityId(fence.environmentId) || !isReliabilityCounter(fence.intentRevision) || !isReliabilityCounter(fence.runtimeGeneration) || !isReliabilityCounter(fence.controllerEpoch) || !isReliabilityProfile(witness.profile) || !digest(witness.sourceConfigSha256) || !digest(witness.effectiveConfigSha256) || !boundedText(witness.provider.id) || witness.provider.id.trim() !== witness.provider.id || witness.provider.id === "" || witness.provider.id !== enrollment.providerId || !boundedText(witness.provider.context) || !boundedText(witness.provider.uid) || !boundedText(witness.provider.sourceContainer) || !Array.isArray(witness.composeFiles) || witness.composeFiles.length < 1 || witness.composeFiles.length > 16 || !witness.composeFiles.every(absolutePath) || !unique(witness.composeFiles) || !Array.isArray(witness.startupServices) || witness.startupServices.length < 1 || witness.startupServices.length > 64 || !witness.startupServices.every((service) => isReliabilityId(service) && service.length <= 64) || !unique(witness.startupServices) || !isReliabilityId(witness.primaryService) || witness.primaryService.length > 64 || !witness.startupServices.includes(witness.primaryService) || !Array.isArray(witness.retainedContainerIds) || witness.retainedContainerIds.length > 256 || !witness.retainedContainerIds.every(digest) || !unique(witness.retainedContainerIds))
|
|
9059
|
+
throw new Error("Invalid capacity startup witness.");
|
|
9060
|
+
}
|
|
9061
|
+
function validate2(record5, identity2) {
|
|
9062
|
+
keys(record5, [
|
|
9063
|
+
"version",
|
|
9064
|
+
"identity",
|
|
9065
|
+
"revision",
|
|
9066
|
+
"state",
|
|
9067
|
+
"worker",
|
|
9068
|
+
"effectSequence",
|
|
9069
|
+
"outcome",
|
|
9070
|
+
"result",
|
|
9071
|
+
"writtenByVersion",
|
|
9072
|
+
...record5.version === 2 ? [
|
|
9073
|
+
"capacity",
|
|
9074
|
+
"enrollment",
|
|
9075
|
+
"activeProfile",
|
|
9076
|
+
"preparation",
|
|
9077
|
+
"phaseSettlement",
|
|
9078
|
+
"startupWitness"
|
|
9079
|
+
] : []
|
|
9080
|
+
]);
|
|
9081
|
+
if (record5.writtenByVersion !== void 0 && compareCliVersions(record5.writtenByVersion, installedCliVersion()) > 0) {
|
|
9082
|
+
throw new Error(
|
|
9083
|
+
`Reliability record was last written by devrouter ${record5.writtenByVersion}, which is newer than the installed ${installedCliVersion()}; upgrade devrouter (devrouter upgrade) before running lifecycle commands.`
|
|
9084
|
+
);
|
|
9085
|
+
}
|
|
7978
9086
|
keys(record5.identity, ["repoPath", "workspace", "provider"]);
|
|
7979
|
-
if (record5.version !== 1 || record5.identity.repoPath !== identity2.repoPath || record5.identity.workspace !== identity2.workspace || record5.identity.provider !== identity2.provider || !isReliabilityCounter(record5.revision) || !isReliabilityCounter(record5.effectSequence)) {
|
|
9087
|
+
if (record5.version !== 1 && record5.version !== 2 || record5.identity.repoPath !== identity2.repoPath || record5.identity.workspace !== identity2.workspace || record5.identity.provider !== identity2.provider || !isReliabilityCounter(record5.revision) || !isReliabilityCounter(record5.effectSequence)) {
|
|
7980
9088
|
throw new Error("Reliability record has invalid version, ownership, or counters.");
|
|
7981
9089
|
}
|
|
9090
|
+
if (record5.version === 2) {
|
|
9091
|
+
const capacity = record5.capacity;
|
|
9092
|
+
if (capacity === void 0) throw new Error("Invalid capacity authority binding.");
|
|
9093
|
+
if (capacity !== null) {
|
|
9094
|
+
keys(capacity, [
|
|
9095
|
+
"reservationId",
|
|
9096
|
+
"operationId",
|
|
9097
|
+
"workerId",
|
|
9098
|
+
"policyRevision",
|
|
9099
|
+
"validUntilMs",
|
|
9100
|
+
"snapshotRevision",
|
|
9101
|
+
"controller",
|
|
9102
|
+
"execSteady"
|
|
9103
|
+
]);
|
|
9104
|
+
if (capacity.controller !== void 0) {
|
|
9105
|
+
keys(capacity.controller, ["store", "epoch"]);
|
|
9106
|
+
if (!isReliabilityId(capacity.controller.store) || !isReliabilityCounter(capacity.controller.epoch) || capacity.controller.epoch < 1)
|
|
9107
|
+
throw new Error("Invalid capacity controller identity.");
|
|
9108
|
+
}
|
|
9109
|
+
}
|
|
9110
|
+
if (capacity !== null && (!isReliabilityId(capacity.reservationId) || !isReliabilityId(capacity.operationId) || !isReliabilityId(capacity.workerId) || !Number.isSafeInteger(capacity.policyRevision) || capacity.policyRevision < 1 || !isReliabilityCounter(capacity.validUntilMs) || capacity.snapshotRevision !== void 0 && (!isReliabilityCounter(capacity.snapshotRevision) || capacity.snapshotRevision < 1)))
|
|
9111
|
+
throw new Error("Invalid capacity authority binding.");
|
|
9112
|
+
}
|
|
7982
9113
|
const state = record5.state;
|
|
7983
9114
|
assertReliabilityState(state);
|
|
7984
|
-
if (state.environmentId !== identityKey(identity2)
|
|
7985
|
-
throw new Error("Reliability record does not
|
|
9115
|
+
if (state.environmentId !== identityKey(identity2)) {
|
|
9116
|
+
throw new Error("Reliability record does not match this workspace.");
|
|
9117
|
+
}
|
|
9118
|
+
if (record5.enrollment !== void 0) {
|
|
9119
|
+
const enrollment = record5.enrollment;
|
|
9120
|
+
keys(enrollment, [
|
|
9121
|
+
"policyRevision",
|
|
9122
|
+
"gitCommonDir",
|
|
9123
|
+
"providerId",
|
|
9124
|
+
"hostDomain",
|
|
9125
|
+
"runtimeDomain",
|
|
9126
|
+
"endpoint",
|
|
9127
|
+
"daemonId",
|
|
9128
|
+
"estimatesDigest"
|
|
9129
|
+
]);
|
|
9130
|
+
const absolute2 = (value) => typeof value === "string" && value.length <= 4096 && !value.includes("\0") && import_node_path30.default.isAbsolute(value) && import_node_path30.default.resolve(value) === value;
|
|
9131
|
+
const providerIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && ![...value].some(
|
|
9132
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127
|
|
9133
|
+
);
|
|
9134
|
+
if (record5.version !== 2 || state.executionPolicy !== "capacity-managed" || !Number.isSafeInteger(enrollment.policyRevision) || enrollment.policyRevision < 1 || !absolute2(enrollment.gitCommonDir) || !absolute2(enrollment.endpoint) || !providerIdentity(enrollment.providerId) || !providerIdentity(enrollment.daemonId) || !isReliabilityId(enrollment.hostDomain) || !isReliabilityId(enrollment.runtimeDomain) || enrollment.hostDomain === enrollment.runtimeDomain || typeof enrollment.estimatesDigest !== "string" || !/^[0-9a-f]{64}$/.test(enrollment.estimatesDigest) || record5.activeProfile !== null && (typeof record5.activeProfile !== "string" || record5.activeProfile.length > 256 || !/^[a-z0-9,-]+$/.test(record5.activeProfile)))
|
|
9135
|
+
throw new Error("Invalid durable capacity enrollment.");
|
|
9136
|
+
} else if (state.executionPolicy !== "manual" || record5.activeProfile !== void 0) {
|
|
9137
|
+
throw new Error("Managed lifecycle requires durable capacity enrollment.");
|
|
7986
9138
|
}
|
|
7987
9139
|
keys(state, [
|
|
7988
9140
|
"contractVersion",
|
|
@@ -8017,6 +9169,52 @@ function validate(record5, identity2) {
|
|
|
8017
9169
|
keys(entry, ["key", "mode", "consumerId", "operationId", "profile", "intentRevision"]);
|
|
8018
9170
|
for (const entry of state.observations)
|
|
8019
9171
|
keys(entry, ["capability", "infrastructure", "application", "observedAtMs", "validForMs"]);
|
|
9172
|
+
if (record5.preparation !== void 0 && record5.preparation !== null) {
|
|
9173
|
+
if (record5.version !== 2 || record5.enrollment === void 0 || state.executionPolicy !== "capacity-managed")
|
|
9174
|
+
throw new Error("Preparation receipt requires durable capacity enrollment.");
|
|
9175
|
+
const preparation = record5.preparation;
|
|
9176
|
+
keys(preparation, ["operationId", "profile", "fence"]);
|
|
9177
|
+
keys(preparation.fence, [
|
|
9178
|
+
"environmentId",
|
|
9179
|
+
"intentRevision",
|
|
9180
|
+
"runtimeGeneration",
|
|
9181
|
+
"controllerEpoch"
|
|
9182
|
+
]);
|
|
9183
|
+
if (!isReliabilityId(preparation.operationId) || !isReliabilityProfile(preparation.profile) || !isReliabilityId(preparation.fence.environmentId) || preparation.fence.environmentId !== state.environmentId || !isReliabilityCounter(preparation.fence.intentRevision) || !isReliabilityCounter(preparation.fence.runtimeGeneration) || !isReliabilityCounter(preparation.fence.controllerEpoch))
|
|
9184
|
+
throw new Error("Invalid preparation receipt.");
|
|
9185
|
+
const preparedOperation = state.operationHistory.find(
|
|
9186
|
+
(entry) => entry.id === preparation.operationId
|
|
9187
|
+
);
|
|
9188
|
+
if (preparedOperation?.kind !== "ensure" || preparedOperation.profile !== preparation.profile || preparedOperation.status !== "COMPLETED")
|
|
9189
|
+
throw new Error("Preparation receipt does not match a completed ensure operation.");
|
|
9190
|
+
}
|
|
9191
|
+
validateCapacityPhaseSettlement(record5, state, record5.enrollment);
|
|
9192
|
+
validateStartupWitness(record5, state, record5.enrollment);
|
|
9193
|
+
const execSteady = record5.capacity?.execSteady;
|
|
9194
|
+
if (execSteady !== void 0) {
|
|
9195
|
+
const enrollment = record5.enrollment;
|
|
9196
|
+
if (record5.version !== 2 || !enrollment || state.executionPolicy !== "capacity-managed")
|
|
9197
|
+
throw new Error("Exec steady receipt requires durable capacity enrollment.");
|
|
9198
|
+
exactKeys(
|
|
9199
|
+
execSteady,
|
|
9200
|
+
["profile", "estimatesDigest", "fence", "totals"],
|
|
9201
|
+
"Invalid exec steady receipt fields."
|
|
9202
|
+
);
|
|
9203
|
+
exactKeys(
|
|
9204
|
+
execSteady.fence,
|
|
9205
|
+
["environmentId", "intentRevision", "runtimeGeneration", "controllerEpoch"],
|
|
9206
|
+
"Invalid exec steady receipt fence."
|
|
9207
|
+
);
|
|
9208
|
+
if (!isReliabilityProfile(execSteady.profile) || typeof execSteady.estimatesDigest !== "string" || !/^[0-9a-f]{64}$/.test(execSteady.estimatesDigest) || execSteady.estimatesDigest !== enrollment.estimatesDigest || execSteady.fence.environmentId !== state.environmentId || !isReliabilityCounter(execSteady.fence.intentRevision) || !isReliabilityCounter(execSteady.fence.runtimeGeneration) || !isReliabilityCounter(execSteady.fence.controllerEpoch))
|
|
9209
|
+
throw new Error("Invalid exec steady receipt.");
|
|
9210
|
+
exactKeys(
|
|
9211
|
+
execSteady.totals,
|
|
9212
|
+
[enrollment.hostDomain, enrollment.runtimeDomain],
|
|
9213
|
+
"Exec steady receipt domains do not match enrollment."
|
|
9214
|
+
);
|
|
9215
|
+
if (!Object.values(execSteady.totals).every(isReliabilityCounter))
|
|
9216
|
+
throw new Error("Invalid exec steady receipt totals.");
|
|
9217
|
+
}
|
|
8020
9218
|
if (state.operation) {
|
|
8021
9219
|
const current = state.operationHistory.find((entry) => entry.id === state.operation?.id);
|
|
8022
9220
|
if (!current || operationKeys.some(
|
|
@@ -8033,6 +9231,12 @@ function validate(record5, identity2) {
|
|
|
8033
9231
|
if (outcome.operationId !== state.operation?.id || !["completed", "not-started", "completion-unknown"].includes(outcome.status) || (outcome.status === "completed" ? !validExit(outcome.exitCode) : outcome.exitCode !== null) || outcome.transport.exitCode !== null && !validExit(outcome.transport.exitCode) || outcome.transport.signal !== null && (typeof outcome.transport.signal !== "string" || !/^SIG[A-Z0-9]{1,16}$/.test(outcome.transport.signal))) {
|
|
8034
9232
|
throw new Error("Reliability execution outcome is invalid.");
|
|
8035
9233
|
}
|
|
9234
|
+
if (record5.result !== void 0 && record5.result !== null) {
|
|
9235
|
+
const serialized = JSON.stringify(record5.result);
|
|
9236
|
+
if (typeof serialized !== "string" || serialized.length > 262144 || record5.result.ok !== true && record5.result.ok !== false || (record5.result.ok ? "message" in record5.result : !("message" in record5.result)) || !record5.result.ok && (typeof record5.result.message !== "string" || record5.result.message.length > 4096)) {
|
|
9237
|
+
throw new Error("Reliability operation result is invalid.");
|
|
9238
|
+
}
|
|
9239
|
+
}
|
|
8036
9240
|
}
|
|
8037
9241
|
if (record5.worker !== null) {
|
|
8038
9242
|
keys(record5.worker, ["id", "operationId", "pid", "birth"]);
|
|
@@ -8041,37 +9245,59 @@ function validate(record5, identity2) {
|
|
|
8041
9245
|
}
|
|
8042
9246
|
}
|
|
8043
9247
|
}
|
|
9248
|
+
function assertCapacityEffect(record5, workerId, nowMs, directory = import_node_path30.default.join(DEVROUTER_HOME, "controller")) {
|
|
9249
|
+
if (record5.version === 1) return;
|
|
9250
|
+
const binding = record5.capacity;
|
|
9251
|
+
if (!binding || binding.workerId !== workerId || binding.operationId !== record5.state.operation?.id || binding.validUntilMs <= nowMs)
|
|
9252
|
+
throw new Error("Capacity effect authority is absent or stale.");
|
|
9253
|
+
if (record5.state.executionPolicy === "capacity-managed") {
|
|
9254
|
+
const controller = new ControllerStore(directory).read();
|
|
9255
|
+
if (!binding.controller || !controller || controller.store !== binding.controller.store || controller.epoch !== binding.controller.epoch)
|
|
9256
|
+
throw new Error("Capacity controller incarnation changed.");
|
|
9257
|
+
}
|
|
9258
|
+
const snapshot = new CapacityStore(directory).read();
|
|
9259
|
+
if (binding.snapshotRevision === void 0 || snapshot.revision !== binding.snapshotRevision)
|
|
9260
|
+
throw new Error("Capacity snapshot authority is absent or stale.");
|
|
9261
|
+
const reservation = snapshot.reservations.find(
|
|
9262
|
+
(entry) => entry.reservationId === binding.reservationId
|
|
9263
|
+
);
|
|
9264
|
+
if (!reservation || reservation.environmentId !== record5.state.environmentId || reservation.operationId !== binding.operationId || reservation.policyRevision !== binding.policyRevision)
|
|
9265
|
+
throw new Error("Capacity effect reservation does not match lifecycle intent.");
|
|
9266
|
+
}
|
|
8044
9267
|
function readReliabilityOperation(identity2) {
|
|
8045
9268
|
const file = reliabilityOperationPath(identity2);
|
|
9269
|
+
const parsed = readBoundedPrivateJson(file, "Reliability record");
|
|
9270
|
+
if (parsed === void 0) return void 0;
|
|
9271
|
+
const record5 = parsed;
|
|
9272
|
+
validate2(record5, identity2);
|
|
9273
|
+
return record5;
|
|
9274
|
+
}
|
|
9275
|
+
function readBoundedPrivateJson(file, label) {
|
|
8046
9276
|
let descriptor;
|
|
8047
9277
|
try {
|
|
8048
|
-
descriptor =
|
|
9278
|
+
descriptor = import_node_fs24.default.openSync(
|
|
8049
9279
|
file,
|
|
8050
|
-
|
|
9280
|
+
import_node_fs24.default.constants.O_RDONLY | import_node_fs24.default.constants.O_NOFOLLOW | import_node_fs24.default.constants.O_NONBLOCK
|
|
8051
9281
|
);
|
|
8052
9282
|
} catch (error) {
|
|
8053
9283
|
if (error.code === "ENOENT") return void 0;
|
|
8054
9284
|
throw error;
|
|
8055
9285
|
}
|
|
8056
9286
|
try {
|
|
8057
|
-
const stat =
|
|
8058
|
-
if (!stat.isFile() || stat.size > MAX_RECORD_BYTES || (stat.mode & 63) !== 0) {
|
|
8059
|
-
throw new Error(
|
|
8060
|
-
}
|
|
8061
|
-
const
|
|
8062
|
-
const count =
|
|
8063
|
-
if (count > MAX_RECORD_BYTES) throw new Error(
|
|
8064
|
-
|
|
8065
|
-
bytes.subarray(0, count).toString("utf8")
|
|
8066
|
-
);
|
|
8067
|
-
validate(record5, identity2);
|
|
8068
|
-
return record5;
|
|
9287
|
+
const stat = import_node_fs24.default.fstatSync(descriptor);
|
|
9288
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || stat.size > MAX_RECORD_BYTES || (stat.mode & 63) !== 0) {
|
|
9289
|
+
throw new Error(`${label} is not a bounded private file.`);
|
|
9290
|
+
}
|
|
9291
|
+
const bytes2 = Buffer.alloc(MAX_RECORD_BYTES + 1);
|
|
9292
|
+
const count = import_node_fs24.default.readSync(descriptor, bytes2, 0, bytes2.length, 0);
|
|
9293
|
+
if (count > MAX_RECORD_BYTES) throw new Error(`${label} exceeds its byte limit.`);
|
|
9294
|
+
return JSON.parse(bytes2.subarray(0, count).toString("utf8"));
|
|
8069
9295
|
} finally {
|
|
8070
|
-
|
|
9296
|
+
import_node_fs24.default.closeSync(descriptor);
|
|
8071
9297
|
}
|
|
8072
9298
|
}
|
|
8073
9299
|
function persist(record5) {
|
|
8074
|
-
|
|
9300
|
+
validate2(record5, record5.identity);
|
|
8075
9301
|
const contents = `${JSON.stringify(record5)}
|
|
8076
9302
|
`;
|
|
8077
9303
|
if (Buffer.byteLength(contents) > MAX_RECORD_BYTES)
|
|
@@ -8082,23 +9308,23 @@ function persist(record5) {
|
|
|
8082
9308
|
} catch (error) {
|
|
8083
9309
|
const reread = readReliabilityOperation(record5.identity);
|
|
8084
9310
|
if (!reread || JSON.stringify(reread) !== JSON.stringify(record5)) throw error;
|
|
8085
|
-
const descriptor =
|
|
9311
|
+
const descriptor = import_node_fs24.default.openSync(file, import_node_fs24.default.constants.O_RDONLY | import_node_fs24.default.constants.O_NOFOLLOW);
|
|
8086
9312
|
try {
|
|
8087
|
-
|
|
9313
|
+
import_node_fs24.default.fsyncSync(descriptor);
|
|
8088
9314
|
} finally {
|
|
8089
|
-
|
|
9315
|
+
import_node_fs24.default.closeSync(descriptor);
|
|
8090
9316
|
}
|
|
8091
|
-
const directory =
|
|
9317
|
+
const directory = import_node_fs24.default.openSync(import_node_path30.default.dirname(file), "r");
|
|
8092
9318
|
try {
|
|
8093
|
-
|
|
9319
|
+
import_node_fs24.default.fsyncSync(directory);
|
|
8094
9320
|
} finally {
|
|
8095
|
-
|
|
9321
|
+
import_node_fs24.default.closeSync(directory);
|
|
8096
9322
|
}
|
|
8097
9323
|
}
|
|
8098
9324
|
}
|
|
8099
9325
|
function updateReliabilityOperation(identity2, operation) {
|
|
8100
9326
|
const file = reliabilityOperationPath(identity2);
|
|
8101
|
-
|
|
9327
|
+
import_node_fs24.default.mkdirSync(import_node_path30.default.dirname(file), { recursive: true, mode: 448 });
|
|
8102
9328
|
return withFileLockSync(`${file}.lock`, { activity: "reliability intent", waitMs: 2e3 }, () => {
|
|
8103
9329
|
const record5 = readReliabilityOperation(identity2) ?? {
|
|
8104
9330
|
version: 1,
|
|
@@ -8111,22 +9337,37 @@ function updateReliabilityOperation(identity2, operation) {
|
|
|
8111
9337
|
};
|
|
8112
9338
|
if (record5.revision === Number.MAX_SAFE_INTEGER)
|
|
8113
9339
|
throw new Error("Reliability record revision is exhausted.");
|
|
9340
|
+
const previousVersion = record5.version;
|
|
9341
|
+
const previousEnrollment = record5.enrollment && { ...record5.enrollment };
|
|
8114
9342
|
const result = operation(record5);
|
|
8115
9343
|
if (result && typeof result.then === "function") {
|
|
8116
9344
|
throw new Error("Reliability record updates must be synchronous.");
|
|
8117
9345
|
}
|
|
8118
|
-
|
|
9346
|
+
if (record5.version < previousVersion || previousEnrollment && !(0, import_node_util5.isDeepStrictEqual)(record5.enrollment, previousEnrollment))
|
|
9347
|
+
throw new Error("Durable capacity enrollment cannot be downgraded or replaced.");
|
|
9348
|
+
record5.writtenByVersion = installedCliVersion();
|
|
9349
|
+
validate2(record5, identity2);
|
|
8119
9350
|
record5.revision += 1;
|
|
8120
9351
|
persist(record5);
|
|
8121
9352
|
return result;
|
|
8122
9353
|
});
|
|
8123
9354
|
}
|
|
9355
|
+
function clearStartupWitness(identity2, fence) {
|
|
9356
|
+
updateReliabilityOperation(identity2, (record5) => {
|
|
9357
|
+
if (record5.startupWitness === void 0 || record5.startupWitness === null) return;
|
|
9358
|
+
const live = reliabilityFence(record5.state);
|
|
9359
|
+
if (Object.entries(fence).some(([key, value]) => value !== live[key]))
|
|
9360
|
+
throw new Error("Startup witness clearing fence changed.");
|
|
9361
|
+
record5.startupWitness = null;
|
|
9362
|
+
});
|
|
9363
|
+
}
|
|
8124
9364
|
|
|
8125
9365
|
// src/core/reliability-worker.ts
|
|
8126
9366
|
var import_node_child_process16 = require("child_process");
|
|
8127
|
-
var
|
|
8128
|
-
var
|
|
8129
|
-
var
|
|
9367
|
+
var import_node_crypto15 = require("crypto");
|
|
9368
|
+
var import_node_fs25 = __toESM(require("fs"));
|
|
9369
|
+
var import_node_path31 = __toESM(require("path"));
|
|
9370
|
+
var DEFAULT_OUTPUT_PAGE_BYTES = 48 * 1024;
|
|
8130
9371
|
function workerGroupAbsent(pid) {
|
|
8131
9372
|
try {
|
|
8132
9373
|
process.kill(-pid, 0);
|
|
@@ -8138,10 +9379,10 @@ function workerGroupAbsent(pid) {
|
|
|
8138
9379
|
|
|
8139
9380
|
// src/core/traefik-route-health.ts
|
|
8140
9381
|
var import_node_child_process17 = require("child_process");
|
|
8141
|
-
var
|
|
8142
|
-
var
|
|
9382
|
+
var import_node_fs26 = __toESM(require("fs"));
|
|
9383
|
+
var import_node_path32 = __toESM(require("path"));
|
|
8143
9384
|
var ROUTER_API_BASE = "http://127.0.0.1:8080/api";
|
|
8144
|
-
var ROUTER_RELOAD_LOCK_FILE =
|
|
9385
|
+
var ROUTER_RELOAD_LOCK_FILE = import_node_path32.default.join(DEVROUTER_HOME, "router-reload.lock");
|
|
8145
9386
|
var ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
|
|
8146
9387
|
var DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
|
|
8147
9388
|
var DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
|
|
@@ -8410,7 +9651,7 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
8410
9651
|
throw new Error(
|
|
8411
9652
|
"Traefik routes did not reload; shared router restart is disabled for this operation."
|
|
8412
9653
|
);
|
|
8413
|
-
|
|
9654
|
+
import_node_fs26.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
8414
9655
|
return withFileLock(
|
|
8415
9656
|
ROUTER_RELOAD_LOCK_FILE,
|
|
8416
9657
|
{
|
|
@@ -8462,19 +9703,19 @@ function assertTraefikRoutesRemoved(routes) {
|
|
|
8462
9703
|
|
|
8463
9704
|
// src/core/workspace-ensure.ts
|
|
8464
9705
|
var import_node_child_process26 = require("child_process");
|
|
8465
|
-
var
|
|
8466
|
-
var
|
|
9706
|
+
var import_node_fs32 = __toESM(require("fs"));
|
|
9707
|
+
var import_node_path37 = __toESM(require("path"));
|
|
8467
9708
|
|
|
8468
9709
|
// src/core/devpod-mutation.ts
|
|
8469
9710
|
var import_node_child_process18 = require("child_process");
|
|
8470
|
-
var
|
|
8471
|
-
var
|
|
8472
|
-
var DEVPOD_MUTATION_LOCK_FILE =
|
|
9711
|
+
var import_node_fs27 = __toESM(require("fs"));
|
|
9712
|
+
var import_node_path33 = __toESM(require("path"));
|
|
9713
|
+
var DEVPOD_MUTATION_LOCK_FILE = import_node_path33.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
|
|
8473
9714
|
var DEVPOD_MUTATION_WAIT_MS = 18e5;
|
|
8474
9715
|
var DevpodStartPostconditionError = class extends Error {
|
|
8475
9716
|
};
|
|
8476
9717
|
function withMutationLock2(activity, target, operation) {
|
|
8477
|
-
|
|
9718
|
+
import_node_fs27.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
8478
9719
|
return withFileLockSync(
|
|
8479
9720
|
DEVPOD_MUTATION_LOCK_FILE,
|
|
8480
9721
|
{
|
|
@@ -8813,7 +10054,7 @@ function transportFailure() {
|
|
|
8813
10054
|
|
|
8814
10055
|
// src/core/managed-host-preparation.ts
|
|
8815
10056
|
var import_node_child_process20 = require("child_process");
|
|
8816
|
-
var
|
|
10057
|
+
var import_node_fs28 = __toESM(require("fs"));
|
|
8817
10058
|
var PREPARATION_TIMEOUT_MS = 6e4;
|
|
8818
10059
|
var CONFIG_READ_ERROR = "Managed host preparation could not read .devrouter.yml.";
|
|
8819
10060
|
var CONFIG_CHANGED_ERROR = "Managed host preparation changed .devrouter.yml; refusing to continue.";
|
|
@@ -8856,7 +10097,7 @@ function runPreparationCommand(argv, cwd) {
|
|
|
8856
10097
|
}
|
|
8857
10098
|
function readConfigBytes(configPath) {
|
|
8858
10099
|
try {
|
|
8859
|
-
return
|
|
10100
|
+
return import_node_fs28.default.readFileSync(configPath);
|
|
8860
10101
|
} catch {
|
|
8861
10102
|
return void 0;
|
|
8862
10103
|
}
|
|
@@ -8891,9 +10132,9 @@ async function runManagedHostPreparation(repoPath, config) {
|
|
|
8891
10132
|
|
|
8892
10133
|
// src/core/managed-post-start.ts
|
|
8893
10134
|
var import_node_child_process21 = require("child_process");
|
|
8894
|
-
var
|
|
8895
|
-
var
|
|
8896
|
-
var
|
|
10135
|
+
var import_node_crypto16 = require("crypto");
|
|
10136
|
+
var import_node_fs29 = __toESM(require("fs"));
|
|
10137
|
+
var import_node_path34 = __toESM(require("path"));
|
|
8897
10138
|
var MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
8898
10139
|
var MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
|
|
8899
10140
|
var RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
|
|
@@ -8906,7 +10147,7 @@ function renderDeliveryScript(targetPath) {
|
|
|
8906
10147
|
if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
|
|
8907
10148
|
throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
|
|
8908
10149
|
}
|
|
8909
|
-
const temporaryPrefix =
|
|
10150
|
+
const temporaryPrefix = import_node_path34.default.posix.basename(targetPath);
|
|
8910
10151
|
return `set -eu
|
|
8911
10152
|
umask 077
|
|
8912
10153
|
runtime_root=/tmp/devrouter
|
|
@@ -8941,32 +10182,32 @@ function deliverRuntimeFile(containerId, targetPath, contents, label) {
|
|
|
8941
10182
|
}
|
|
8942
10183
|
}
|
|
8943
10184
|
function readRegularFileBytes(filePath) {
|
|
8944
|
-
if (!
|
|
8945
|
-
return
|
|
10185
|
+
if (!import_node_fs29.default.existsSync(filePath) || !import_node_fs29.default.lstatSync(filePath).isFile()) return void 0;
|
|
10186
|
+
return import_node_fs29.default.readFileSync(filePath);
|
|
8946
10187
|
}
|
|
8947
10188
|
function readRegularFile(filePath) {
|
|
8948
10189
|
return readRegularFileBytes(filePath)?.toString("utf-8");
|
|
8949
10190
|
}
|
|
8950
10191
|
function adapterFingerprint(adapter) {
|
|
8951
|
-
return (0,
|
|
10192
|
+
return (0, import_node_crypto16.createHash)("sha256").update(adapter).digest("hex");
|
|
8952
10193
|
}
|
|
8953
10194
|
function resolveProcessHelperPath() {
|
|
8954
10195
|
const candidates = [
|
|
8955
|
-
|
|
8956
|
-
|
|
10196
|
+
import_node_path34.default.resolve(__dirname, "..", "bin", "devrouter-process"),
|
|
10197
|
+
import_node_path34.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
|
|
8957
10198
|
];
|
|
8958
|
-
const helperPath = candidates.find((candidate) =>
|
|
10199
|
+
const helperPath = candidates.find((candidate) => import_node_fs29.default.existsSync(candidate));
|
|
8959
10200
|
if (!helperPath) {
|
|
8960
10201
|
throw new Error("Could not locate the packaged devrouter-process helper.");
|
|
8961
10202
|
}
|
|
8962
10203
|
return helperPath;
|
|
8963
10204
|
}
|
|
8964
10205
|
function resolveManagedPostStartPlan(repoPath) {
|
|
8965
|
-
const adapterPath =
|
|
10206
|
+
const adapterPath = import_node_path34.default.join(repoPath, MANAGED_ADAPTER_PATH);
|
|
8966
10207
|
const adapterBytes = readRegularFileBytes(adapterPath);
|
|
8967
10208
|
const adapterText = adapterBytes?.toString("utf-8");
|
|
8968
|
-
const dockerfilePath =
|
|
8969
|
-
const devcontainerPath =
|
|
10209
|
+
const dockerfilePath = import_node_path34.default.join(repoPath, ".devcontainer", "Dockerfile");
|
|
10210
|
+
const devcontainerPath = import_node_path34.default.join(repoPath, ".devcontainer", "devcontainer.json");
|
|
8970
10211
|
const dockerfile = readRegularFile(dockerfilePath) ?? "";
|
|
8971
10212
|
const devcontainer = readRegularFile(devcontainerPath) ?? "";
|
|
8972
10213
|
const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
|
|
@@ -9007,7 +10248,7 @@ function resolveManagedPostStartPlan(repoPath) {
|
|
|
9007
10248
|
}
|
|
9008
10249
|
function runManagedPostStart(options) {
|
|
9009
10250
|
if (options.plan.kind !== "runtime") return;
|
|
9010
|
-
const helper =
|
|
10251
|
+
const helper = import_node_fs29.default.readFileSync(resolveProcessHelperPath());
|
|
9011
10252
|
deliverRuntimeFile(
|
|
9012
10253
|
options.container.id,
|
|
9013
10254
|
RUNTIME_HELPER_PATH,
|
|
@@ -9556,14 +10797,14 @@ function record4(value) {
|
|
|
9556
10797
|
}
|
|
9557
10798
|
return value;
|
|
9558
10799
|
}
|
|
9559
|
-
function
|
|
10800
|
+
function text3(value) {
|
|
9560
10801
|
if (typeof value !== "string" || !value || value.includes("\0")) {
|
|
9561
10802
|
throw new NetworkInventoryError("Docker network inventory contains an invalid identity.");
|
|
9562
10803
|
}
|
|
9563
10804
|
return value;
|
|
9564
10805
|
}
|
|
9565
|
-
function
|
|
9566
|
-
const result =
|
|
10806
|
+
function id4(value) {
|
|
10807
|
+
const result = text3(value);
|
|
9567
10808
|
if (!FULL_ID.test(result))
|
|
9568
10809
|
throw new NetworkInventoryError("Docker inventory requires full object IDs.");
|
|
9569
10810
|
return result;
|
|
@@ -9573,7 +10814,7 @@ function rows(output) {
|
|
|
9573
10814
|
return trimmed ? trimmed.split(/\r?\n/).map((line) => JSON.parse(line)) : [];
|
|
9574
10815
|
}
|
|
9575
10816
|
function ids(output) {
|
|
9576
|
-
const values = output.trim() ? output.trim().split(/\r?\n/).map(
|
|
10817
|
+
const values = output.trim() ? output.trim().split(/\r?\n/).map(id4) : [];
|
|
9577
10818
|
if (values.length > MAX_IDS || new Set(values).size !== values.length) {
|
|
9578
10819
|
throw new NetworkInventoryError("Docker network inventory has duplicate or too many objects.");
|
|
9579
10820
|
}
|
|
@@ -9619,7 +10860,7 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
9619
10860
|
read2(["info", "--format", '{"id":{{json .ID}},"pools":{{json .DefaultAddressPools}}}'])
|
|
9620
10861
|
)
|
|
9621
10862
|
);
|
|
9622
|
-
result.daemonId =
|
|
10863
|
+
result.daemonId = text3(info.id);
|
|
9623
10864
|
if (options.expectedDaemonId && result.daemonId !== options.expectedDaemonId) {
|
|
9624
10865
|
throw new NetworkInventoryError("Docker daemon identity changed before network inspection.");
|
|
9625
10866
|
}
|
|
@@ -9627,7 +10868,7 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
9627
10868
|
throw new NetworkInventoryError("Docker default pools are unavailable.");
|
|
9628
10869
|
result.pools = info.pools.map((value) => {
|
|
9629
10870
|
const pool = record4(value);
|
|
9630
|
-
const base =
|
|
10871
|
+
const base = text3(pool.Base);
|
|
9631
10872
|
if (!Number.isSafeInteger(pool.Size) || Number(pool.Size) < 0 || Number(pool.Size) > 128) {
|
|
9632
10873
|
throw new NetworkInventoryError("Docker default pool allocation size is invalid.");
|
|
9633
10874
|
}
|
|
@@ -9642,11 +10883,11 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
9642
10883
|
throw new NetworkInventoryError("Docker network IPAM or endpoint evidence is unavailable.");
|
|
9643
10884
|
}
|
|
9644
10885
|
return {
|
|
9645
|
-
id:
|
|
9646
|
-
name:
|
|
9647
|
-
driver:
|
|
10886
|
+
id: id4(network.id),
|
|
10887
|
+
name: text3(network.name),
|
|
10888
|
+
driver: text3(network.driver),
|
|
9648
10889
|
subnets: (network.ipam === null ? [] : network.ipam).map(
|
|
9649
|
-
(entry) =>
|
|
10890
|
+
(entry) => text3(record4(entry).Subnet)
|
|
9650
10891
|
),
|
|
9651
10892
|
activeEndpoints: Number(network.activeEndpoints),
|
|
9652
10893
|
retainedContainerIds: [],
|
|
@@ -9662,11 +10903,11 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
9662
10903
|
const inspected = [];
|
|
9663
10904
|
for (const value of containers) {
|
|
9664
10905
|
const container = record4(value);
|
|
9665
|
-
const containerId =
|
|
10906
|
+
const containerId = id4(container.id);
|
|
9666
10907
|
inspected.push(containerId);
|
|
9667
10908
|
for (const membership of Object.values(record4(container.networks))) {
|
|
9668
10909
|
const reference = record4(membership);
|
|
9669
|
-
const networkId =
|
|
10910
|
+
const networkId = id4(reference.NetworkID);
|
|
9670
10911
|
const network = byId.get(networkId);
|
|
9671
10912
|
if (!network)
|
|
9672
10913
|
throw new NetworkInventoryError("A retained container references an unobserved network.");
|
|
@@ -9684,7 +10925,7 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
9684
10925
|
"Docker object population changed during network inspection."
|
|
9685
10926
|
);
|
|
9686
10927
|
}
|
|
9687
|
-
const finalId =
|
|
10928
|
+
const finalId = text3(JSON.parse(read2(["info", "--format", "{{json .ID}}"])));
|
|
9688
10929
|
if (finalId !== result.daemonId)
|
|
9689
10930
|
throw new NetworkInventoryError("Docker daemon identity changed during inspection.");
|
|
9690
10931
|
result.status = "complete";
|
|
@@ -9697,36 +10938,36 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
9697
10938
|
|
|
9698
10939
|
// src/core/network-routes.ts
|
|
9699
10940
|
var import_node_child_process23 = require("child_process");
|
|
9700
|
-
var
|
|
10941
|
+
var import_node_net3 = require("net");
|
|
9701
10942
|
|
|
9702
10943
|
// src/core/network-local-daemon.ts
|
|
9703
|
-
var
|
|
9704
|
-
var
|
|
10944
|
+
var import_node_fs30 = __toESM(require("fs"));
|
|
10945
|
+
var import_node_path35 = __toESM(require("path"));
|
|
9705
10946
|
function hasLocalDockerNetworkNamespace(endpoint, procRoot = "/proc") {
|
|
9706
10947
|
const deadline = Date.now() + 2e3;
|
|
9707
10948
|
try {
|
|
9708
10949
|
if (!endpoint.startsWith("unix:///")) return false;
|
|
9709
10950
|
const socket = endpoint.slice("unix://".length);
|
|
9710
|
-
const table =
|
|
10951
|
+
const table = import_node_fs30.default.readFileSync(import_node_path35.default.join(procRoot, "net/unix"), "utf8");
|
|
9711
10952
|
if (Buffer.byteLength(table) > 1024 * 1024) return false;
|
|
9712
10953
|
const rows2 = table.split("\n").map((line) => line.trim().split(/\s+/)).filter((row) => row[7] === socket && row[3] === "00010000");
|
|
9713
10954
|
if (rows2.length !== 1 || !/^\d+$/.test(rows2[0][6])) return false;
|
|
9714
10955
|
const inode = `socket:[${rows2[0][6]}]`;
|
|
9715
|
-
const ownNamespace =
|
|
9716
|
-
const pids =
|
|
10956
|
+
const ownNamespace = import_node_fs30.default.readlinkSync(import_node_path35.default.join(procRoot, "self/ns/net"));
|
|
10957
|
+
const pids = import_node_fs30.default.readdirSync(procRoot).filter((name) => /^\d+$/.test(name));
|
|
9717
10958
|
if (pids.length > 4096) return false;
|
|
9718
10959
|
for (const pid of pids) {
|
|
9719
10960
|
if (Date.now() > deadline) return false;
|
|
9720
10961
|
try {
|
|
9721
|
-
const root =
|
|
9722
|
-
if (
|
|
9723
|
-
if (
|
|
9724
|
-
const descriptors =
|
|
10962
|
+
const root = import_node_path35.default.join(procRoot, pid);
|
|
10963
|
+
if (import_node_path35.default.basename(import_node_fs30.default.readlinkSync(import_node_path35.default.join(root, "exe"))) !== "dockerd") continue;
|
|
10964
|
+
if (import_node_fs30.default.readlinkSync(import_node_path35.default.join(root, "ns/net")) !== ownNamespace) continue;
|
|
10965
|
+
const descriptors = import_node_fs30.default.readdirSync(import_node_path35.default.join(root, "fd"));
|
|
9725
10966
|
if (descriptors.length > 4096) continue;
|
|
9726
10967
|
if (descriptors.some((fd) => {
|
|
9727
10968
|
if (Date.now() > deadline) return false;
|
|
9728
10969
|
try {
|
|
9729
|
-
return
|
|
10970
|
+
return import_node_fs30.default.readlinkSync(import_node_path35.default.join(root, "fd", fd)) === inode;
|
|
9730
10971
|
} catch {
|
|
9731
10972
|
return false;
|
|
9732
10973
|
}
|
|
@@ -9752,7 +10993,7 @@ function parseLinuxNetworkRoutes(output) {
|
|
|
9752
10993
|
const value = row;
|
|
9753
10994
|
if (typeof value.dst !== "string") throw new Error();
|
|
9754
10995
|
if (value.dst === "default") continue;
|
|
9755
|
-
if ((0,
|
|
10996
|
+
if ((0, import_node_net3.isIP)(value.dst.split("/")[0]) === 6) continue;
|
|
9756
10997
|
const cidr = parseIPv4Cidr(value.dst.includes("/") ? value.dst : `${value.dst}/32`);
|
|
9757
10998
|
if (!cidr || typeof value.dev !== "string" || !value.dev) throw new Error();
|
|
9758
10999
|
routes.push({ cidr, interface: value.dev, source: "host" });
|
|
@@ -9801,10 +11042,10 @@ function inspectLegacyNetworkCapacity(input) {
|
|
|
9801
11042
|
}
|
|
9802
11043
|
|
|
9803
11044
|
// src/core/network-managed.ts
|
|
9804
|
-
var
|
|
11045
|
+
var import_node_crypto19 = require("crypto");
|
|
9805
11046
|
|
|
9806
11047
|
// src/core/network-compose.ts
|
|
9807
|
-
var
|
|
11048
|
+
var import_node_crypto17 = require("crypto");
|
|
9808
11049
|
var SAFE_ENDPOINT_UPPER_BOUND_SEMANTICS = "full-lifecycle-demand";
|
|
9809
11050
|
function isRecord4(value) {
|
|
9810
11051
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -10610,19 +11851,19 @@ function stableValue(value) {
|
|
|
10610
11851
|
}
|
|
10611
11852
|
function fingerprintComposeNetworkModel(input) {
|
|
10612
11853
|
const projection = normalizedFingerprintProjection(input);
|
|
10613
|
-
return (0,
|
|
11854
|
+
return (0, import_node_crypto17.createHash)("sha256").update(stableValue(projection), "utf8").digest("hex");
|
|
10614
11855
|
}
|
|
10615
11856
|
|
|
10616
11857
|
// src/core/network-compose-files.ts
|
|
10617
11858
|
var import_node_child_process24 = require("child_process");
|
|
10618
|
-
var
|
|
10619
|
-
var
|
|
10620
|
-
var
|
|
11859
|
+
var import_node_crypto18 = require("crypto");
|
|
11860
|
+
var import_node_fs31 = __toESM(require("fs"));
|
|
11861
|
+
var import_node_path36 = __toESM(require("path"));
|
|
10621
11862
|
var import_yaml4 = __toESM(require("yaml"));
|
|
10622
11863
|
var OVERLAY = "docker-compose.devrouter-network.yml";
|
|
10623
11864
|
var MARKER = "# devrouter:managed workspace network";
|
|
10624
|
-
var
|
|
10625
|
-
function
|
|
11865
|
+
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
11866
|
+
function object3(value) {
|
|
10626
11867
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
10627
11868
|
throw new Error("Network Compose evidence is malformed.");
|
|
10628
11869
|
return value;
|
|
@@ -10630,20 +11871,20 @@ function object2(value) {
|
|
|
10630
11871
|
function inspectNetworkComposeFiles(input) {
|
|
10631
11872
|
const authoredNetworks = {};
|
|
10632
11873
|
for (const file of input.plan.composeFiles) {
|
|
10633
|
-
const stat =
|
|
10634
|
-
if (!stat.isFile() || stat.size >
|
|
11874
|
+
const stat = import_node_fs31.default.lstatSync(file);
|
|
11875
|
+
if (!stat.isFile() || stat.size > MAX_BYTES2)
|
|
10635
11876
|
throw new Error("Network Compose source is unavailable or too large.");
|
|
10636
11877
|
let source;
|
|
10637
11878
|
try {
|
|
10638
|
-
source =
|
|
11879
|
+
source = object3(import_yaml4.default.parse(import_node_fs31.default.readFileSync(file, "utf8")));
|
|
10639
11880
|
} catch {
|
|
10640
11881
|
throw new Error("Network Compose source could not be parsed safely.");
|
|
10641
11882
|
}
|
|
10642
11883
|
if (source.networks !== void 0) {
|
|
10643
|
-
for (const [name, declaration] of Object.entries(
|
|
11884
|
+
for (const [name, declaration] of Object.entries(object3(source.networks))) {
|
|
10644
11885
|
authoredNetworks[name] = {
|
|
10645
11886
|
...authoredNetworks[name],
|
|
10646
|
-
...declaration === null ? {} :
|
|
11887
|
+
...declaration === null ? {} : object3(declaration)
|
|
10647
11888
|
};
|
|
10648
11889
|
}
|
|
10649
11890
|
}
|
|
@@ -10671,13 +11912,13 @@ function inspectNetworkComposeFiles(input) {
|
|
|
10671
11912
|
env,
|
|
10672
11913
|
encoding: "utf8",
|
|
10673
11914
|
timeout: 1e4,
|
|
10674
|
-
maxBuffer:
|
|
11915
|
+
maxBuffer: MAX_BYTES2
|
|
10675
11916
|
}
|
|
10676
11917
|
);
|
|
10677
11918
|
if (result.error || result.status !== 0)
|
|
10678
11919
|
throw new Error("Complete network Compose evidence is unavailable.");
|
|
10679
11920
|
try {
|
|
10680
|
-
return { compose:
|
|
11921
|
+
return { compose: object3(JSON.parse(result.stdout)), authoredNetworks };
|
|
10681
11922
|
} catch {
|
|
10682
11923
|
throw new Error("Network Compose evidence is malformed.");
|
|
10683
11924
|
}
|
|
@@ -10685,7 +11926,7 @@ function inspectNetworkComposeFiles(input) {
|
|
|
10685
11926
|
function prepareNetworkComposeFiles(plan, subnet) {
|
|
10686
11927
|
const overlay = deriveComposeNetworkOverlay(subnet);
|
|
10687
11928
|
if (overlay.status !== "ready" || !overlay.overlay) throw new Error("Network subnet is invalid.");
|
|
10688
|
-
const overlayPath =
|
|
11929
|
+
const overlayPath = import_node_path36.default.join(plan.composeDirectory, OVERLAY);
|
|
10689
11930
|
const ignored = (0, import_node_child_process24.spawnSync)("git", [
|
|
10690
11931
|
"-C",
|
|
10691
11932
|
plan.composeDirectory,
|
|
@@ -10697,12 +11938,12 @@ function prepareNetworkComposeFiles(plan, subnet) {
|
|
|
10697
11938
|
]);
|
|
10698
11939
|
if (ignored.status !== 0)
|
|
10699
11940
|
throw new Error("Generated network overlay must be ignored before allocation.");
|
|
10700
|
-
const
|
|
11941
|
+
const bytes2 = `${MARKER}
|
|
10701
11942
|
${import_yaml4.default.stringify(overlay.overlay)}`;
|
|
10702
11943
|
const assertOwnership = () => {
|
|
10703
|
-
if (!
|
|
10704
|
-
const stat =
|
|
10705
|
-
if (!stat.isFile() || stat.size >
|
|
11944
|
+
if (!import_node_fs31.default.existsSync(overlayPath)) return;
|
|
11945
|
+
const stat = import_node_fs31.default.lstatSync(overlayPath);
|
|
11946
|
+
if (!stat.isFile() || stat.size > MAX_BYTES2 || import_node_fs31.default.readFileSync(overlayPath, "utf8") !== bytes2)
|
|
10706
11947
|
throw new Error("Retained network overlay changed; repair is required.");
|
|
10707
11948
|
};
|
|
10708
11949
|
assertOwnership();
|
|
@@ -10716,13 +11957,13 @@ ${JSON.stringify(extended.config, null, 2)}
|
|
|
10716
11957
|
...plan,
|
|
10717
11958
|
contents,
|
|
10718
11959
|
composeFiles: [...plan.composeFiles, overlayPath],
|
|
10719
|
-
effectiveConfigSha256: (0,
|
|
11960
|
+
effectiveConfigSha256: (0, import_node_crypto18.createHash)("sha256").update(contents).digest("hex")
|
|
10720
11961
|
};
|
|
10721
11962
|
return {
|
|
10722
11963
|
plan: result,
|
|
10723
11964
|
write: () => {
|
|
10724
11965
|
assertOwnership();
|
|
10725
|
-
writeFileAtomically(overlayPath,
|
|
11966
|
+
writeFileAtomically(overlayPath, bytes2);
|
|
10726
11967
|
}
|
|
10727
11968
|
};
|
|
10728
11969
|
}
|
|
@@ -11034,7 +12275,7 @@ function createManagedNetworkSession(input) {
|
|
|
11034
12275
|
"Insufficient endpoint headroom; request an allowed larger network before first allocation."
|
|
11035
12276
|
);
|
|
11036
12277
|
const authority = readNetworkOperationAuthority(identity2);
|
|
11037
|
-
const fingerprint = (0,
|
|
12278
|
+
const fingerprint = (0, import_node_crypto19.createHash)("sha256").update(input.plan().sourceConfigSha256).update(fingerprintComposeNetworkModel(model)).digest("hex");
|
|
11038
12279
|
if (!saved) {
|
|
11039
12280
|
if (policy.status !== "valid")
|
|
11040
12281
|
throw new Error("New network allocation requires valid machine policy.");
|
|
@@ -11218,11 +12459,11 @@ var DEFAULT_READINESS_TIMEOUT_MS = 12e4;
|
|
|
11218
12459
|
var POLL_INTERVAL_MS = 1e3;
|
|
11219
12460
|
function assertOverlay(container, repoPath) {
|
|
11220
12461
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
11221
|
-
if (!workingDir || !sameWorkspacePath(workingDir,
|
|
12462
|
+
if (!workingDir || !sameWorkspacePath(workingDir, import_node_path37.default.join(repoPath, ".devcontainer"))) {
|
|
11222
12463
|
throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
|
|
11223
12464
|
}
|
|
11224
12465
|
const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
|
|
11225
|
-
const expectedOverlay =
|
|
12466
|
+
const expectedOverlay = import_node_path37.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
11226
12467
|
if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
|
|
11227
12468
|
throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
|
|
11228
12469
|
}
|
|
@@ -11417,7 +12658,7 @@ function hasExactManagedComposeProject(repoPath, state) {
|
|
|
11417
12658
|
}).some((container) => {
|
|
11418
12659
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
11419
12660
|
return Boolean(
|
|
11420
|
-
workingDir && sameWorkspacePath(workingDir,
|
|
12661
|
+
workingDir && sameWorkspacePath(workingDir, import_node_path37.default.join(repoPath, ".devcontainer"))
|
|
11421
12662
|
);
|
|
11422
12663
|
});
|
|
11423
12664
|
} catch {
|
|
@@ -11801,7 +13042,7 @@ function resolvePrimaryTarget(repoPath) {
|
|
|
11801
13042
|
}
|
|
11802
13043
|
function isPrimaryCheckout(repoPath) {
|
|
11803
13044
|
try {
|
|
11804
|
-
return
|
|
13045
|
+
return import_node_fs32.default.statSync(import_node_path37.default.join(repoPath, ".git")).isDirectory();
|
|
11805
13046
|
} catch {
|
|
11806
13047
|
return false;
|
|
11807
13048
|
}
|
|
@@ -11864,8 +13105,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
11864
13105
|
const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
11865
13106
|
let devpodId = target.devpodId;
|
|
11866
13107
|
if (target.kind === "linked") {
|
|
11867
|
-
const overlayPath =
|
|
11868
|
-
if (!
|
|
13108
|
+
const overlayPath = import_node_path37.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
13109
|
+
if (!import_node_fs32.default.existsSync(overlayPath)) {
|
|
11869
13110
|
throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
|
|
11870
13111
|
}
|
|
11871
13112
|
}
|
|
@@ -11935,7 +13176,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
11935
13176
|
}
|
|
11936
13177
|
const apps = proxyAppsFromConfig(runtime.config);
|
|
11937
13178
|
const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
|
|
11938
|
-
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ??
|
|
13179
|
+
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path37.default.basename(repoPath)) ?? "app";
|
|
11939
13180
|
for (const [index, app] of apps.entries()) {
|
|
11940
13181
|
if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
|
|
11941
13182
|
const owner = target.kind === "linked" ? "workspace" : "checkout";
|
|
@@ -12186,6 +13427,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
12186
13427
|
claimLifecycleEffect();
|
|
12187
13428
|
writeManagedRuntimeState(state);
|
|
12188
13429
|
capturedStopState = state;
|
|
13430
|
+
clearActiveLifecycleStartupWitness();
|
|
12189
13431
|
};
|
|
12190
13432
|
if (options.repair) capture();
|
|
12191
13433
|
else
|
|
@@ -12754,9 +13996,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
12754
13996
|
}
|
|
12755
13997
|
const repairLocked = () => {
|
|
12756
13998
|
const runtime = resolveWorkspaceRuntimeOrDefault(repoPath);
|
|
12757
|
-
|
|
13999
|
+
import_node_fs32.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
12758
14000
|
return withFileLock(
|
|
12759
|
-
|
|
14001
|
+
import_node_path37.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
|
|
12760
14002
|
{
|
|
12761
14003
|
activity: "Managed runtime repair",
|
|
12762
14004
|
target: repoPath,
|
|
@@ -12828,6 +14070,7 @@ var lockHeld = false;
|
|
|
12828
14070
|
var stopProjects = [];
|
|
12829
14071
|
var stopBaselineState;
|
|
12830
14072
|
var BUSY_LIFECYCLE_WAIT_MS = 30 * 60 * 1e3;
|
|
14073
|
+
var CAPACITY_CLIENT_WAIT_MS = 30 * 60 * 1e3;
|
|
12831
14074
|
function matchesFence(record5, fence) {
|
|
12832
14075
|
return Object.entries(reliabilityFence(record5.state)).every(
|
|
12833
14076
|
([key, value]) => value === fence[key]
|
|
@@ -12844,6 +14087,7 @@ function claimActiveLifecycleEffect() {
|
|
|
12844
14087
|
if (!matchesFence(record5, worker.fence) || worker.kind !== "stop" && record5.worker?.id !== worker.workerId) {
|
|
12845
14088
|
throw new Error("Lifecycle intent superseded this worker.");
|
|
12846
14089
|
}
|
|
14090
|
+
if (worker.kind !== "stop") assertCapacityEffect(record5, worker.workerId, Date.now());
|
|
12847
14091
|
if (record5.effectSequence === Number.MAX_SAFE_INTEGER)
|
|
12848
14092
|
throw new Error("Lifecycle effect sequence exhausted.");
|
|
12849
14093
|
record5.effectSequence += 1;
|
|
@@ -12861,7 +14105,9 @@ async function withLifecycleOperationLock(repoPath, operation) {
|
|
|
12861
14105
|
function stepRecord(record5, event) {
|
|
12862
14106
|
const transition2 = stepReliability(record5.state, event, Date.now());
|
|
12863
14107
|
if (!["accepted", "joined"].includes(transition2.outcome))
|
|
12864
|
-
throw new Error(
|
|
14108
|
+
throw new Error(
|
|
14109
|
+
`Lifecycle transition is ${transition2.outcome}.${transition2.reason ? ` ${transition2.reason}` : ""}`
|
|
14110
|
+
);
|
|
12865
14111
|
record5.state = transition2.state;
|
|
12866
14112
|
}
|
|
12867
14113
|
function reconcileDrained(record5) {
|
|
@@ -12896,6 +14142,13 @@ function reconcileDrained(record5) {
|
|
|
12896
14142
|
record5.worker = null;
|
|
12897
14143
|
}
|
|
12898
14144
|
}
|
|
14145
|
+
function clearActiveLifecycleStartupWitness() {
|
|
14146
|
+
const request = activeWorker;
|
|
14147
|
+
if (!request || request.kind === "stop") return;
|
|
14148
|
+
const record5 = readReliabilityOperation(request.identity);
|
|
14149
|
+
if (record5?.startupWitness?.operationId !== request.operationId) return;
|
|
14150
|
+
clearStartupWitness(request.identity, reliabilityFence(record5.state));
|
|
14151
|
+
}
|
|
12899
14152
|
async function acquireWorkerLock(repoPath, operation) {
|
|
12900
14153
|
const deadline = Date.now() + 3e4;
|
|
12901
14154
|
for (; ; ) {
|
|
@@ -12952,7 +14205,7 @@ async function executeLifecycleWorker(request, operation) {
|
|
|
12952
14205
|
const owned = containers.filter(
|
|
12953
14206
|
(container) => sameWorkspacePath(
|
|
12954
14207
|
container.labels["com.docker.compose.project.working_dir"] ?? "",
|
|
12955
|
-
|
|
14208
|
+
import_node_path38.default.join(request.repoPath, ".devcontainer")
|
|
12956
14209
|
)
|
|
12957
14210
|
);
|
|
12958
14211
|
const projects = owned.map((container) => container.labels["com.docker.compose.project"]);
|
|
@@ -12978,7 +14231,7 @@ async function executeLifecycleWorker(request, operation) {
|
|
|
12978
14231
|
}
|
|
12979
14232
|
});
|
|
12980
14233
|
}
|
|
12981
|
-
function recordLifecycleOutcome(outcome) {
|
|
14234
|
+
function recordLifecycleOutcome(outcome, result) {
|
|
12982
14235
|
const request = activeWorker;
|
|
12983
14236
|
if (request?.kind !== "exec") return;
|
|
12984
14237
|
updateReliabilityOperation(request.identity, (record5) => {
|
|
@@ -12996,27 +14249,40 @@ function recordLifecycleOutcome(outcome) {
|
|
|
12996
14249
|
};
|
|
12997
14250
|
stepRecord(record5, event);
|
|
12998
14251
|
record5.outcome = { ...outcome, operationId: request.operationId };
|
|
14252
|
+
if (result !== void 0) record5.result = result;
|
|
12999
14253
|
});
|
|
13000
14254
|
}
|
|
13001
|
-
function recordLifecycleCompletion(exitCode) {
|
|
14255
|
+
function recordLifecycleCompletion(exitCode, preparedProfile, result) {
|
|
13002
14256
|
const request = activeWorker;
|
|
13003
14257
|
if (!request || request.kind === "stop") return;
|
|
13004
14258
|
updateReliabilityOperation(request.identity, (record5) => {
|
|
13005
14259
|
if (!matchesFence(record5, request.fence)) return;
|
|
14260
|
+
if (preparedProfile !== void 0 && record5.enrollment) {
|
|
14261
|
+
if (request.kind !== "ensure" || record5.worker?.id !== request.workerId || record5.state.operation?.id !== request.operationId || record5.state.profile !== preparedProfile)
|
|
14262
|
+
throw new Error("Prepared profile does not match the active lifecycle worker.");
|
|
14263
|
+
record5.activeProfile = preparedProfile;
|
|
14264
|
+
record5.preparation = {
|
|
14265
|
+
operationId: request.operationId,
|
|
14266
|
+
profile: preparedProfile,
|
|
14267
|
+
fence: { ...request.fence }
|
|
14268
|
+
};
|
|
14269
|
+
}
|
|
13006
14270
|
stepRecord(record5, {
|
|
13007
14271
|
...request.fence,
|
|
13008
14272
|
type: "completion",
|
|
13009
14273
|
operationId: request.operationId,
|
|
13010
14274
|
exitCode
|
|
13011
14275
|
});
|
|
14276
|
+
if (result !== void 0) record5.result = result;
|
|
13012
14277
|
});
|
|
13013
14278
|
}
|
|
13014
|
-
function recordLifecycleUnknown() {
|
|
14279
|
+
function recordLifecycleUnknown(result) {
|
|
13015
14280
|
const request = activeWorker;
|
|
13016
14281
|
if (!request || request.kind === "stop") return;
|
|
13017
14282
|
updateReliabilityOperation(request.identity, (record5) => {
|
|
13018
14283
|
if (!matchesFence(record5, request.fence)) return;
|
|
13019
14284
|
stepRecord(record5, { ...request.fence, type: "interrupted", operationId: request.operationId });
|
|
14285
|
+
if (result !== void 0) record5.result = result;
|
|
13020
14286
|
});
|
|
13021
14287
|
}
|
|
13022
14288
|
function proveLifecycleStopped() {
|
|
@@ -13040,7 +14306,7 @@ function proveLifecycleStopped() {
|
|
|
13040
14306
|
const remaining = inspectWorkspaceContainers().filter(
|
|
13041
14307
|
(container) => sameWorkspacePath(
|
|
13042
14308
|
container.labels["com.docker.compose.project.working_dir"] ?? "",
|
|
13043
|
-
|
|
14309
|
+
import_node_path38.default.join(request.repoPath, ".devcontainer")
|
|
13044
14310
|
)
|
|
13045
14311
|
);
|
|
13046
14312
|
if (remaining.some(
|
|
@@ -13053,16 +14319,48 @@ function proveLifecycleStopped() {
|
|
|
13053
14319
|
(route) => sameWorkspacePath(route.repoPath, request.repoPath)
|
|
13054
14320
|
);
|
|
13055
14321
|
if (routes.length) throw new Error("Workspace routes remain published after stop.");
|
|
13056
|
-
updateReliabilityOperation(request.identity, (record5) => {
|
|
14322
|
+
const settlement = updateReliabilityOperation(request.identity, (record5) => {
|
|
13057
14323
|
if (!matchesFence(record5, request.fence) || record5.worker)
|
|
13058
14324
|
throw new Error("Stop proof was superseded or an earlier worker remains.");
|
|
14325
|
+
if (record5.version === 2) {
|
|
14326
|
+
if (record5.capacity) record5.capacity.validUntilMs = 0;
|
|
14327
|
+
return record5.state.environmentId;
|
|
14328
|
+
}
|
|
13059
14329
|
stepRecord(record5, {
|
|
13060
14330
|
...request.fence,
|
|
13061
14331
|
type: "stop-proof",
|
|
13062
14332
|
workloadsStopped: true,
|
|
13063
14333
|
routesRemoved: true
|
|
13064
14334
|
});
|
|
14335
|
+
return void 0;
|
|
13065
14336
|
});
|
|
14337
|
+
if (settlement) {
|
|
14338
|
+
const capacity = new CapacityStore(import_node_path38.default.join(DEVROUTER_HOME, "controller"));
|
|
14339
|
+
for (let attempt = 0; ; attempt++) {
|
|
14340
|
+
try {
|
|
14341
|
+
capacity.settleEnvironmentAfterStop(settlement, capacity.read().revision);
|
|
14342
|
+
break;
|
|
14343
|
+
} catch (error) {
|
|
14344
|
+
if (!(error instanceof CapacitySnapshotChangedError) || attempt >= 2) throw error;
|
|
14345
|
+
}
|
|
14346
|
+
}
|
|
14347
|
+
updateReliabilityOperation(request.identity, (record5) => {
|
|
14348
|
+
if (!matchesFence(record5, request.fence) || record5.worker || record5.state.environmentId !== settlement || record5.capacity && record5.capacity.validUntilMs !== 0)
|
|
14349
|
+
throw new Error("Capacity settlement was superseded before journal confirmation.");
|
|
14350
|
+
record5.capacity = null;
|
|
14351
|
+
if (record5.enrollment) {
|
|
14352
|
+
record5.phaseSettlement = null;
|
|
14353
|
+
record5.activeProfile = null;
|
|
14354
|
+
record5.preparation = null;
|
|
14355
|
+
}
|
|
14356
|
+
stepRecord(record5, {
|
|
14357
|
+
...request.fence,
|
|
14358
|
+
type: "stop-proof",
|
|
14359
|
+
workloadsStopped: true,
|
|
14360
|
+
routesRemoved: true
|
|
14361
|
+
});
|
|
14362
|
+
});
|
|
14363
|
+
}
|
|
13066
14364
|
};
|
|
13067
14365
|
if (stopBaselineState)
|
|
13068
14366
|
withMutationLock("Settle retained stop", stopBaselineState.repoPath, settle);
|
|
@@ -13198,7 +14496,7 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
13198
14496
|
}
|
|
13199
14497
|
assertRunningDevpod(devpod.id, repoPath);
|
|
13200
14498
|
const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
|
|
13201
|
-
const statusMarker = `__DEVROUTER_EXIT_${(0,
|
|
14499
|
+
const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto21.randomUUID)()}__:`;
|
|
13202
14500
|
const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
|
|
13203
14501
|
const literalCommand = command.map(quotePosixArg).join(" ");
|
|
13204
14502
|
const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
|
|
@@ -13388,11 +14686,11 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
13388
14686
|
|
|
13389
14687
|
// src/core/workspace-lifecycle.ts
|
|
13390
14688
|
var import_node_child_process29 = require("child_process");
|
|
13391
|
-
var
|
|
13392
|
-
var
|
|
14689
|
+
var import_node_fs34 = __toESM(require("fs"));
|
|
14690
|
+
var import_node_path39 = __toESM(require("path"));
|
|
13393
14691
|
|
|
13394
14692
|
// src/core/route-state.ts
|
|
13395
|
-
var
|
|
14693
|
+
var import_node_fs33 = __toESM(require("fs"));
|
|
13396
14694
|
function listRoutesForWorktreePath(worktreePath, routes = listHostRouteState()) {
|
|
13397
14695
|
return routes.filter((route) => sameWorkspacePath(route.repoPath, worktreePath));
|
|
13398
14696
|
}
|
|
@@ -13597,20 +14895,26 @@ process.once("message", async (message) => {
|
|
|
13597
14895
|
request.command ?? [],
|
|
13598
14896
|
request.retainedExecProof
|
|
13599
14897
|
) : await devpodExecOutcome(request.repoPath, request.command ?? []);
|
|
13600
|
-
recordLifecycleOutcome(outcome);
|
|
14898
|
+
recordLifecycleOutcome(outcome, { ok: true, value: outcome });
|
|
13601
14899
|
return outcome;
|
|
13602
14900
|
}
|
|
13603
14901
|
const ensured = await workspaceEnsure(request.repoPath, request.options);
|
|
13604
14902
|
recordLifecycleCompletion(
|
|
13605
|
-
ensured.applicationReadiness?.status === "application-error" ? 1 : 0
|
|
14903
|
+
ensured.applicationReadiness?.status === "application-error" ? 1 : 0,
|
|
14904
|
+
ensured.managedRuntime?.status === "ready" ? ensured.profile : void 0,
|
|
14905
|
+
{ ok: true, value: ensured }
|
|
13606
14906
|
);
|
|
13607
14907
|
return ensured;
|
|
13608
14908
|
});
|
|
13609
14909
|
result = { ok: true, value };
|
|
13610
14910
|
} catch (error) {
|
|
13611
14911
|
try {
|
|
13612
|
-
|
|
13613
|
-
|
|
14912
|
+
const failure = {
|
|
14913
|
+
ok: false,
|
|
14914
|
+
message: error instanceof Error ? error.message : String(error)
|
|
14915
|
+
};
|
|
14916
|
+
if (error instanceof ExecutionOutcomeError) recordLifecycleOutcome(error.outcome, failure);
|
|
14917
|
+
else recordLifecycleUnknown(failure);
|
|
13614
14918
|
} catch {
|
|
13615
14919
|
}
|
|
13616
14920
|
result = { ok: false, message: error instanceof Error ? error.message : String(error) };
|