@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.
@@ -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 import_node_child_process28 = require("child_process");
27
- var import_node_crypto17 = require("crypto");
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 processBirthIdentity(pid) {
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 fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
150
- const startTime = fields[19];
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
- const startedAt = result.status === 0 ? result.stdout.trim().replace(/\s+/g, " ") : "";
160
- return startedAt ? `ps:${(0, import_node_crypto2.createHash)("sha256").update(startedAt).digest("hex")}` : void 0;
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 fields = value.split(":");
164
- const pid = Number(fields[0]);
184
+ const fields2 = value.split(":");
185
+ const pid = Number(fields2[0]);
165
186
  if (!Number.isInteger(pid) || pid <= 0) return void 0;
166
- if (fields.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(fields[2])) {
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(fields[1], "base64url").toString("utf-8");
191
+ const processBirth = Buffer.from(fields2[1], "base64url").toString("utf-8");
171
192
  const canonical = Buffer.from(processBirth).toString("base64url");
172
- return canonical === fields[1] && /^(proc|ps):/.test(processBirth) ? { pid, processBirth } : { pid };
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 = processBirthIdentity(process.pid);
286
- if (!processBirth) {
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-denied; run the canonical command in a permitted host context`
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(processBirth).toString("base64url")}:${ownerId}`;
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((id3) => !expectedSet.has(id3))) {
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((id3) => id3.trim()).filter(Boolean);
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, id3) {
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: ${id3}`;
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 id3 of ids2) {
1043
- const result = (0, import_node_child_process3.spawnSync)("docker", ["--host", endpoint, "inspect", "--format", "{{.Id}}", id3], {
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, id3)) {
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 import_node_child_process27 = require("child_process");
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 import_node_fs19 = __toESM(require("fs"));
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, featureDirectory, devsyRoot, containers } = options;
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(devsyRoot), import_node_path12.default.relative(devsyRoot, file))) {
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 bytes = Buffer.byteLength(uid);
3224
- return bytes === 16 || bytes === 40 ? uid : providerId;
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((id3) => inspectProviderRunnerContainers(baseline.endpoint, id3).length !== 0))
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 id3 = buildHostRouteId(input.repoPath, input.name);
3885
+ const id5 = buildHostRouteId(input.repoPath, input.name);
3862
3886
  return hostRouteFromInput(
3863
3887
  input,
3864
- routes.find((route) => route.id === id3),
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
- ["version", "devrouter", "project", "secretManager", "managedRuntime", "profiles", "apps"],
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.67" : "0.0.0-dev";
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 import_node_crypto7 = require("crypto");
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 import_node_crypto7.X509Certificate(pem);
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 false;
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 import_node_crypto9 = require("crypto");
5637
- var import_node_fs18 = __toESM(require("fs"));
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 import_node_crypto8 = require("crypto");
5642
- var import_node_fs17 = __toESM(require("fs"));
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 import_node_fs16 = __toESM(require("fs"));
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 = import_node_fs16.default.lstatSync(filePath);
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 = import_node_fs16.default.openSync(
6573
+ fileDescriptor = import_node_fs17.default.openSync(
6194
6574
  filePath,
6195
- import_node_fs16.default.constants.O_RDONLY | import_node_fs16.default.constants.O_NONBLOCK | (import_node_fs16.default.constants.O_NOFOLLOW ?? 0)
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 = import_node_fs16.default.fstatSync(fileDescriptor);
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 = import_node_fs16.default.readSync(fileDescriptor, buffer, offset, buffer.length - offset, null);
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 = import_node_fs16.default.fstatSync(fileDescriptor);
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
- import_node_fs16.default.closeSync(fileDescriptor);
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 = import_node_fs17.default.openSync(
7035
+ descriptor = import_node_fs18.default.openSync(
6656
7036
  store.filePath,
6657
- import_node_fs17.default.constants.O_RDONLY | import_node_fs17.default.constants.O_NONBLOCK | (import_node_fs17.default.constants.O_NOFOLLOW ?? 0)
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 = import_node_fs17.default.fstatSync(descriptor);
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 = import_node_fs17.default.readSync(descriptor, buffer, count, buffer.length - count, null);
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 = import_node_fs17.default.fstatSync(descriptor);
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
- import_node_fs17.default.closeSync(descriptor);
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
- import_node_fs17.default.mkdirSync(store.root, { recursive: true, mode: 448 });
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 = import_node_fs17.default.lstatSync(directory);
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, import_node_crypto8.createHash)("sha256").update(value).digest("hex");
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, import_node_crypto9.createHash)("sha256").update(repoPath).digest("hex");
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 = import_node_fs18.default.readdirSync(root);
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 = import_node_fs18.default.lstatSync(full);
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 = import_node_fs18.default.openSync(
7258
+ const descriptor = import_node_fs19.default.openSync(
6879
7259
  full,
6880
- import_node_fs18.default.constants.O_RDONLY | import_node_fs18.default.constants.O_NONBLOCK | import_node_fs18.default.constants.O_NOFOLLOW
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 (!import_node_fs18.default.fstatSync(descriptor).isFile()) throw new Error();
6884
- const bytes = Buffer.alloc(NETWORK_CLAIMS_MAX_FILE_BYTES + 1);
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 < bytes.length) {
6887
- const count = import_node_fs18.default.readSync(descriptor, bytes, size, bytes.length - size, null);
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(bytes.subarray(0, size).toString("utf8"));
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
- import_node_fs18.default.closeSync(descriptor);
7277
+ import_node_fs19.default.closeSync(descriptor);
6898
7278
  }
6899
- if (typeof value.daemonId !== "string" || `${(0, import_node_crypto9.createHash)("sha256").update(value.daemonId).digest("hex")}.json` !== file)
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 import_node_crypto10 = require("crypto");
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, import_node_crypto10.createHash)("sha256").update(JSON.stringify({ name: definition.name, agent, options, exec })).digest("hex");
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 text3 = object(value).value;
6999
- if (typeof text3 !== "string") throw new Error();
7000
- return text3 || null;
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
- import_node_fs19.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
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
- import_node_fs19.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
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 id3 = devsyId ?? existing?.id;
7201
- if (id3) {
7202
- const before = inspectDevsyWorkspaceOwnership(workspaces, id3, repoPath);
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 id3;
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 import_node_path33 = __toESM(require("path"));
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(state, "accepted");
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.intentRevision > 0)
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.executionPolicy === "manual" && result.state.operation) {
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 import_node_crypto11 = require("crypto");
8085
- var import_node_fs20 = __toESM(require("fs"));
8086
- var import_node_path25 = __toESM(require("path"));
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.67" : "0.0.0-dev";
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 (!import_node_path25.default.isAbsolute(identity2.repoPath) || import_node_path25.default.resolve(identity2.repoPath) !== identity2.repoPath || identity2.repoPath.includes("\0") || identity2.workspace !== null && !isReliabilityId(identity2.workspace) || !["devpod", "devsy"].includes(identity2.provider)) {
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, import_node_crypto11.createHash)("sha256").update(identity2.repoPath).digest("hex");
9059
+ return (0, import_node_crypto14.createHash)("sha256").update(identity2.repoPath).digest("hex");
8106
9060
  }
8107
9061
  function reliabilityOperationPath(identity2) {
8108
- return import_node_path25.default.join(DEVROUTER_HOME, "reliability", `${identityKey(identity2)}.json`);
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 validate(record5, identity2) {
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
- "writtenByVersion"
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) || state.executionPolicy !== "manual") {
8138
- throw new Error("Reliability record does not grant manual authority for this workspace.");
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 = import_node_fs20.default.openSync(
9375
+ descriptor = import_node_fs25.default.openSync(
8202
9376
  file,
8203
- import_node_fs20.default.constants.O_RDONLY | import_node_fs20.default.constants.O_NOFOLLOW | import_node_fs20.default.constants.O_NONBLOCK
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 = import_node_fs20.default.fstatSync(descriptor);
8211
- if (!stat.isFile() || stat.size > MAX_RECORD_BYTES || (stat.mode & 63) !== 0) {
8212
- throw new Error("Reliability record is not a bounded private file.");
8213
- }
8214
- const bytes = Buffer.alloc(MAX_RECORD_BYTES + 1);
8215
- const count = import_node_fs20.default.readSync(descriptor, bytes, 0, bytes.length, 0);
8216
- if (count > MAX_RECORD_BYTES) throw new Error("Reliability record exceeds its byte limit.");
8217
- const record5 = JSON.parse(
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
- import_node_fs20.default.closeSync(descriptor);
9393
+ import_node_fs25.default.closeSync(descriptor);
8224
9394
  }
8225
9395
  }
8226
9396
  function persist(record5) {
8227
- validate(record5, record5.identity);
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 = import_node_fs20.default.openSync(file, import_node_fs20.default.constants.O_RDONLY | import_node_fs20.default.constants.O_NOFOLLOW);
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
- import_node_fs20.default.fsyncSync(descriptor);
9410
+ import_node_fs25.default.fsyncSync(descriptor);
8241
9411
  } finally {
8242
- import_node_fs20.default.closeSync(descriptor);
9412
+ import_node_fs25.default.closeSync(descriptor);
8243
9413
  }
8244
- const directory = import_node_fs20.default.openSync(import_node_path25.default.dirname(file), "r");
9414
+ const directory = import_node_fs25.default.openSync(import_node_path30.default.dirname(file), "r");
8245
9415
  try {
8246
- import_node_fs20.default.fsyncSync(directory);
9416
+ import_node_fs25.default.fsyncSync(directory);
8247
9417
  } finally {
8248
- import_node_fs20.default.closeSync(directory);
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
- import_node_fs20.default.mkdirSync(import_node_path25.default.dirname(file), { recursive: true, mode: 448 });
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
- validate(record5, identity2);
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 import_node_crypto12 = require("crypto");
8282
- var import_node_fs21 = __toESM(require("fs"));
8283
- var import_node_path26 = __toESM(require("path"));
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 import_node_fs22 = __toESM(require("fs"));
8296
- var import_node_path27 = __toESM(require("path"));
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 = import_node_path27.default.join(DEVROUTER_HOME, "router-reload.lock");
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
- import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
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 import_node_child_process26 = require("child_process");
8619
- var import_node_fs28 = __toESM(require("fs"));
8620
- var import_node_path32 = __toESM(require("path"));
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 import_node_fs23 = __toESM(require("fs"));
8625
- var import_node_path28 = __toESM(require("path"));
8626
- var DEVPOD_MUTATION_LOCK_FILE = import_node_path28.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
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
- import_node_fs23.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
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/http-route-probe.ts
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, import_node_child_process19.spawnSync)("curl", args, { encoding: "utf-8" });
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, import_node_child_process19.spawnSync)("curl", args, {
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 import_node_child_process20 = require("child_process");
8970
- var import_node_fs24 = __toESM(require("fs"));
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, import_node_child_process20.spawn)(argv[0], argv.slice(1), {
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 import_node_fs24.default.readFileSync(configPath);
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 import_node_child_process21 = require("child_process");
9048
- var import_node_crypto13 = require("crypto");
9049
- var import_node_fs25 = __toESM(require("fs"));
9050
- var import_node_path29 = __toESM(require("path"));
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 = import_node_path29.default.posix.basename(targetPath);
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, import_node_child_process21.spawnSync)(
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 (!import_node_fs25.default.existsSync(filePath) || !import_node_fs25.default.lstatSync(filePath).isFile()) return void 0;
9099
- return import_node_fs25.default.readFileSync(filePath);
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, import_node_crypto13.createHash)("sha256").update(adapter).digest("hex");
10551
+ return (0, import_node_crypto16.createHash)("sha256").update(adapter).digest("hex");
9106
10552
  }
9107
10553
  function resolveProcessHelperPath() {
9108
10554
  const candidates = [
9109
- import_node_path29.default.resolve(__dirname, "..", "bin", "devrouter-process"),
9110
- import_node_path29.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
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) => import_node_fs25.default.existsSync(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 = import_node_path29.default.join(repoPath, MANAGED_ADAPTER_PATH);
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 = import_node_path29.default.join(repoPath, ".devcontainer", "Dockerfile");
9123
- const devcontainerPath = import_node_path29.default.join(repoPath, ".devcontainer", "devcontainer.json");
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 = import_node_fs25.default.readFileSync(resolveProcessHelperPath());
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, import_node_child_process21.spawnSync)(
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, import_node_child_process21.spawnSync)(
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 import_node_child_process22 = require("child_process");
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, import_node_child_process22.spawnSync)("docker", ["--host", endpoint, ...args], {
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 text2(value) {
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 id2(value) {
9720
- const result = text2(value);
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(id2) : [];
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 = text2(info.id);
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 = text2(pool.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: id2(network.id),
9800
- name: text2(network.name),
9801
- driver: text2(network.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) => text2(record4(entry).Subnet)
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 = id2(container.id);
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 = id2(reference.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 = text2(JSON.parse(read2(["info", "--format", "{{json .ID}}"])));
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 import_node_child_process23 = require("child_process");
9854
- var import_node_net2 = require("net");
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 import_node_fs26 = __toESM(require("fs"));
9858
- var import_node_path30 = __toESM(require("path"));
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 = import_node_fs26.default.readFileSync(import_node_path30.default.join(procRoot, "net/unix"), "utf8");
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 = import_node_fs26.default.readlinkSync(import_node_path30.default.join(procRoot, "self/ns/net"));
9870
- const pids = import_node_fs26.default.readdirSync(procRoot).filter((name) => /^\d+$/.test(name));
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 = import_node_path30.default.join(procRoot, pid);
9876
- if (import_node_path30.default.basename(import_node_fs26.default.readlinkSync(import_node_path30.default.join(root, "exe"))) !== "dockerd") continue;
9877
- if (import_node_fs26.default.readlinkSync(import_node_path30.default.join(root, "ns/net")) !== ownNamespace) continue;
9878
- const descriptors = import_node_fs26.default.readdirSync(import_node_path30.default.join(root, "fd"));
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 import_node_fs26.default.readlinkSync(import_node_path30.default.join(root, "fd", fd)) === inode;
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, import_node_net2.isIP)(value.dst.split("/")[0]) === 6) continue;
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, import_node_child_process23.spawnSync)("ip", ["-j", "-4", "route", "show", "table", "all"], {
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 import_node_crypto16 = require("crypto");
11404
+ var import_node_crypto19 = require("crypto");
9959
11405
 
9960
11406
  // src/core/network-compose.ts
9961
- var import_node_crypto14 = require("crypto");
11407
+ var import_node_crypto17 = require("crypto");
9962
11408
  var SAFE_ENDPOINT_UPPER_BOUND_SEMANTICS = "full-lifecycle-demand";
9963
- function isRecord4(value) {
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 isRecord4(value) ? value : void 0;
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 (!isRecord4(input.metadata)) {
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 (isRecord4(value)) {
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 = isRecord4(input.compose) ? input.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 (!isRecord4(rawService)) {
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 (!isRecord4(value)) {
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 && !isRecord4(attachment)) {
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 isRecord4(declaration) && hasOwn(declaration, key);
11574
+ return isRecord5(declaration) && hasOwn(declaration, key);
10129
11575
  }
10130
11576
  function authoredValue(authored, name, key) {
10131
11577
  const declaration = authored[name];
10132
- return isRecord4(declaration) ? declaration[key] : void 0;
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 isRecord4(declaration) ? declaration[key] : void 0;
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 && !isRecord4(authoredDeclaration)) {
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 && !isRecord4(normalizedDeclaration)) {
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 (!isRecord4(normalizedIpam)) {
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 = isRecord4(value) && hasOwn(value, "count") ? value.count : value;
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 (!isRecord4(service.deploy)) {
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 (isRecord4(raw)) {
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 = isRecord4(input.compose) ? input.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 (!isRecord4(rawService)) {
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 (!isRecord4(nativeConfig)) {
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 (!isRecord4(value)) {
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) => isRecord4(entry) && typeof entry.subnet === "string" ? entry.subnet : "unknown"
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 = isRecord4(value) && hasOwn(value, "count") ? value.count : value;
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 (!isRecord4(value)) {
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 = isRecord4(input.compose) ? input.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] = isRecord4(service) ? {
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 (!isRecord4(value)) {
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) => !isRecord4(entry) || typeof entry.subnet !== "string")) {
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 (isRecord4(value)) {
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, import_node_crypto14.createHash)("sha256").update(stableValue(projection), "utf8").digest("hex");
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 import_node_child_process24 = require("child_process");
10772
- var import_node_crypto15 = require("crypto");
10773
- var import_node_fs27 = __toESM(require("fs"));
10774
- var import_node_path31 = __toESM(require("path"));
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 MAX_BYTES = 2 * 1024 * 1024;
10779
- function object2(value) {
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 = import_node_fs27.default.lstatSync(file);
10788
- if (!stat.isFile() || stat.size > MAX_BYTES)
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 = object2(import_yaml4.default.parse(import_node_fs27.default.readFileSync(file, "utf8")));
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(object2(source.networks))) {
12243
+ for (const [name, declaration] of Object.entries(object3(source.networks))) {
10798
12244
  authoredNetworks[name] = {
10799
12245
  ...authoredNetworks[name],
10800
- ...declaration === null ? {} : object2(declaration)
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, import_node_child_process24.spawnSync)(
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: MAX_BYTES
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: object2(JSON.parse(result.stdout)), authoredNetworks };
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 = import_node_path31.default.join(plan.composeDirectory, OVERLAY);
10843
- const ignored = (0, import_node_child_process24.spawnSync)("git", [
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 bytes = `${MARKER}
12300
+ const bytes2 = `${MARKER}
10855
12301
  ${import_yaml4.default.stringify(overlay.overlay)}`;
10856
12302
  const assertOwnership = () => {
10857
- if (!import_node_fs27.default.existsSync(overlayPath)) return;
10858
- const stat = import_node_fs27.default.lstatSync(overlayPath);
10859
- if (!stat.isFile() || stat.size > MAX_BYTES || import_node_fs27.default.readFileSync(overlayPath, "utf8") !== bytes)
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, import_node_crypto15.createHash)("sha256").update(contents).digest("hex")
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, bytes);
12325
+ writeFileAtomically(overlayPath, bytes2);
10880
12326
  }
10881
12327
  };
