@devrouter/cli 0.0.67 → 0.0.69
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 +1821 -285
- package/dist/devrouter.js +19245 -14619
- package/package.json +2 -1
- package/upgrade-prompts/0.0.68.md +51 -0
- package/upgrade-prompts/0.0.69.md +41 -0
|
@@ -23,8 +23,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
|
|
25
25
|
// src/core/devpod-exec.ts
|
|
26
|
-
var
|
|
27
|
-
var
|
|
26
|
+
var import_node_child_process29 = require("child_process");
|
|
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
|
}
|
|
@@ -1747,7 +1769,7 @@ function selectDevpodWorkspace(workspaces, repoPath) {
|
|
|
1747
1769
|
}
|
|
1748
1770
|
|
|
1749
1771
|
// src/core/devsy-exec.ts
|
|
1750
|
-
var
|
|
1772
|
+
var import_node_child_process28 = require("child_process");
|
|
1751
1773
|
|
|
1752
1774
|
// src/core/devsy-exec-proof.ts
|
|
1753
1775
|
var import_node_fs6 = __toESM(require("fs"));
|
|
@@ -1836,7 +1858,7 @@ function revalidateDevsyExecProof(repoPath, proof) {
|
|
|
1836
1858
|
|
|
1837
1859
|
// src/core/devsy-mutation.ts
|
|
1838
1860
|
var import_node_child_process15 = require("child_process");
|
|
1839
|
-
var
|
|
1861
|
+
var import_node_fs20 = __toESM(require("fs"));
|
|
1840
1862
|
var import_node_path24 = __toESM(require("path"));
|
|
1841
1863
|
|
|
1842
1864
|
// src/core/devsy-agent.ts
|
|
@@ -1996,6 +2018,7 @@ function requireReadyDevsyAgent(options = {}) {
|
|
|
1996
2018
|
}
|
|
1997
2019
|
|
|
1998
2020
|
// src/core/managed-devsy-stop.ts
|
|
2021
|
+
var import_node_fs16 = __toESM(require("fs"));
|
|
1999
2022
|
var import_node_os4 = __toESM(require("os"));
|
|
2000
2023
|
var import_node_path20 = __toESM(require("path"));
|
|
2001
2024
|
var import_node_util4 = require("util");
|
|
@@ -2473,7 +2496,7 @@ function stopExactManagedService(containerId, service, options) {
|
|
|
2473
2496
|
var import_node_fs10 = __toESM(require("fs"));
|
|
2474
2497
|
var import_node_path12 = __toESM(require("path"));
|
|
2475
2498
|
function proveManagedComposePopulation(options) {
|
|
2476
|
-
const { plan, repoPath, composeProject,
|
|
2499
|
+
const { plan, repoPath, composeProject, providerRoot, featureDirectory, containers } = options;
|
|
2477
2500
|
const services = /* @__PURE__ */ new Set();
|
|
2478
2501
|
for (const container of containers) {
|
|
2479
2502
|
const service = container.labels["com.docker.compose.service"] ?? "";
|
|
@@ -2491,7 +2514,7 @@ function proveManagedComposePopulation(options) {
|
|
|
2491
2514
|
for (const file of files.slice(plan.composeFiles.length)) {
|
|
2492
2515
|
if (import_node_path12.default.dirname(file) !== featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
|
|
2493
2516
|
import_node_path12.default.basename(file)
|
|
2494
|
-
) || import_node_fs10.default.realpathSync(file) !== import_node_path12.default.join(import_node_fs10.default.realpathSync(
|
|
2517
|
+
) || import_node_fs10.default.realpathSync(file) !== import_node_path12.default.join(import_node_fs10.default.realpathSync(providerRoot), import_node_path12.default.relative(providerRoot, file))) {
|
|
2495
2518
|
throw new Error("Managed stop refuses a foreign or escaped provider Compose file.");
|
|
2496
2519
|
}
|
|
2497
2520
|
}
|
|
@@ -3220,8 +3243,8 @@ function managedRunnerId(uid, providerId) {
|
|
|
3220
3243
|
if (uid === void 0) {
|
|
3221
3244
|
return providerId;
|
|
3222
3245
|
}
|
|
3223
|
-
const
|
|
3224
|
-
return
|
|
3246
|
+
const bytes2 = Buffer.byteLength(uid);
|
|
3247
|
+
return bytes2 === 16 || bytes2 === 40 ? uid : providerId;
|
|
3225
3248
|
}
|
|
3226
3249
|
function identityOf(value) {
|
|
3227
3250
|
return {
|
|
@@ -3309,7 +3332,7 @@ function captureManagedStopBaseline(state, plan, primaryId, expectedEndpoint) {
|
|
|
3309
3332
|
repoPath: state.repoPath,
|
|
3310
3333
|
composeProject: state.composeProject,
|
|
3311
3334
|
featureDirectory,
|
|
3312
|
-
devsyRoot,
|
|
3335
|
+
providerRoot: devsyRoot,
|
|
3313
3336
|
containers
|
|
3314
3337
|
});
|
|
3315
3338
|
if (primary.id !== primaryId)
|
|
@@ -3413,7 +3436,7 @@ function proveReplacementStopAbsence(state, baseline, replacement) {
|
|
|
3413
3436
|
const newRunner = managedRunnerId(before.uid, baseline.providerId);
|
|
3414
3437
|
const runners = oldRunner === newRunner ? [oldRunner] : [oldRunner, newRunner];
|
|
3415
3438
|
const assertEmptyPopulation = () => {
|
|
3416
|
-
if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || runners.some((
|
|
3439
|
+
if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || runners.some((id5) => inspectProviderRunnerContainers(baseline.endpoint, id5).length !== 0))
|
|
3417
3440
|
throw new Error("Replacement stop observed a remaining workspace population.");
|
|
3418
3441
|
};
|
|
3419
3442
|
assertEmptyPopulation();
|
|
@@ -3496,6 +3519,7 @@ function managedStopRouteReferences(state) {
|
|
|
3496
3519
|
}
|
|
3497
3520
|
|
|
3498
3521
|
// src/core/repo-config.ts
|
|
3522
|
+
var import_node_crypto7 = require("crypto");
|
|
3499
3523
|
var import_node_fs14 = __toESM(require("fs"));
|
|
3500
3524
|
var import_node_path18 = __toESM(require("path"));
|
|
3501
3525
|
var import_yaml3 = __toESM(require("yaml"));
|
|
@@ -3858,10 +3882,10 @@ function replaceHostRoutesForRepo(repoPath, inputs) {
|
|
|
3858
3882
|
}
|
|
3859
3883
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3860
3884
|
const replacements = inputs.map((input) => {
|
|
3861
|
-
const
|
|
3885
|
+
const id5 = buildHostRouteId(input.repoPath, input.name);
|
|
3862
3886
|
return hostRouteFromInput(
|
|
3863
3887
|
input,
|
|
3864
|
-
routes.find((route) => route.id ===
|
|
3888
|
+
routes.find((route) => route.id === id5),
|
|
3865
3889
|
now
|
|
3866
3890
|
);
|
|
3867
3891
|
});
|
|
@@ -3903,6 +3927,10 @@ var VALID_HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-
|
|
|
3903
3927
|
var DEVROUTER_VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
3904
3928
|
var VALID_ENV_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/i;
|
|
3905
3929
|
var VALID_ENV_VAR_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3930
|
+
var MAX_CAPACITY_PROFILES = 256;
|
|
3931
|
+
var MAX_CAPACITY_OPERATIONS = 64;
|
|
3932
|
+
var MAX_CAPACITY_TRANSITIONS = 1024;
|
|
3933
|
+
var MAX_CAPACITY_NAME_LENGTH = 64;
|
|
3906
3934
|
var UPSTREAM_TEMPLATE_RE = /^(?:\$\{WORKSPACE\}|[a-zA-Z0-9._-])+:\d{1,5}$/;
|
|
3907
3935
|
function assertUpstreamSpec(upstream, label) {
|
|
3908
3936
|
if (upstream.includes(WORKSPACE_PLACEHOLDER)) {
|
|
@@ -4458,11 +4486,265 @@ function parseManagedRuntime(value, configPath) {
|
|
|
4458
4486
|
processes
|
|
4459
4487
|
};
|
|
4460
4488
|
}
|
|
4489
|
+
function parseCapacitySafeInteger(value, pathLabel, minimum) {
|
|
4490
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || Object.is(value, -0)) {
|
|
4491
|
+
throw new Error(`${pathLabel} must be a safe integer.`);
|
|
4492
|
+
}
|
|
4493
|
+
if (value < minimum) {
|
|
4494
|
+
throw new Error(`${pathLabel} must be at least ${minimum}.`);
|
|
4495
|
+
}
|
|
4496
|
+
return value;
|
|
4497
|
+
}
|
|
4498
|
+
function parseCapacityName(value, pathLabel) {
|
|
4499
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
4500
|
+
throw new Error(`${pathLabel} must be a non-empty string.`);
|
|
4501
|
+
}
|
|
4502
|
+
if (value !== value.trim()) {
|
|
4503
|
+
throw new Error(`${pathLabel} must not have leading or trailing whitespace.`);
|
|
4504
|
+
}
|
|
4505
|
+
if (value.length > MAX_CAPACITY_NAME_LENGTH) {
|
|
4506
|
+
throw new Error(
|
|
4507
|
+
`${pathLabel} exceeds the maximum length of ${MAX_CAPACITY_NAME_LENGTH} characters.`
|
|
4508
|
+
);
|
|
4509
|
+
}
|
|
4510
|
+
if ([...value].some(
|
|
4511
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127
|
|
4512
|
+
)) {
|
|
4513
|
+
throw new Error(`${pathLabel} must not contain control characters.`);
|
|
4514
|
+
}
|
|
4515
|
+
return value;
|
|
4516
|
+
}
|
|
4517
|
+
function parseCapacityProfileKey(value, pathLabel, declaredProfiles) {
|
|
4518
|
+
const key = parseCapacityName(value, pathLabel);
|
|
4519
|
+
if (key === "full") return key;
|
|
4520
|
+
const names = key.split(",");
|
|
4521
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4522
|
+
for (const [index, name] of names.entries()) {
|
|
4523
|
+
if (!PROFILE_NAME_RE.test(name)) {
|
|
4524
|
+
throw new Error(`${pathLabel}[${index}] is not a valid profile name.`);
|
|
4525
|
+
}
|
|
4526
|
+
if (seen.has(name)) {
|
|
4527
|
+
throw new Error(`${pathLabel} contains duplicate profile '${name}'.`);
|
|
4528
|
+
}
|
|
4529
|
+
if (!declaredProfiles || !Object.hasOwn(declaredProfiles, name)) {
|
|
4530
|
+
throw new Error(`${pathLabel} references undefined profile '${name}'.`);
|
|
4531
|
+
}
|
|
4532
|
+
seen.add(name);
|
|
4533
|
+
}
|
|
4534
|
+
return [...names].sort().join(",");
|
|
4535
|
+
}
|
|
4536
|
+
function parseCapacityDimension(value, pathLabel, runtime) {
|
|
4537
|
+
const dimension = ensureObject(value, pathLabel);
|
|
4538
|
+
ensureAllowedKeys(dimension, ["steadyBytes", "startupTotalBytes"], pathLabel);
|
|
4539
|
+
const minimum = runtime ? 1 : 0;
|
|
4540
|
+
const steadyBytes = parseCapacitySafeInteger(
|
|
4541
|
+
dimension.steadyBytes,
|
|
4542
|
+
`${pathLabel}.steadyBytes`,
|
|
4543
|
+
minimum
|
|
4544
|
+
);
|
|
4545
|
+
const startupTotalBytes = parseCapacitySafeInteger(
|
|
4546
|
+
dimension.startupTotalBytes,
|
|
4547
|
+
`${pathLabel}.startupTotalBytes`,
|
|
4548
|
+
minimum
|
|
4549
|
+
);
|
|
4550
|
+
if (startupTotalBytes < steadyBytes) {
|
|
4551
|
+
throw new Error(`${pathLabel}.startupTotalBytes must be at least steadyBytes.`);
|
|
4552
|
+
}
|
|
4553
|
+
return { steadyBytes, startupTotalBytes };
|
|
4554
|
+
}
|
|
4555
|
+
function parseCapacityOperation(value, pathLabel) {
|
|
4556
|
+
const operation = ensureObject(value, pathLabel);
|
|
4557
|
+
ensureAllowedKeys(operation, ["hostIncrementBytes", "runtimeIncrementBytes"], pathLabel);
|
|
4558
|
+
return {
|
|
4559
|
+
hostIncrementBytes: parseCapacitySafeInteger(
|
|
4560
|
+
operation.hostIncrementBytes,
|
|
4561
|
+
`${pathLabel}.hostIncrementBytes`,
|
|
4562
|
+
0
|
|
4563
|
+
),
|
|
4564
|
+
runtimeIncrementBytes: parseCapacitySafeInteger(
|
|
4565
|
+
operation.runtimeIncrementBytes,
|
|
4566
|
+
`${pathLabel}.runtimeIncrementBytes`,
|
|
4567
|
+
0
|
|
4568
|
+
)
|
|
4569
|
+
};
|
|
4570
|
+
}
|
|
4571
|
+
function parseCapacityOperations(value, pathLabel) {
|
|
4572
|
+
const rawOperations = ensureObject(value, pathLabel);
|
|
4573
|
+
const operationEntries = Object.entries(rawOperations);
|
|
4574
|
+
if (operationEntries.length === 0) {
|
|
4575
|
+
throw new Error(`${pathLabel} must define at least one named operation.`);
|
|
4576
|
+
}
|
|
4577
|
+
if (operationEntries.length > MAX_CAPACITY_OPERATIONS) {
|
|
4578
|
+
throw new Error(`${pathLabel} exceeds the maximum of ${MAX_CAPACITY_OPERATIONS} operations.`);
|
|
4579
|
+
}
|
|
4580
|
+
const operations = {};
|
|
4581
|
+
for (const [operationName, operationValue] of operationEntries) {
|
|
4582
|
+
const name = parseCapacityName(operationName, `${pathLabel} key`);
|
|
4583
|
+
if (!/^[a-z][a-z0-9._-]*$/.test(name)) {
|
|
4584
|
+
throw new Error(`${pathLabel}.${operationName} is not a valid operation name.`);
|
|
4585
|
+
}
|
|
4586
|
+
operations[name] = parseCapacityOperation(operationValue, `${pathLabel}.${operationName}`);
|
|
4587
|
+
}
|
|
4588
|
+
return operations;
|
|
4589
|
+
}
|
|
4590
|
+
function parseCapacityTransition(value, pathLabel) {
|
|
4591
|
+
const transition2 = ensureObject(value, pathLabel);
|
|
4592
|
+
ensureAllowedKeys(transition2, ["hostTotalBytes", "runtimeTotalBytes"], pathLabel);
|
|
4593
|
+
return {
|
|
4594
|
+
hostTotalBytes: parseCapacitySafeInteger(
|
|
4595
|
+
transition2.hostTotalBytes,
|
|
4596
|
+
`${pathLabel}.hostTotalBytes`,
|
|
4597
|
+
0
|
|
4598
|
+
),
|
|
4599
|
+
runtimeTotalBytes: parseCapacitySafeInteger(
|
|
4600
|
+
transition2.runtimeTotalBytes,
|
|
4601
|
+
`${pathLabel}.runtimeTotalBytes`,
|
|
4602
|
+
1
|
|
4603
|
+
)
|
|
4604
|
+
};
|
|
4605
|
+
}
|
|
4606
|
+
function parseCapacityEstimates(value, configPath, declaredProfiles) {
|
|
4607
|
+
if (value === void 0) return void 0;
|
|
4608
|
+
const pathLabel = `${configPath}.capacity`;
|
|
4609
|
+
const capacity = ensureObject(value, pathLabel);
|
|
4610
|
+
ensureAllowedKeys(capacity, ["version", "profiles", "transitions"], pathLabel);
|
|
4611
|
+
const version = parseCapacitySafeInteger(capacity.version, `${pathLabel}.version`, 1);
|
|
4612
|
+
if (version !== 1) {
|
|
4613
|
+
throw new Error(`${pathLabel}.version must be 1.`);
|
|
4614
|
+
}
|
|
4615
|
+
const rawProfiles = ensureObject(capacity.profiles, `${pathLabel}.profiles`);
|
|
4616
|
+
const profileEntries = Object.entries(rawProfiles);
|
|
4617
|
+
if (profileEntries.length === 0) {
|
|
4618
|
+
throw new Error(`${pathLabel}.profiles must define at least one exact profile combination.`);
|
|
4619
|
+
}
|
|
4620
|
+
if (profileEntries.length > MAX_CAPACITY_PROFILES) {
|
|
4621
|
+
throw new Error(
|
|
4622
|
+
`${pathLabel}.profiles exceeds the maximum of ${MAX_CAPACITY_PROFILES} combinations.`
|
|
4623
|
+
);
|
|
4624
|
+
}
|
|
4625
|
+
const profiles = {};
|
|
4626
|
+
for (const [rawKey, profileValue] of profileEntries) {
|
|
4627
|
+
const profileKey = parseCapacityProfileKey(
|
|
4628
|
+
rawKey,
|
|
4629
|
+
`${pathLabel}.profiles key`,
|
|
4630
|
+
declaredProfiles
|
|
4631
|
+
);
|
|
4632
|
+
if (Object.hasOwn(profiles, profileKey)) {
|
|
4633
|
+
throw new Error(
|
|
4634
|
+
`${pathLabel}.profiles contains aliases for the same exact combination '${profileKey}'.`
|
|
4635
|
+
);
|
|
4636
|
+
}
|
|
4637
|
+
const profile = ensureObject(profileValue, `${pathLabel}.profiles.${rawKey}`);
|
|
4638
|
+
ensureAllowedKeys(
|
|
4639
|
+
profile,
|
|
4640
|
+
["host", "runtime", "operations"],
|
|
4641
|
+
`${pathLabel}.profiles.${rawKey}`
|
|
4642
|
+
);
|
|
4643
|
+
profiles[profileKey] = {
|
|
4644
|
+
host: parseCapacityDimension(profile.host, `${pathLabel}.profiles.${rawKey}.host`, false),
|
|
4645
|
+
runtime: parseCapacityDimension(
|
|
4646
|
+
profile.runtime,
|
|
4647
|
+
`${pathLabel}.profiles.${rawKey}.runtime`,
|
|
4648
|
+
true
|
|
4649
|
+
),
|
|
4650
|
+
operations: parseCapacityOperations(
|
|
4651
|
+
profile.operations,
|
|
4652
|
+
`${pathLabel}.profiles.${rawKey}.operations`
|
|
4653
|
+
)
|
|
4654
|
+
};
|
|
4655
|
+
}
|
|
4656
|
+
let transitions;
|
|
4657
|
+
if (capacity.transitions !== void 0) {
|
|
4658
|
+
const rawTransitions = ensureObject(capacity.transitions, `${pathLabel}.transitions`);
|
|
4659
|
+
const transitionEntries = Object.entries(rawTransitions);
|
|
4660
|
+
if (transitionEntries.length === 0) {
|
|
4661
|
+
throw new Error(`${pathLabel}.transitions must define at least one transition.`);
|
|
4662
|
+
}
|
|
4663
|
+
transitions = {};
|
|
4664
|
+
let transitionCount = 0;
|
|
4665
|
+
for (const [rawSource, targetValue] of transitionEntries) {
|
|
4666
|
+
const source = parseCapacityProfileKey(
|
|
4667
|
+
rawSource,
|
|
4668
|
+
`${pathLabel}.transitions source key`,
|
|
4669
|
+
declaredProfiles
|
|
4670
|
+
);
|
|
4671
|
+
if (!Object.hasOwn(profiles, source)) {
|
|
4672
|
+
throw new Error(
|
|
4673
|
+
`${pathLabel}.transitions source '${source}' must have a profile estimate.`
|
|
4674
|
+
);
|
|
4675
|
+
}
|
|
4676
|
+
if (Object.hasOwn(transitions, source)) {
|
|
4677
|
+
throw new Error(`${pathLabel}.transitions contains aliases for source '${source}'.`);
|
|
4678
|
+
}
|
|
4679
|
+
const rawTargets = ensureObject(targetValue, `${pathLabel}.transitions.${rawSource}`);
|
|
4680
|
+
const targetEntries = Object.entries(rawTargets);
|
|
4681
|
+
if (targetEntries.length === 0) {
|
|
4682
|
+
throw new Error(`${pathLabel}.transitions.${rawSource} must define a target.`);
|
|
4683
|
+
}
|
|
4684
|
+
const targets = {};
|
|
4685
|
+
for (const [rawTarget, transitionValue] of targetEntries) {
|
|
4686
|
+
transitionCount += 1;
|
|
4687
|
+
if (transitionCount > MAX_CAPACITY_TRANSITIONS) {
|
|
4688
|
+
throw new Error(
|
|
4689
|
+
`${pathLabel}.transitions exceeds the maximum of ${MAX_CAPACITY_TRANSITIONS} entries.`
|
|
4690
|
+
);
|
|
4691
|
+
}
|
|
4692
|
+
const target = parseCapacityProfileKey(
|
|
4693
|
+
rawTarget,
|
|
4694
|
+
`${pathLabel}.transitions.${rawSource} target key`,
|
|
4695
|
+
declaredProfiles
|
|
4696
|
+
);
|
|
4697
|
+
if (!Object.hasOwn(profiles, target)) {
|
|
4698
|
+
throw new Error(
|
|
4699
|
+
`${pathLabel}.transitions target '${target}' must have a profile estimate.`
|
|
4700
|
+
);
|
|
4701
|
+
}
|
|
4702
|
+
if (Object.hasOwn(targets, target)) {
|
|
4703
|
+
throw new Error(
|
|
4704
|
+
`${pathLabel}.transitions.${rawSource} contains aliases for '${target}'.`
|
|
4705
|
+
);
|
|
4706
|
+
}
|
|
4707
|
+
const transition2 = parseCapacityTransition(
|
|
4708
|
+
transitionValue,
|
|
4709
|
+
`${pathLabel}.transitions.${rawSource}.${rawTarget}`
|
|
4710
|
+
);
|
|
4711
|
+
const sourceProfile = profiles[source];
|
|
4712
|
+
const targetProfile = profiles[target];
|
|
4713
|
+
if (transition2.hostTotalBytes < Math.max(sourceProfile.host.steadyBytes, targetProfile.host.steadyBytes)) {
|
|
4714
|
+
throw new Error(
|
|
4715
|
+
`${pathLabel}.transitions.${rawSource}.${rawTarget}.hostTotalBytes must cover both steady host estimates.`
|
|
4716
|
+
);
|
|
4717
|
+
}
|
|
4718
|
+
if (transition2.runtimeTotalBytes < Math.max(sourceProfile.runtime.steadyBytes, targetProfile.runtime.steadyBytes)) {
|
|
4719
|
+
throw new Error(
|
|
4720
|
+
`${pathLabel}.transitions.${rawSource}.${rawTarget}.runtimeTotalBytes must cover both steady runtime estimates.`
|
|
4721
|
+
);
|
|
4722
|
+
}
|
|
4723
|
+
targets[target] = transition2;
|
|
4724
|
+
}
|
|
4725
|
+
transitions[source] = targets;
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
return {
|
|
4729
|
+
version: 1,
|
|
4730
|
+
profiles,
|
|
4731
|
+
...transitions ? { transitions } : {}
|
|
4732
|
+
};
|
|
4733
|
+
}
|
|
4461
4734
|
function parseConfig(raw, configPath) {
|
|
4462
4735
|
const root = ensureObject(raw, configPath);
|
|
4463
4736
|
ensureAllowedKeys(
|
|
4464
4737
|
root,
|
|
4465
|
-
[
|
|
4738
|
+
[
|
|
4739
|
+
"version",
|
|
4740
|
+
"devrouter",
|
|
4741
|
+
"project",
|
|
4742
|
+
"secretManager",
|
|
4743
|
+
"managedRuntime",
|
|
4744
|
+
"profiles",
|
|
4745
|
+
"capacity",
|
|
4746
|
+
"apps"
|
|
4747
|
+
],
|
|
4466
4748
|
configPath
|
|
4467
4749
|
);
|
|
4468
4750
|
const version = toIntegerOrThrow(root.version, `${configPath}.version`);
|
|
@@ -4534,6 +4816,7 @@ function parseConfig(raw, configPath) {
|
|
|
4534
4816
|
}
|
|
4535
4817
|
const managedRuntime = parseManagedRuntime(root.managedRuntime, configPath);
|
|
4536
4818
|
const profiles = parseProfiles(root.profiles, configPath, apps, managedRuntime);
|
|
4819
|
+
const capacity = parseCapacityEstimates(root.capacity, configPath, profiles);
|
|
4537
4820
|
return {
|
|
4538
4821
|
version: 1,
|
|
4539
4822
|
...devrouter ? { devrouter } : {},
|
|
@@ -4541,6 +4824,7 @@ function parseConfig(raw, configPath) {
|
|
|
4541
4824
|
...secretManager ? { secretManager } : {},
|
|
4542
4825
|
...managedRuntime ? { managedRuntime } : {},
|
|
4543
4826
|
...profiles ? { profiles } : {},
|
|
4827
|
+
...capacity ? { capacity } : {},
|
|
4544
4828
|
apps
|
|
4545
4829
|
};
|
|
4546
4830
|
}
|
|
@@ -4875,7 +5159,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs14.default.rea
|
|
|
4875
5159
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
4876
5160
|
const requiredVersion = config.devrouter?.version;
|
|
4877
5161
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
4878
|
-
const cliVersion = true ? "0.0.
|
|
5162
|
+
const cliVersion = true ? "0.0.69" : "0.0.0-dev";
|
|
4879
5163
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
4880
5164
|
hasWarnedVersionMismatch = true;
|
|
4881
5165
|
process.stderr.write(
|
|
@@ -5054,7 +5338,7 @@ async function ensureNetwork(name) {
|
|
|
5054
5338
|
|
|
5055
5339
|
// src/core/tls.ts
|
|
5056
5340
|
var import_node_child_process13 = require("child_process");
|
|
5057
|
-
var
|
|
5341
|
+
var import_node_crypto8 = require("crypto");
|
|
5058
5342
|
var import_node_fs15 = __toESM(require("fs"));
|
|
5059
5343
|
var import_node_path19 = __toESM(require("path"));
|
|
5060
5344
|
var DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
|
|
@@ -5124,7 +5408,7 @@ function parseDnsHostsFromSubjectAltName(subjectAltName) {
|
|
|
5124
5408
|
return normalizeUniqueHosts(names);
|
|
5125
5409
|
}
|
|
5126
5410
|
function parseCertificateDnsHosts(pem) {
|
|
5127
|
-
const certificate = new
|
|
5411
|
+
const certificate = new import_node_crypto8.X509Certificate(pem);
|
|
5128
5412
|
const subjectAltName = certificate.subjectAltName ?? "";
|
|
5129
5413
|
if (subjectAltName.length === 0) {
|
|
5130
5414
|
return [];
|
|
@@ -5399,12 +5683,108 @@ function containerIdentity(container) {
|
|
|
5399
5683
|
mounts: container.mounts.map((mount) => JSON.stringify(mount)).sort()
|
|
5400
5684
|
});
|
|
5401
5685
|
}
|
|
5686
|
+
function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
|
|
5687
|
+
if (!import_node_fs16.default.existsSync(import_node_path20.default.join(repoPath, ".devrouter.yml"))) return false;
|
|
5688
|
+
const linked = isLinkedWorktree(repoPath);
|
|
5689
|
+
const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
|
|
5690
|
+
const registration2 = () => {
|
|
5691
|
+
resetWorkspaceRuntimeCaches();
|
|
5692
|
+
if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy")
|
|
5693
|
+
throw new Error("Initial managed stop provider selection changed.");
|
|
5694
|
+
const owner2 = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
|
|
5695
|
+
if (owner2.status !== "owned")
|
|
5696
|
+
throw new Error("Initial managed stop requires one exact Devsy registration.");
|
|
5697
|
+
if (linked) {
|
|
5698
|
+
const record5 = workspace ? readWorkspaceOwnership(repoPath, workspace) : void 0;
|
|
5699
|
+
if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace)
|
|
5700
|
+
throw new Error("Initial managed stop workspace ownership changed.");
|
|
5701
|
+
}
|
|
5702
|
+
return owner2.workspace;
|
|
5703
|
+
};
|
|
5704
|
+
const runtime = loadRuntimeConfig(repoPath, workspace ?? "");
|
|
5705
|
+
if (!runtime.config.managedRuntime) return false;
|
|
5706
|
+
const owner = registration2();
|
|
5707
|
+
if (!owner.context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(owner.context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId))
|
|
5708
|
+
throw new Error("Initial managed stop requires exact provider context.");
|
|
5709
|
+
const plan = inspectManagedDevcontainerConfig({
|
|
5710
|
+
repoPath,
|
|
5711
|
+
config: runtime.config,
|
|
5712
|
+
profile: runtime.resolvedProfile,
|
|
5713
|
+
linked
|
|
5714
|
+
});
|
|
5715
|
+
const providerRoot = import_node_path20.default.resolve(process.env.DEVSY_HOME || import_node_path20.default.join(import_node_os4.default.homedir(), ".devsy"));
|
|
5716
|
+
const featureDirectory = import_node_path20.default.join(
|
|
5717
|
+
providerRoot,
|
|
5718
|
+
"contexts",
|
|
5719
|
+
owner.context,
|
|
5720
|
+
"workspaces",
|
|
5721
|
+
devsyId,
|
|
5722
|
+
"agent",
|
|
5723
|
+
".docker-compose"
|
|
5724
|
+
);
|
|
5725
|
+
const observe = () => {
|
|
5726
|
+
if (!(0, import_node_util4.isDeepStrictEqual)(registration2(), owner) || readManagedRuntimeState(repoPath, workspace))
|
|
5727
|
+
throw new Error("Initial managed stop ownership or runtime state changed.");
|
|
5728
|
+
const matches = inspectWorkspaceContainers().filter(
|
|
5729
|
+
(c) => sameWorkspacePath(
|
|
5730
|
+
c.labels["com.docker.compose.project.working_dir"] ?? "",
|
|
5731
|
+
plan.composeDirectory
|
|
5732
|
+
)
|
|
5733
|
+
);
|
|
5734
|
+
if (!matches.length) return [];
|
|
5735
|
+
const projects = new Set(matches.map((c) => c.labels["com.docker.compose.project"]));
|
|
5736
|
+
if (projects.size !== 1 || !matches[0]?.labels["com.docker.compose.project"])
|
|
5737
|
+
throw new Error("Initial managed stop requires one exact Compose project.");
|
|
5738
|
+
const population = inspectManagedStopContainers(
|
|
5739
|
+
matches[0].labels["com.docker.compose.project"]
|
|
5740
|
+
);
|
|
5741
|
+
if (!sameSet(
|
|
5742
|
+
matches.map((c) => c.id),
|
|
5743
|
+
population.map((c) => c.id)
|
|
5744
|
+
))
|
|
5745
|
+
throw new Error("Initial managed stop population changed.");
|
|
5746
|
+
proveManagedComposePopulation({
|
|
5747
|
+
plan,
|
|
5748
|
+
repoPath,
|
|
5749
|
+
composeProject: matches[0].labels["com.docker.compose.project"],
|
|
5750
|
+
providerRoot,
|
|
5751
|
+
featureDirectory,
|
|
5752
|
+
containers: population
|
|
5753
|
+
});
|
|
5754
|
+
return population;
|
|
5755
|
+
};
|
|
5756
|
+
const initial = observe();
|
|
5757
|
+
const stable = () => {
|
|
5758
|
+
const current = observe();
|
|
5759
|
+
if (!sameSet(
|
|
5760
|
+
initial.map((c) => c.id),
|
|
5761
|
+
current.map((c) => c.id)
|
|
5762
|
+
) || current.some((c) => {
|
|
5763
|
+
const prior = initial.find((p) => p.id === c.id);
|
|
5764
|
+
return !prior || containerIdentity(prior) !== containerIdentity(c) || !prior.state.Running && c.state.Running;
|
|
5765
|
+
}))
|
|
5766
|
+
throw new Error("Initial managed stop population or identity changed.");
|
|
5767
|
+
return current;
|
|
5768
|
+
};
|
|
5769
|
+
stable();
|
|
5770
|
+
if (!initial.length) return false;
|
|
5771
|
+
for (const container of initial) {
|
|
5772
|
+
const current = stable().find((c) => c.id === container.id);
|
|
5773
|
+
if (current?.state.Running)
|
|
5774
|
+
stopExactManagedService(current.id, current.labels["com.docker.compose.service"] ?? "", {
|
|
5775
|
+
timeoutMs: 3e4
|
|
5776
|
+
});
|
|
5777
|
+
}
|
|
5778
|
+
if (stable().some((c) => c.state.Running))
|
|
5779
|
+
throw new Error("Initial managed stop left a workload running.");
|
|
5780
|
+
return true;
|
|
5781
|
+
}
|
|
5402
5782
|
function stopRetainedManagedDevsyWorkspace(options) {
|
|
5403
5783
|
const { repoPath, devsyId } = options;
|
|
5404
5784
|
const linked = isLinkedWorktree(repoPath);
|
|
5405
5785
|
const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
|
|
5406
5786
|
const retainedState = readManagedRuntimeState(repoPath, workspace);
|
|
5407
|
-
if (!retainedState) return
|
|
5787
|
+
if (!retainedState) return stopInitialManagedDevsyWorkspace(repoPath, devsyId);
|
|
5408
5788
|
const state = retainedState;
|
|
5409
5789
|
if (state.devpodId !== devsyId || linked && !workspace) {
|
|
5410
5790
|
throw new Error("Managed stop requires the exact retained workspace identity.");
|
|
@@ -5533,7 +5913,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
|
|
|
5533
5913
|
repoPath,
|
|
5534
5914
|
composeProject: state.composeProject,
|
|
5535
5915
|
featureDirectory,
|
|
5536
|
-
devsyRoot,
|
|
5916
|
+
providerRoot: devsyRoot,
|
|
5537
5917
|
containers
|
|
5538
5918
|
});
|
|
5539
5919
|
if (previous && (!sameSet(
|
|
@@ -5633,13 +6013,13 @@ function networkProviderEnvironment(binding, source) {
|
|
|
5633
6013
|
}
|
|
5634
6014
|
|
|
5635
6015
|
// src/core/network-claim-lookup.ts
|
|
5636
|
-
var
|
|
5637
|
-
var
|
|
6016
|
+
var import_node_crypto10 = require("crypto");
|
|
6017
|
+
var import_node_fs19 = __toESM(require("fs"));
|
|
5638
6018
|
var import_node_path23 = __toESM(require("path"));
|
|
5639
6019
|
|
|
5640
6020
|
// src/core/network-claims.ts
|
|
5641
|
-
var
|
|
5642
|
-
var
|
|
6021
|
+
var import_node_crypto9 = require("crypto");
|
|
6022
|
+
var import_node_fs18 = __toESM(require("fs"));
|
|
5643
6023
|
var import_node_path22 = __toESM(require("path"));
|
|
5644
6024
|
|
|
5645
6025
|
// src/core/network-capacity.ts
|
|
@@ -5981,7 +6361,7 @@ function overlaps(left, right) {
|
|
|
5981
6361
|
}
|
|
5982
6362
|
|
|
5983
6363
|
// src/core/network-policy.ts
|
|
5984
|
-
var
|
|
6364
|
+
var import_node_fs17 = __toESM(require("fs"));
|
|
5985
6365
|
var import_node_path21 = __toESM(require("path"));
|
|
5986
6366
|
var NETWORK_POLICY_VERSION = 1;
|
|
5987
6367
|
var NETWORK_POLICY_FILE = import_node_path21.default.join(DEVROUTER_HOME, "network-policy.json");
|
|
@@ -6177,7 +6557,7 @@ function hasControlCharacter(value) {
|
|
|
6177
6557
|
function readPolicyFile(filePath) {
|
|
6178
6558
|
let initialStats;
|
|
6179
6559
|
try {
|
|
6180
|
-
initialStats =
|
|
6560
|
+
initialStats = import_node_fs17.default.lstatSync(filePath);
|
|
6181
6561
|
} catch (error) {
|
|
6182
6562
|
if (isErrno(error, "ENOENT")) return { status: "absent" };
|
|
6183
6563
|
return { status: "invalid", error: "network policy could not be read" };
|
|
@@ -6190,16 +6570,16 @@ function readPolicyFile(filePath) {
|
|
|
6190
6570
|
}
|
|
6191
6571
|
let fileDescriptor;
|
|
6192
6572
|
try {
|
|
6193
|
-
fileDescriptor =
|
|
6573
|
+
fileDescriptor = import_node_fs17.default.openSync(
|
|
6194
6574
|
filePath,
|
|
6195
|
-
|
|
6575
|
+
import_node_fs17.default.constants.O_RDONLY | import_node_fs17.default.constants.O_NONBLOCK | (import_node_fs17.default.constants.O_NOFOLLOW ?? 0)
|
|
6196
6576
|
);
|
|
6197
6577
|
} catch (error) {
|
|
6198
6578
|
if (isErrno(error, "ENOENT")) return { status: "absent" };
|
|
6199
6579
|
return { status: "invalid", error: "network policy could not be read" };
|
|
6200
6580
|
}
|
|
6201
6581
|
try {
|
|
6202
|
-
const openedStats =
|
|
6582
|
+
const openedStats = import_node_fs17.default.fstatSync(fileDescriptor);
|
|
6203
6583
|
if (!openedStats.isFile()) {
|
|
6204
6584
|
return { status: "invalid", error: "network policy must be a regular file" };
|
|
6205
6585
|
}
|
|
@@ -6212,12 +6592,12 @@ function readPolicyFile(filePath) {
|
|
|
6212
6592
|
const buffer = Buffer.alloc(openedStats.size);
|
|
6213
6593
|
let offset = 0;
|
|
6214
6594
|
while (offset < buffer.length) {
|
|
6215
|
-
const bytesRead =
|
|
6595
|
+
const bytesRead = import_node_fs17.default.readSync(fileDescriptor, buffer, offset, buffer.length - offset, null);
|
|
6216
6596
|
if (bytesRead === 0)
|
|
6217
6597
|
return { status: "invalid", error: "network policy changed during read" };
|
|
6218
6598
|
offset += bytesRead;
|
|
6219
6599
|
}
|
|
6220
|
-
const finalStats =
|
|
6600
|
+
const finalStats = import_node_fs17.default.fstatSync(fileDescriptor);
|
|
6221
6601
|
if (!finalStats.isFile() || finalStats.size !== openedStats.size) {
|
|
6222
6602
|
return { status: "invalid", error: "network policy changed during read" };
|
|
6223
6603
|
}
|
|
@@ -6226,7 +6606,7 @@ function readPolicyFile(filePath) {
|
|
|
6226
6606
|
return { status: "invalid", error: "network policy could not be read" };
|
|
6227
6607
|
} finally {
|
|
6228
6608
|
try {
|
|
6229
|
-
|
|
6609
|
+
import_node_fs17.default.closeSync(fileDescriptor);
|
|
6230
6610
|
} catch {
|
|
6231
6611
|
}
|
|
6232
6612
|
}
|
|
@@ -6652,27 +7032,27 @@ function assertExactKeys(value, keys2) {
|
|
|
6652
7032
|
function readDocument(store) {
|
|
6653
7033
|
let descriptor;
|
|
6654
7034
|
try {
|
|
6655
|
-
descriptor =
|
|
7035
|
+
descriptor = import_node_fs18.default.openSync(
|
|
6656
7036
|
store.filePath,
|
|
6657
|
-
|
|
7037
|
+
import_node_fs18.default.constants.O_RDONLY | import_node_fs18.default.constants.O_NONBLOCK | (import_node_fs18.default.constants.O_NOFOLLOW ?? 0)
|
|
6658
7038
|
);
|
|
6659
7039
|
} catch (error) {
|
|
6660
7040
|
if (isErrno2(error, "ENOENT")) return void 0;
|
|
6661
7041
|
throw claimError("invalid-state", "network claim state could not be read");
|
|
6662
7042
|
}
|
|
6663
7043
|
try {
|
|
6664
|
-
const stat =
|
|
7044
|
+
const stat = import_node_fs18.default.fstatSync(descriptor);
|
|
6665
7045
|
if (!stat.isFile() || stat.size > NETWORK_CLAIMS_MAX_FILE_BYTES || (stat.mode & 63) !== 0) {
|
|
6666
7046
|
throw claimError("invalid-state", "network claim state is not a bounded private file");
|
|
6667
7047
|
}
|
|
6668
7048
|
const buffer = Buffer.alloc(NETWORK_CLAIMS_MAX_FILE_BYTES + 1);
|
|
6669
7049
|
let count = 0;
|
|
6670
7050
|
while (count < buffer.length) {
|
|
6671
|
-
const bytesRead =
|
|
7051
|
+
const bytesRead = import_node_fs18.default.readSync(descriptor, buffer, count, buffer.length - count, null);
|
|
6672
7052
|
if (bytesRead === 0) break;
|
|
6673
7053
|
count += bytesRead;
|
|
6674
7054
|
}
|
|
6675
|
-
const finalStat =
|
|
7055
|
+
const finalStat = import_node_fs18.default.fstatSync(descriptor);
|
|
6676
7056
|
if (count > NETWORK_CLAIMS_MAX_FILE_BYTES || finalStat.size !== stat.size) {
|
|
6677
7057
|
throw claimError("invalid-state", "network claim state changed during read");
|
|
6678
7058
|
}
|
|
@@ -6684,7 +7064,7 @@ function readDocument(store) {
|
|
|
6684
7064
|
}
|
|
6685
7065
|
return validateDocument(parsed, store.daemonId);
|
|
6686
7066
|
} finally {
|
|
6687
|
-
|
|
7067
|
+
import_node_fs18.default.closeSync(descriptor);
|
|
6688
7068
|
}
|
|
6689
7069
|
}
|
|
6690
7070
|
function validateDocument(value, daemonId) {
|
|
@@ -6788,7 +7168,7 @@ function withStoreLock(daemonId, options, operation) {
|
|
|
6788
7168
|
if (!Number.isSafeInteger(waitMs) || waitMs < 0 || waitMs > MAX_LOCK_WAIT_MS) {
|
|
6789
7169
|
throw claimError("invalid-input", "network claim lock wait is invalid");
|
|
6790
7170
|
}
|
|
6791
|
-
|
|
7171
|
+
import_node_fs18.default.mkdirSync(store.root, { recursive: true, mode: 448 });
|
|
6792
7172
|
assertDirectory(store.root);
|
|
6793
7173
|
return withFileLockSync(
|
|
6794
7174
|
store.lockPath,
|
|
@@ -6819,7 +7199,7 @@ function validateRoot(root) {
|
|
|
6819
7199
|
function assertDirectory(directory) {
|
|
6820
7200
|
let stat;
|
|
6821
7201
|
try {
|
|
6822
|
-
stat =
|
|
7202
|
+
stat = import_node_fs18.default.lstatSync(directory);
|
|
6823
7203
|
} catch {
|
|
6824
7204
|
throw claimError("invalid-state", "network claim root could not be read");
|
|
6825
7205
|
}
|
|
@@ -6828,7 +7208,7 @@ function assertDirectory(directory) {
|
|
|
6828
7208
|
}
|
|
6829
7209
|
}
|
|
6830
7210
|
function hashKey(value) {
|
|
6831
|
-
return (0,
|
|
7211
|
+
return (0, import_node_crypto9.createHash)("sha256").update(value).digest("hex");
|
|
6832
7212
|
}
|
|
6833
7213
|
function cloneFence(fence) {
|
|
6834
7214
|
return { ...fence };
|
|
@@ -6856,12 +7236,12 @@ function claimError(code, message) {
|
|
|
6856
7236
|
function networkOwnerKey(repoPath) {
|
|
6857
7237
|
if (!import_node_path23.default.isAbsolute(repoPath) || import_node_path23.default.resolve(repoPath) !== repoPath)
|
|
6858
7238
|
throw new Error("Invalid network owner path.");
|
|
6859
|
-
return (0,
|
|
7239
|
+
return (0, import_node_crypto10.createHash)("sha256").update(repoPath).digest("hex");
|
|
6860
7240
|
}
|
|
6861
7241
|
function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
|
|
6862
7242
|
let names;
|
|
6863
7243
|
try {
|
|
6864
|
-
names =
|
|
7244
|
+
names = import_node_fs19.default.readdirSync(root);
|
|
6865
7245
|
} catch (error) {
|
|
6866
7246
|
if (error.code === "ENOENT") return void 0;
|
|
6867
7247
|
throw new Error("Network claims cannot be inspected; repair is required.");
|
|
@@ -6871,32 +7251,32 @@ function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
|
|
|
6871
7251
|
let found;
|
|
6872
7252
|
for (const file of files) {
|
|
6873
7253
|
const full = import_node_path23.default.join(root, file);
|
|
6874
|
-
const stat =
|
|
7254
|
+
const stat = import_node_fs19.default.lstatSync(full);
|
|
6875
7255
|
if (!stat.isFile() || stat.size > NETWORK_CLAIMS_MAX_FILE_BYTES)
|
|
6876
7256
|
throw new Error("Invalid retained network claim file.");
|
|
6877
7257
|
let value;
|
|
6878
|
-
const descriptor =
|
|
7258
|
+
const descriptor = import_node_fs19.default.openSync(
|
|
6879
7259
|
full,
|
|
6880
|
-
|
|
7260
|
+
import_node_fs19.default.constants.O_RDONLY | import_node_fs19.default.constants.O_NONBLOCK | import_node_fs19.default.constants.O_NOFOLLOW
|
|
6881
7261
|
);
|
|
6882
7262
|
try {
|
|
6883
|
-
if (!
|
|
6884
|
-
const
|
|
7263
|
+
if (!import_node_fs19.default.fstatSync(descriptor).isFile()) throw new Error();
|
|
7264
|
+
const bytes2 = Buffer.alloc(NETWORK_CLAIMS_MAX_FILE_BYTES + 1);
|
|
6885
7265
|
let size = 0;
|
|
6886
|
-
while (size <
|
|
6887
|
-
const count =
|
|
7266
|
+
while (size < bytes2.length) {
|
|
7267
|
+
const count = import_node_fs19.default.readSync(descriptor, bytes2, size, bytes2.length - size, null);
|
|
6888
7268
|
if (!count) break;
|
|
6889
7269
|
size += count;
|
|
6890
7270
|
}
|
|
6891
7271
|
if (size > NETWORK_CLAIMS_MAX_FILE_BYTES) throw new Error();
|
|
6892
|
-
value = JSON.parse(
|
|
7272
|
+
value = JSON.parse(bytes2.subarray(0, size).toString("utf8"));
|
|
6893
7273
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error();
|
|
6894
7274
|
} catch {
|
|
6895
7275
|
throw new Error("Retained network claim could not be read safely.");
|
|
6896
7276
|
} finally {
|
|
6897
|
-
|
|
7277
|
+
import_node_fs19.default.closeSync(descriptor);
|
|
6898
7278
|
}
|
|
6899
|
-
if (typeof value.daemonId !== "string" || `${(0,
|
|
7279
|
+
if (typeof value.daemonId !== "string" || `${(0, import_node_crypto10.createHash)("sha256").update(value.daemonId).digest("hex")}.json` !== file)
|
|
6900
7280
|
throw new Error("Retained network daemon identity is invalid.");
|
|
6901
7281
|
const claim2 = readNetworkClaim({
|
|
6902
7282
|
daemonId: value.daemonId,
|
|
@@ -6913,7 +7293,7 @@ function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
|
|
|
6913
7293
|
|
|
6914
7294
|
// src/core/network-provider-inspect.ts
|
|
6915
7295
|
var import_node_child_process14 = require("child_process");
|
|
6916
|
-
var
|
|
7296
|
+
var import_node_crypto11 = require("crypto");
|
|
6917
7297
|
function object(value) {
|
|
6918
7298
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6919
7299
|
throw new Error("Invalid provider evidence.");
|
|
@@ -6933,7 +7313,7 @@ function qualifyNetworkProviderDefinition(value, provider) {
|
|
|
6933
7313
|
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 !== "") {
|
|
6934
7314
|
throw new Error("Docker provider definition is not qualified for network allocation.");
|
|
6935
7315
|
}
|
|
6936
|
-
return (0,
|
|
7316
|
+
return (0, import_node_crypto11.createHash)("sha256").update(JSON.stringify({ name: definition.name, agent, options, exec })).digest("hex");
|
|
6937
7317
|
}
|
|
6938
7318
|
function read(provider, args) {
|
|
6939
7319
|
const result = (0, import_node_child_process14.spawnSync)(provider, args, {
|
|
@@ -6995,9 +7375,9 @@ function inspectNetworkProviderBinding(input) {
|
|
|
6995
7375
|
const option = (key, localOnly = false) => {
|
|
6996
7376
|
const value = localOnly ? stored[key] : stored[key] ?? settings[key];
|
|
6997
7377
|
if (value === void 0) return null;
|
|
6998
|
-
const
|
|
6999
|
-
if (typeof
|
|
7000
|
-
return
|
|
7378
|
+
const text4 = object(value).value;
|
|
7379
|
+
if (typeof text4 !== "string") throw new Error();
|
|
7380
|
+
return text4 || null;
|
|
7001
7381
|
};
|
|
7002
7382
|
if (workspaceProvider && workspaceProvider.name !== "docker") throw new Error();
|
|
7003
7383
|
if (option("DOCKER_ELEVATION") && option("DOCKER_ELEVATION") !== "none") throw new Error();
|
|
@@ -7064,7 +7444,7 @@ function failedStartMayHaveAttached(devsyId, repoPath) {
|
|
|
7064
7444
|
}
|
|
7065
7445
|
}
|
|
7066
7446
|
function withMutationLock(activity, target, operation) {
|
|
7067
|
-
|
|
7447
|
+
import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
7068
7448
|
return withFileLockSync(
|
|
7069
7449
|
DEVSY_MUTATION_LOCK_FILE,
|
|
7070
7450
|
{
|
|
@@ -7078,7 +7458,7 @@ function withMutationLock(activity, target, operation) {
|
|
|
7078
7458
|
);
|
|
7079
7459
|
}
|
|
7080
7460
|
function withMutationLockAsync(activity, target, operation) {
|
|
7081
|
-
|
|
7461
|
+
import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
7082
7462
|
return withFileLock(
|
|
7083
7463
|
DEVSY_MUTATION_LOCK_FILE,
|
|
7084
7464
|
{
|
|
@@ -7197,12 +7577,12 @@ function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
|
|
|
7197
7577
|
function assertDevsyTarget(devsyId, repoPath) {
|
|
7198
7578
|
const workspaces = listDevsyWorkspaces();
|
|
7199
7579
|
const existing = selectDevsyWorkspace(workspaces, repoPath);
|
|
7200
|
-
const
|
|
7201
|
-
if (
|
|
7202
|
-
const before = inspectDevsyWorkspaceOwnership(workspaces,
|
|
7580
|
+
const id5 = devsyId ?? existing?.id;
|
|
7581
|
+
if (id5) {
|
|
7582
|
+
const before = inspectDevsyWorkspaceOwnership(workspaces, id5, repoPath);
|
|
7203
7583
|
if (before.status === "conflict") throw new Error(before.reason);
|
|
7204
7584
|
}
|
|
7205
|
-
return
|
|
7585
|
+
return id5;
|
|
7206
7586
|
}
|
|
7207
7587
|
async function startDevsyWorkspace(options) {
|
|
7208
7588
|
const activity = options.recreate ? "Devsy recreate" : "Devsy start";
|
|
@@ -7314,7 +7694,572 @@ var ExecutionOutcomeError = class extends Error {
|
|
|
7314
7694
|
};
|
|
7315
7695
|
|
|
7316
7696
|
// src/core/reliability-lifecycle.ts
|
|
7317
|
-
var
|
|
7697
|
+
var import_node_crypto20 = require("crypto");
|
|
7698
|
+
var import_node_path39 = __toESM(require("path"));
|
|
7699
|
+
var import_node_util6 = require("util");
|
|
7700
|
+
|
|
7701
|
+
// src/core/capacity-accounting.ts
|
|
7702
|
+
function bytes(value) {
|
|
7703
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
7704
|
+
}
|
|
7705
|
+
function evaluateCapacity(budgets, samples, charges, requested, nowMs, maxSampleAgeMs, pools = []) {
|
|
7706
|
+
if (!bytes(nowMs) || !bytes(maxSampleAgeMs)) throw new Error("Invalid capacity sample clock.");
|
|
7707
|
+
for (const domain of Object.keys(requested.totals).sort()) {
|
|
7708
|
+
const fail2 = (reason2) => ({ admitted: false, domain, reason: reason2 });
|
|
7709
|
+
const budget = budgets[domain];
|
|
7710
|
+
const sample = samples[domain];
|
|
7711
|
+
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]))
|
|
7712
|
+
return fail2("unknown");
|
|
7713
|
+
if (sample.sampledAtMs > nowMs || nowMs - sample.sampledAtMs > maxSampleAgeMs)
|
|
7714
|
+
return fail2("stale");
|
|
7715
|
+
if (sample.pressure !== "normal")
|
|
7716
|
+
return fail2(sample.pressure === "pressured" ? "pressure" : "unknown");
|
|
7717
|
+
const totals = /* @__PURE__ */ new Map();
|
|
7718
|
+
let startups = 0;
|
|
7719
|
+
let heavy = 0;
|
|
7720
|
+
for (const charge of charges) {
|
|
7721
|
+
if (!Object.hasOwn(charge.totals, domain)) continue;
|
|
7722
|
+
if (!bytes(charge.totals[domain]) || totals.has(charge.environmentId)) return fail2("unknown");
|
|
7723
|
+
totals.set(charge.environmentId, charge.totals[domain]);
|
|
7724
|
+
if (charge.environmentId !== requested.environmentId) {
|
|
7725
|
+
if (charge.startup) startups++;
|
|
7726
|
+
if (charge.heavy) heavy++;
|
|
7727
|
+
}
|
|
7728
|
+
}
|
|
7729
|
+
totals.set(
|
|
7730
|
+
requested.environmentId,
|
|
7731
|
+
Math.max(totals.get(requested.environmentId) ?? 0, requested.totals[domain])
|
|
7732
|
+
);
|
|
7733
|
+
for (const [environment, observed] of Object.entries(sample.ownedBytes))
|
|
7734
|
+
totals.set(environment, Math.max(totals.get(environment) ?? 0, observed));
|
|
7735
|
+
let used = BigInt(sample.unmanagedBytes) + BigInt(sample.sharedBytes);
|
|
7736
|
+
for (const total of totals.values()) used += BigInt(total);
|
|
7737
|
+
for (const pool of pools) {
|
|
7738
|
+
if (pool.hostDomain !== domain) continue;
|
|
7739
|
+
if (!bytes(pool.hostChargeCeilingBytes)) return fail2("unknown");
|
|
7740
|
+
used += BigInt(pool.hostChargeCeilingBytes);
|
|
7741
|
+
}
|
|
7742
|
+
if (used > BigInt(budget.capacityBytes) - BigInt(budget.protectedHeadroomBytes))
|
|
7743
|
+
return fail2("memory");
|
|
7744
|
+
const existing = charges.find((charge) => charge.environmentId === requested.environmentId);
|
|
7745
|
+
if ((requested.startup || existing?.startup) && startups >= budget.startupSlots)
|
|
7746
|
+
return fail2("startup-slot");
|
|
7747
|
+
if ((requested.heavy || existing?.heavy) && heavy >= budget.heavySlots)
|
|
7748
|
+
return fail2("heavy-slot");
|
|
7749
|
+
}
|
|
7750
|
+
if (Object.keys(requested.totals).length === 0)
|
|
7751
|
+
throw new Error("Capacity request has no domains.");
|
|
7752
|
+
return { admitted: true };
|
|
7753
|
+
}
|
|
7754
|
+
|
|
7755
|
+
// src/core/capacity-policy.ts
|
|
7756
|
+
var import_node_fs21 = __toESM(require("fs"));
|
|
7757
|
+
var import_node_path25 = __toESM(require("path"));
|
|
7758
|
+
|
|
7759
|
+
// src/core/capacity-store.ts
|
|
7760
|
+
var import_node_fs22 = __toESM(require("fs"));
|
|
7761
|
+
var import_node_path26 = __toESM(require("path"));
|
|
7762
|
+
var MAX_BYTES = 1048576;
|
|
7763
|
+
var CapacitySnapshotChangedError = class extends Error {
|
|
7764
|
+
constructor() {
|
|
7765
|
+
super("Capacity snapshot changed.");
|
|
7766
|
+
this.name = "CapacitySnapshotChangedError";
|
|
7767
|
+
}
|
|
7768
|
+
};
|
|
7769
|
+
function assertPrivateDirectory(directoryPath) {
|
|
7770
|
+
import_node_fs22.default.mkdirSync(directoryPath, { recursive: true, mode: 448 });
|
|
7771
|
+
const directory = import_node_fs22.default.lstatSync(directoryPath);
|
|
7772
|
+
if (!directory.isDirectory() || directory.isSymbolicLink() || directory.uid !== process.getuid?.() || (directory.mode & 63) !== 0)
|
|
7773
|
+
throw new Error("Capacity reservation directory is not private.");
|
|
7774
|
+
}
|
|
7775
|
+
function object2(value, fields2) {
|
|
7776
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length !== fields2.length || Object.keys(value).some((key) => !fields2.includes(key)))
|
|
7777
|
+
throw new Error("Invalid capacity reservation fields.");
|
|
7778
|
+
}
|
|
7779
|
+
function id2(value) {
|
|
7780
|
+
return typeof value === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(value);
|
|
7781
|
+
}
|
|
7782
|
+
function validatePools(value) {
|
|
7783
|
+
if (!Array.isArray(value) || value.length > 256) throw new Error("Invalid capacity pools.");
|
|
7784
|
+
const daemons = /* @__PURE__ */ new Set();
|
|
7785
|
+
const runtimes = /* @__PURE__ */ new Set();
|
|
7786
|
+
for (const pool of value) {
|
|
7787
|
+
object2(pool, ["daemonId", "runtimeDomain", "hostDomain", "hostChargeCeilingBytes"]);
|
|
7788
|
+
if (typeof pool.daemonId !== "string" || pool.daemonId.length === 0 || pool.daemonId.length > 256 || pool.daemonId !== pool.daemonId.trim() || [...pool.daemonId].some(
|
|
7789
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127
|
|
7790
|
+
) || !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))
|
|
7791
|
+
throw new Error("Invalid capacity pool identity or ceiling.");
|
|
7792
|
+
daemons.add(pool.daemonId);
|
|
7793
|
+
runtimes.add(pool.runtimeDomain);
|
|
7794
|
+
}
|
|
7795
|
+
}
|
|
7796
|
+
function validate(value) {
|
|
7797
|
+
const hasPools = !!value && typeof value === "object" && Object.hasOwn(value, "pools");
|
|
7798
|
+
object2(value, ["version", "revision", "reservations", ...hasPools ? ["pools"] : []]);
|
|
7799
|
+
if (hasPools) validatePools(value.pools);
|
|
7800
|
+
if (value.version !== 1 || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0 || !Array.isArray(value.reservations) || value.reservations.length > 256)
|
|
7801
|
+
throw new Error("Invalid capacity reservation snapshot.");
|
|
7802
|
+
const environments = /* @__PURE__ */ new Set();
|
|
7803
|
+
const operations = /* @__PURE__ */ new Set();
|
|
7804
|
+
const reservations = /* @__PURE__ */ new Set();
|
|
7805
|
+
for (const reservation of value.reservations) {
|
|
7806
|
+
object2(reservation, [
|
|
7807
|
+
"environmentId",
|
|
7808
|
+
"operationId",
|
|
7809
|
+
"reservationId",
|
|
7810
|
+
"policyRevision",
|
|
7811
|
+
"totals",
|
|
7812
|
+
"startup",
|
|
7813
|
+
"heavy"
|
|
7814
|
+
]);
|
|
7815
|
+
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))
|
|
7816
|
+
throw new Error("Invalid capacity reservation identity.");
|
|
7817
|
+
environments.add(reservation.environmentId);
|
|
7818
|
+
operations.add(reservation.operationId);
|
|
7819
|
+
reservations.add(reservation.reservationId);
|
|
7820
|
+
const totals = reservation.totals;
|
|
7821
|
+
if (!totals || typeof totals !== "object" || Array.isArray(totals) || Object.keys(totals).length < 1 || Object.keys(totals).length > 256 || Object.entries(totals).some(
|
|
7822
|
+
([key, bytes2]) => !id2(key) || !Number.isSafeInteger(bytes2) || Number(bytes2) < 0
|
|
7823
|
+
))
|
|
7824
|
+
throw new Error("Invalid capacity reservation totals.");
|
|
7825
|
+
}
|
|
7826
|
+
}
|
|
7827
|
+
function mergePools(pools, incoming) {
|
|
7828
|
+
let changed = false;
|
|
7829
|
+
for (const requested of incoming) {
|
|
7830
|
+
const retained = pools.find((pool) => pool.daemonId === requested.daemonId);
|
|
7831
|
+
if (retained) {
|
|
7832
|
+
if (retained.hostDomain !== requested.hostDomain || retained.runtimeDomain !== requested.runtimeDomain)
|
|
7833
|
+
throw new Error("Capacity pool binding changed.");
|
|
7834
|
+
if (requested.hostChargeCeilingBytes > retained.hostChargeCeilingBytes) {
|
|
7835
|
+
retained.hostChargeCeilingBytes = requested.hostChargeCeilingBytes;
|
|
7836
|
+
changed = true;
|
|
7837
|
+
}
|
|
7838
|
+
} else {
|
|
7839
|
+
pools.push(structuredClone(requested));
|
|
7840
|
+
changed = true;
|
|
7841
|
+
}
|
|
7842
|
+
}
|
|
7843
|
+
validatePools(pools);
|
|
7844
|
+
return changed;
|
|
7845
|
+
}
|
|
7846
|
+
function serializeSnapshot(snapshot) {
|
|
7847
|
+
const contents = `${JSON.stringify(snapshot)}
|
|
7848
|
+
`;
|
|
7849
|
+
if (Buffer.byteLength(contents) > MAX_BYTES)
|
|
7850
|
+
throw new Error("Capacity reservation snapshot exceeds byte limit.");
|
|
7851
|
+
return contents;
|
|
7852
|
+
}
|
|
7853
|
+
var CapacityStore = class {
|
|
7854
|
+
constructor(directory) {
|
|
7855
|
+
this.directory = directory;
|
|
7856
|
+
this.file = import_node_path26.default.join(directory, "capacity-reservations.json");
|
|
7857
|
+
}
|
|
7858
|
+
file;
|
|
7859
|
+
read() {
|
|
7860
|
+
let descriptor;
|
|
7861
|
+
try {
|
|
7862
|
+
descriptor = import_node_fs22.default.openSync(this.file, import_node_fs22.default.constants.O_RDONLY | import_node_fs22.default.constants.O_NOFOLLOW);
|
|
7863
|
+
} catch (error) {
|
|
7864
|
+
if (error.code === "ENOENT")
|
|
7865
|
+
return { version: 1, revision: 0, reservations: [] };
|
|
7866
|
+
throw error;
|
|
7867
|
+
}
|
|
7868
|
+
try {
|
|
7869
|
+
const stat = import_node_fs22.default.fstatSync(descriptor);
|
|
7870
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0 || stat.size > MAX_BYTES)
|
|
7871
|
+
throw new Error("Unsafe capacity reservation snapshot.");
|
|
7872
|
+
const buffer = Buffer.alloc(MAX_BYTES + 1);
|
|
7873
|
+
const count = import_node_fs22.default.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
7874
|
+
if (count > MAX_BYTES) throw new Error("Capacity reservation snapshot exceeds byte limit.");
|
|
7875
|
+
const value = JSON.parse(buffer.subarray(0, count).toString("utf8"));
|
|
7876
|
+
validate(value);
|
|
7877
|
+
return value;
|
|
7878
|
+
} finally {
|
|
7879
|
+
import_node_fs22.default.closeSync(descriptor);
|
|
7880
|
+
}
|
|
7881
|
+
}
|
|
7882
|
+
/**
|
|
7883
|
+
* Caller must revoke journal authority and prove exact physical cessation first.
|
|
7884
|
+
* An absent reservation still advances the revision to fence that proof.
|
|
7885
|
+
*/
|
|
7886
|
+
settleEnvironmentAfterStop(environmentId, expectedRevision) {
|
|
7887
|
+
if (!id2(environmentId)) throw new Error("Invalid capacity environment ID.");
|
|
7888
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
|
|
7889
|
+
throw new Error("Invalid capacity settlement revision.");
|
|
7890
|
+
assertPrivateDirectory(this.directory);
|
|
7891
|
+
return withFileLockSync(
|
|
7892
|
+
`${this.file}.lock`,
|
|
7893
|
+
{ activity: "capacity settlement", waitMs: 100 },
|
|
7894
|
+
() => {
|
|
7895
|
+
const snapshot = this.read();
|
|
7896
|
+
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
7897
|
+
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
7898
|
+
throw new Error("Capacity reservation revision exhausted.");
|
|
7899
|
+
const index = snapshot.reservations.findIndex(
|
|
7900
|
+
(entry) => entry.environmentId === environmentId
|
|
7901
|
+
);
|
|
7902
|
+
if (index >= 0) snapshot.reservations.splice(index, 1);
|
|
7903
|
+
snapshot.revision++;
|
|
7904
|
+
validate(snapshot);
|
|
7905
|
+
writeFileAtomically(this.file, serializeSnapshot(snapshot));
|
|
7906
|
+
return { settled: index >= 0, revision: snapshot.revision };
|
|
7907
|
+
}
|
|
7908
|
+
);
|
|
7909
|
+
}
|
|
7910
|
+
/** Retain positively observed pools even when their charge already exceeds admission budgets. */
|
|
7911
|
+
mergeObservedPools(pools, expectedRevision) {
|
|
7912
|
+
validatePools(pools);
|
|
7913
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
|
|
7914
|
+
throw new Error("Invalid capacity expected revision.");
|
|
7915
|
+
assertPrivateDirectory(this.directory);
|
|
7916
|
+
return withFileLockSync(
|
|
7917
|
+
`${this.file}.lock`,
|
|
7918
|
+
{ activity: "capacity pool observation", waitMs: 100 },
|
|
7919
|
+
() => {
|
|
7920
|
+
const snapshot = this.read();
|
|
7921
|
+
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
7922
|
+
const merged = structuredClone(snapshot.pools ?? []);
|
|
7923
|
+
if (!mergePools(merged, pools)) return { changed: false, revision: snapshot.revision };
|
|
7924
|
+
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
7925
|
+
throw new Error("Capacity reservation revision exhausted.");
|
|
7926
|
+
snapshot.pools = merged;
|
|
7927
|
+
snapshot.revision++;
|
|
7928
|
+
validate(snapshot);
|
|
7929
|
+
writeFileAtomically(this.file, serializeSnapshot(snapshot));
|
|
7930
|
+
return { changed: true, revision: snapshot.revision };
|
|
7931
|
+
}
|
|
7932
|
+
);
|
|
7933
|
+
}
|
|
7934
|
+
/**
|
|
7935
|
+
* Caller must revoke pool launch authority and prove exact VM cessation first.
|
|
7936
|
+
* Container or environment cessation alone never permits releasing this ceiling.
|
|
7937
|
+
*/
|
|
7938
|
+
settlePoolAfterCessation(identity2, expectedRevision) {
|
|
7939
|
+
object2(identity2, ["daemonId", "runtimeDomain", "hostDomain"]);
|
|
7940
|
+
validatePools([{ ...identity2, hostChargeCeilingBytes: 1 }]);
|
|
7941
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
|
|
7942
|
+
throw new Error("Invalid capacity settlement revision.");
|
|
7943
|
+
assertPrivateDirectory(this.directory);
|
|
7944
|
+
return withFileLockSync(
|
|
7945
|
+
`${this.file}.lock`,
|
|
7946
|
+
{ activity: "capacity pool settlement", waitMs: 100 },
|
|
7947
|
+
() => {
|
|
7948
|
+
const snapshot = this.read();
|
|
7949
|
+
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
7950
|
+
const pools = snapshot.pools ?? [];
|
|
7951
|
+
const index = pools.findIndex((pool2) => pool2.daemonId === identity2.daemonId);
|
|
7952
|
+
const pool = pools[index];
|
|
7953
|
+
if (pool && (pool.hostDomain !== identity2.hostDomain || pool.runtimeDomain !== identity2.runtimeDomain) || pools.some(
|
|
7954
|
+
(entry) => entry.runtimeDomain === identity2.runtimeDomain && entry.daemonId !== identity2.daemonId
|
|
7955
|
+
) || snapshot.reservations.some((entry) => Object.hasOwn(entry.totals, identity2.runtimeDomain)))
|
|
7956
|
+
throw new Error("Capacity pool cessation identity remains bound.");
|
|
7957
|
+
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
7958
|
+
throw new Error("Capacity reservation revision exhausted.");
|
|
7959
|
+
if (pool) pools.splice(index, 1);
|
|
7960
|
+
snapshot.revision++;
|
|
7961
|
+
validate(snapshot);
|
|
7962
|
+
writeFileAtomically(this.file, serializeSnapshot(snapshot));
|
|
7963
|
+
return { settled: !!pool, revision: snapshot.revision };
|
|
7964
|
+
}
|
|
7965
|
+
);
|
|
7966
|
+
}
|
|
7967
|
+
/** Caller persists phase proof and revokes authority before reducing this exact row. */
|
|
7968
|
+
reduceAfterPhase(target, expectedRevision) {
|
|
7969
|
+
validate({ version: 1, revision: 0, reservations: [target] });
|
|
7970
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
|
|
7971
|
+
throw new Error("Invalid capacity settlement revision.");
|
|
7972
|
+
assertPrivateDirectory(this.directory);
|
|
7973
|
+
withFileLockSync(
|
|
7974
|
+
`${this.file}.lock`,
|
|
7975
|
+
{ activity: "capacity phase settlement", waitMs: 100 },
|
|
7976
|
+
() => {
|
|
7977
|
+
const snapshot = this.read();
|
|
7978
|
+
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
7979
|
+
const index = snapshot.reservations.findIndex(
|
|
7980
|
+
(entry) => entry.environmentId === target.environmentId
|
|
7981
|
+
);
|
|
7982
|
+
const current = snapshot.reservations[index];
|
|
7983
|
+
if (!current || current.reservationId !== target.reservationId || current.operationId !== target.operationId || current.policyRevision !== target.policyRevision)
|
|
7984
|
+
throw new Error("Capacity phase settlement predecessor changed.");
|
|
7985
|
+
if (Object.keys(current.totals).length !== Object.keys(target.totals).length || Object.entries(target.totals).some(
|
|
7986
|
+
([domain, bytes2]) => !Object.hasOwn(current.totals, domain) || bytes2 > current.totals[domain]
|
|
7987
|
+
) || target.startup && !current.startup || target.heavy && !current.heavy)
|
|
7988
|
+
throw new Error("Capacity phase settlement cannot acquire resources.");
|
|
7989
|
+
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
7990
|
+
throw new Error("Capacity reservation revision exhausted.");
|
|
7991
|
+
snapshot.reservations[index] = structuredClone(target);
|
|
7992
|
+
snapshot.revision++;
|
|
7993
|
+
validate(snapshot);
|
|
7994
|
+
writeFileAtomically(this.file, serializeSnapshot(snapshot));
|
|
7995
|
+
}
|
|
7996
|
+
);
|
|
7997
|
+
}
|
|
7998
|
+
reserve(request, budgets, samples, nowMs, maxSampleAgeMs, previous, expectedRevision, requestedPool) {
|
|
7999
|
+
validate({ version: 1, revision: 0, reservations: [request] });
|
|
8000
|
+
if (requestedPool !== void 0) {
|
|
8001
|
+
validatePools([requestedPool]);
|
|
8002
|
+
if (!Object.hasOwn(request.totals, requestedPool.hostDomain) || !Object.hasOwn(request.totals, requestedPool.runtimeDomain))
|
|
8003
|
+
throw new Error("Capacity pool request lacks its bound domains.");
|
|
8004
|
+
}
|
|
8005
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
|
|
8006
|
+
throw new Error("Invalid capacity expected revision.");
|
|
8007
|
+
assertPrivateDirectory(this.directory);
|
|
8008
|
+
return withFileLockSync(
|
|
8009
|
+
`${this.file}.lock`,
|
|
8010
|
+
{ activity: "capacity reservation", waitMs: 100 },
|
|
8011
|
+
() => {
|
|
8012
|
+
const snapshot = this.read();
|
|
8013
|
+
if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
|
|
8014
|
+
const pools = structuredClone(snapshot.pools ?? []);
|
|
8015
|
+
const poolChanged = mergePools(pools, requestedPool === void 0 ? [] : [requestedPool]);
|
|
8016
|
+
const existing = snapshot.reservations.find(
|
|
8017
|
+
(entry) => entry.environmentId === request.environmentId
|
|
8018
|
+
);
|
|
8019
|
+
if (previous) {
|
|
8020
|
+
if (snapshot.revision !== previous.revision || !existing || existing.reservationId !== previous.reservationId || existing.operationId !== previous.operationId || existing.policyRevision !== request.policyRevision)
|
|
8021
|
+
throw new Error("Capacity expansion predecessor changed.");
|
|
8022
|
+
const totals = { ...existing.totals };
|
|
8023
|
+
for (const [domain, bytes2] of Object.entries(request.totals))
|
|
8024
|
+
totals[domain] = Math.max(totals[domain] ?? 0, bytes2);
|
|
8025
|
+
request = {
|
|
8026
|
+
...request,
|
|
8027
|
+
totals,
|
|
8028
|
+
startup: existing.startup || request.startup,
|
|
8029
|
+
heavy: existing.heavy || request.heavy
|
|
8030
|
+
};
|
|
8031
|
+
} else if (existing) {
|
|
8032
|
+
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(
|
|
8033
|
+
([domain, bytes2]) => request.totals[domain] !== bytes2
|
|
8034
|
+
))
|
|
8035
|
+
throw new Error("Environment retains an earlier capacity reservation.");
|
|
8036
|
+
}
|
|
8037
|
+
const decision = evaluateCapacity(
|
|
8038
|
+
budgets,
|
|
8039
|
+
samples,
|
|
8040
|
+
snapshot.reservations,
|
|
8041
|
+
request,
|
|
8042
|
+
nowMs,
|
|
8043
|
+
maxSampleAgeMs,
|
|
8044
|
+
pools
|
|
8045
|
+
);
|
|
8046
|
+
if (!decision.admitted) return decision;
|
|
8047
|
+
if (!previous && existing && !poolChanged)
|
|
8048
|
+
return { admitted: true, revision: snapshot.revision, joined: true };
|
|
8049
|
+
if (snapshot.revision === Number.MAX_SAFE_INTEGER)
|
|
8050
|
+
throw new Error("Capacity reservation revision exhausted.");
|
|
8051
|
+
snapshot.revision++;
|
|
8052
|
+
if (poolChanged) snapshot.pools = pools;
|
|
8053
|
+
if (existing)
|
|
8054
|
+
snapshot.reservations[snapshot.reservations.indexOf(existing)] = structuredClone(request);
|
|
8055
|
+
else snapshot.reservations.push(structuredClone(request));
|
|
8056
|
+
validate(snapshot);
|
|
8057
|
+
writeFileAtomically(this.file, serializeSnapshot(snapshot));
|
|
8058
|
+
return { admitted: true, revision: snapshot.revision, joined: false };
|
|
8059
|
+
}
|
|
8060
|
+
);
|
|
8061
|
+
}
|
|
8062
|
+
};
|
|
8063
|
+
|
|
8064
|
+
// src/core/controller-client.ts
|
|
8065
|
+
var import_node_crypto12 = require("crypto");
|
|
8066
|
+
var import_node_fs23 = __toESM(require("fs"));
|
|
8067
|
+
var import_node_net2 = __toESM(require("net"));
|
|
8068
|
+
var import_node_path28 = __toESM(require("path"));
|
|
8069
|
+
|
|
8070
|
+
// src/core/controller-protocol.ts
|
|
8071
|
+
var import_node_path27 = __toESM(require("path"));
|
|
8072
|
+
var CONTROLLER_FRAME_BYTES = 64 * 1024;
|
|
8073
|
+
var CONTROLLER_MAX_OPERATION_REQUEST_BYTES = 32 * 1024;
|
|
8074
|
+
var CONTROLLER_PROFILE_TOKEN_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
8075
|
+
var CONTROLLER_PROFILE_RE = new RegExp(
|
|
8076
|
+
`^(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)})(?:,(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)}))*$`
|
|
8077
|
+
);
|
|
8078
|
+
|
|
8079
|
+
// src/core/controller-store.ts
|
|
8080
|
+
var import_node_crypto13 = require("crypto");
|
|
8081
|
+
var import_node_fs24 = __toESM(require("fs"));
|
|
8082
|
+
var import_node_path29 = __toESM(require("path"));
|
|
8083
|
+
var CONTROLLER_SNAPSHOT_BYTES = 1048576;
|
|
8084
|
+
function fail() {
|
|
8085
|
+
throw new Error("Invalid controller snapshot.");
|
|
8086
|
+
}
|
|
8087
|
+
function fields(value, expected) {
|
|
8088
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail();
|
|
8089
|
+
const actual = Object.keys(value);
|
|
8090
|
+
if (actual.length !== expected.length || actual.some((key) => !expected.includes(key))) fail();
|
|
8091
|
+
}
|
|
8092
|
+
function text2(value, max = 256) {
|
|
8093
|
+
return typeof value === "string" && value.length > 0 && value.length <= max && ![...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
|
|
8094
|
+
}
|
|
8095
|
+
function id3(value) {
|
|
8096
|
+
return text2(value, 128) && /^[a-zA-Z0-9_-]+$/.test(value);
|
|
8097
|
+
}
|
|
8098
|
+
function counter(value) {
|
|
8099
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
8100
|
+
}
|
|
8101
|
+
function validateControllerSnapshot(value) {
|
|
8102
|
+
fields(value, [
|
|
8103
|
+
"version",
|
|
8104
|
+
"store",
|
|
8105
|
+
"epoch",
|
|
8106
|
+
"revision",
|
|
8107
|
+
"nextSequence",
|
|
8108
|
+
"environments",
|
|
8109
|
+
"sessions",
|
|
8110
|
+
"events"
|
|
8111
|
+
]);
|
|
8112
|
+
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)
|
|
8113
|
+
fail();
|
|
8114
|
+
const environments = /* @__PURE__ */ new Set();
|
|
8115
|
+
const checkouts = /* @__PURE__ */ new Set();
|
|
8116
|
+
for (const env of value.environments) {
|
|
8117
|
+
fields(env, [
|
|
8118
|
+
"id",
|
|
8119
|
+
"repoPath",
|
|
8120
|
+
"workspace",
|
|
8121
|
+
"provider",
|
|
8122
|
+
"providerId",
|
|
8123
|
+
"profile",
|
|
8124
|
+
"fingerprint"
|
|
8125
|
+
]);
|
|
8126
|
+
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)))
|
|
8127
|
+
fail();
|
|
8128
|
+
environments.add(env.id);
|
|
8129
|
+
checkouts.add(env.repoPath);
|
|
8130
|
+
}
|
|
8131
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
8132
|
+
const usedEnvironments = /* @__PURE__ */ new Set();
|
|
8133
|
+
for (const session of value.sessions) {
|
|
8134
|
+
fields(session, [
|
|
8135
|
+
"id",
|
|
8136
|
+
"environmentId",
|
|
8137
|
+
"generation",
|
|
8138
|
+
"requirements",
|
|
8139
|
+
"renewedAtMs",
|
|
8140
|
+
...Object.hasOwn(session, "observation") ? ["observation"] : []
|
|
8141
|
+
]);
|
|
8142
|
+
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(
|
|
8143
|
+
(r) => !text2(r, 132) || !/^(runtime|app:[a-z0-9][a-z0-9-]*)$/.test(r)
|
|
8144
|
+
))
|
|
8145
|
+
fail();
|
|
8146
|
+
if (session.observation !== void 0) {
|
|
8147
|
+
const observation = session.observation;
|
|
8148
|
+
fields(observation, [
|
|
8149
|
+
"status",
|
|
8150
|
+
"sampledAtMs",
|
|
8151
|
+
"validUntilMs",
|
|
8152
|
+
"journalRevision",
|
|
8153
|
+
"runtimeFingerprint"
|
|
8154
|
+
]);
|
|
8155
|
+
if (![
|
|
8156
|
+
"READY",
|
|
8157
|
+
"APP_ERROR",
|
|
8158
|
+
"BLOCKED",
|
|
8159
|
+
"STOPPED",
|
|
8160
|
+
"STARTING",
|
|
8161
|
+
"RECOVERING",
|
|
8162
|
+
"WAITING_CAPACITY",
|
|
8163
|
+
"PARKED_CAPACITY",
|
|
8164
|
+
"UNKNOWN"
|
|
8165
|
+
].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))
|
|
8166
|
+
fail();
|
|
8167
|
+
}
|
|
8168
|
+
sessions.add(session.id);
|
|
8169
|
+
usedEnvironments.add(session.environmentId);
|
|
8170
|
+
}
|
|
8171
|
+
if (usedEnvironments.size !== environments.size) fail();
|
|
8172
|
+
let previous = 0;
|
|
8173
|
+
for (const event of value.events) {
|
|
8174
|
+
fields(event, ["sequence", "session", "generation", "kind"]);
|
|
8175
|
+
if (!counter(event.sequence) || event.sequence <= previous || event.sequence >= value.nextSequence || !id3(event.session) || !id3(event.generation) || !["acquired", "renewed", "released", "expired", "invalidated", "observed"].includes(
|
|
8176
|
+
String(event.kind)
|
|
8177
|
+
))
|
|
8178
|
+
fail();
|
|
8179
|
+
previous = event.sequence;
|
|
8180
|
+
}
|
|
8181
|
+
if (Buffer.byteLength(JSON.stringify(value.events)) > 262144 || Buffer.byteLength(JSON.stringify(value)) + 1 > CONTROLLER_SNAPSHOT_BYTES)
|
|
8182
|
+
fail();
|
|
8183
|
+
}
|
|
8184
|
+
var ControllerStore = class {
|
|
8185
|
+
constructor(directory, write = writeFileAtomically) {
|
|
8186
|
+
this.directory = directory;
|
|
8187
|
+
this.write = write;
|
|
8188
|
+
this.file = import_node_path29.default.join(directory, "snapshot.json");
|
|
8189
|
+
}
|
|
8190
|
+
file;
|
|
8191
|
+
read() {
|
|
8192
|
+
let fd;
|
|
8193
|
+
try {
|
|
8194
|
+
fd = import_node_fs24.default.openSync(
|
|
8195
|
+
this.file,
|
|
8196
|
+
import_node_fs24.default.constants.O_RDONLY | import_node_fs24.default.constants.O_NOFOLLOW | import_node_fs24.default.constants.O_NONBLOCK
|
|
8197
|
+
);
|
|
8198
|
+
} catch (error) {
|
|
8199
|
+
if (error.code === "ENOENT") return void 0;
|
|
8200
|
+
throw new Error("Controller snapshot cannot be opened.");
|
|
8201
|
+
}
|
|
8202
|
+
try {
|
|
8203
|
+
const stat = import_node_fs24.default.fstatSync(fd);
|
|
8204
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0 || stat.size > CONTROLLER_SNAPSHOT_BYTES)
|
|
8205
|
+
fail();
|
|
8206
|
+
const bytes2 = Buffer.alloc(CONTROLLER_SNAPSHOT_BYTES + 1);
|
|
8207
|
+
let length = 0;
|
|
8208
|
+
while (length < bytes2.length) {
|
|
8209
|
+
const count = import_node_fs24.default.readSync(fd, bytes2, length, bytes2.length - length, null);
|
|
8210
|
+
if (!count) break;
|
|
8211
|
+
length += count;
|
|
8212
|
+
}
|
|
8213
|
+
if (length > CONTROLLER_SNAPSHOT_BYTES) fail();
|
|
8214
|
+
const value = JSON.parse(bytes2.subarray(0, length).toString("utf8"));
|
|
8215
|
+
validateControllerSnapshot(value);
|
|
8216
|
+
return value;
|
|
8217
|
+
} catch {
|
|
8218
|
+
throw new Error("Controller snapshot is unreadable or invalid.");
|
|
8219
|
+
} finally {
|
|
8220
|
+
import_node_fs24.default.closeSync(fd);
|
|
8221
|
+
}
|
|
8222
|
+
}
|
|
8223
|
+
persist(snapshot) {
|
|
8224
|
+
validateControllerSnapshot(snapshot);
|
|
8225
|
+
this.read();
|
|
8226
|
+
const contents = `${JSON.stringify(snapshot)}
|
|
8227
|
+
`;
|
|
8228
|
+
try {
|
|
8229
|
+
this.write(this.file, contents);
|
|
8230
|
+
} catch {
|
|
8231
|
+
const current = this.read();
|
|
8232
|
+
if (!current || `${JSON.stringify(current)}
|
|
8233
|
+
` !== contents)
|
|
8234
|
+
throw new Error("Controller snapshot commit failed.");
|
|
8235
|
+
for (const file of [this.file, this.directory]) {
|
|
8236
|
+
const fd = import_node_fs24.default.openSync(file, import_node_fs24.default.constants.O_RDONLY | import_node_fs24.default.constants.O_NOFOLLOW);
|
|
8237
|
+
try {
|
|
8238
|
+
import_node_fs24.default.fsyncSync(fd);
|
|
8239
|
+
} finally {
|
|
8240
|
+
import_node_fs24.default.closeSync(fd);
|
|
8241
|
+
}
|
|
8242
|
+
}
|
|
8243
|
+
}
|
|
8244
|
+
}
|
|
8245
|
+
startIncarnation() {
|
|
8246
|
+
const previous = this.read();
|
|
8247
|
+
if (previous && (previous.epoch === Number.MAX_SAFE_INTEGER || previous.revision === Number.MAX_SAFE_INTEGER))
|
|
8248
|
+
throw new Error("Controller snapshot counters exhausted.");
|
|
8249
|
+
const snapshot = {
|
|
8250
|
+
version: 1,
|
|
8251
|
+
store: previous?.store ?? (0, import_node_crypto13.randomUUID)(),
|
|
8252
|
+
epoch: (previous?.epoch ?? 0) + 1,
|
|
8253
|
+
revision: (previous?.revision ?? 0) + 1,
|
|
8254
|
+
nextSequence: 1,
|
|
8255
|
+
environments: [],
|
|
8256
|
+
sessions: [],
|
|
8257
|
+
events: []
|
|
8258
|
+
};
|
|
8259
|
+
this.persist(snapshot);
|
|
8260
|
+
return snapshot;
|
|
8261
|
+
}
|
|
8262
|
+
};
|
|
7318
8263
|
|
|
7319
8264
|
// src/core/reliability-contract.ts
|
|
7320
8265
|
var RELIABILITY_CONTRACT_VERSION = 2;
|
|
@@ -7643,7 +8588,6 @@ function canExecAfterInterruptedEnsure(state) {
|
|
|
7643
8588
|
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));
|
|
7644
8589
|
}
|
|
7645
8590
|
function handleOperationRequest(state, event) {
|
|
7646
|
-
if (state.executionPolicy !== "manual") return unchanged(state, "blocked");
|
|
7647
8591
|
const previous = state.operationHistory.find((entry) => entry.key === event.key);
|
|
7648
8592
|
if (previous)
|
|
7649
8593
|
return unchanged(
|
|
@@ -7652,6 +8596,8 @@ function handleOperationRequest(state, event) {
|
|
|
7652
8596
|
);
|
|
7653
8597
|
if (state.operationHistory.some((entry) => entry.id === event.operationId))
|
|
7654
8598
|
return unchanged(state, "conflict");
|
|
8599
|
+
if (state.executionPolicy !== "manual" && state.operationHistory.length >= RELIABILITY_MAX_ITEMS)
|
|
8600
|
+
return unchanged(state, "blocked");
|
|
7655
8601
|
if (event.kind === "exec" && canExecAfterInterruptedEnsure(state) && event.recoverInterruptedEnsure !== true)
|
|
7656
8602
|
return blocked(
|
|
7657
8603
|
state,
|
|
@@ -7674,7 +8620,7 @@ function handleOperationRequest(state, event) {
|
|
|
7674
8620
|
state,
|
|
7675
8621
|
`exec requires a running workspace (runtimeRunning=${event.runtimeRunning}, desired='${state.desired}').`
|
|
7676
8622
|
);
|
|
7677
|
-
const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
|
|
8623
|
+
const rollover = state.executionPolicy === "manual" && state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
|
|
7678
8624
|
let retired = -1;
|
|
7679
8625
|
if (rollover) {
|
|
7680
8626
|
const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
|
|
@@ -7696,6 +8642,7 @@ function handleOperationRequest(state, event) {
|
|
|
7696
8642
|
state.desired = "running";
|
|
7697
8643
|
state.phase = "queued";
|
|
7698
8644
|
state.profile = event.profile;
|
|
8645
|
+
if (state.executionPolicy === "capacity-managed") state.admission = "waiting";
|
|
7699
8646
|
state.stopProof = { workloadsStopped: false, routesRemoved: false };
|
|
7700
8647
|
state.consumers = [cloneConsumer(event.consumer)];
|
|
7701
8648
|
state.requests = [
|
|
@@ -7721,7 +8668,11 @@ function handleOperationRequest(state, event) {
|
|
|
7721
8668
|
profile: event.profile,
|
|
7722
8669
|
consumer: cloneConsumer(event.consumer)
|
|
7723
8670
|
});
|
|
7724
|
-
return transition(
|
|
8671
|
+
return transition(
|
|
8672
|
+
state,
|
|
8673
|
+
"accepted",
|
|
8674
|
+
state.executionPolicy === "capacity-managed" ? [effect(state, "request-admission", event.operationId)] : []
|
|
8675
|
+
);
|
|
7725
8676
|
}
|
|
7726
8677
|
function handleAdmission(state, event) {
|
|
7727
8678
|
if (state.executionPolicy === "manual" || state.desired !== "running" && state.desired !== "parked-for-capacity" || state.requests.length === 0) {
|
|
@@ -7846,7 +8797,7 @@ function handleObservation(state, event, nowMs) {
|
|
|
7846
8797
|
return transition(state, "accepted");
|
|
7847
8798
|
}
|
|
7848
8799
|
function handleStop(state) {
|
|
7849
|
-
if (state.desired === "stopped-by-user" && state.
|
|
8800
|
+
if (state.desired === "stopped-by-user" && state.phase === "stopping")
|
|
7850
8801
|
return unchanged(state, "joined");
|
|
7851
8802
|
const intentRevision = advance(state.intentRevision);
|
|
7852
8803
|
if (intentRevision === null) return unchanged(state, "blocked");
|
|
@@ -8068,7 +9019,7 @@ function stepReliability(state, event, nowMs) {
|
|
|
8068
9019
|
if (!isReliabilityCounter(nowMs) || nowMs < state.observationsAfterMs)
|
|
8069
9020
|
throw new Error("Invalid reliability clock.");
|
|
8070
9021
|
const result = applyReliabilityEvent(state, event, nowMs);
|
|
8071
|
-
if (result.outcome === "accepted" && result.state.
|
|
9022
|
+
if (result.outcome === "accepted" && result.state.operation) {
|
|
8072
9023
|
const operation = result.state.operation;
|
|
8073
9024
|
result.state.operationHistory = result.state.operationHistory.map(
|
|
8074
9025
|
(entry) => entry.id === operation.id ? { ...entry, ...operation } : entry
|
|
@@ -8081,11 +9032,12 @@ function stepReliability(state, event, nowMs) {
|
|
|
8081
9032
|
}
|
|
8082
9033
|
|
|
8083
9034
|
// src/core/reliability-operation-store.ts
|
|
8084
|
-
var
|
|
8085
|
-
var
|
|
8086
|
-
var
|
|
9035
|
+
var import_node_crypto14 = require("crypto");
|
|
9036
|
+
var import_node_fs25 = __toESM(require("fs"));
|
|
9037
|
+
var import_node_path30 = __toESM(require("path"));
|
|
9038
|
+
var import_node_util5 = require("util");
|
|
8087
9039
|
function installedCliVersion() {
|
|
8088
|
-
return true ? "0.0.
|
|
9040
|
+
return true ? "0.0.69" : "0.0.0-dev";
|
|
8089
9041
|
}
|
|
8090
9042
|
function compareCliVersions(left, right) {
|
|
8091
9043
|
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -8098,21 +9050,112 @@ function compareCliVersions(left, right) {
|
|
|
8098
9050
|
return 0;
|
|
8099
9051
|
}
|
|
8100
9052
|
var MAX_RECORD_BYTES = 1048576;
|
|
9053
|
+
var MAX_RELIABILITY_JOURNALS = 256;
|
|
9054
|
+
var MAX_RELIABILITY_DIRECTORY_ENTRIES = MAX_RELIABILITY_JOURNALS * 2;
|
|
8101
9055
|
function identityKey(identity2) {
|
|
8102
|
-
if (!
|
|
9056
|
+
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)) {
|
|
8103
9057
|
throw new Error("Invalid reliability workspace identity.");
|
|
8104
9058
|
}
|
|
8105
|
-
return (0,
|
|
9059
|
+
return (0, import_node_crypto14.createHash)("sha256").update(identity2.repoPath).digest("hex");
|
|
8106
9060
|
}
|
|
8107
9061
|
function reliabilityOperationPath(identity2) {
|
|
8108
|
-
return
|
|
9062
|
+
return import_node_path30.default.join(DEVROUTER_HOME, "reliability", `${identityKey(identity2)}.json`);
|
|
8109
9063
|
}
|
|
8110
9064
|
function keys(value, expected) {
|
|
8111
9065
|
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !expected.includes(key))) {
|
|
8112
9066
|
throw new Error("Reliability record contains unsupported fields.");
|
|
8113
9067
|
}
|
|
8114
9068
|
}
|
|
8115
|
-
function
|
|
9069
|
+
function exactKeys(value, expected, message) {
|
|
9070
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length !== expected.length || Object.keys(value).some((key) => !expected.includes(key))) {
|
|
9071
|
+
throw new Error(message);
|
|
9072
|
+
}
|
|
9073
|
+
}
|
|
9074
|
+
function validateCapacityPhaseSettlement(record5, state, enrollment) {
|
|
9075
|
+
const settlement = record5.phaseSettlement;
|
|
9076
|
+
if (settlement === void 0 || settlement === null) return;
|
|
9077
|
+
if (record5.version !== 2 || !enrollment || state.executionPolicy !== "capacity-managed")
|
|
9078
|
+
throw new Error("Phase settlement requires durable capacity enrollment.");
|
|
9079
|
+
exactKeys(
|
|
9080
|
+
settlement,
|
|
9081
|
+
["id", "fence", "target", "estimatesDigest"],
|
|
9082
|
+
"Invalid capacity phase settlement fields."
|
|
9083
|
+
);
|
|
9084
|
+
exactKeys(
|
|
9085
|
+
settlement.fence,
|
|
9086
|
+
["environmentId", "intentRevision", "runtimeGeneration", "controllerEpoch"],
|
|
9087
|
+
"Invalid capacity phase settlement fence."
|
|
9088
|
+
);
|
|
9089
|
+
exactKeys(
|
|
9090
|
+
settlement.target,
|
|
9091
|
+
[
|
|
9092
|
+
"environmentId",
|
|
9093
|
+
"operationId",
|
|
9094
|
+
"reservationId",
|
|
9095
|
+
"policyRevision",
|
|
9096
|
+
"totals",
|
|
9097
|
+
"startup",
|
|
9098
|
+
"heavy"
|
|
9099
|
+
],
|
|
9100
|
+
"Invalid capacity phase settlement target fields."
|
|
9101
|
+
);
|
|
9102
|
+
const target = settlement.target;
|
|
9103
|
+
const totals = target.totals;
|
|
9104
|
+
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))
|
|
9105
|
+
throw new Error("Invalid capacity phase settlement.");
|
|
9106
|
+
if (!totals || typeof totals !== "object" || Array.isArray(totals) || Object.keys(totals).length < 1 || Object.keys(totals).length > 256 || Object.entries(totals).some(
|
|
9107
|
+
([domain, bytes2]) => !isReliabilityId(domain) || !Number.isSafeInteger(bytes2) || bytes2 < 0
|
|
9108
|
+
))
|
|
9109
|
+
throw new Error("Invalid capacity phase settlement totals.");
|
|
9110
|
+
const capacity = record5.capacity;
|
|
9111
|
+
if (capacity?.validUntilMs !== 0 || target.operationId !== capacity?.operationId || target.reservationId !== capacity?.reservationId || target.policyRevision !== capacity?.policyRevision || settlement.estimatesDigest !== enrollment.estimatesDigest)
|
|
9112
|
+
throw new Error("Capacity phase settlement does not match durable capacity authority.");
|
|
9113
|
+
const targetDomains = Object.keys(totals);
|
|
9114
|
+
if (targetDomains.length !== 2 || !targetDomains.includes(enrollment.hostDomain) || !targetDomains.includes(enrollment.runtimeDomain))
|
|
9115
|
+
throw new Error("Capacity phase settlement domains do not match enrollment.");
|
|
9116
|
+
}
|
|
9117
|
+
function validateStartupWitness(record5, state, enrollment) {
|
|
9118
|
+
const witness = record5.startupWitness;
|
|
9119
|
+
if (witness === void 0 || witness === null) return;
|
|
9120
|
+
if (record5.version !== 2 || !enrollment || state.executionPolicy !== "capacity-managed")
|
|
9121
|
+
throw new Error("Startup witness requires current durable managed intent.");
|
|
9122
|
+
exactKeys(
|
|
9123
|
+
witness,
|
|
9124
|
+
[
|
|
9125
|
+
"operationId",
|
|
9126
|
+
"fence",
|
|
9127
|
+
"provider",
|
|
9128
|
+
"profile",
|
|
9129
|
+
"sourceConfigSha256",
|
|
9130
|
+
"effectiveConfigSha256",
|
|
9131
|
+
"composeFiles",
|
|
9132
|
+
"primaryService",
|
|
9133
|
+
"startupServices",
|
|
9134
|
+
"retainedContainerIds"
|
|
9135
|
+
],
|
|
9136
|
+
"Invalid capacity startup witness fields."
|
|
9137
|
+
);
|
|
9138
|
+
exactKeys(
|
|
9139
|
+
witness.fence,
|
|
9140
|
+
["environmentId", "intentRevision", "runtimeGeneration", "controllerEpoch"],
|
|
9141
|
+
"Invalid capacity startup witness fence."
|
|
9142
|
+
);
|
|
9143
|
+
exactKeys(
|
|
9144
|
+
witness.provider,
|
|
9145
|
+
["id", "context", "uid", "sourceContainer"],
|
|
9146
|
+
"Invalid capacity startup witness provider fields."
|
|
9147
|
+
);
|
|
9148
|
+
const digest = (value) => typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
|
|
9149
|
+
const boundedText = (value) => typeof value === "string" && value.length <= 256 && ![...value].some(
|
|
9150
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127
|
|
9151
|
+
);
|
|
9152
|
+
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;
|
|
9153
|
+
const unique = (values) => new Set(values).size === values.length;
|
|
9154
|
+
const fence = witness.fence;
|
|
9155
|
+
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))
|
|
9156
|
+
throw new Error("Invalid capacity startup witness.");
|
|
9157
|
+
}
|
|
9158
|
+
function validate2(record5, identity2) {
|
|
8116
9159
|
keys(record5, [
|
|
8117
9160
|
"version",
|
|
8118
9161
|
"identity",
|
|
@@ -8121,7 +9164,16 @@ function validate(record5, identity2) {
|
|
|
8121
9164
|
"worker",
|
|
8122
9165
|
"effectSequence",
|
|
8123
9166
|
"outcome",
|
|
8124
|
-
"
|
|
9167
|
+
"result",
|
|
9168
|
+
"writtenByVersion",
|
|
9169
|
+
...record5.version === 2 ? [
|
|
9170
|
+
"capacity",
|
|
9171
|
+
"enrollment",
|
|
9172
|
+
"activeProfile",
|
|
9173
|
+
"preparation",
|
|
9174
|
+
"phaseSettlement",
|
|
9175
|
+
"startupWitness"
|
|
9176
|
+
] : []
|
|
8125
9177
|
]);
|
|
8126
9178
|
if (record5.writtenByVersion !== void 0 && compareCliVersions(record5.writtenByVersion, installedCliVersion()) > 0) {
|
|
8127
9179
|
throw new Error(
|
|
@@ -8129,13 +9181,57 @@ function validate(record5, identity2) {
|
|
|
8129
9181
|
);
|
|
8130
9182
|
}
|
|
8131
9183
|
keys(record5.identity, ["repoPath", "workspace", "provider"]);
|
|
8132
|
-
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)) {
|
|
9184
|
+
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)) {
|
|
8133
9185
|
throw new Error("Reliability record has invalid version, ownership, or counters.");
|
|
8134
9186
|
}
|
|
9187
|
+
if (record5.version === 2) {
|
|
9188
|
+
const capacity = record5.capacity;
|
|
9189
|
+
if (capacity === void 0) throw new Error("Invalid capacity authority binding.");
|
|
9190
|
+
if (capacity !== null) {
|
|
9191
|
+
keys(capacity, [
|
|
9192
|
+
"reservationId",
|
|
9193
|
+
"operationId",
|
|
9194
|
+
"workerId",
|
|
9195
|
+
"policyRevision",
|
|
9196
|
+
"validUntilMs",
|
|
9197
|
+
"snapshotRevision",
|
|
9198
|
+
"controller",
|
|
9199
|
+
"execSteady"
|
|
9200
|
+
]);
|
|
9201
|
+
if (capacity.controller !== void 0) {
|
|
9202
|
+
keys(capacity.controller, ["store", "epoch"]);
|
|
9203
|
+
if (!isReliabilityId(capacity.controller.store) || !isReliabilityCounter(capacity.controller.epoch) || capacity.controller.epoch < 1)
|
|
9204
|
+
throw new Error("Invalid capacity controller identity.");
|
|
9205
|
+
}
|
|
9206
|
+
}
|
|
9207
|
+
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)))
|
|
9208
|
+
throw new Error("Invalid capacity authority binding.");
|
|
9209
|
+
}
|
|
8135
9210
|
const state = record5.state;
|
|
8136
9211
|
assertReliabilityState(state);
|
|
8137
|
-
if (state.environmentId !== identityKey(identity2)
|
|
8138
|
-
throw new Error("Reliability record does not
|
|
9212
|
+
if (state.environmentId !== identityKey(identity2)) {
|
|
9213
|
+
throw new Error("Reliability record does not match this workspace.");
|
|
9214
|
+
}
|
|
9215
|
+
if (record5.enrollment !== void 0) {
|
|
9216
|
+
const enrollment = record5.enrollment;
|
|
9217
|
+
keys(enrollment, [
|
|
9218
|
+
"policyRevision",
|
|
9219
|
+
"gitCommonDir",
|
|
9220
|
+
"providerId",
|
|
9221
|
+
"hostDomain",
|
|
9222
|
+
"runtimeDomain",
|
|
9223
|
+
"endpoint",
|
|
9224
|
+
"daemonId",
|
|
9225
|
+
"estimatesDigest"
|
|
9226
|
+
]);
|
|
9227
|
+
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;
|
|
9228
|
+
const providerIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && ![...value].some(
|
|
9229
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127
|
|
9230
|
+
);
|
|
9231
|
+
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)))
|
|
9232
|
+
throw new Error("Invalid durable capacity enrollment.");
|
|
9233
|
+
} else if (state.executionPolicy !== "manual" || record5.activeProfile !== void 0) {
|
|
9234
|
+
throw new Error("Managed lifecycle requires durable capacity enrollment.");
|
|
8139
9235
|
}
|
|
8140
9236
|
keys(state, [
|
|
8141
9237
|
"contractVersion",
|
|
@@ -8170,6 +9266,52 @@ function validate(record5, identity2) {
|
|
|
8170
9266
|
keys(entry, ["key", "mode", "consumerId", "operationId", "profile", "intentRevision"]);
|
|
8171
9267
|
for (const entry of state.observations)
|
|
8172
9268
|
keys(entry, ["capability", "infrastructure", "application", "observedAtMs", "validForMs"]);
|
|
9269
|
+
if (record5.preparation !== void 0 && record5.preparation !== null) {
|
|
9270
|
+
if (record5.version !== 2 || record5.enrollment === void 0 || state.executionPolicy !== "capacity-managed")
|
|
9271
|
+
throw new Error("Preparation receipt requires durable capacity enrollment.");
|
|
9272
|
+
const preparation = record5.preparation;
|
|
9273
|
+
keys(preparation, ["operationId", "profile", "fence"]);
|
|
9274
|
+
keys(preparation.fence, [
|
|
9275
|
+
"environmentId",
|
|
9276
|
+
"intentRevision",
|
|
9277
|
+
"runtimeGeneration",
|
|
9278
|
+
"controllerEpoch"
|
|
9279
|
+
]);
|
|
9280
|
+
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))
|
|
9281
|
+
throw new Error("Invalid preparation receipt.");
|
|
9282
|
+
const preparedOperation = state.operationHistory.find(
|
|
9283
|
+
(entry) => entry.id === preparation.operationId
|
|
9284
|
+
);
|
|
9285
|
+
if (preparedOperation?.kind !== "ensure" || preparedOperation.profile !== preparation.profile || preparedOperation.status !== "COMPLETED")
|
|
9286
|
+
throw new Error("Preparation receipt does not match a completed ensure operation.");
|
|
9287
|
+
}
|
|
9288
|
+
validateCapacityPhaseSettlement(record5, state, record5.enrollment);
|
|
9289
|
+
validateStartupWitness(record5, state, record5.enrollment);
|
|
9290
|
+
const execSteady = record5.capacity?.execSteady;
|
|
9291
|
+
if (execSteady !== void 0) {
|
|
9292
|
+
const enrollment = record5.enrollment;
|
|
9293
|
+
if (record5.version !== 2 || !enrollment || state.executionPolicy !== "capacity-managed")
|
|
9294
|
+
throw new Error("Exec steady receipt requires durable capacity enrollment.");
|
|
9295
|
+
exactKeys(
|
|
9296
|
+
execSteady,
|
|
9297
|
+
["profile", "estimatesDigest", "fence", "totals"],
|
|
9298
|
+
"Invalid exec steady receipt fields."
|
|
9299
|
+
);
|
|
9300
|
+
exactKeys(
|
|
9301
|
+
execSteady.fence,
|
|
9302
|
+
["environmentId", "intentRevision", "runtimeGeneration", "controllerEpoch"],
|
|
9303
|
+
"Invalid exec steady receipt fence."
|
|
9304
|
+
);
|
|
9305
|
+
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))
|
|
9306
|
+
throw new Error("Invalid exec steady receipt.");
|
|
9307
|
+
exactKeys(
|
|
9308
|
+
execSteady.totals,
|
|
9309
|
+
[enrollment.hostDomain, enrollment.runtimeDomain],
|
|
9310
|
+
"Exec steady receipt domains do not match enrollment."
|
|
9311
|
+
);
|
|
9312
|
+
if (!Object.values(execSteady.totals).every(isReliabilityCounter))
|
|
9313
|
+
throw new Error("Invalid exec steady receipt totals.");
|
|
9314
|
+
}
|
|
8173
9315
|
if (state.operation) {
|
|
8174
9316
|
const current = state.operationHistory.find((entry) => entry.id === state.operation?.id);
|
|
8175
9317
|
if (!current || operationKeys.some(
|
|
@@ -8186,6 +9328,12 @@ function validate(record5, identity2) {
|
|
|
8186
9328
|
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))) {
|
|
8187
9329
|
throw new Error("Reliability execution outcome is invalid.");
|
|
8188
9330
|
}
|
|
9331
|
+
if (record5.result !== void 0 && record5.result !== null) {
|
|
9332
|
+
const serialized = JSON.stringify(record5.result);
|
|
9333
|
+
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)) {
|
|
9334
|
+
throw new Error("Reliability operation result is invalid.");
|
|
9335
|
+
}
|
|
9336
|
+
}
|
|
8189
9337
|
}
|
|
8190
9338
|
if (record5.worker !== null) {
|
|
8191
9339
|
keys(record5.worker, ["id", "operationId", "pid", "birth"]);
|
|
@@ -8194,37 +9342,59 @@ function validate(record5, identity2) {
|
|
|
8194
9342
|
}
|
|
8195
9343
|
}
|
|
8196
9344
|
}
|
|
9345
|
+
function assertCapacityEffect(record5, workerId, nowMs, directory = import_node_path30.default.join(DEVROUTER_HOME, "controller")) {
|
|
9346
|
+
if (record5.version === 1) return;
|
|
9347
|
+
const binding = record5.capacity;
|
|
9348
|
+
if (!binding || binding.workerId !== workerId || binding.operationId !== record5.state.operation?.id || binding.validUntilMs <= nowMs)
|
|
9349
|
+
throw new Error("Capacity effect authority is absent or stale.");
|
|
9350
|
+
if (record5.state.executionPolicy === "capacity-managed") {
|
|
9351
|
+
const controller = new ControllerStore(directory).read();
|
|
9352
|
+
if (!binding.controller || !controller || controller.store !== binding.controller.store || controller.epoch !== binding.controller.epoch)
|
|
9353
|
+
throw new Error("Capacity controller incarnation changed.");
|
|
9354
|
+
}
|
|
9355
|
+
const snapshot = new CapacityStore(directory).read();
|
|
9356
|
+
if (binding.snapshotRevision === void 0 || snapshot.revision !== binding.snapshotRevision)
|
|
9357
|
+
throw new Error("Capacity snapshot authority is absent or stale.");
|
|
9358
|
+
const reservation = snapshot.reservations.find(
|
|
9359
|
+
(entry) => entry.reservationId === binding.reservationId
|
|
9360
|
+
);
|
|
9361
|
+
if (!reservation || reservation.environmentId !== record5.state.environmentId || reservation.operationId !== binding.operationId || reservation.policyRevision !== binding.policyRevision)
|
|
9362
|
+
throw new Error("Capacity effect reservation does not match lifecycle intent.");
|
|
9363
|
+
}
|
|
8197
9364
|
function readReliabilityOperation(identity2) {
|
|
8198
9365
|
const file = reliabilityOperationPath(identity2);
|
|
9366
|
+
const parsed = readBoundedPrivateJson(file, "Reliability record");
|
|
9367
|
+
if (parsed === void 0) return void 0;
|
|
9368
|
+
const record5 = parsed;
|
|
9369
|
+
validate2(record5, identity2);
|
|
9370
|
+
return record5;
|
|
9371
|
+
}
|
|
9372
|
+
function readBoundedPrivateJson(file, label) {
|
|
8199
9373
|
let descriptor;
|
|
8200
9374
|
try {
|
|
8201
|
-
descriptor =
|
|
9375
|
+
descriptor = import_node_fs25.default.openSync(
|
|
8202
9376
|
file,
|
|
8203
|
-
|
|
9377
|
+
import_node_fs25.default.constants.O_RDONLY | import_node_fs25.default.constants.O_NOFOLLOW | import_node_fs25.default.constants.O_NONBLOCK
|
|
8204
9378
|
);
|
|
8205
9379
|
} catch (error) {
|
|
8206
9380
|
if (error.code === "ENOENT") return void 0;
|
|
8207
9381
|
throw error;
|
|
8208
9382
|
}
|
|
8209
9383
|
try {
|
|
8210
|
-
const stat =
|
|
8211
|
-
if (!stat.isFile() || stat.size > MAX_RECORD_BYTES || (stat.mode & 63) !== 0) {
|
|
8212
|
-
throw new Error(
|
|
8213
|
-
}
|
|
8214
|
-
const
|
|
8215
|
-
const count =
|
|
8216
|
-
if (count > MAX_RECORD_BYTES) throw new Error(
|
|
8217
|
-
|
|
8218
|
-
bytes.subarray(0, count).toString("utf8")
|
|
8219
|
-
);
|
|
8220
|
-
validate(record5, identity2);
|
|
8221
|
-
return record5;
|
|
9384
|
+
const stat = import_node_fs25.default.fstatSync(descriptor);
|
|
9385
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || stat.size > MAX_RECORD_BYTES || (stat.mode & 63) !== 0) {
|
|
9386
|
+
throw new Error(`${label} is not a bounded private file.`);
|
|
9387
|
+
}
|
|
9388
|
+
const bytes2 = Buffer.alloc(MAX_RECORD_BYTES + 1);
|
|
9389
|
+
const count = import_node_fs25.default.readSync(descriptor, bytes2, 0, bytes2.length, 0);
|
|
9390
|
+
if (count > MAX_RECORD_BYTES) throw new Error(`${label} exceeds its byte limit.`);
|
|
9391
|
+
return JSON.parse(bytes2.subarray(0, count).toString("utf8"));
|
|
8222
9392
|
} finally {
|
|
8223
|
-
|
|
9393
|
+
import_node_fs25.default.closeSync(descriptor);
|
|
8224
9394
|
}
|
|
8225
9395
|
}
|
|
8226
9396
|
function persist(record5) {
|
|
8227
|
-
|
|
9397
|
+
validate2(record5, record5.identity);
|
|
8228
9398
|
const contents = `${JSON.stringify(record5)}
|
|
8229
9399
|
`;
|
|
8230
9400
|
if (Buffer.byteLength(contents) > MAX_RECORD_BYTES)
|
|
@@ -8235,23 +9405,23 @@ function persist(record5) {
|
|
|
8235
9405
|
} catch (error) {
|
|
8236
9406
|
const reread = readReliabilityOperation(record5.identity);
|
|
8237
9407
|
if (!reread || JSON.stringify(reread) !== JSON.stringify(record5)) throw error;
|
|
8238
|
-
const descriptor =
|
|
9408
|
+
const descriptor = import_node_fs25.default.openSync(file, import_node_fs25.default.constants.O_RDONLY | import_node_fs25.default.constants.O_NOFOLLOW);
|
|
8239
9409
|
try {
|
|
8240
|
-
|
|
9410
|
+
import_node_fs25.default.fsyncSync(descriptor);
|
|
8241
9411
|
} finally {
|
|
8242
|
-
|
|
9412
|
+
import_node_fs25.default.closeSync(descriptor);
|
|
8243
9413
|
}
|
|
8244
|
-
const directory =
|
|
9414
|
+
const directory = import_node_fs25.default.openSync(import_node_path30.default.dirname(file), "r");
|
|
8245
9415
|
try {
|
|
8246
|
-
|
|
9416
|
+
import_node_fs25.default.fsyncSync(directory);
|
|
8247
9417
|
} finally {
|
|
8248
|
-
|
|
9418
|
+
import_node_fs25.default.closeSync(directory);
|
|
8249
9419
|
}
|
|
8250
9420
|
}
|
|
8251
9421
|
}
|
|
8252
9422
|
function updateReliabilityOperation(identity2, operation) {
|
|
8253
9423
|
const file = reliabilityOperationPath(identity2);
|
|
8254
|
-
|
|
9424
|
+
import_node_fs25.default.mkdirSync(import_node_path30.default.dirname(file), { recursive: true, mode: 448 });
|
|
8255
9425
|
return withFileLockSync(`${file}.lock`, { activity: "reliability intent", waitMs: 2e3 }, () => {
|
|
8256
9426
|
const record5 = readReliabilityOperation(identity2) ?? {
|
|
8257
9427
|
version: 1,
|
|
@@ -8264,23 +9434,37 @@ function updateReliabilityOperation(identity2, operation) {
|
|
|
8264
9434
|
};
|
|
8265
9435
|
if (record5.revision === Number.MAX_SAFE_INTEGER)
|
|
8266
9436
|
throw new Error("Reliability record revision is exhausted.");
|
|
9437
|
+
const previousVersion = record5.version;
|
|
9438
|
+
const previousEnrollment = record5.enrollment && { ...record5.enrollment };
|
|
8267
9439
|
const result = operation(record5);
|
|
8268
9440
|
if (result && typeof result.then === "function") {
|
|
8269
9441
|
throw new Error("Reliability record updates must be synchronous.");
|
|
8270
9442
|
}
|
|
9443
|
+
if (record5.version < previousVersion || previousEnrollment && !(0, import_node_util5.isDeepStrictEqual)(record5.enrollment, previousEnrollment))
|
|
9444
|
+
throw new Error("Durable capacity enrollment cannot be downgraded or replaced.");
|
|
8271
9445
|
record5.writtenByVersion = installedCliVersion();
|
|
8272
|
-
|
|
9446
|
+
validate2(record5, identity2);
|
|
8273
9447
|
record5.revision += 1;
|
|
8274
9448
|
persist(record5);
|
|
8275
9449
|
return result;
|
|
8276
9450
|
});
|
|
8277
9451
|
}
|
|
9452
|
+
function clearStartupWitness(identity2, fence) {
|
|
9453
|
+
updateReliabilityOperation(identity2, (record5) => {
|
|
9454
|
+
if (record5.startupWitness === void 0 || record5.startupWitness === null) return;
|
|
9455
|
+
const live = reliabilityFence(record5.state);
|
|
9456
|
+
if (Object.entries(fence).some(([key, value]) => value !== live[key]))
|
|
9457
|
+
throw new Error("Startup witness clearing fence changed.");
|
|
9458
|
+
record5.startupWitness = null;
|
|
9459
|
+
});
|
|
9460
|
+
}
|
|
8278
9461
|
|
|
8279
9462
|
// src/core/reliability-worker.ts
|
|
8280
9463
|
var import_node_child_process16 = require("child_process");
|
|
8281
|
-
var
|
|
8282
|
-
var
|
|
8283
|
-
var
|
|
9464
|
+
var import_node_crypto15 = require("crypto");
|
|
9465
|
+
var import_node_fs26 = __toESM(require("fs"));
|
|
9466
|
+
var import_node_path31 = __toESM(require("path"));
|
|
9467
|
+
var DEFAULT_OUTPUT_PAGE_BYTES = 48 * 1024;
|
|
8284
9468
|
function workerGroupAbsent(pid) {
|
|
8285
9469
|
try {
|
|
8286
9470
|
process.kill(-pid, 0);
|
|
@@ -8292,10 +9476,10 @@ function workerGroupAbsent(pid) {
|
|
|
8292
9476
|
|
|
8293
9477
|
// src/core/traefik-route-health.ts
|
|
8294
9478
|
var import_node_child_process17 = require("child_process");
|
|
8295
|
-
var
|
|
8296
|
-
var
|
|
9479
|
+
var import_node_fs27 = __toESM(require("fs"));
|
|
9480
|
+
var import_node_path32 = __toESM(require("path"));
|
|
8297
9481
|
var ROUTER_API_BASE = "http://127.0.0.1:8080/api";
|
|
8298
|
-
var ROUTER_RELOAD_LOCK_FILE =
|
|
9482
|
+
var ROUTER_RELOAD_LOCK_FILE = import_node_path32.default.join(DEVROUTER_HOME, "router-reload.lock");
|
|
8299
9483
|
var ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
|
|
8300
9484
|
var DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
|
|
8301
9485
|
var DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
|
|
@@ -8564,7 +9748,7 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
|
8564
9748
|
throw new Error(
|
|
8565
9749
|
"Traefik routes did not reload; shared router restart is disabled for this operation."
|
|
8566
9750
|
);
|
|
8567
|
-
|
|
9751
|
+
import_node_fs27.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
8568
9752
|
return withFileLock(
|
|
8569
9753
|
ROUTER_RELOAD_LOCK_FILE,
|
|
8570
9754
|
{
|
|
@@ -8615,20 +9799,20 @@ function assertTraefikRoutesRemoved(routes) {
|
|
|
8615
9799
|
}
|
|
8616
9800
|
|
|
8617
9801
|
// src/core/workspace-ensure.ts
|
|
8618
|
-
var
|
|
8619
|
-
var
|
|
8620
|
-
var
|
|
9802
|
+
var import_node_child_process27 = require("child_process");
|
|
9803
|
+
var import_node_fs33 = __toESM(require("fs"));
|
|
9804
|
+
var import_node_path38 = __toESM(require("path"));
|
|
8621
9805
|
|
|
8622
9806
|
// src/core/devpod-mutation.ts
|
|
8623
9807
|
var import_node_child_process18 = require("child_process");
|
|
8624
|
-
var
|
|
8625
|
-
var
|
|
8626
|
-
var DEVPOD_MUTATION_LOCK_FILE =
|
|
9808
|
+
var import_node_fs28 = __toESM(require("fs"));
|
|
9809
|
+
var import_node_path33 = __toESM(require("path"));
|
|
9810
|
+
var DEVPOD_MUTATION_LOCK_FILE = import_node_path33.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
|
|
8627
9811
|
var DEVPOD_MUTATION_WAIT_MS = 18e5;
|
|
8628
9812
|
var DevpodStartPostconditionError = class extends Error {
|
|
8629
9813
|
};
|
|
8630
9814
|
function withMutationLock2(activity, target, operation) {
|
|
8631
|
-
|
|
9815
|
+
import_node_fs28.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
8632
9816
|
return withFileLockSync(
|
|
8633
9817
|
DEVPOD_MUTATION_LOCK_FILE,
|
|
8634
9818
|
{
|
|
@@ -8829,8 +10013,270 @@ async function startDevpodWorkspace(options) {
|
|
|
8829
10013
|
});
|
|
8830
10014
|
}
|
|
8831
10015
|
|
|
8832
|
-
// src/core/
|
|
10016
|
+
// src/core/host-port-claims.ts
|
|
8833
10017
|
var import_node_child_process19 = require("child_process");
|
|
10018
|
+
var import_node_path34 = __toESM(require("path"));
|
|
10019
|
+
var COMPOSE_RENDER_TIMEOUT_MS = 3e4;
|
|
10020
|
+
var COMPOSE_RENDER_MAX_BUFFER = 8 * 1024 * 1024;
|
|
10021
|
+
var DOCKER_TIMEOUT_MS = 1e4;
|
|
10022
|
+
var DOCKER_MAX_BUFFER = 1024 * 1024;
|
|
10023
|
+
var MAX_DESIRED_HOST_PORTS = 1024;
|
|
10024
|
+
var MAX_RUNNING_HOLDERS = 512;
|
|
10025
|
+
function isRecord4(value) {
|
|
10026
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10027
|
+
}
|
|
10028
|
+
function optionalLabel(labels, key) {
|
|
10029
|
+
const value = isRecord4(labels) ? labels[key] : void 0;
|
|
10030
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
10031
|
+
}
|
|
10032
|
+
function normalizeHostIp(value) {
|
|
10033
|
+
if (typeof value !== "string") return null;
|
|
10034
|
+
const trimmed = value.trim();
|
|
10035
|
+
if (trimmed.length === 0 || trimmed === "0.0.0.0" || trimmed === "::") return null;
|
|
10036
|
+
return trimmed;
|
|
10037
|
+
}
|
|
10038
|
+
function parseSinglePortValue(value, service, label) {
|
|
10039
|
+
if (typeof value === "number") {
|
|
10040
|
+
if (!Number.isInteger(value) || value <= 0 || value > 65535) {
|
|
10041
|
+
throw new Error(`Service '${service}' has an out-of-range ${label} port.`);
|
|
10042
|
+
}
|
|
10043
|
+
return value;
|
|
10044
|
+
}
|
|
10045
|
+
if (typeof value === "string") {
|
|
10046
|
+
const parsed = Number(value);
|
|
10047
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
|
|
10048
|
+
throw new Error(`Service '${service}' has an out-of-range ${label} port '${value}'.`);
|
|
10049
|
+
}
|
|
10050
|
+
return parsed;
|
|
10051
|
+
}
|
|
10052
|
+
throw new Error(`Service '${service}' has an unsupported ${label} port value.`);
|
|
10053
|
+
}
|
|
10054
|
+
function parseFixedPublishedPort(value, service) {
|
|
10055
|
+
if (value === void 0 || value === null || value === "" || value === 0 || value === "0") {
|
|
10056
|
+
return null;
|
|
10057
|
+
}
|
|
10058
|
+
if (typeof value === "string" && value.includes("-")) {
|
|
10059
|
+
return null;
|
|
10060
|
+
}
|
|
10061
|
+
return parseSinglePortValue(value, service, "published host");
|
|
10062
|
+
}
|
|
10063
|
+
function parsePublishedRange(value, service) {
|
|
10064
|
+
const match = /^(\d+)-(\d+)$/.exec(value.trim());
|
|
10065
|
+
if (!match) return void 0;
|
|
10066
|
+
const lower = Number(match[1]);
|
|
10067
|
+
const upper = Number(match[2]);
|
|
10068
|
+
if (!Number.isInteger(lower) || !Number.isInteger(upper) || lower <= 0 || upper < lower || upper > 65535) {
|
|
10069
|
+
throw new Error(`Service '${service}' publishes an invalid host port range '${value}'.`);
|
|
10070
|
+
}
|
|
10071
|
+
return [lower, upper];
|
|
10072
|
+
}
|
|
10073
|
+
function resolveFixedPublishedHostPorts(renderedConfig) {
|
|
10074
|
+
const services = isRecord4(renderedConfig) ? renderedConfig.services : void 0;
|
|
10075
|
+
const bindings = [];
|
|
10076
|
+
const pushBinding = (binding) => {
|
|
10077
|
+
bindings.push(binding);
|
|
10078
|
+
if (bindings.length > MAX_DESIRED_HOST_PORTS) {
|
|
10079
|
+
throw new Error(
|
|
10080
|
+
`The managed Compose model declares more than ${MAX_DESIRED_HOST_PORTS} fixed host ports.`
|
|
10081
|
+
);
|
|
10082
|
+
}
|
|
10083
|
+
};
|
|
10084
|
+
for (const [service, serviceValue] of Object.entries(isRecord4(services) ? services : {})) {
|
|
10085
|
+
const serviceRecord = isRecord4(serviceValue) ? serviceValue : {};
|
|
10086
|
+
if (!Array.isArray(serviceRecord.ports)) continue;
|
|
10087
|
+
for (const port of serviceRecord.ports) {
|
|
10088
|
+
if (!isRecord4(port)) {
|
|
10089
|
+
throw new Error(`Service '${service}' declares an unsupported port entry.`);
|
|
10090
|
+
}
|
|
10091
|
+
parseSinglePortValue(port.target, service, "container target");
|
|
10092
|
+
const hostIp = normalizeHostIp(port.host_ip);
|
|
10093
|
+
const protocol = typeof port.protocol === "string" && port.protocol ? port.protocol : "tcp";
|
|
10094
|
+
if (typeof port.published !== "string" || !port.published.includes("-")) {
|
|
10095
|
+
const hostPort = parseFixedPublishedPort(port.published, service);
|
|
10096
|
+
if (hostPort === null) continue;
|
|
10097
|
+
pushBinding({ service, hostIp, hostPort, protocol });
|
|
10098
|
+
continue;
|
|
10099
|
+
}
|
|
10100
|
+
const range = parsePublishedRange(port.published, service);
|
|
10101
|
+
if (!range) {
|
|
10102
|
+
throw new Error(
|
|
10103
|
+
`Service '${service}' publishes an unsupported host port value '${String(port.published)}'.`
|
|
10104
|
+
);
|
|
10105
|
+
}
|
|
10106
|
+
for (let hostPort = range[0]; hostPort <= range[1]; hostPort += 1) {
|
|
10107
|
+
pushBinding({ service, hostIp, hostPort, protocol });
|
|
10108
|
+
}
|
|
10109
|
+
}
|
|
10110
|
+
}
|
|
10111
|
+
return bindings;
|
|
10112
|
+
}
|
|
10113
|
+
function hostPortBindingsConflict(desired, holder) {
|
|
10114
|
+
return desired.hostPort === holder.hostPort && desired.protocol === holder.protocol && // Wildcards collide with everything in their protocol family; specific
|
|
10115
|
+
// addresses only collide with themselves. IPv6 nuance intentionally
|
|
10116
|
+
// over-approximates: a loud refusal beats a missed kernel collision.
|
|
10117
|
+
(desired.hostIp === null || holder.hostIp === null || desired.hostIp === holder.hostIp);
|
|
10118
|
+
}
|
|
10119
|
+
var HOST_PORT_INSPECT_TEMPLATE = '{"id":{{json .Id}},"name":{{json .Name}},"labels":{"com.docker.compose.project":{{json (index .Config.Labels "com.docker.compose.project")}},"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}}},"ports":{{json .NetworkSettings.Ports}}}';
|
|
10120
|
+
function runDocker(args) {
|
|
10121
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", args, {
|
|
10122
|
+
...networkDockerOptions(),
|
|
10123
|
+
encoding: "utf-8",
|
|
10124
|
+
timeout: DOCKER_TIMEOUT_MS,
|
|
10125
|
+
maxBuffer: DOCKER_MAX_BUFFER
|
|
10126
|
+
});
|
|
10127
|
+
if (result.error || result.status !== 0) {
|
|
10128
|
+
const reason2 = (result.stderr || result.stdout || result.error?.message || "unknown error").toString().trim();
|
|
10129
|
+
throw new Error(`docker ${String(args[0])} failed: ${reason2}`);
|
|
10130
|
+
}
|
|
10131
|
+
if (typeof result.stdout !== "string") {
|
|
10132
|
+
throw new Error(`docker ${String(args[0])} returned invalid output.`);
|
|
10133
|
+
}
|
|
10134
|
+
return result.stdout;
|
|
10135
|
+
}
|
|
10136
|
+
function parseHolderPortMap(value) {
|
|
10137
|
+
const ports = [];
|
|
10138
|
+
for (const [key, entries] of Object.entries(isRecord4(value) ? value : {})) {
|
|
10139
|
+
const separator = key.lastIndexOf("/");
|
|
10140
|
+
if (separator <= 0) continue;
|
|
10141
|
+
const protocol = key.slice(separator + 1);
|
|
10142
|
+
if (!Array.isArray(entries)) continue;
|
|
10143
|
+
for (const entry of entries) {
|
|
10144
|
+
if (!isRecord4(entry) || entry.HostPort === void 0 || entry.HostPort === null) continue;
|
|
10145
|
+
const bound = Number(entry.HostPort);
|
|
10146
|
+
if (!Number.isInteger(bound) || bound <= 0) continue;
|
|
10147
|
+
ports.push({ hostIp: normalizeHostIp(entry.HostIp), hostPort: bound, protocol });
|
|
10148
|
+
}
|
|
10149
|
+
}
|
|
10150
|
+
return ports;
|
|
10151
|
+
}
|
|
10152
|
+
function parseHolderSnapshots(stdout) {
|
|
10153
|
+
return stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
10154
|
+
let parsed;
|
|
10155
|
+
try {
|
|
10156
|
+
parsed = JSON.parse(line);
|
|
10157
|
+
} catch {
|
|
10158
|
+
throw new Error("Host-port holder inspection returned malformed JSON.");
|
|
10159
|
+
}
|
|
10160
|
+
if (!isRecord4(parsed) || typeof parsed.id !== "string" || parsed.id.length === 0) {
|
|
10161
|
+
throw new Error("Host-port holder inspection returned an invalid container record.");
|
|
10162
|
+
}
|
|
10163
|
+
const name = typeof parsed.name === "string" ? parsed.name.replace(/^\//, "") : parsed.id;
|
|
10164
|
+
return {
|
|
10165
|
+
id: parsed.id,
|
|
10166
|
+
name,
|
|
10167
|
+
composeProject: optionalLabel(parsed.labels, "com.docker.compose.project"),
|
|
10168
|
+
workingDir: optionalLabel(parsed.labels, "com.docker.compose.project.working_dir"),
|
|
10169
|
+
ports: parseHolderPortMap(parsed.ports)
|
|
10170
|
+
};
|
|
10171
|
+
});
|
|
10172
|
+
}
|
|
10173
|
+
function listHolderSnapshots() {
|
|
10174
|
+
const listed = runDocker(["ps", "--filter", "status=running", "--format", "{{.ID}}"]);
|
|
10175
|
+
const ids2 = listed.split(/\r?\n/).map((id5) => id5.trim()).filter(Boolean);
|
|
10176
|
+
if (ids2.length === 0) return [];
|
|
10177
|
+
if (ids2.length > MAX_RUNNING_HOLDERS) {
|
|
10178
|
+
throw new Error(
|
|
10179
|
+
`Host-port holder inspection found ${ids2.length} running containers, above its bound.`
|
|
10180
|
+
);
|
|
10181
|
+
}
|
|
10182
|
+
const inspected = runDocker(["inspect", "--format", HOST_PORT_INSPECT_TEMPLATE, ...ids2]);
|
|
10183
|
+
return parseHolderSnapshots(inspected);
|
|
10184
|
+
}
|
|
10185
|
+
function renderManagedComposeModel(plan, workspace) {
|
|
10186
|
+
const fileArgs = plan.composeFiles.flatMap((file) => ["-f", file]);
|
|
10187
|
+
const result = (0, import_node_child_process19.spawnSync)(
|
|
10188
|
+
"docker",
|
|
10189
|
+
[
|
|
10190
|
+
"compose",
|
|
10191
|
+
"--profile",
|
|
10192
|
+
"*",
|
|
10193
|
+
"--project-directory",
|
|
10194
|
+
plan.composeDirectory,
|
|
10195
|
+
...fileArgs,
|
|
10196
|
+
"config",
|
|
10197
|
+
"--format",
|
|
10198
|
+
"json"
|
|
10199
|
+
],
|
|
10200
|
+
{
|
|
10201
|
+
cwd: plan.composeDirectory,
|
|
10202
|
+
encoding: "utf-8",
|
|
10203
|
+
env: managedComposeEnvironment(workspace),
|
|
10204
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10205
|
+
timeout: COMPOSE_RENDER_TIMEOUT_MS,
|
|
10206
|
+
maxBuffer: COMPOSE_RENDER_MAX_BUFFER
|
|
10207
|
+
}
|
|
10208
|
+
);
|
|
10209
|
+
if (result.status !== 0 || result.error || !result.stdout?.trim()) {
|
|
10210
|
+
const reason2 = (result.stderr || result.error?.message || "unknown error").toString().trim().split(/\r?\n/).slice(-3).join("; ");
|
|
10211
|
+
throw new Error(
|
|
10212
|
+
`Host-port claims could not render the managed Compose model that the start would bind: ${reason2}`
|
|
10213
|
+
);
|
|
10214
|
+
}
|
|
10215
|
+
try {
|
|
10216
|
+
return JSON.parse(result.stdout);
|
|
10217
|
+
} catch {
|
|
10218
|
+
throw new Error("Host-port claims received a malformed rendered Compose model.");
|
|
10219
|
+
}
|
|
10220
|
+
}
|
|
10221
|
+
function attributeHolderWorktree(workingDir) {
|
|
10222
|
+
if (import_node_path34.default.basename(workingDir) !== ".devcontainer") return void 0;
|
|
10223
|
+
return comparableWorkspacePath(import_node_path34.default.dirname(workingDir));
|
|
10224
|
+
}
|
|
10225
|
+
function holderAttribution(holder, repoPath) {
|
|
10226
|
+
const attribution = {};
|
|
10227
|
+
if (holder.composeProject) attribution.holderComposeProject = holder.composeProject;
|
|
10228
|
+
const worktree = holder.workingDir ? attributeHolderWorktree(holder.workingDir) : void 0;
|
|
10229
|
+
if (!worktree) return attribution;
|
|
10230
|
+
attribution.holderWorktreePath = worktree;
|
|
10231
|
+
try {
|
|
10232
|
+
const owner = listWorkspaceOwnership(repoPath).find(
|
|
10233
|
+
(record5) => comparableWorkspacePath(record5.worktreePath) === worktree
|
|
10234
|
+
);
|
|
10235
|
+
if (owner) {
|
|
10236
|
+
attribution.holderWorkspace = owner.workspace;
|
|
10237
|
+
if (owner.branch) attribution.holderBranch = owner.branch;
|
|
10238
|
+
}
|
|
10239
|
+
} catch {
|
|
10240
|
+
}
|
|
10241
|
+
return attribution;
|
|
10242
|
+
}
|
|
10243
|
+
function remediationFor(holder, attribution) {
|
|
10244
|
+
if (attribution.holderWorktreePath) {
|
|
10245
|
+
return `Stop the holding workspace with 'devrouter stop ${attribution.holderWorktreePath}', or override the consumer's published host binding through its own configuration, then run ensure again.`;
|
|
10246
|
+
}
|
|
10247
|
+
return `Stop or reconfigure the holding container '${holder.name}' and run ensure again; devrouter never rewrites consumer-declared host bindings.`;
|
|
10248
|
+
}
|
|
10249
|
+
function detectHostPortClaimConflicts(options) {
|
|
10250
|
+
const desired = resolveFixedPublishedHostPorts(
|
|
10251
|
+
renderManagedComposeModel(options.plan, options.workspace)
|
|
10252
|
+
);
|
|
10253
|
+
if (desired.length === 0) return [];
|
|
10254
|
+
const holders = listHolderSnapshots();
|
|
10255
|
+
const devcontainerDir = import_node_path34.default.join(options.repoPath, ".devcontainer");
|
|
10256
|
+
const conflicts = [];
|
|
10257
|
+
for (const binding of desired) {
|
|
10258
|
+
for (const holder of holders) {
|
|
10259
|
+
for (const holderPort of holder.ports) {
|
|
10260
|
+
if (!hostPortBindingsConflict(binding, holderPort)) continue;
|
|
10261
|
+
if (holder.workingDir && sameWorkspacePath(holder.workingDir, devcontainerDir)) continue;
|
|
10262
|
+
const attribution = holderAttribution(holder, options.repoPath);
|
|
10263
|
+
conflicts.push({
|
|
10264
|
+
service: binding.service,
|
|
10265
|
+
hostIp: binding.hostIp,
|
|
10266
|
+
hostPort: binding.hostPort,
|
|
10267
|
+
protocol: binding.protocol,
|
|
10268
|
+
holderContainer: holder.name,
|
|
10269
|
+
...attribution,
|
|
10270
|
+
remediation: remediationFor(holder, attribution)
|
|
10271
|
+
});
|
|
10272
|
+
}
|
|
10273
|
+
}
|
|
10274
|
+
}
|
|
10275
|
+
return conflicts;
|
|
10276
|
+
}
|
|
10277
|
+
|
|
10278
|
+
// src/core/http-route-probe.ts
|
|
10279
|
+
var import_node_child_process20 = require("child_process");
|
|
8834
10280
|
var DEFAULT_MAX_TIME_SECONDS = 5;
|
|
8835
10281
|
var PARENT_TIMEOUT_PADDING_MS = 250;
|
|
8836
10282
|
var CONTRACT_MAX_BUFFER = 1024;
|
|
@@ -8863,7 +10309,7 @@ function probeHttpRoute(host, options = {}) {
|
|
|
8863
10309
|
args.push("--cacert", CERT_FILE);
|
|
8864
10310
|
}
|
|
8865
10311
|
args.push(url);
|
|
8866
|
-
const result = (0,
|
|
10312
|
+
const result = (0, import_node_child_process20.spawnSync)("curl", args, { encoding: "utf-8" });
|
|
8867
10313
|
const stdout = result.stdout?.trim() ?? "";
|
|
8868
10314
|
const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
|
|
8869
10315
|
const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
|
|
@@ -8882,7 +10328,7 @@ function probeHttpReadinessSync(host, readiness, maxTimeSeconds, repoPath) {
|
|
|
8882
10328
|
}
|
|
8883
10329
|
let result;
|
|
8884
10330
|
try {
|
|
8885
|
-
result = (0,
|
|
10331
|
+
result = (0, import_node_child_process20.spawnSync)("curl", args, {
|
|
8886
10332
|
encoding: "utf-8",
|
|
8887
10333
|
maxBuffer: CONTRACT_MAX_BUFFER,
|
|
8888
10334
|
timeout: Math.ceil(parentTimeoutMs)
|
|
@@ -8966,8 +10412,8 @@ function transportFailure() {
|
|
|
8966
10412
|
}
|
|
8967
10413
|
|
|
8968
10414
|
// src/core/managed-host-preparation.ts
|
|
8969
|
-
var
|
|
8970
|
-
var
|
|
10415
|
+
var import_node_child_process21 = require("child_process");
|
|
10416
|
+
var import_node_fs29 = __toESM(require("fs"));
|
|
8971
10417
|
var PREPARATION_TIMEOUT_MS = 6e4;
|
|
8972
10418
|
var CONFIG_READ_ERROR = "Managed host preparation could not read .devrouter.yml.";
|
|
8973
10419
|
var CONFIG_CHANGED_ERROR = "Managed host preparation changed .devrouter.yml; refusing to continue.";
|
|
@@ -8978,7 +10424,7 @@ function runPreparationCommand(argv, cwd) {
|
|
|
8978
10424
|
return new Promise((resolve) => {
|
|
8979
10425
|
let child;
|
|
8980
10426
|
try {
|
|
8981
|
-
child = (0,
|
|
10427
|
+
child = (0, import_node_child_process21.spawn)(argv[0], argv.slice(1), {
|
|
8982
10428
|
cwd,
|
|
8983
10429
|
detached: true,
|
|
8984
10430
|
shell: false,
|
|
@@ -9010,7 +10456,7 @@ function runPreparationCommand(argv, cwd) {
|
|
|
9010
10456
|
}
|
|
9011
10457
|
function readConfigBytes(configPath) {
|
|
9012
10458
|
try {
|
|
9013
|
-
return
|
|
10459
|
+
return import_node_fs29.default.readFileSync(configPath);
|
|
9014
10460
|
} catch {
|
|
9015
10461
|
return void 0;
|
|
9016
10462
|
}
|
|
@@ -9044,10 +10490,10 @@ async function runManagedHostPreparation(repoPath, config) {
|
|
|
9044
10490
|
}
|
|
9045
10491
|
|
|
9046
10492
|
// src/core/managed-post-start.ts
|
|
9047
|
-
var
|
|
9048
|
-
var
|
|
9049
|
-
var
|
|
9050
|
-
var
|
|
10493
|
+
var import_node_child_process22 = require("child_process");
|
|
10494
|
+
var import_node_crypto16 = require("crypto");
|
|
10495
|
+
var import_node_fs30 = __toESM(require("fs"));
|
|
10496
|
+
var import_node_path35 = __toESM(require("path"));
|
|
9051
10497
|
var MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
9052
10498
|
var MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
|
|
9053
10499
|
var RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
|
|
@@ -9060,7 +10506,7 @@ function renderDeliveryScript(targetPath) {
|
|
|
9060
10506
|
if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
|
|
9061
10507
|
throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
|
|
9062
10508
|
}
|
|
9063
|
-
const temporaryPrefix =
|
|
10509
|
+
const temporaryPrefix = import_node_path35.default.posix.basename(targetPath);
|
|
9064
10510
|
return `set -eu
|
|
9065
10511
|
umask 077
|
|
9066
10512
|
runtime_root=/tmp/devrouter
|
|
@@ -9084,7 +10530,7 @@ function commandFailure3(result) {
|
|
|
9084
10530
|
return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
|
|
9085
10531
|
}
|
|
9086
10532
|
function deliverRuntimeFile(containerId, targetPath, contents, label) {
|
|
9087
|
-
const result = (0,
|
|
10533
|
+
const result = (0, import_node_child_process22.spawnSync)(
|
|
9088
10534
|
"docker",
|
|
9089
10535
|
["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
|
|
9090
10536
|
{ input: contents, encoding: "utf-8", ...networkDockerOptions() }
|
|
@@ -9095,32 +10541,32 @@ function deliverRuntimeFile(containerId, targetPath, contents, label) {
|
|
|
9095
10541
|
}
|
|
9096
10542
|
}
|
|
9097
10543
|
function readRegularFileBytes(filePath) {
|
|
9098
|
-
if (!
|
|
9099
|
-
return
|
|
10544
|
+
if (!import_node_fs30.default.existsSync(filePath) || !import_node_fs30.default.lstatSync(filePath).isFile()) return void 0;
|
|
10545
|
+
return import_node_fs30.default.readFileSync(filePath);
|
|
9100
10546
|
}
|
|
9101
10547
|
function readRegularFile(filePath) {
|
|
9102
10548
|
return readRegularFileBytes(filePath)?.toString("utf-8");
|
|
9103
10549
|
}
|
|
9104
10550
|
function adapterFingerprint(adapter) {
|
|
9105
|
-
return (0,
|
|
10551
|
+
return (0, import_node_crypto16.createHash)("sha256").update(adapter).digest("hex");
|
|
9106
10552
|
}
|
|
9107
10553
|
function resolveProcessHelperPath() {
|
|
9108
10554
|
const candidates = [
|
|
9109
|
-
|
|
9110
|
-
|
|
10555
|
+
import_node_path35.default.resolve(__dirname, "..", "bin", "devrouter-process"),
|
|
10556
|
+
import_node_path35.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
|
|
9111
10557
|
];
|
|
9112
|
-
const helperPath = candidates.find((candidate) =>
|
|
10558
|
+
const helperPath = candidates.find((candidate) => import_node_fs30.default.existsSync(candidate));
|
|
9113
10559
|
if (!helperPath) {
|
|
9114
10560
|
throw new Error("Could not locate the packaged devrouter-process helper.");
|
|
9115
10561
|
}
|
|
9116
10562
|
return helperPath;
|
|
9117
10563
|
}
|
|
9118
10564
|
function resolveManagedPostStartPlan(repoPath) {
|
|
9119
|
-
const adapterPath =
|
|
10565
|
+
const adapterPath = import_node_path35.default.join(repoPath, MANAGED_ADAPTER_PATH);
|
|
9120
10566
|
const adapterBytes = readRegularFileBytes(adapterPath);
|
|
9121
10567
|
const adapterText = adapterBytes?.toString("utf-8");
|
|
9122
|
-
const dockerfilePath =
|
|
9123
|
-
const devcontainerPath =
|
|
10568
|
+
const dockerfilePath = import_node_path35.default.join(repoPath, ".devcontainer", "Dockerfile");
|
|
10569
|
+
const devcontainerPath = import_node_path35.default.join(repoPath, ".devcontainer", "devcontainer.json");
|
|
9124
10570
|
const dockerfile = readRegularFile(dockerfilePath) ?? "";
|
|
9125
10571
|
const devcontainer = readRegularFile(devcontainerPath) ?? "";
|
|
9126
10572
|
const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
|
|
@@ -9161,7 +10607,7 @@ function resolveManagedPostStartPlan(repoPath) {
|
|
|
9161
10607
|
}
|
|
9162
10608
|
function runManagedPostStart(options) {
|
|
9163
10609
|
if (options.plan.kind !== "runtime") return;
|
|
9164
|
-
const helper =
|
|
10610
|
+
const helper = import_node_fs30.default.readFileSync(resolveProcessHelperPath());
|
|
9165
10611
|
deliverRuntimeFile(
|
|
9166
10612
|
options.container.id,
|
|
9167
10613
|
RUNTIME_HELPER_PATH,
|
|
@@ -9175,7 +10621,7 @@ function runManagedPostStart(options) {
|
|
|
9175
10621
|
options.plan.adapterContents,
|
|
9176
10622
|
"the managed post-start adapter"
|
|
9177
10623
|
);
|
|
9178
|
-
const started2 = (0,
|
|
10624
|
+
const started2 = (0, import_node_child_process22.spawnSync)(
|
|
9179
10625
|
"docker",
|
|
9180
10626
|
[
|
|
9181
10627
|
"exec",
|
|
@@ -9216,7 +10662,7 @@ function assertSafeProcessName(name) {
|
|
|
9216
10662
|
}
|
|
9217
10663
|
function runManagedProcessAction(options) {
|
|
9218
10664
|
assertSafeProcessName(options.name);
|
|
9219
|
-
const result = (0,
|
|
10665
|
+
const result = (0, import_node_child_process22.spawnSync)(
|
|
9220
10666
|
"docker",
|
|
9221
10667
|
[
|
|
9222
10668
|
"exec",
|
|
@@ -9681,7 +11127,7 @@ function collectManagedRuntimeStatus(options) {
|
|
|
9681
11127
|
}
|
|
9682
11128
|
|
|
9683
11129
|
// src/core/network-inventory.ts
|
|
9684
|
-
var
|
|
11130
|
+
var import_node_child_process23 = require("child_process");
|
|
9685
11131
|
var NetworkInventoryError = class extends Error {
|
|
9686
11132
|
};
|
|
9687
11133
|
var MAX_IDS = 4096;
|
|
@@ -9693,7 +11139,7 @@ function readDocker(endpoint, args, timeoutMs) {
|
|
|
9693
11139
|
const env = { ...process.env };
|
|
9694
11140
|
delete env.DOCKER_CONTEXT;
|
|
9695
11141
|
delete env.DOCKER_HOST;
|
|
9696
|
-
const result = (0,
|
|
11142
|
+
const result = (0, import_node_child_process23.spawnSync)("docker", ["--host", endpoint, ...args], {
|
|
9697
11143
|
env,
|
|
9698
11144
|
encoding: "utf-8",
|
|
9699
11145
|
timeout: timeoutMs,
|
|
@@ -9710,14 +11156,14 @@ function record4(value) {
|
|
|
9710
11156
|
}
|
|
9711
11157
|
return value;
|
|
9712
11158
|
}
|
|
9713
|
-
function
|
|
11159
|
+
function text3(value) {
|
|
9714
11160
|
if (typeof value !== "string" || !value || value.includes("\0")) {
|
|
9715
11161
|
throw new NetworkInventoryError("Docker network inventory contains an invalid identity.");
|
|
9716
11162
|
}
|
|
9717
11163
|
return value;
|
|
9718
11164
|
}
|
|
9719
|
-
function
|
|
9720
|
-
const result =
|
|
11165
|
+
function id4(value) {
|
|
11166
|
+
const result = text3(value);
|
|
9721
11167
|
if (!FULL_ID.test(result))
|
|
9722
11168
|
throw new NetworkInventoryError("Docker inventory requires full object IDs.");
|
|
9723
11169
|
return result;
|
|
@@ -9727,7 +11173,7 @@ function rows(output) {
|
|
|
9727
11173
|
return trimmed ? trimmed.split(/\r?\n/).map((line) => JSON.parse(line)) : [];
|
|
9728
11174
|
}
|
|
9729
11175
|
function ids(output) {
|
|
9730
|
-
const values = output.trim() ? output.trim().split(/\r?\n/).map(
|
|
11176
|
+
const values = output.trim() ? output.trim().split(/\r?\n/).map(id4) : [];
|
|
9731
11177
|
if (values.length > MAX_IDS || new Set(values).size !== values.length) {
|
|
9732
11178
|
throw new NetworkInventoryError("Docker network inventory has duplicate or too many objects.");
|
|
9733
11179
|
}
|
|
@@ -9773,7 +11219,7 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
9773
11219
|
read2(["info", "--format", '{"id":{{json .ID}},"pools":{{json .DefaultAddressPools}}}'])
|
|
9774
11220
|
)
|
|
9775
11221
|
);
|
|
9776
|
-
result.daemonId =
|
|
11222
|
+
result.daemonId = text3(info.id);
|
|
9777
11223
|
if (options.expectedDaemonId && result.daemonId !== options.expectedDaemonId) {
|
|
9778
11224
|
throw new NetworkInventoryError("Docker daemon identity changed before network inspection.");
|
|
9779
11225
|
}
|
|
@@ -9781,7 +11227,7 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
9781
11227
|
throw new NetworkInventoryError("Docker default pools are unavailable.");
|
|
9782
11228
|
result.pools = info.pools.map((value) => {
|
|
9783
11229
|
const pool = record4(value);
|
|
9784
|
-
const base =
|
|
11230
|
+
const base = text3(pool.Base);
|
|
9785
11231
|
if (!Number.isSafeInteger(pool.Size) || Number(pool.Size) < 0 || Number(pool.Size) > 128) {
|
|
9786
11232
|
throw new NetworkInventoryError("Docker default pool allocation size is invalid.");
|
|
9787
11233
|
}
|
|
@@ -9796,11 +11242,11 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
9796
11242
|
throw new NetworkInventoryError("Docker network IPAM or endpoint evidence is unavailable.");
|
|
9797
11243
|
}
|
|
9798
11244
|
return {
|
|
9799
|
-
id:
|
|
9800
|
-
name:
|
|
9801
|
-
driver:
|
|
11245
|
+
id: id4(network.id),
|
|
11246
|
+
name: text3(network.name),
|
|
11247
|
+
driver: text3(network.driver),
|
|
9802
11248
|
subnets: (network.ipam === null ? [] : network.ipam).map(
|
|
9803
|
-
(entry) =>
|
|
11249
|
+
(entry) => text3(record4(entry).Subnet)
|
|
9804
11250
|
),
|
|
9805
11251
|
activeEndpoints: Number(network.activeEndpoints),
|
|
9806
11252
|
retainedContainerIds: [],
|
|
@@ -9816,11 +11262,11 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
9816
11262
|
const inspected = [];
|
|
9817
11263
|
for (const value of containers) {
|
|
9818
11264
|
const container = record4(value);
|
|
9819
|
-
const containerId =
|
|
11265
|
+
const containerId = id4(container.id);
|
|
9820
11266
|
inspected.push(containerId);
|
|
9821
11267
|
for (const membership of Object.values(record4(container.networks))) {
|
|
9822
11268
|
const reference = record4(membership);
|
|
9823
|
-
const networkId =
|
|
11269
|
+
const networkId = id4(reference.NetworkID);
|
|
9824
11270
|
const network = byId.get(networkId);
|
|
9825
11271
|
if (!network)
|
|
9826
11272
|
throw new NetworkInventoryError("A retained container references an unobserved network.");
|
|
@@ -9838,7 +11284,7 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
9838
11284
|
"Docker object population changed during network inspection."
|
|
9839
11285
|
);
|
|
9840
11286
|
}
|
|
9841
|
-
const finalId =
|
|
11287
|
+
const finalId = text3(JSON.parse(read2(["info", "--format", "{{json .ID}}"])));
|
|
9842
11288
|
if (finalId !== result.daemonId)
|
|
9843
11289
|
throw new NetworkInventoryError("Docker daemon identity changed during inspection.");
|
|
9844
11290
|
result.status = "complete";
|
|
@@ -9850,37 +11296,37 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
9850
11296
|
}
|
|
9851
11297
|
|
|
9852
11298
|
// src/core/network-routes.ts
|
|
9853
|
-
var
|
|
9854
|
-
var
|
|
11299
|
+
var import_node_child_process24 = require("child_process");
|
|
11300
|
+
var import_node_net3 = require("net");
|
|
9855
11301
|
|
|
9856
11302
|
// src/core/network-local-daemon.ts
|
|
9857
|
-
var
|
|
9858
|
-
var
|
|
11303
|
+
var import_node_fs31 = __toESM(require("fs"));
|
|
11304
|
+
var import_node_path36 = __toESM(require("path"));
|
|
9859
11305
|
function hasLocalDockerNetworkNamespace(endpoint, procRoot = "/proc") {
|
|
9860
11306
|
const deadline = Date.now() + 2e3;
|
|
9861
11307
|
try {
|
|
9862
11308
|
if (!endpoint.startsWith("unix:///")) return false;
|
|
9863
11309
|
const socket = endpoint.slice("unix://".length);
|
|
9864
|
-
const table =
|
|
11310
|
+
const table = import_node_fs31.default.readFileSync(import_node_path36.default.join(procRoot, "net/unix"), "utf8");
|
|
9865
11311
|
if (Buffer.byteLength(table) > 1024 * 1024) return false;
|
|
9866
11312
|
const rows2 = table.split("\n").map((line) => line.trim().split(/\s+/)).filter((row) => row[7] === socket && row[3] === "00010000");
|
|
9867
11313
|
if (rows2.length !== 1 || !/^\d+$/.test(rows2[0][6])) return false;
|
|
9868
11314
|
const inode = `socket:[${rows2[0][6]}]`;
|
|
9869
|
-
const ownNamespace =
|
|
9870
|
-
const pids =
|
|
11315
|
+
const ownNamespace = import_node_fs31.default.readlinkSync(import_node_path36.default.join(procRoot, "self/ns/net"));
|
|
11316
|
+
const pids = import_node_fs31.default.readdirSync(procRoot).filter((name) => /^\d+$/.test(name));
|
|
9871
11317
|
if (pids.length > 4096) return false;
|
|
9872
11318
|
for (const pid of pids) {
|
|
9873
11319
|
if (Date.now() > deadline) return false;
|
|
9874
11320
|
try {
|
|
9875
|
-
const root =
|
|
9876
|
-
if (
|
|
9877
|
-
if (
|
|
9878
|
-
const descriptors =
|
|
11321
|
+
const root = import_node_path36.default.join(procRoot, pid);
|
|
11322
|
+
if (import_node_path36.default.basename(import_node_fs31.default.readlinkSync(import_node_path36.default.join(root, "exe"))) !== "dockerd") continue;
|
|
11323
|
+
if (import_node_fs31.default.readlinkSync(import_node_path36.default.join(root, "ns/net")) !== ownNamespace) continue;
|
|
11324
|
+
const descriptors = import_node_fs31.default.readdirSync(import_node_path36.default.join(root, "fd"));
|
|
9879
11325
|
if (descriptors.length > 4096) continue;
|
|
9880
11326
|
if (descriptors.some((fd) => {
|
|
9881
11327
|
if (Date.now() > deadline) return false;
|
|
9882
11328
|
try {
|
|
9883
|
-
return
|
|
11329
|
+
return import_node_fs31.default.readlinkSync(import_node_path36.default.join(root, "fd", fd)) === inode;
|
|
9884
11330
|
} catch {
|
|
9885
11331
|
return false;
|
|
9886
11332
|
}
|
|
@@ -9906,7 +11352,7 @@ function parseLinuxNetworkRoutes(output) {
|
|
|
9906
11352
|
const value = row;
|
|
9907
11353
|
if (typeof value.dst !== "string") throw new Error();
|
|
9908
11354
|
if (value.dst === "default") continue;
|
|
9909
|
-
if ((0,
|
|
11355
|
+
if ((0, import_node_net3.isIP)(value.dst.split("/")[0]) === 6) continue;
|
|
9910
11356
|
const cidr = parseIPv4Cidr(value.dst.includes("/") ? value.dst : `${value.dst}/32`);
|
|
9911
11357
|
if (!cidr || typeof value.dev !== "string" || !value.dev) throw new Error();
|
|
9912
11358
|
routes.push({ cidr, interface: value.dev, source: "host" });
|
|
@@ -9919,7 +11365,7 @@ function parseLinuxNetworkRoutes(output) {
|
|
|
9919
11365
|
function collectNetworkRoutes(options) {
|
|
9920
11366
|
if ((options.platform ?? process.platform) !== "linux" || !hasLocalDockerNetworkNamespace(options.endpoint))
|
|
9921
11367
|
return { status: "unknown", routes: [] };
|
|
9922
|
-
const result = (0,
|
|
11368
|
+
const result = (0, import_node_child_process24.spawnSync)("ip", ["-j", "-4", "route", "show", "table", "all"], {
|
|
9923
11369
|
encoding: "utf8",
|
|
9924
11370
|
timeout: 5e3,
|
|
9925
11371
|
maxBuffer: 1024 * 1024
|
|
@@ -9955,12 +11401,12 @@ function inspectLegacyNetworkCapacity(input) {
|
|
|
9955
11401
|
}
|
|
9956
11402
|
|
|
9957
11403
|
// src/core/network-managed.ts
|
|
9958
|
-
var
|
|
11404
|
+
var import_node_crypto19 = require("crypto");
|
|
9959
11405
|
|
|
9960
11406
|
// src/core/network-compose.ts
|
|
9961
|
-
var
|
|
11407
|
+
var import_node_crypto17 = require("crypto");
|
|
9962
11408
|
var SAFE_ENDPOINT_UPPER_BOUND_SEMANTICS = "full-lifecycle-demand";
|
|
9963
|
-
function
|
|
11409
|
+
function isRecord5(value) {
|
|
9964
11410
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9965
11411
|
}
|
|
9966
11412
|
function hasOwn(record5, key) {
|
|
@@ -9978,13 +11424,13 @@ function sortedUnique2(values) {
|
|
|
9978
11424
|
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
9979
11425
|
}
|
|
9980
11426
|
function readMap(value) {
|
|
9981
|
-
return
|
|
11427
|
+
return isRecord5(value) ? value : void 0;
|
|
9982
11428
|
}
|
|
9983
11429
|
function readMode(input) {
|
|
9984
11430
|
if (input.mode !== void 0) {
|
|
9985
11431
|
return input.mode;
|
|
9986
11432
|
}
|
|
9987
|
-
if (!
|
|
11433
|
+
if (!isRecord5(input.metadata)) {
|
|
9988
11434
|
return void 0;
|
|
9989
11435
|
}
|
|
9990
11436
|
for (const key of ["mode", "runtimeMode", "checkoutMode"]) {
|
|
@@ -10001,7 +11447,7 @@ function readExternal(value) {
|
|
|
10001
11447
|
if (typeof value === "boolean") {
|
|
10002
11448
|
return { value, known: true };
|
|
10003
11449
|
}
|
|
10004
|
-
if (
|
|
11450
|
+
if (isRecord5(value)) {
|
|
10005
11451
|
return { value: true, known: true };
|
|
10006
11452
|
}
|
|
10007
11453
|
return { known: false };
|
|
@@ -10016,7 +11462,7 @@ function readDriver(value) {
|
|
|
10016
11462
|
return { value: value.toLowerCase(), known: true };
|
|
10017
11463
|
}
|
|
10018
11464
|
function inspectNetworks(input, reasons) {
|
|
10019
|
-
const compose =
|
|
11465
|
+
const compose = isRecord5(input.compose) ? input.compose : {};
|
|
10020
11466
|
const normalizedValue2 = compose.networks;
|
|
10021
11467
|
const authoredValue2 = input.authoredNetworks;
|
|
10022
11468
|
const normalized = normalizedValue2 === void 0 ? {} : readMap(normalizedValue2);
|
|
@@ -10045,7 +11491,7 @@ function inspectNetworks(input, reasons) {
|
|
|
10045
11491
|
);
|
|
10046
11492
|
} else {
|
|
10047
11493
|
for (const [serviceName, rawService] of Object.entries(services)) {
|
|
10048
|
-
if (!
|
|
11494
|
+
if (!isRecord5(rawService)) {
|
|
10049
11495
|
addReason(reasons, reason("malformed-service", `Service ${serviceName} is not an object.`));
|
|
10050
11496
|
continue;
|
|
10051
11497
|
}
|
|
@@ -10090,7 +11536,7 @@ function parseServiceNetworks(value, serviceName, reasons) {
|
|
|
10090
11536
|
}
|
|
10091
11537
|
return { names: sortedUnique2(names), known: true };
|
|
10092
11538
|
}
|
|
10093
|
-
if (!
|
|
11539
|
+
if (!isRecord5(value)) {
|
|
10094
11540
|
addReason(
|
|
10095
11541
|
reasons,
|
|
10096
11542
|
reason(
|
|
@@ -10110,7 +11556,7 @@ function parseServiceNetworks(value, serviceName, reasons) {
|
|
|
10110
11556
|
);
|
|
10111
11557
|
continue;
|
|
10112
11558
|
}
|
|
10113
|
-
if (attachment !== null && !
|
|
11559
|
+
if (attachment !== null && !isRecord5(attachment)) {
|
|
10114
11560
|
known = false;
|
|
10115
11561
|
addReason(
|
|
10116
11562
|
reasons,
|
|
@@ -10125,27 +11571,27 @@ function parseServiceNetworks(value, serviceName, reasons) {
|
|
|
10125
11571
|
}
|
|
10126
11572
|
function authoredHas(authored, name, key) {
|
|
10127
11573
|
const declaration = authored[name];
|
|
10128
|
-
return
|
|
11574
|
+
return isRecord5(declaration) && hasOwn(declaration, key);
|
|
10129
11575
|
}
|
|
10130
11576
|
function authoredValue(authored, name, key) {
|
|
10131
11577
|
const declaration = authored[name];
|
|
10132
|
-
return
|
|
11578
|
+
return isRecord5(declaration) ? declaration[key] : void 0;
|
|
10133
11579
|
}
|
|
10134
11580
|
function normalizedValue(normalized, name, key) {
|
|
10135
11581
|
const declaration = normalized[name];
|
|
10136
|
-
return
|
|
11582
|
+
return isRecord5(declaration) ? declaration[key] : void 0;
|
|
10137
11583
|
}
|
|
10138
11584
|
function classifyNetwork(name, inspection, reasons) {
|
|
10139
11585
|
const authoredDeclaration = inspection.authored[name];
|
|
10140
11586
|
const normalizedDeclaration = inspection.normalized[name];
|
|
10141
|
-
if (authoredDeclaration !== void 0 && !
|
|
11587
|
+
if (authoredDeclaration !== void 0 && !isRecord5(authoredDeclaration)) {
|
|
10142
11588
|
addReason(
|
|
10143
11589
|
reasons,
|
|
10144
11590
|
reason("malformed-authored-network", `Authored network ${name} is not an object.`)
|
|
10145
11591
|
);
|
|
10146
11592
|
return { external: false, private: false };
|
|
10147
11593
|
}
|
|
10148
|
-
if (normalizedDeclaration !== void 0 && !
|
|
11594
|
+
if (normalizedDeclaration !== void 0 && !isRecord5(normalizedDeclaration)) {
|
|
10149
11595
|
addReason(
|
|
10150
11596
|
reasons,
|
|
10151
11597
|
reason("malformed-normalized-network", `Normalized network ${name} is not an object.`)
|
|
@@ -10226,7 +11672,7 @@ function classifyNetwork(name, inspection, reasons) {
|
|
|
10226
11672
|
} else {
|
|
10227
11673
|
const normalizedIpam = normalizedValue(inspection.normalized, name, "ipam");
|
|
10228
11674
|
if (normalizedIpam !== void 0) {
|
|
10229
|
-
if (!
|
|
11675
|
+
if (!isRecord5(normalizedIpam)) {
|
|
10230
11676
|
addReason(
|
|
10231
11677
|
reasons,
|
|
10232
11678
|
reason("malformed-normalized-ipam", `Network ${name} has malformed normalized IPAM.`)
|
|
@@ -10328,7 +11774,7 @@ function parseCount(value, field, reasons) {
|
|
|
10328
11774
|
if (value === void 0) {
|
|
10329
11775
|
return { value: 0, known: true };
|
|
10330
11776
|
}
|
|
10331
|
-
const candidate =
|
|
11777
|
+
const candidate = isRecord5(value) && hasOwn(value, "count") ? value.count : value;
|
|
10332
11778
|
if (typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate < 0) {
|
|
10333
11779
|
addReason(reasons, reason("malformed-count", `${field} must be a non-negative integer.`));
|
|
10334
11780
|
return { value: 0, known: false };
|
|
@@ -10344,7 +11790,7 @@ function parseReplicas(service, serviceName, reasons) {
|
|
|
10344
11790
|
candidates.push({ field: "scale", value: service.scale });
|
|
10345
11791
|
}
|
|
10346
11792
|
if (hasOwn(service, "deploy")) {
|
|
10347
|
-
if (!
|
|
11793
|
+
if (!isRecord5(service.deploy)) {
|
|
10348
11794
|
addReason(
|
|
10349
11795
|
reasons,
|
|
10350
11796
|
reason("malformed-replicas", `Service ${serviceName} deploy metadata is malformed.`)
|
|
@@ -10399,7 +11845,7 @@ function parseUpperBound(input, reasons) {
|
|
|
10399
11845
|
const raw = input.endpointUpperBound;
|
|
10400
11846
|
let value = raw;
|
|
10401
11847
|
let semantics = input.endpointUpperBoundSemantics;
|
|
10402
|
-
if (
|
|
11848
|
+
if (isRecord5(raw)) {
|
|
10403
11849
|
value = raw.value ?? raw.endpoints ?? raw.count;
|
|
10404
11850
|
semantics = raw.semantics ?? semantics;
|
|
10405
11851
|
}
|
|
@@ -10464,7 +11910,7 @@ function deriveComposeNetworkDemand(input) {
|
|
|
10464
11910
|
)
|
|
10465
11911
|
);
|
|
10466
11912
|
}
|
|
10467
|
-
const compose =
|
|
11913
|
+
const compose = isRecord5(input.compose) ? input.compose : {};
|
|
10468
11914
|
const services = readMap(compose.services);
|
|
10469
11915
|
const serviceEndpointsByService = {};
|
|
10470
11916
|
const replicasByService = {};
|
|
@@ -10482,7 +11928,7 @@ function deriveComposeNetworkDemand(input) {
|
|
|
10482
11928
|
serviceDemandKnown = false;
|
|
10483
11929
|
} else {
|
|
10484
11930
|
for (const [serviceName, rawService] of Object.entries(services)) {
|
|
10485
|
-
if (!
|
|
11931
|
+
if (!isRecord5(rawService)) {
|
|
10486
11932
|
addReason(reasons, reason("malformed-service", `Service ${serviceName} is not an object.`));
|
|
10487
11933
|
serviceDemandKnown = false;
|
|
10488
11934
|
continue;
|
|
@@ -10584,7 +12030,7 @@ function deriveComposeNetworkOverlay(subnet) {
|
|
|
10584
12030
|
};
|
|
10585
12031
|
}
|
|
10586
12032
|
function extendDockerComposeFile(nativeConfig, overlayPath) {
|
|
10587
|
-
if (!
|
|
12033
|
+
if (!isRecord5(nativeConfig)) {
|
|
10588
12034
|
return {
|
|
10589
12035
|
status: "unknown",
|
|
10590
12036
|
reasons: [
|
|
@@ -10635,7 +12081,7 @@ function replicaProjection(service) {
|
|
|
10635
12081
|
return replicas.known ? replicas.value : "unknown";
|
|
10636
12082
|
}
|
|
10637
12083
|
function redactedNetworkProjection(value) {
|
|
10638
|
-
if (!
|
|
12084
|
+
if (!isRecord5(value)) {
|
|
10639
12085
|
return value === void 0 ? void 0 : "unknown";
|
|
10640
12086
|
}
|
|
10641
12087
|
const projection = {};
|
|
@@ -10654,7 +12100,7 @@ function redactedNetworkProjection(value) {
|
|
|
10654
12100
|
const config = Array.isArray(ipam?.config) ? ipam.config : void 0;
|
|
10655
12101
|
projection.ipam = {
|
|
10656
12102
|
config: config === void 0 ? "unknown" : config.map(
|
|
10657
|
-
(entry) =>
|
|
12103
|
+
(entry) => isRecord5(entry) && typeof entry.subnet === "string" ? entry.subnet : "unknown"
|
|
10658
12104
|
)
|
|
10659
12105
|
};
|
|
10660
12106
|
} else if (key === "enable_ipv6") {
|
|
@@ -10670,7 +12116,7 @@ function redactedCountProjection(value) {
|
|
|
10670
12116
|
if (value === void 0) {
|
|
10671
12117
|
return 0;
|
|
10672
12118
|
}
|
|
10673
|
-
const candidate =
|
|
12119
|
+
const candidate = isRecord5(value) && hasOwn(value, "count") ? value.count : value;
|
|
10674
12120
|
return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0 ? candidate : "unknown";
|
|
10675
12121
|
}
|
|
10676
12122
|
function redactedUpperBoundProjection(value) {
|
|
@@ -10680,7 +12126,7 @@ function redactedUpperBoundProjection(value) {
|
|
|
10680
12126
|
if (typeof value === "number") {
|
|
10681
12127
|
return Number.isSafeInteger(value) && value >= 0 ? value : "unknown";
|
|
10682
12128
|
}
|
|
10683
|
-
if (!
|
|
12129
|
+
if (!isRecord5(value)) {
|
|
10684
12130
|
return "unknown";
|
|
10685
12131
|
}
|
|
10686
12132
|
const candidate = value.value ?? value.endpoints ?? value.count;
|
|
@@ -10690,12 +12136,12 @@ function redactedUpperBoundProjection(value) {
|
|
|
10690
12136
|
};
|
|
10691
12137
|
}
|
|
10692
12138
|
function normalizedFingerprintProjection(input) {
|
|
10693
|
-
const compose =
|
|
12139
|
+
const compose = isRecord5(input.compose) ? input.compose : {};
|
|
10694
12140
|
const services = readMap(compose.services);
|
|
10695
12141
|
const serviceProjection = {};
|
|
10696
12142
|
for (const serviceName of Object.keys(services ?? {}).sort()) {
|
|
10697
12143
|
const service = services?.[serviceName];
|
|
10698
|
-
serviceProjection[serviceName] =
|
|
12144
|
+
serviceProjection[serviceName] = isRecord5(service) ? {
|
|
10699
12145
|
networks: projectionNetworkMembership(service.networks),
|
|
10700
12146
|
replicas: replicaProjection(service)
|
|
10701
12147
|
} : "unknown";
|
|
@@ -10727,14 +12173,14 @@ function normalizedFingerprintProjection(input) {
|
|
|
10727
12173
|
};
|
|
10728
12174
|
}
|
|
10729
12175
|
function redactedOverlayProjection(value) {
|
|
10730
|
-
if (!
|
|
12176
|
+
if (!isRecord5(value)) {
|
|
10731
12177
|
return value === void 0 ? void 0 : "unknown";
|
|
10732
12178
|
}
|
|
10733
12179
|
const networks = readMap(value.networks);
|
|
10734
12180
|
const defaultNetwork = readMap(networks?.default);
|
|
10735
12181
|
const ipam = readMap(defaultNetwork?.ipam);
|
|
10736
12182
|
const config = Array.isArray(ipam?.config) ? ipam.config : void 0;
|
|
10737
|
-
if (config === void 0 || config.some((entry) => !
|
|
12183
|
+
if (config === void 0 || config.some((entry) => !isRecord5(entry) || typeof entry.subnet !== "string")) {
|
|
10738
12184
|
return "unknown";
|
|
10739
12185
|
}
|
|
10740
12186
|
return {
|
|
@@ -10757,26 +12203,26 @@ function stableValue(value) {
|
|
|
10757
12203
|
if (Array.isArray(value)) {
|
|
10758
12204
|
return `[${value.map((item) => stableValue(item)).join(",")}]`;
|
|
10759
12205
|
}
|
|
10760
|
-
if (
|
|
12206
|
+
if (isRecord5(value)) {
|
|
10761
12207
|
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableValue(value[key])}`).join(",")}}`;
|
|
10762
12208
|
}
|
|
10763
12209
|
return JSON.stringify(String(value));
|
|
10764
12210
|
}
|
|
10765
12211
|
function fingerprintComposeNetworkModel(input) {
|
|
10766
12212
|
const projection = normalizedFingerprintProjection(input);
|
|
10767
|
-
return (0,
|
|
12213
|
+
return (0, import_node_crypto17.createHash)("sha256").update(stableValue(projection), "utf8").digest("hex");
|
|
10768
12214
|
}
|
|
10769
12215
|
|
|
10770
12216
|
// src/core/network-compose-files.ts
|
|
10771
|
-
var
|
|
10772
|
-
var
|
|
10773
|
-
var
|
|
10774
|
-
var
|
|
12217
|
+
var import_node_child_process25 = require("child_process");
|
|
12218
|
+
var import_node_crypto18 = require("crypto");
|
|
12219
|
+
var import_node_fs32 = __toESM(require("fs"));
|
|
12220
|
+
var import_node_path37 = __toESM(require("path"));
|
|
10775
12221
|
var import_yaml4 = __toESM(require("yaml"));
|
|
10776
12222
|
var OVERLAY = "docker-compose.devrouter-network.yml";
|
|
10777
12223
|
var MARKER = "# devrouter:managed workspace network";
|
|
10778
|
-
var
|
|
10779
|
-
function
|
|
12224
|
+
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
12225
|
+
function object3(value) {
|
|
10780
12226
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
10781
12227
|
throw new Error("Network Compose evidence is malformed.");
|
|
10782
12228
|
return value;
|
|
@@ -10784,20 +12230,20 @@ function object2(value) {
|
|
|
10784
12230
|
function inspectNetworkComposeFiles(input) {
|
|
10785
12231
|
const authoredNetworks = {};
|
|
10786
12232
|
for (const file of input.plan.composeFiles) {
|
|
10787
|
-
const stat =
|
|
10788
|
-
if (!stat.isFile() || stat.size >
|
|
12233
|
+
const stat = import_node_fs32.default.lstatSync(file);
|
|
12234
|
+
if (!stat.isFile() || stat.size > MAX_BYTES2)
|
|
10789
12235
|
throw new Error("Network Compose source is unavailable or too large.");
|
|
10790
12236
|
let source;
|
|
10791
12237
|
try {
|
|
10792
|
-
source =
|
|
12238
|
+
source = object3(import_yaml4.default.parse(import_node_fs32.default.readFileSync(file, "utf8")));
|
|
10793
12239
|
} catch {
|
|
10794
12240
|
throw new Error("Network Compose source could not be parsed safely.");
|
|
10795
12241
|
}
|
|
10796
12242
|
if (source.networks !== void 0) {
|
|
10797
|
-
for (const [name, declaration] of Object.entries(
|
|
12243
|
+
for (const [name, declaration] of Object.entries(object3(source.networks))) {
|
|
10798
12244
|
authoredNetworks[name] = {
|
|
10799
12245
|
...authoredNetworks[name],
|
|
10800
|
-
...declaration === null ? {} :
|
|
12246
|
+
...declaration === null ? {} : object3(declaration)
|
|
10801
12247
|
};
|
|
10802
12248
|
}
|
|
10803
12249
|
}
|
|
@@ -10805,7 +12251,7 @@ function inspectNetworkComposeFiles(input) {
|
|
|
10805
12251
|
const env = managedComposeEnvironment(input.workspace);
|
|
10806
12252
|
delete env.DOCKER_CONTEXT;
|
|
10807
12253
|
delete env.DOCKER_HOST;
|
|
10808
|
-
const result = (0,
|
|
12254
|
+
const result = (0, import_node_child_process25.spawnSync)(
|
|
10809
12255
|
"docker",
|
|
10810
12256
|
[
|
|
10811
12257
|
"--host",
|
|
@@ -10825,13 +12271,13 @@ function inspectNetworkComposeFiles(input) {
|
|
|
10825
12271
|
env,
|
|
10826
12272
|
encoding: "utf8",
|
|
10827
12273
|
timeout: 1e4,
|
|
10828
|
-
maxBuffer:
|
|
12274
|
+
maxBuffer: MAX_BYTES2
|
|
10829
12275
|
}
|
|
10830
12276
|
);
|
|
10831
12277
|
if (result.error || result.status !== 0)
|
|
10832
12278
|
throw new Error("Complete network Compose evidence is unavailable.");
|
|
10833
12279
|
try {
|
|
10834
|
-
return { compose:
|
|
12280
|
+
return { compose: object3(JSON.parse(result.stdout)), authoredNetworks };
|
|
10835
12281
|
} catch {
|
|
10836
12282
|
throw new Error("Network Compose evidence is malformed.");
|
|
10837
12283
|
}
|
|
@@ -10839,8 +12285,8 @@ function inspectNetworkComposeFiles(input) {
|
|
|
10839
12285
|
function prepareNetworkComposeFiles(plan, subnet) {
|
|
10840
12286
|
const overlay = deriveComposeNetworkOverlay(subnet);
|
|
10841
12287
|
if (overlay.status !== "ready" || !overlay.overlay) throw new Error("Network subnet is invalid.");
|
|
10842
|
-
const overlayPath =
|
|
10843
|
-
const ignored = (0,
|
|
12288
|
+
const overlayPath = import_node_path37.default.join(plan.composeDirectory, OVERLAY);
|
|
12289
|
+
const ignored = (0, import_node_child_process25.spawnSync)("git", [
|
|
10844
12290
|
"-C",
|
|
10845
12291
|
plan.composeDirectory,
|
|
10846
12292
|
"check-ignore",
|
|
@@ -10851,12 +12297,12 @@ function prepareNetworkComposeFiles(plan, subnet) {
|
|
|
10851
12297
|
]);
|
|
10852
12298
|
if (ignored.status !== 0)
|
|
10853
12299
|
throw new Error("Generated network overlay must be ignored before allocation.");
|
|
10854
|
-
const
|
|
12300
|
+
const bytes2 = `${MARKER}
|
|
10855
12301
|
${import_yaml4.default.stringify(overlay.overlay)}`;
|
|
10856
12302
|
const assertOwnership = () => {
|
|
10857
|
-
if (!
|
|
10858
|
-
const stat =
|
|
10859
|
-
if (!stat.isFile() || stat.size >
|
|
12303
|
+
if (!import_node_fs32.default.existsSync(overlayPath)) return;
|
|
12304
|
+
const stat = import_node_fs32.default.lstatSync(overlayPath);
|
|
12305
|
+
if (!stat.isFile() || stat.size > MAX_BYTES2 || import_node_fs32.default.readFileSync(overlayPath, "utf8") !== bytes2)
|
|
10860
12306
|
throw new Error("Retained network overlay changed; repair is required.");
|
|
10861
12307
|
};
|
|
10862
12308
|
assertOwnership();
|
|
@@ -10870,19 +12316,19 @@ ${JSON.stringify(extended.config, null, 2)}
|
|
|
10870
12316
|
...plan,
|
|
10871
12317
|
contents,
|
|
10872
12318
|
composeFiles: [...plan.composeFiles, overlayPath],
|
|
10873
|
-
effectiveConfigSha256: (0,
|
|
12319
|
+
effectiveConfigSha256: (0, import_node_crypto18.createHash)("sha256").update(contents).digest("hex")
|
|
10874
12320
|
};
|
|
10875
12321
|
return {
|
|
10876
12322
|
plan: result,
|
|
10877
12323
|
write: () => {
|
|
10878
12324
|
assertOwnership();
|
|
10879
|
-
writeFileAtomically(overlayPath,
|
|
12325
|
+
writeFileAtomically(overlayPath, bytes2);
|
|
10880
12326
|
}
|
|
10881
12327
|
};
|
|
10882
12328
|
}
|
|
10883
12329
|
|
|
10884
12330
|
// src/core/network-connected-route.ts
|
|
10885
|
-
var
|
|
12331
|
+
var import_node_child_process26 = require("child_process");
|
|
10886
12332
|
function bounds(cidr) {
|
|
10887
12333
|
const canonical = parseIPv4Cidr(cidr);
|
|
10888
12334
|
if (!canonical) throw new Error("Invalid connected route.");
|
|
@@ -10907,7 +12353,7 @@ function inspectClaimedBridge(claim2, networkId) {
|
|
|
10907
12353
|
const env = { ...process.env };
|
|
10908
12354
|
delete env.DOCKER_CONTEXT;
|
|
10909
12355
|
delete env.DOCKER_HOST;
|
|
10910
|
-
const result = (0,
|
|
12356
|
+
const result = (0, import_node_child_process26.spawnSync)(
|
|
10911
12357
|
"docker",
|
|
10912
12358
|
[
|
|
10913
12359
|
"--host",
|
|
@@ -10934,7 +12380,7 @@ function inspectClaimedBridge(claim2, networkId) {
|
|
|
10934
12380
|
const [first, last] = bounds(claim2.subnet);
|
|
10935
12381
|
const [address] = bounds(gateway);
|
|
10936
12382
|
if (address <= first || address >= last) throw new Error();
|
|
10937
|
-
const interfaces = (0,
|
|
12383
|
+
const interfaces = (0, import_node_child_process26.spawnSync)("ip", ["-j", "-d", "-4", "address", "show", "dev", bridge], {
|
|
10938
12384
|
encoding: "utf8",
|
|
10939
12385
|
timeout: 5e3,
|
|
10940
12386
|
maxBuffer: 65536
|
|
@@ -11188,7 +12634,7 @@ function createManagedNetworkSession(input) {
|
|
|
11188
12634
|
"Insufficient endpoint headroom; request an allowed larger network before first allocation."
|
|
11189
12635
|
);
|
|
11190
12636
|
const authority = readNetworkOperationAuthority(identity2);
|
|
11191
|
-
const fingerprint = (0,
|
|
12637
|
+
const fingerprint = (0, import_node_crypto19.createHash)("sha256").update(input.plan().sourceConfigSha256).update(fingerprintComposeNetworkModel(model)).digest("hex");
|
|
11192
12638
|
if (!saved) {
|
|
11193
12639
|
if (policy.status !== "valid")
|
|
11194
12640
|
throw new Error("New network allocation requires valid machine policy.");
|
|
@@ -11372,11 +12818,11 @@ var DEFAULT_READINESS_TIMEOUT_MS = 12e4;
|
|
|
11372
12818
|
var POLL_INTERVAL_MS = 1e3;
|
|
11373
12819
|
function assertOverlay(container, repoPath) {
|
|
11374
12820
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
11375
|
-
if (!workingDir || !sameWorkspacePath(workingDir,
|
|
12821
|
+
if (!workingDir || !sameWorkspacePath(workingDir, import_node_path38.default.join(repoPath, ".devcontainer"))) {
|
|
11376
12822
|
throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
|
|
11377
12823
|
}
|
|
11378
12824
|
const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
|
|
11379
|
-
const expectedOverlay =
|
|
12825
|
+
const expectedOverlay = import_node_path38.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
11380
12826
|
if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
|
|
11381
12827
|
throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
|
|
11382
12828
|
}
|
|
@@ -11571,7 +13017,7 @@ function hasExactManagedComposeProject(repoPath, state) {
|
|
|
11571
13017
|
}).some((container) => {
|
|
11572
13018
|
const workingDir = container.labels["com.docker.compose.project.working_dir"];
|
|
11573
13019
|
return Boolean(
|
|
11574
|
-
workingDir && sameWorkspacePath(workingDir,
|
|
13020
|
+
workingDir && sameWorkspacePath(workingDir, import_node_path38.default.join(repoPath, ".devcontainer"))
|
|
11575
13021
|
);
|
|
11576
13022
|
});
|
|
11577
13023
|
} catch {
|
|
@@ -11679,7 +13125,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
11679
13125
|
target
|
|
11680
13126
|
});
|
|
11681
13127
|
if (target.kind === "linked") {
|
|
11682
|
-
const workspaceEnv = (0,
|
|
13128
|
+
const workspaceEnv = (0, import_node_child_process27.spawnSync)(
|
|
11683
13129
|
"docker",
|
|
11684
13130
|
["exec", appContainer.id, "printenv", "WORKSPACE"],
|
|
11685
13131
|
{ encoding: "utf-8", ...networkDockerOptions() }
|
|
@@ -11689,7 +13135,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
11689
13135
|
`Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
|
|
11690
13136
|
);
|
|
11691
13137
|
}
|
|
11692
|
-
const devrouterWorkspaceEnv = (0,
|
|
13138
|
+
const devrouterWorkspaceEnv = (0, import_node_child_process27.spawnSync)(
|
|
11693
13139
|
"docker",
|
|
11694
13140
|
["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
|
|
11695
13141
|
{ encoding: "utf-8", ...networkDockerOptions() }
|
|
@@ -11700,7 +13146,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
11700
13146
|
);
|
|
11701
13147
|
}
|
|
11702
13148
|
}
|
|
11703
|
-
const gitCheck = (0,
|
|
13149
|
+
const gitCheck = (0, import_node_child_process27.spawnSync)(
|
|
11704
13150
|
"docker",
|
|
11705
13151
|
[
|
|
11706
13152
|
"exec",
|
|
@@ -11955,14 +13401,14 @@ function resolvePrimaryTarget(repoPath) {
|
|
|
11955
13401
|
}
|
|
11956
13402
|
function isPrimaryCheckout(repoPath) {
|
|
11957
13403
|
try {
|
|
11958
|
-
return
|
|
13404
|
+
return import_node_fs33.default.statSync(import_node_path38.default.join(repoPath, ".git")).isDirectory();
|
|
11959
13405
|
} catch {
|
|
11960
13406
|
return false;
|
|
11961
13407
|
}
|
|
11962
13408
|
}
|
|
11963
13409
|
function openUrls(urls) {
|
|
11964
13410
|
for (const url of urls) {
|
|
11965
|
-
const opened = (0,
|
|
13411
|
+
const opened = (0, import_node_child_process27.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
11966
13412
|
if (opened.status !== 0) {
|
|
11967
13413
|
process.stderr.write(`Warning: could not open '${url}'.
|
|
11968
13414
|
`);
|
|
@@ -12018,8 +13464,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
12018
13464
|
const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
|
|
12019
13465
|
let devpodId = target.devpodId;
|
|
12020
13466
|
if (target.kind === "linked") {
|
|
12021
|
-
const overlayPath =
|
|
12022
|
-
if (!
|
|
13467
|
+
const overlayPath = import_node_path38.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
|
|
13468
|
+
if (!import_node_fs33.default.existsSync(overlayPath)) {
|
|
12023
13469
|
throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
|
|
12024
13470
|
}
|
|
12025
13471
|
}
|
|
@@ -12087,9 +13533,38 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
12087
13533
|
});
|
|
12088
13534
|
}
|
|
12089
13535
|
}
|
|
13536
|
+
if (managedPlan) {
|
|
13537
|
+
let hostPortConflicts = [];
|
|
13538
|
+
try {
|
|
13539
|
+
hostPortConflicts = detectHostPortClaimConflicts({
|
|
13540
|
+
repoPath,
|
|
13541
|
+
plan: managedPlan,
|
|
13542
|
+
workspace: managedWorkspaceEnv
|
|
13543
|
+
});
|
|
13544
|
+
} catch (error) {
|
|
13545
|
+
throw new Error(
|
|
13546
|
+
`Managed admission could not verify fixed host-port claims before start: ${error instanceof Error ? error.message : String(error)}`
|
|
13547
|
+
);
|
|
13548
|
+
}
|
|
13549
|
+
if (hostPortConflicts.length > 0) {
|
|
13550
|
+
return {
|
|
13551
|
+
kind: target.kind,
|
|
13552
|
+
repoPath,
|
|
13553
|
+
...target.kind === "linked" ? { workspace: target.workspace } : {},
|
|
13554
|
+
profile: runtime.profile,
|
|
13555
|
+
// A refusal precedes the provider start, so the exact id may not
|
|
13556
|
+
// be known yet on a primary checkout.
|
|
13557
|
+
devpodId: devpodId ?? "(absent)",
|
|
13558
|
+
urls: [],
|
|
13559
|
+
recreated: false,
|
|
13560
|
+
tlsRefreshed: false,
|
|
13561
|
+
hostPortConflicts
|
|
13562
|
+
};
|
|
13563
|
+
}
|
|
13564
|
+
}
|
|
12090
13565
|
const apps = proxyAppsFromConfig(runtime.config);
|
|
12091
13566
|
const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
|
|
12092
|
-
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ??
|
|
13567
|
+
const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path38.default.basename(repoPath)) ?? "app";
|
|
12093
13568
|
for (const [index, app] of apps.entries()) {
|
|
12094
13569
|
if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
|
|
12095
13570
|
const owner = target.kind === "linked" ? "workspace" : "checkout";
|
|
@@ -12204,7 +13679,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
12204
13679
|
repairMutationStarted = true;
|
|
12205
13680
|
transitionPhase = "service-start";
|
|
12206
13681
|
claimLifecycleEffect();
|
|
12207
|
-
const result = (0,
|
|
13682
|
+
const result = (0, import_node_child_process27.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
|
|
12208
13683
|
...networkDockerOptions(),
|
|
12209
13684
|
encoding: "utf-8",
|
|
12210
13685
|
timeout: DEFAULT_READINESS_TIMEOUT_MS,
|
|
@@ -12340,6 +13815,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
12340
13815
|
claimLifecycleEffect();
|
|
12341
13816
|
writeManagedRuntimeState(state);
|
|
12342
13817
|
capturedStopState = state;
|
|
13818
|
+
clearActiveLifecycleStartupWitness();
|
|
12343
13819
|
};
|
|
12344
13820
|
if (options.repair) capture();
|
|
12345
13821
|
else
|
|
@@ -12908,9 +14384,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
12908
14384
|
}
|
|
12909
14385
|
const repairLocked = () => {
|
|
12910
14386
|
const runtime = resolveWorkspaceRuntimeOrDefault(repoPath);
|
|
12911
|
-
|
|
14387
|
+
import_node_fs33.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
12912
14388
|
return withFileLock(
|
|
12913
|
-
|
|
14389
|
+
import_node_path38.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
|
|
12914
14390
|
{
|
|
12915
14391
|
activity: "Managed runtime repair",
|
|
12916
14392
|
target: repoPath,
|
|
@@ -12982,6 +14458,7 @@ var lockHeld = false;
|
|
|
12982
14458
|
var stopProjects = [];
|
|
12983
14459
|
var stopBaselineState;
|
|
12984
14460
|
var BUSY_LIFECYCLE_WAIT_MS = 30 * 60 * 1e3;
|
|
14461
|
+
var CAPACITY_CLIENT_WAIT_MS = 30 * 60 * 1e3;
|
|
12985
14462
|
function matchesFence(record5, fence) {
|
|
12986
14463
|
return Object.entries(reliabilityFence(record5.state)).every(
|
|
12987
14464
|
([key, value]) => value === fence[key]
|
|
@@ -12998,6 +14475,7 @@ function claimActiveLifecycleEffect() {
|
|
|
12998
14475
|
if (!matchesFence(record5, worker.fence) || worker.kind !== "stop" && record5.worker?.id !== worker.workerId) {
|
|
12999
14476
|
throw new Error("Lifecycle intent superseded this worker.");
|
|
13000
14477
|
}
|
|
14478
|
+
if (worker.kind !== "stop") assertCapacityEffect(record5, worker.workerId, Date.now());
|
|
13001
14479
|
if (record5.effectSequence === Number.MAX_SAFE_INTEGER)
|
|
13002
14480
|
throw new Error("Lifecycle effect sequence exhausted.");
|
|
13003
14481
|
record5.effectSequence += 1;
|
|
@@ -13052,6 +14530,13 @@ function reconcileDrained(record5) {
|
|
|
13052
14530
|
record5.worker = null;
|
|
13053
14531
|
}
|
|
13054
14532
|
}
|
|
14533
|
+
function clearActiveLifecycleStartupWitness() {
|
|
14534
|
+
const request = activeWorker;
|
|
14535
|
+
if (!request || request.kind === "stop") return;
|
|
14536
|
+
const record5 = readReliabilityOperation(request.identity);
|
|
14537
|
+
if (record5?.startupWitness?.operationId !== request.operationId) return;
|
|
14538
|
+
clearStartupWitness(request.identity, reliabilityFence(record5.state));
|
|
14539
|
+
}
|
|
13055
14540
|
async function acquireWorkerLock(repoPath, operation) {
|
|
13056
14541
|
const deadline = Date.now() + 3e4;
|
|
13057
14542
|
for (; ; ) {
|
|
@@ -13108,7 +14593,7 @@ async function executeLifecycleWorker(request, operation) {
|
|
|
13108
14593
|
const owned = containers.filter(
|
|
13109
14594
|
(container) => sameWorkspacePath(
|
|
13110
14595
|
container.labels["com.docker.compose.project.working_dir"] ?? "",
|
|
13111
|
-
|
|
14596
|
+
import_node_path39.default.join(request.repoPath, ".devcontainer")
|
|
13112
14597
|
)
|
|
13113
14598
|
);
|
|
13114
14599
|
const projects = owned.map((container) => container.labels["com.docker.compose.project"]);
|
|
@@ -13134,7 +14619,7 @@ async function executeLifecycleWorker(request, operation) {
|
|
|
13134
14619
|
}
|
|
13135
14620
|
});
|
|
13136
14621
|
}
|
|
13137
|
-
function recordLifecycleOutcome(outcome) {
|
|
14622
|
+
function recordLifecycleOutcome(outcome, result) {
|
|
13138
14623
|
const request = activeWorker;
|
|
13139
14624
|
if (request?.kind !== "exec") return;
|
|
13140
14625
|
updateReliabilityOperation(request.identity, (record5) => {
|
|
@@ -13152,27 +14637,40 @@ function recordLifecycleOutcome(outcome) {
|
|
|
13152
14637
|
};
|
|
13153
14638
|
stepRecord(record5, event);
|
|
13154
14639
|
record5.outcome = { ...outcome, operationId: request.operationId };
|
|
14640
|
+
if (result !== void 0) record5.result = result;
|
|
13155
14641
|
});
|
|
13156
14642
|
}
|
|
13157
|
-
function recordLifecycleCompletion(exitCode) {
|
|
14643
|
+
function recordLifecycleCompletion(exitCode, preparedProfile, result) {
|
|
13158
14644
|
const request = activeWorker;
|
|
13159
14645
|
if (!request || request.kind === "stop") return;
|
|
13160
14646
|
updateReliabilityOperation(request.identity, (record5) => {
|
|
13161
14647
|
if (!matchesFence(record5, request.fence)) return;
|
|
14648
|
+
if (preparedProfile !== void 0 && record5.enrollment) {
|
|
14649
|
+
if (request.kind !== "ensure" || record5.worker?.id !== request.workerId || record5.state.operation?.id !== request.operationId || record5.state.profile !== preparedProfile)
|
|
14650
|
+
throw new Error("Prepared profile does not match the active lifecycle worker.");
|
|
14651
|
+
record5.activeProfile = preparedProfile;
|
|
14652
|
+
record5.preparation = {
|
|
14653
|
+
operationId: request.operationId,
|
|
14654
|
+
profile: preparedProfile,
|
|
14655
|
+
fence: { ...request.fence }
|
|
14656
|
+
};
|
|
14657
|
+
}
|
|
13162
14658
|
stepRecord(record5, {
|
|
13163
14659
|
...request.fence,
|
|
13164
14660
|
type: "completion",
|
|
13165
14661
|
operationId: request.operationId,
|
|
13166
14662
|
exitCode
|
|
13167
14663
|
});
|
|
14664
|
+
if (result !== void 0) record5.result = result;
|
|
13168
14665
|
});
|
|
13169
14666
|
}
|
|
13170
|
-
function recordLifecycleUnknown() {
|
|
14667
|
+
function recordLifecycleUnknown(result) {
|
|
13171
14668
|
const request = activeWorker;
|
|
13172
14669
|
if (!request || request.kind === "stop") return;
|
|
13173
14670
|
updateReliabilityOperation(request.identity, (record5) => {
|
|
13174
14671
|
if (!matchesFence(record5, request.fence)) return;
|
|
13175
14672
|
stepRecord(record5, { ...request.fence, type: "interrupted", operationId: request.operationId });
|
|
14673
|
+
if (result !== void 0) record5.result = result;
|
|
13176
14674
|
});
|
|
13177
14675
|
}
|
|
13178
14676
|
function proveLifecycleStopped() {
|
|
@@ -13196,7 +14694,7 @@ function proveLifecycleStopped() {
|
|
|
13196
14694
|
const remaining = inspectWorkspaceContainers().filter(
|
|
13197
14695
|
(container) => sameWorkspacePath(
|
|
13198
14696
|
container.labels["com.docker.compose.project.working_dir"] ?? "",
|
|
13199
|
-
|
|
14697
|
+
import_node_path39.default.join(request.repoPath, ".devcontainer")
|
|
13200
14698
|
)
|
|
13201
14699
|
);
|
|
13202
14700
|
if (remaining.some(
|
|
@@ -13209,16 +14707,48 @@ function proveLifecycleStopped() {
|
|
|
13209
14707
|
(route) => sameWorkspacePath(route.repoPath, request.repoPath)
|
|
13210
14708
|
);
|
|
13211
14709
|
if (routes.length) throw new Error("Workspace routes remain published after stop.");
|
|
13212
|
-
updateReliabilityOperation(request.identity, (record5) => {
|
|
14710
|
+
const settlement = updateReliabilityOperation(request.identity, (record5) => {
|
|
13213
14711
|
if (!matchesFence(record5, request.fence) || record5.worker)
|
|
13214
14712
|
throw new Error("Stop proof was superseded or an earlier worker remains.");
|
|
14713
|
+
if (record5.version === 2) {
|
|
14714
|
+
if (record5.capacity) record5.capacity.validUntilMs = 0;
|
|
14715
|
+
return record5.state.environmentId;
|
|
14716
|
+
}
|
|
13215
14717
|
stepRecord(record5, {
|
|
13216
14718
|
...request.fence,
|
|
13217
14719
|
type: "stop-proof",
|
|
13218
14720
|
workloadsStopped: true,
|
|
13219
14721
|
routesRemoved: true
|
|
13220
14722
|
});
|
|
14723
|
+
return void 0;
|
|
13221
14724
|
});
|
|
14725
|
+
if (settlement) {
|
|
14726
|
+
const capacity = new CapacityStore(import_node_path39.default.join(DEVROUTER_HOME, "controller"));
|
|
14727
|
+
for (let attempt = 0; ; attempt++) {
|
|
14728
|
+
try {
|
|
14729
|
+
capacity.settleEnvironmentAfterStop(settlement, capacity.read().revision);
|
|
14730
|
+
break;
|
|
14731
|
+
} catch (error) {
|
|
14732
|
+
if (!(error instanceof CapacitySnapshotChangedError) || attempt >= 2) throw error;
|
|
14733
|
+
}
|
|
14734
|
+
}
|
|
14735
|
+
updateReliabilityOperation(request.identity, (record5) => {
|
|
14736
|
+
if (!matchesFence(record5, request.fence) || record5.worker || record5.state.environmentId !== settlement || record5.capacity && record5.capacity.validUntilMs !== 0)
|
|
14737
|
+
throw new Error("Capacity settlement was superseded before journal confirmation.");
|
|
14738
|
+
record5.capacity = null;
|
|
14739
|
+
if (record5.enrollment) {
|
|
14740
|
+
record5.phaseSettlement = null;
|
|
14741
|
+
record5.activeProfile = null;
|
|
14742
|
+
record5.preparation = null;
|
|
14743
|
+
}
|
|
14744
|
+
stepRecord(record5, {
|
|
14745
|
+
...request.fence,
|
|
14746
|
+
type: "stop-proof",
|
|
14747
|
+
workloadsStopped: true,
|
|
14748
|
+
routesRemoved: true
|
|
14749
|
+
});
|
|
14750
|
+
});
|
|
14751
|
+
}
|
|
13222
14752
|
};
|
|
13223
14753
|
if (stopBaselineState)
|
|
13224
14754
|
withMutationLock("Settle retained stop", stopBaselineState.repoPath, settle);
|
|
@@ -13259,7 +14789,7 @@ async function devsyExecOutcome(repoPath, command, retainedProof) {
|
|
|
13259
14789
|
const launch = () => {
|
|
13260
14790
|
if (retainedProof) revalidateDevsyExecProof(repoPath, retainedProof);
|
|
13261
14791
|
claimLifecycleEffect();
|
|
13262
|
-
return (0,
|
|
14792
|
+
return (0, import_node_child_process28.spawn)("devsy", args, { stdio: "inherit" });
|
|
13263
14793
|
};
|
|
13264
14794
|
child = retainedProof ? withMutationLock("Execute retained workspace", repoPath, launch) : launch();
|
|
13265
14795
|
} catch (error) {
|
|
@@ -13307,7 +14837,7 @@ function ensureGuidance2(repoPath) {
|
|
|
13307
14837
|
return `Run 'devrouter ensure ${repoPath}' first.`;
|
|
13308
14838
|
}
|
|
13309
14839
|
function assertRunningDevpod(devpodId, repoPath) {
|
|
13310
|
-
const result = (0,
|
|
14840
|
+
const result = (0, import_node_child_process29.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
|
|
13311
14841
|
encoding: "utf-8"
|
|
13312
14842
|
});
|
|
13313
14843
|
if (result.status !== 0) {
|
|
@@ -13354,7 +14884,7 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
13354
14884
|
}
|
|
13355
14885
|
assertRunningDevpod(devpod.id, repoPath);
|
|
13356
14886
|
const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
|
|
13357
|
-
const statusMarker = `__DEVROUTER_EXIT_${(0,
|
|
14887
|
+
const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto21.randomUUID)()}__:`;
|
|
13358
14888
|
const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
|
|
13359
14889
|
const literalCommand = command.map(quotePosixArg).join(" ");
|
|
13360
14890
|
const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
|
|
@@ -13486,7 +15016,7 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
13486
15016
|
let child;
|
|
13487
15017
|
try {
|
|
13488
15018
|
claimLifecycleEffect();
|
|
13489
|
-
child = (0,
|
|
15019
|
+
child = (0, import_node_child_process29.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
|
|
13490
15020
|
} catch (error) {
|
|
13491
15021
|
settled = true;
|
|
13492
15022
|
reject(
|
|
@@ -13543,12 +15073,12 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
13543
15073
|
}
|
|
13544
15074
|
|
|
13545
15075
|
// src/core/workspace-lifecycle.ts
|
|
13546
|
-
var
|
|
13547
|
-
var
|
|
13548
|
-
var
|
|
15076
|
+
var import_node_child_process30 = require("child_process");
|
|
15077
|
+
var import_node_fs35 = __toESM(require("fs"));
|
|
15078
|
+
var import_node_path40 = __toESM(require("path"));
|
|
13549
15079
|
|
|
13550
15080
|
// src/core/route-state.ts
|
|
13551
|
-
var
|
|
15081
|
+
var import_node_fs34 = __toESM(require("fs"));
|
|
13552
15082
|
function listRoutesForWorktreePath(worktreePath, routes = listHostRouteState()) {
|
|
13553
15083
|
return routes.filter((route) => sameWorkspacePath(route.repoPath, worktreePath));
|
|
13554
15084
|
}
|
|
@@ -13753,20 +15283,26 @@ process.once("message", async (message) => {
|
|
|
13753
15283
|
request.command ?? [],
|
|
13754
15284
|
request.retainedExecProof
|
|
13755
15285
|
) : await devpodExecOutcome(request.repoPath, request.command ?? []);
|
|
13756
|
-
recordLifecycleOutcome(outcome);
|
|
15286
|
+
recordLifecycleOutcome(outcome, { ok: true, value: outcome });
|
|
13757
15287
|
return outcome;
|
|
13758
15288
|
}
|
|
13759
15289
|
const ensured = await workspaceEnsure(request.repoPath, request.options);
|
|
13760
15290
|
recordLifecycleCompletion(
|
|
13761
|
-
ensured.applicationReadiness?.status === "application-error" ? 1 : 0
|
|
15291
|
+
ensured.applicationReadiness?.status === "application-error" || (ensured.hostPortConflicts?.length ?? 0) > 0 ? 1 : 0,
|
|
15292
|
+
ensured.managedRuntime?.status === "ready" ? ensured.profile : void 0,
|
|
15293
|
+
{ ok: true, value: ensured }
|
|
13762
15294
|
);
|
|
13763
15295
|
return ensured;
|
|
13764
15296
|
});
|
|
13765
15297
|
result = { ok: true, value };
|
|
13766
15298
|
} catch (error) {
|
|
13767
15299
|
try {
|
|
13768
|
-
|
|
13769
|
-
|
|
15300
|
+
const failure = {
|
|
15301
|
+
ok: false,
|
|
15302
|
+
message: error instanceof Error ? error.message : String(error)
|
|
15303
|
+
};
|
|
15304
|
+
if (error instanceof ExecutionOutcomeError) recordLifecycleOutcome(error.outcome, failure);
|
|
15305
|
+
else recordLifecycleUnknown(failure);
|
|
13770
15306
|
} catch {
|
|
13771
15307
|
}
|
|
13772
15308
|
result = { ok: false, message: error instanceof Error ? error.message : String(error) };
|