10882
12328
  }
10883
12329
 
10884
12330
  // src/core/network-connected-route.ts
10885
- var import_node_child_process25 = require("child_process");
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, import_node_child_process25.spawnSync)(
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, import_node_child_process25.spawnSync)("ip", ["-j", "-d", "-4", "address", "show", "dev", bridge], {
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, import_node_crypto16.createHash)("sha256").update(input.plan().sourceConfigSha256).update(fingerprintComposeNetworkModel(model)).digest("hex");
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, import_node_path32.default.join(repoPath, ".devcontainer"))) {
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 = import_node_path32.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
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, import_node_path32.default.join(repoPath, ".devcontainer"))
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, import_node_child_process26.spawnSync)(
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, import_node_child_process26.spawnSync)(
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, import_node_child_process26.spawnSync)(
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 import_node_fs28.default.statSync(import_node_path32.default.join(repoPath, ".git")).isDirectory();
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, import_node_child_process26.spawnSync)("open", [url], { encoding: "utf-8" });
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 = import_node_path32.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
12022
- if (!import_node_fs28.default.existsSync(overlayPath)) {
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 ?? import_node_path32.default.basename(repoPath)) ?? "app";
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, import_node_child_process26.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
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
- import_node_fs28.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
14387
+ import_node_fs33.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
12912
14388
  return withFileLock(
12913
- import_node_path32.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
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
- import_node_path33.default.join(request.repoPath, ".devcontainer")
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
- import_node_path33.default.join(request.repoPath, ".devcontainer")
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, import_node_child_process27.spawn)("devsy", args, { stdio: "inherit" });
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, import_node_child_process28.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
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, import_node_crypto17.randomUUID)()}__:`;
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, import_node_child_process28.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
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 import_node_child_process29 = require("child_process");
13547
- var import_node_fs30 = __toESM(require("fs"));
13548
- var import_node_path34 = __toESM(require("path"));
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 import_node_fs29 = __toESM(require("fs"));
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
- if (error instanceof ExecutionOutcomeError) recordLifecycleOutcome(error.outcome);
13769
- else recordLifecycleUnknown();
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) };