@devrouter/cli 0.5.5 → 0.5.6

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_process32 = require("child_process");
27
- var import_node_crypto23 = require("crypto");
26
+ var import_node_child_process33 = require("child_process");
27
+ var import_node_crypto24 = require("crypto");
28
28
 
29
29
  // src/core/devpod-environment.ts
30
30
  var import_node_child_process3 = require("child_process");
@@ -1982,7 +1982,7 @@ function selectDevpodWorkspace(workspaces, repoPath) {
1982
1982
  }
1983
1983
 
1984
1984
  // src/core/devsy-exec.ts
1985
- var import_node_child_process31 = require("child_process");
1985
+ var import_node_child_process32 = require("child_process");
1986
1986
 
1987
1987
  // src/core/devsy-exec-proof.ts
1988
1988
  var import_node_fs7 = __toESM(require("fs"));
@@ -3390,6 +3390,12 @@ function validateState(value, repoPath, workspace) {
3390
3390
  if (typeof candidate.profile !== "string" || candidate.profile.length === 0) {
3391
3391
  throw new Error("Managed runtime state has no canonical profile.");
3392
3392
  }
3393
+ if (candidate.variant !== void 0 && (typeof candidate.variant !== "string" || candidate.variant.length > 64 || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(candidate.variant))) {
3394
+ throw new Error("Managed runtime state has an invalid runtime variant.");
3395
+ }
3396
+ if (candidate.hostPortDeclarationsSha256 !== void 0 && (typeof candidate.hostPortDeclarationsSha256 !== "string" || !/^[a-f0-9]{64}$/.test(candidate.hostPortDeclarationsSha256))) {
3397
+ throw new Error("Managed runtime state has an invalid host-port declaration fingerprint.");
3398
+ }
3393
3399
  if (!candidate.desired || typeof candidate.desired !== "object") {
3394
3400
  throw new Error("Managed runtime state has no desired resource set.");
3395
3401
  }
@@ -3416,6 +3422,8 @@ function validateState(value, repoPath, workspace) {
3416
3422
  devpodId: candidate.devpodId,
3417
3423
  composeProject: candidate.composeProject,
3418
3424
  profile: candidate.profile,
3425
+ ...candidate.variant !== void 0 ? { variant: candidate.variant } : {},
3426
+ ...candidate.hostPortDeclarationsSha256 !== void 0 ? { hostPortDeclarationsSha256: candidate.hostPortDeclarationsSha256 } : {},
3419
3427
  desired: {
3420
3428
  apps: [...desired.apps],
3421
3429
  services: [...desired.services],
@@ -4055,6 +4063,89 @@ function captureManagedStopBaseline(state, plan, primaryId, expectedEndpoint) {
4055
4063
  prove(state, baseline);
4056
4064
  return baseline;
4057
4065
  }
4066
+ function recoverManagedStopBaseline(state, readPlan, beforePersist) {
4067
+ const baseline = validateManagedStopBaseline(state.stopBaseline, state);
4068
+ const plan = readPlan();
4069
+ const owner = registration(state);
4070
+ if (Object.entries(owner).some(
4071
+ ([key, value]) => baseline[key] !== value
4072
+ ) || baseline.sourceContainer)
4073
+ throw new Error("Stop recovery requires the unchanged provider registration.");
4074
+ const observe = () => {
4075
+ if (!(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state) || !(0, import_node_util3.isDeepStrictEqual)(registration(state), owner) || !(0, import_node_util3.isDeepStrictEqual)(readPlan(), plan) || resolveManagedStopEndpoint() !== baseline.endpoint || inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId || listDevpodWorkspacesRaw({ allowMissingExecutable: true }).some(
4076
+ (entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
4077
+ ))
4078
+ throw new Error("Stop recovery authority changed.");
4079
+ const currentOwner = inspectDevsyWorkspaceOwnership(
4080
+ listDevsyWorkspaces(),
4081
+ state.devpodId,
4082
+ state.repoPath
4083
+ );
4084
+ if (currentOwner.status !== "owned") throw new Error("Stop recovery owner is unavailable.");
4085
+ proveLocalDockerSelection(currentOwner.workspace);
4086
+ assertManagedStopContainersAbsent(
4087
+ baseline.endpoint,
4088
+ baseline.containers.map((container) => container.id)
4089
+ );
4090
+ const containers = inspectManagedStopContainers(baseline.project, baseline.endpoint);
4091
+ if (!(0, import_node_util3.isDeepStrictEqual)(
4092
+ containers.map((container) => container.labels["com.docker.compose.service"]).sort(),
4093
+ [...plan.desiredServices].sort()
4094
+ ))
4095
+ throw new Error("Stop recovery requires the complete selected service population.");
4096
+ const primary2 = containers.find(
4097
+ (container) => container.labels["com.docker.compose.service"] === plan.primaryService
4098
+ );
4099
+ if (!primary2 || !(0, import_node_util3.isDeepStrictEqual)(
4100
+ inspectProviderRunnerContainers(
4101
+ baseline.endpoint,
4102
+ managedRunnerId(baseline.uid, baseline.providerId)
4103
+ ).sort(),
4104
+ [primary2.id]
4105
+ ))
4106
+ throw new Error("Stop recovery provider runner population changed.");
4107
+ assertManagedContainerConfigUnchanged({
4108
+ plan,
4109
+ containers,
4110
+ workspace: state.workspace ? {
4111
+ token: state.workspace,
4112
+ gitCommonDir: resolveGitCommonDir(state.repoPath)
4113
+ } : void 0
4114
+ });
4115
+ if (!(0, import_node_util3.isDeepStrictEqual)(registration(state), owner) || !(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state) || !(0, import_node_util3.isDeepStrictEqual)(readPlan(), plan) || resolveManagedStopEndpoint() !== baseline.endpoint || inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
4116
+ throw new Error("Stop recovery authority changed during configuration inspection.");
4117
+ return containers;
4118
+ };
4119
+ const first = observe();
4120
+ const candidate = {
4121
+ ...state,
4122
+ sourceConfigSha256: plan.sourceConfigSha256,
4123
+ effectiveConfigSha256: plan.effectiveConfigSha256,
4124
+ status: "degraded",
4125
+ transitionPhase: "stop-recovery",
4126
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4127
+ stopBaseline: void 0
4128
+ };
4129
+ const primary = first.find(
4130
+ (container) => container.labels["com.docker.compose.service"] === plan.primaryService
4131
+ );
4132
+ if (!primary) throw new Error("Stop recovery primary is unavailable.");
4133
+ candidate.stopBaseline = captureManagedStopBaseline(
4134
+ candidate,
4135
+ plan,
4136
+ primary.id,
4137
+ baseline.endpoint
4138
+ );
4139
+ if (!(0, import_node_util3.isDeepStrictEqual)(
4140
+ first.map(identity).sort((a, b) => compareText(a.id, b.id)),
4141
+ observe().map(identity).sort((a, b) => compareText(a.id, b.id))
4142
+ ))
4143
+ throw new Error("Stop recovery population changed during inspection.");
4144
+ beforePersist();
4145
+ claimLifecycleEffect();
4146
+ writeManagedRuntimeState(candidate);
4147
+ return candidate;
4148
+ }
4058
4149
  function proveRetainedManagedStop(state) {
4059
4150
  const baseline = validateManagedStopBaseline(state.stopBaseline, state);
4060
4151
  if (!(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state)) {
@@ -5399,7 +5490,7 @@ function retain(session, previous) {
5399
5490
 
5400
5491
  // src/core/reliability-operation-store.ts
5401
5492
  function installedCliVersion() {
5402
- return true ? "0.5.5" : "0.0.0-dev";
5493
+ return true ? "0.5.6" : "0.0.0-dev";
5403
5494
  }
5404
5495
  function compareCliVersions(left, right) {
5405
5496
  const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -6158,6 +6249,9 @@ var MAX_CAPACITY_PROFILES = 256;
6158
6249
  var MAX_CAPACITY_OPERATIONS = 64;
6159
6250
  var MAX_CAPACITY_TRANSITIONS = 1024;
6160
6251
  var MAX_CAPACITY_NAME_LENGTH = 64;
6252
+ var MAX_RUNTIME_VARIANTS = 32;
6253
+ var MAX_RUNTIME_VARIANT_NAME_LENGTH = 64;
6254
+ var RUNTIME_VARIANT_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
6161
6255
  var UPSTREAM_TEMPLATE_RE = /^(?:\$\{WORKSPACE\}|[a-zA-Z0-9._-])+:\d{1,5}$/;
6162
6256
  function assertUpstreamSpec(upstream, label) {
6163
6257
  if (upstream.includes(WORKSPACE_PLACEHOLDER)) {
@@ -6634,6 +6728,53 @@ function parsePrepareCommand(value, pathLabel) {
6634
6728
  return item;
6635
6729
  });
6636
6730
  }
6731
+ function assertRuntimeVariantName(value, pathLabel) {
6732
+ if (value.length === 0 || value.length > MAX_RUNTIME_VARIANT_NAME_LENGTH || !RUNTIME_VARIANT_NAME_RE.test(value)) {
6733
+ throw new Error(
6734
+ `${pathLabel} must use lowercase letters or digits separated by single hyphens, at most ${MAX_RUNTIME_VARIANT_NAME_LENGTH} characters.`
6735
+ );
6736
+ }
6737
+ if (value === "default") {
6738
+ throw new Error(`${pathLabel} 'default' is implicit and reserved.`);
6739
+ }
6740
+ }
6741
+ function parseRuntimeVariants(value, pathLabel) {
6742
+ if (value === void 0) {
6743
+ return void 0;
6744
+ }
6745
+ if (!Array.isArray(value)) {
6746
+ throw new Error(`${pathLabel} must be an array of strings.`);
6747
+ }
6748
+ if (value.length > MAX_RUNTIME_VARIANTS) {
6749
+ throw new Error(`${pathLabel} accepts at most ${MAX_RUNTIME_VARIANTS} additional names.`);
6750
+ }
6751
+ const seen = /* @__PURE__ */ new Set();
6752
+ return value.map((item, index) => {
6753
+ const itemPath = `${pathLabel}[${index}]`;
6754
+ if (typeof item !== "string") {
6755
+ throw new Error(`${itemPath} must be a string.`);
6756
+ }
6757
+ assertRuntimeVariantName(item, itemPath);
6758
+ if (seen.has(item)) {
6759
+ throw new Error(`${pathLabel} contains duplicate '${item}'.`);
6760
+ }
6761
+ seen.add(item);
6762
+ return item;
6763
+ });
6764
+ }
6765
+ function resolveRuntimeVariant(config, requested) {
6766
+ if (requested === void 0) {
6767
+ return "default";
6768
+ }
6769
+ if (requested === "default") {
6770
+ return requested;
6771
+ }
6772
+ assertRuntimeVariantName(requested, "Runtime variant");
6773
+ if (!config.managedRuntime?.variants?.includes(requested)) {
6774
+ throw new Error(`Runtime variant '${requested}' is not declared in managedRuntime.variants.`);
6775
+ }
6776
+ return requested;
6777
+ }
6637
6778
  function parseManagedRuntime(value, configPath) {
6638
6779
  if (value === void 0) {
6639
6780
  return void 0;
@@ -6641,9 +6782,13 @@ function parseManagedRuntime(value, configPath) {
6641
6782
  const managedRuntime = ensureObject(value, `${configPath}.managedRuntime`);
6642
6783
  ensureAllowedKeys(
6643
6784
  managedRuntime,
6644
- ["devcontainer", "processes", "network"],
6785
+ ["devcontainer", "processes", "network", "variants"],
6645
6786
  `${configPath}.managedRuntime`
6646
6787
  );
6788
+ const variants = parseRuntimeVariants(
6789
+ managedRuntime.variants,
6790
+ `${configPath}.managedRuntime.variants`
6791
+ );
6647
6792
  let network;
6648
6793
  if (managedRuntime.network !== void 0) {
6649
6794
  const value2 = ensureObject(managedRuntime.network, `${configPath}.managedRuntime.network`);
@@ -6669,7 +6814,7 @@ function parseManagedRuntime(value, configPath) {
6669
6814
  );
6670
6815
  ensureAllowedKeys(
6671
6816
  devcontainer,
6672
- ["baseServices", "profileServices", "prepareCommand"],
6817
+ ["baseServices", "profileServices", "prepareCommand", "hostPorts"],
6673
6818
  `${configPath}.managedRuntime.devcontainer`
6674
6819
  );
6675
6820
  const baseServices = parseRequiredUniqueStringArray(
@@ -6691,6 +6836,41 @@ function parseManagedRuntime(value, configPath) {
6691
6836
  `${configPath}.managedRuntime.devcontainer.baseServices and profileServices overlap: ${overlappingServices.join(", ")}.`
6692
6837
  );
6693
6838
  }
6839
+ let hostPorts;
6840
+ if (devcontainer.hostPorts !== void 0) {
6841
+ const ports = ensureObject(
6842
+ devcontainer.hostPorts,
6843
+ `${configPath}.managedRuntime.devcontainer.hostPorts`
6844
+ );
6845
+ if (Object.keys(ports).length > 32)
6846
+ throw new Error("managedRuntime.devcontainer.hostPorts accepts at most 32 declarations.");
6847
+ hostPorts = {};
6848
+ const pairs = /* @__PURE__ */ new Set();
6849
+ const registered = /* @__PURE__ */ new Set([...baseServices, ...profileServices]);
6850
+ for (const [name, rawPort] of Object.entries(ports)) {
6851
+ if (name.length > 64 || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name))
6852
+ throw new Error(
6853
+ "Host-port names must be lowercase kebab-case identifiers of at most 64 characters."
6854
+ );
6855
+ const port = ensureObject(rawPort, `managedRuntime.devcontainer.hostPorts.${name}`);
6856
+ ensureAllowedKeys(
6857
+ port,
6858
+ ["service", "target"],
6859
+ `managedRuntime.devcontainer.hostPorts.${name}`
6860
+ );
6861
+ if (typeof port.service !== "string" || !registered.has(port.service))
6862
+ throw new Error(`Host port '${name}' must reference a registered base or profile service.`);
6863
+ if (!Number.isInteger(port.target) || port.target < 1 || port.target > 65535)
6864
+ throw new Error(`Host port '${name}' target must be an integer from 1 to 65535.`);
6865
+ const pair = `${port.service}:${port.target}`;
6866
+ if (pairs.has(pair)) throw new Error(`Host port '${name}' duplicates a service and target.`);
6867
+ pairs.add(pair);
6868
+ Object.defineProperty(hostPorts, name, {
6869
+ value: { service: port.service, target: port.target },
6870
+ enumerable: true
6871
+ });
6872
+ }
6873
+ }
6694
6874
  const processes = parseRequiredUniqueStringArray(
6695
6875
  managedRuntime.processes,
6696
6876
  `${configPath}.managedRuntime.processes`
@@ -6705,10 +6885,12 @@ function parseManagedRuntime(value, configPath) {
6705
6885
  }
6706
6886
  return {
6707
6887
  ...network ? { network } : {},
6888
+ ...variants !== void 0 ? { variants } : {},
6708
6889
  devcontainer: {
6709
6890
  baseServices,
6710
6891
  profileServices,
6711
- ...prepareCommand ? { prepareCommand } : {}
6892
+ ...prepareCommand ? { prepareCommand } : {},
6893
+ ...hostPorts !== void 0 ? { hostPorts } : {}
6712
6894
  },
6713
6895
  processes
6714
6896
  };
@@ -7409,7 +7591,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs19.default.rea
7409
7591
  const config = parseConfig(parsed ?? {}, configPath);
7410
7592
  const requiredVersion = config.devrouter?.version;
7411
7593
  if (requiredVersion && !hasWarnedVersionMismatch) {
7412
- const cliVersion = true ? "0.5.5" : "0.0.0-dev";
7594
+ const cliVersion = true ? "0.5.6" : "0.0.0-dev";
7413
7595
  if (cliVersion !== "0.0.0-dev" && compareSemver2(requiredVersion, cliVersion) > 0) {
7414
7596
  hasWarnedVersionMismatch = true;
7415
7597
  process.stderr.write(
@@ -10466,151 +10648,1171 @@ var ExecutionOutcomeError = class extends Error {
10466
10648
  };
10467
10649
 
10468
10650
  // src/core/reliability-lifecycle.ts
10469
- var import_node_crypto22 = require("crypto");
10651
+ var import_node_crypto23 = require("crypto");
10470
10652
  var import_node_path41 = __toESM(require("path"));
10471
10653
  var import_node_util7 = require("util");
10472
10654
 
10473
10655
  // src/core/capacity-policy.ts
10474
10656
  var import_node_fs27 = __toESM(require("fs"));
10475
10657
  var import_node_path31 = __toESM(require("path"));
10476
-
10477
- // src/core/controller-client.ts
10478
- var import_node_crypto15 = require("crypto");
10479
- var import_node_fs28 = __toESM(require("fs"));
10480
- var import_node_net2 = __toESM(require("net"));
10481
- var import_node_path33 = __toESM(require("path"));
10482
-
10483
- // src/core/controller-protocol.ts
10484
- var import_node_path32 = __toESM(require("path"));
10485
- var CONTROLLER_FRAME_BYTES = 64 * 1024;
10486
- var CONTROLLER_MAX_OPERATION_REQUEST_BYTES = 32 * 1024;
10487
- var CONTROLLER_PROFILE_TOKEN_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
10488
- var CONTROLLER_PROFILE_RE = new RegExp(
10489
- `^(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)})(?:,(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)}))*$`
10490
- );
10491
-
10492
- // src/core/controller-monitor.ts
10493
- var import_node_crypto16 = require("crypto");
10494
- var import_node_util6 = require("util");
10495
-
10496
- // src/core/reliability-output.ts
10497
- function reliabilityAttention(state) {
10498
- assertReliabilityState(state);
10499
- const completeStop = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
10500
- if (state.desired === "stopped-by-user") return completeStop ? void 0 : "stop-incomplete";
10501
- if (state.operation?.status === "COMPLETION_UNKNOWN" || state.operation?.status === "INTERRUPTED")
10502
- return "operation-unknown";
10503
- if (state.desired === "parked-for-capacity")
10504
- return completeStop ? "capacity-parked" : "stop-incomplete";
10505
- if (state.admission === "denied-unadmittable") return "unadmittable";
10506
- if (state.admission === "waiting") return "capacity-waiting";
10507
- if (state.phase === "recovering") return "recovering";
10508
- if (state.phase === "queued" || state.phase === "starting" || state.phase === "verifying")
10509
- return "starting";
10510
- return void 0;
10511
- }
10512
-
10513
- // src/core/recovery-budget.ts
10514
- function findRecoveryUnit(incident, unit) {
10515
- return (incident.units ?? []).find((record5) => record5.key === unit.key);
10516
- }
10517
- function restartsFor(kind, limits) {
10518
- return kind === "service" ? limits.maxServiceRestarts : limits.maxProcessRestarts;
10519
- }
10520
- function recoveryBudgetRefusal(input) {
10521
- const startedAtMs = input.incident.startedAtMs ?? input.nowMs;
10522
- const elapsed = input.activeElapsedMs ?? Math.max(0, input.nowMs - startedAtMs);
10523
- if (elapsed > input.limits.windowSeconds * 1e3) return "window-closed";
10524
- if (input.unit !== void 0) {
10525
- const record5 = findRecoveryUnit(input.incident, input.unit);
10526
- if (record5 !== void 0 && record5.actions >= restartsFor(input.unit.kind, input.limits))
10527
- return "unit-exhausted";
10658
+ var MAX_DOMAINS = 256;
10659
+ var MAX_ENROLLMENTS = 256;
10660
+ var MAX_PROFILES_PER_ENROLLMENT = 64;
10661
+ var MAX_STRING_LENGTH = 256;
10662
+ var MAX_PATH_LENGTH = 4096;
10663
+ function readCapacityPolicy(directory) {
10664
+ const absolute2 = import_node_path31.default.resolve(directory);
10665
+ let current = import_node_path31.default.parse(absolute2).root;
10666
+ for (const part of absolute2.slice(current.length).split(import_node_path31.default.sep)) {
10667
+ current = import_node_path31.default.join(current, part);
10668
+ let stat;
10669
+ try {
10670
+ stat = import_node_fs27.default.lstatSync(current);
10671
+ } catch (error) {
10672
+ if (error.code === "ENOENT") return void 0;
10673
+ throw error;
10674
+ }
10675
+ if (!stat.isDirectory() || stat.isSymbolicLink())
10676
+ throw new Error("Unsafe capacity policy directory.");
10677
+ if (current === absolute2 && (stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0))
10678
+ throw new Error("Capacity policy directory is not private.");
10528
10679
  }
10529
- if (input.incident.correctiveActionsTaken >= input.incident.actionLimit)
10530
- return "budget-exhausted";
10531
- return void 0;
10680
+ let descriptor;
10681
+ try {
10682
+ descriptor = import_node_fs27.default.openSync(
10683
+ import_node_path31.default.join(absolute2, "capacity-policy.json"),
10684
+ import_node_fs27.default.constants.O_RDONLY | import_node_fs27.default.constants.O_NOFOLLOW | import_node_fs27.default.constants.O_NONBLOCK
10685
+ );
10686
+ } catch (error) {
10687
+ if (error.code === "ENOENT") return void 0;
10688
+ throw error;
10689
+ }
10690
+ try {
10691
+ const stat = import_node_fs27.default.fstatSync(descriptor);
10692
+ const limit = 1048576;
10693
+ if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0 || stat.size > limit)
10694
+ throw new Error("Capacity policy is not a bounded private file.");
10695
+ const bytes2 = Buffer.alloc(limit + 1);
10696
+ const count = import_node_fs27.default.readSync(descriptor, bytes2, 0, bytes2.length, 0);
10697
+ if (count > limit) throw new Error("Capacity policy exceeds byte limit.");
10698
+ return parseCapacityPolicy(JSON.parse(bytes2.subarray(0, count).toString("utf8")));
10699
+ } finally {
10700
+ import_node_fs27.default.closeSync(descriptor);
10701
+ }
10702
+ }
10703
+ var DOMAIN_ID_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
10704
+ var PROFILE_NAME_RE2 = /^[a-z0-9]+(-[a-z0-9]+)*$/;
10705
+ var WORKSPACE_TOKEN_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
10706
+ var SHA256_RE = /^[0-9a-f]{64}$/;
10707
+ var DEFAULT_CAPACITY_RECOVERY = {
10708
+ enabled: false,
10709
+ maxProcessRestarts: 2,
10710
+ maxServiceRestarts: 1,
10711
+ maxCorrectiveActions: 3,
10712
+ windowSeconds: 600,
10713
+ observationSeconds: 30,
10714
+ resumeDwellSeconds: 300
10715
+ };
10716
+ function ensureObject2(value, label) {
10717
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10718
+ throw new Error(`${label} must be an object.`);
10719
+ }
10720
+ return value;
10532
10721
  }
10533
- function claimRecoveryUnit(input) {
10534
- const reason2 = recoveryBudgetRefusal(input);
10535
- if (reason2 !== void 0) return { ok: false, reason: reason2 };
10536
- const units = (input.incident.units ?? []).map((record5) => ({ ...record5 }));
10537
- if (input.unit !== void 0) {
10538
- const index = units.findIndex((record5) => record5.key === input.unit?.key);
10539
- if (index === -1) {
10540
- if (units.length >= RELIABILITY_MAX_ITEMS) return { ok: false, reason: "unit-exhausted" };
10541
- units.push({ ...input.unit, actions: 1, lastActionAtMs: input.nowMs });
10542
- } else {
10543
- const record5 = units[index];
10544
- units[index] = {
10545
- ...record5,
10546
- actions: record5.actions + 1,
10547
- lastActionAtMs: input.nowMs
10548
- };
10722
+ function ensureAllowedKeys2(value, allowedKeys, label) {
10723
+ const allowed = new Set(allowedKeys);
10724
+ for (const key of Object.keys(value)) {
10725
+ if (!allowed.has(key)) {
10726
+ throw new Error(`${label}.${key} is not supported.`);
10549
10727
  }
10550
10728
  }
10551
- return {
10552
- ok: true,
10553
- incident: {
10554
- ...input.incident,
10555
- startedAtMs: input.incident.startedAtMs ?? input.nowMs,
10556
- correctiveActionsTaken: input.incident.correctiveActionsTaken + 1,
10557
- units
10558
- }
10559
- };
10560
10729
  }
10561
-
10562
- // src/core/reliability-model.ts
10563
- var INFRASTRUCTURE_VALUES = ["healthy", "failed", "unknown"];
10564
- var APPLICATION_VALUES = ["verified", "unready", "unverified"];
10565
- var ADMISSION_VALUES = ["admitted", "waiting", "unknown", "denied-unadmittable"];
10566
- function record3(value) {
10567
- return typeof value === "object" && value !== null && !Array.isArray(value);
10730
+ function parseBoundedString(value, label, maxLength = MAX_STRING_LENGTH) {
10731
+ if (typeof value !== "string" || value.length === 0) {
10732
+ throw new Error(`${label} must be a non-empty string.`);
10733
+ }
10734
+ if (value !== value.trim()) {
10735
+ throw new Error(`${label} must not have leading or trailing whitespace.`);
10736
+ }
10737
+ if (value.length > maxLength) {
10738
+ throw new Error(`${label} exceeds the maximum length of ${maxLength} characters.`);
10739
+ }
10740
+ if ([...value].some(
10741
+ (character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127
10742
+ )) {
10743
+ throw new Error(`${label} must not contain control characters.`);
10744
+ }
10745
+ return value;
10568
10746
  }
10569
- function oneOf2(value, values) {
10570
- return typeof value === "string" && values.includes(value);
10747
+ function parseSafeInteger(value, label, minimum) {
10748
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || Object.is(value, -0)) {
10749
+ throw new Error(`${label} must be a safe integer.`);
10750
+ }
10751
+ if (value < minimum) {
10752
+ throw new Error(`${label} must be at least ${minimum}.`);
10753
+ }
10754
+ return value;
10571
10755
  }
10572
- function uniqueIds2(values) {
10573
- return new Set(values).size === values.length && values.every(isReliabilityId);
10756
+ function parsePositiveInteger(value, label) {
10757
+ return parseSafeInteger(value, label, 1);
10574
10758
  }
10575
- function isFence(value) {
10576
- return record3(value) && isReliabilityId(value.environmentId) && isReliabilityCounter(value.intentRevision) && isReliabilityCounter(value.runtimeGeneration) && isReliabilityCounter(value.controllerEpoch);
10759
+ function parseCanonicalAbsolutePath(value, label) {
10760
+ const parsed = parseBoundedString(value, label, MAX_PATH_LENGTH);
10761
+ if (!import_node_path31.default.posix.isAbsolute(parsed)) {
10762
+ throw new Error(`${label} must be an absolute path.`);
10763
+ }
10764
+ if (import_node_path31.default.posix.resolve(parsed) !== parsed) {
10765
+ throw new Error(`${label} must be a canonical absolute path.`);
10766
+ }
10767
+ return parsed;
10577
10768
  }
10578
- function isConsumer(value) {
10579
- return record3(value) && isReliabilityId(value.id) && typeof value.pinned === "boolean" && Array.isArray(value.requiredCapabilities) && value.requiredCapabilities.length <= 128 && uniqueIds2(value.requiredCapabilities);
10769
+ function parseDomainId(value, label) {
10770
+ const id5 = parseBoundedString(value, label, 64);
10771
+ if (!DOMAIN_ID_RE.test(id5)) {
10772
+ throw new Error(`${label} must be a lowercase alphanumeric or hyphen domain ID.`);
10773
+ }
10774
+ return id5;
10580
10775
  }
10581
- function isObservation(value) {
10582
- return record3(value) && isReliabilityId(value.capability) && oneOf2(value.infrastructure, INFRASTRUCTURE_VALUES) && oneOf2(value.application, APPLICATION_VALUES) && isReliabilityCounter(value.observedAtMs) && isReliabilityCounter(value.validForMs);
10776
+ function parseProfileName(value, label) {
10777
+ const combination = parseBoundedString(value, label, 64);
10778
+ const names = combination.split(",");
10779
+ const seen = /* @__PURE__ */ new Set();
10780
+ for (const [index, name] of names.entries()) {
10781
+ if (!PROFILE_NAME_RE2.test(name)) {
10782
+ throw new Error(
10783
+ `${label}[${index}] must be a lowercase alphanumeric or hyphen profile name.`
10784
+ );
10785
+ }
10786
+ if (seen.has(name)) {
10787
+ throw new Error(`${label} contains duplicate profile '${name}'.`);
10788
+ }
10789
+ seen.add(name);
10790
+ }
10791
+ return [...names].sort().join(",");
10583
10792
  }
10584
- function assertReliabilityEvent(value) {
10585
- if (!record3(value) || !isFence(value) || typeof value.type !== "string") {
10586
- throw new Error("Invalid reliability event.");
10793
+ function parseUniqueProfiles(value, label) {
10794
+ if (!Array.isArray(value) || value.length === 0) {
10795
+ throw new Error(`${label} must be a non-empty array of profile names.`);
10587
10796
  }
10588
- let valid = false;
10589
- switch (value.type) {
10590
- case "request":
10591
- valid = oneOf2(value.mode, ["start", "attach"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer);
10592
- break;
10593
- case "operation-request":
10594
- valid = oneOf2(value.kind, ["ensure", "exec"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer) && typeof value.runtimeRunning === "boolean" && (value.recoverInterruptedEnsure === void 0 || typeof value.recoverInterruptedEnsure === "boolean");
10595
- break;
10596
- case "drained":
10597
- valid = isReliabilityId(value.operationId);
10598
- break;
10599
- case "release":
10600
- valid = isReliabilityId(value.consumerId);
10601
- break;
10602
- case "stop":
10603
- case "park":
10604
- case "dispatch":
10605
- valid = true;
10606
- break;
10607
- case "admission":
10608
- valid = oneOf2(value.result, ADMISSION_VALUES);
10609
- break;
10610
- case "resume":
10611
- valid = typeof value.pressureDwellSatisfied === "boolean" && isReliabilityId(value.operationId);
10612
- break;
10613
- case "dispatch-persisted":
10797
+ if (value.length > MAX_PROFILES_PER_ENROLLMENT) {
10798
+ throw new Error(
10799
+ `${label} exceeds the maximum of ${MAX_PROFILES_PER_ENROLLMENT} profile names.`
10800
+ );
10801
+ }
10802
+ const profiles = [];
10803
+ const seen = /* @__PURE__ */ new Set();
10804
+ for (const [index, entry] of value.entries()) {
10805
+ const profile = parseProfileName(entry, `${label}[${index}]`);
10806
+ if (seen.has(profile)) {
10807
+ throw new Error(`${label} contains duplicate or aliased profile combination '${profile}'.`);
10808
+ }
10809
+ seen.add(profile);
10810
+ profiles.push(profile);
10811
+ }
10812
+ return profiles;
10813
+ }
10814
+ function parseDigest(value, label) {
10815
+ const digest = parseBoundedString(value, label, 64);
10816
+ if (!SHA256_RE.test(digest)) {
10817
+ throw new Error(`${label} must be a lowercase SHA-256 hex digest.`);
10818
+ }
10819
+ return digest;
10820
+ }
10821
+ function parseDomain(value, domainId) {
10822
+ const label = `domains.${domainId}`;
10823
+ const domain = ensureObject2(value, label);
10824
+ const kind = parseBoundedString(domain.kind, `${label}.kind`);
10825
+ if (kind === "host") {
10826
+ ensureAllowedKeys2(
10827
+ domain,
10828
+ [
10829
+ "kind",
10830
+ "adapter",
10831
+ "capacityBytes",
10832
+ "protectedHeadroomBytes",
10833
+ "unmanagedAllowanceBytes",
10834
+ "startupSlots",
10835
+ "heavySlots"
10836
+ ],
10837
+ label
10838
+ );
10839
+ const adapter = parseBoundedString(domain.adapter, `${label}.adapter`);
10840
+ if (adapter !== "macos-host-v1" && adapter !== "macos-declared-v1") {
10841
+ throw new Error(`${label}.adapter must be 'macos-host-v1' or 'macos-declared-v1'.`);
10842
+ }
10843
+ const capacityBytes = parsePositiveInteger(domain.capacityBytes, `${label}.capacityBytes`);
10844
+ const protectedHeadroomBytes = parsePositiveInteger(
10845
+ domain.protectedHeadroomBytes,
10846
+ `${label}.protectedHeadroomBytes`
10847
+ );
10848
+ if (protectedHeadroomBytes >= capacityBytes) {
10849
+ throw new Error(`${label}.protectedHeadroomBytes must be less than capacityBytes.`);
10850
+ }
10851
+ const startupSlots = parsePositiveInteger(domain.startupSlots, `${label}.startupSlots`);
10852
+ const heavySlots = parsePositiveInteger(domain.heavySlots, `${label}.heavySlots`);
10853
+ if (adapter === "macos-host-v1") {
10854
+ if (Object.hasOwn(domain, "unmanagedAllowanceBytes")) {
10855
+ throw new Error(
10856
+ `${label}.unmanagedAllowanceBytes is only supported for adapter 'macos-declared-v1'.`
10857
+ );
10858
+ }
10859
+ return {
10860
+ kind: "host",
10861
+ adapter: "macos-host-v1",
10862
+ capacityBytes,
10863
+ protectedHeadroomBytes,
10864
+ startupSlots,
10865
+ heavySlots
10866
+ };
10867
+ }
10868
+ const unmanagedAllowanceBytes = parseSafeInteger(
10869
+ domain.unmanagedAllowanceBytes,
10870
+ `${label}.unmanagedAllowanceBytes`,
10871
+ 0
10872
+ );
10873
+ if (unmanagedAllowanceBytes > capacityBytes - protectedHeadroomBytes) {
10874
+ throw new Error(
10875
+ `${label}.unmanagedAllowanceBytes must not exceed capacityBytes minus protectedHeadroomBytes.`
10876
+ );
10877
+ }
10878
+ return {
10879
+ kind: "host",
10880
+ adapter: "macos-declared-v1",
10881
+ capacityBytes,
10882
+ protectedHeadroomBytes,
10883
+ unmanagedAllowanceBytes,
10884
+ startupSlots,
10885
+ heavySlots
10886
+ };
10887
+ }
10888
+ if (kind === "runtime") {
10889
+ ensureAllowedKeys2(
10890
+ domain,
10891
+ [
10892
+ "kind",
10893
+ "adapter",
10894
+ "endpoint",
10895
+ "daemonId",
10896
+ "hostDomain",
10897
+ "hostChargeCeilingBytes",
10898
+ "capacityBytes",
10899
+ "protectedHeadroomBytes",
10900
+ "guestUnmanagedAllowanceBytes",
10901
+ "startupSlots",
10902
+ "heavySlots"
10903
+ ],
10904
+ label
10905
+ );
10906
+ const adapter = parseBoundedString(domain.adapter, `${label}.adapter`);
10907
+ if (adapter !== "orbstack-local-v1" && adapter !== "orbstack-declared-v1") {
10908
+ throw new Error(`${label}.adapter must be 'orbstack-local-v1' or 'orbstack-declared-v1'.`);
10909
+ }
10910
+ const endpoint = parseCanonicalAbsolutePath(domain.endpoint, `${label}.endpoint`);
10911
+ const daemonId = parseBoundedString(domain.daemonId, `${label}.daemonId`);
10912
+ const hostDomain = parseDomainId(domain.hostDomain, `${label}.hostDomain`);
10913
+ const hostChargeCeilingBytes = parsePositiveInteger(
10914
+ domain.hostChargeCeilingBytes,
10915
+ `${label}.hostChargeCeilingBytes`
10916
+ );
10917
+ const capacityBytes = parsePositiveInteger(domain.capacityBytes, `${label}.capacityBytes`);
10918
+ const protectedHeadroomBytes = parsePositiveInteger(
10919
+ domain.protectedHeadroomBytes,
10920
+ `${label}.protectedHeadroomBytes`
10921
+ );
10922
+ if (protectedHeadroomBytes >= capacityBytes) {
10923
+ throw new Error(`${label}.protectedHeadroomBytes must be less than capacityBytes.`);
10924
+ }
10925
+ const startupSlots = parsePositiveInteger(domain.startupSlots, `${label}.startupSlots`);
10926
+ const heavySlots = parsePositiveInteger(domain.heavySlots, `${label}.heavySlots`);
10927
+ if (adapter === "orbstack-local-v1") {
10928
+ if (Object.hasOwn(domain, "guestUnmanagedAllowanceBytes")) {
10929
+ throw new Error(
10930
+ `${label}.guestUnmanagedAllowanceBytes is only supported for adapter 'orbstack-declared-v1'.`
10931
+ );
10932
+ }
10933
+ return {
10934
+ kind: "runtime",
10935
+ adapter: "orbstack-local-v1",
10936
+ endpoint,
10937
+ daemonId,
10938
+ hostDomain,
10939
+ hostChargeCeilingBytes,
10940
+ capacityBytes,
10941
+ protectedHeadroomBytes,
10942
+ startupSlots,
10943
+ heavySlots
10944
+ };
10945
+ }
10946
+ return {
10947
+ kind: "runtime",
10948
+ adapter: "orbstack-declared-v1",
10949
+ endpoint,
10950
+ daemonId,
10951
+ hostDomain,
10952
+ hostChargeCeilingBytes,
10953
+ capacityBytes,
10954
+ protectedHeadroomBytes,
10955
+ guestUnmanagedAllowanceBytes: parseSafeInteger(
10956
+ domain.guestUnmanagedAllowanceBytes,
10957
+ `${label}.guestUnmanagedAllowanceBytes`,
10958
+ 0
10959
+ ),
10960
+ startupSlots,
10961
+ heavySlots
10962
+ };
10963
+ }
10964
+ throw new Error(`${label}.kind must be 'host' or 'runtime'.`);
10965
+ }
10966
+ function parseScheduling(value) {
10967
+ const label = "scheduling";
10968
+ const scheduling = ensureObject2(value, label);
10969
+ ensureAllowedKeys2(
10970
+ scheduling,
10971
+ [
10972
+ "maxQueuedPerDomain",
10973
+ "maxQueuedTotal",
10974
+ "queueLifetimeSeconds",
10975
+ "clientWaitSeconds",
10976
+ "maxClientWaitSeconds",
10977
+ "watchSeconds",
10978
+ "sampleIntervalSeconds",
10979
+ "maxSampleAgeSeconds"
10980
+ ],
10981
+ label
10982
+ );
10983
+ const result = {
10984
+ maxQueuedPerDomain: parsePositiveInteger(
10985
+ scheduling.maxQueuedPerDomain,
10986
+ `${label}.maxQueuedPerDomain`
10987
+ ),
10988
+ maxQueuedTotal: parsePositiveInteger(scheduling.maxQueuedTotal, `${label}.maxQueuedTotal`),
10989
+ queueLifetimeSeconds: parsePositiveInteger(
10990
+ scheduling.queueLifetimeSeconds,
10991
+ `${label}.queueLifetimeSeconds`
10992
+ ),
10993
+ // Reserved for future runtime binding: validated and bounded today, but no
10994
+ // consumer reads these scheduling knobs yet. Runtime constants remain
10995
+ // authoritative (controller watch window, CLI admission wait, tick cadence).
10996
+ clientWaitSeconds: parseSafeInteger(
10997
+ scheduling.clientWaitSeconds,
10998
+ `${label}.clientWaitSeconds`,
10999
+ 0
11000
+ ),
11001
+ maxClientWaitSeconds: parsePositiveInteger(
11002
+ scheduling.maxClientWaitSeconds,
11003
+ `${label}.maxClientWaitSeconds`
11004
+ ),
11005
+ watchSeconds: parsePositiveInteger(scheduling.watchSeconds, `${label}.watchSeconds`),
11006
+ sampleIntervalSeconds: parsePositiveInteger(
11007
+ scheduling.sampleIntervalSeconds,
11008
+ `${label}.sampleIntervalSeconds`
11009
+ ),
11010
+ maxSampleAgeSeconds: parsePositiveInteger(
11011
+ scheduling.maxSampleAgeSeconds,
11012
+ `${label}.maxSampleAgeSeconds`
11013
+ )
11014
+ };
11015
+ if (result.clientWaitSeconds > result.maxClientWaitSeconds) {
11016
+ throw new Error(`${label}.clientWaitSeconds must not exceed maxClientWaitSeconds.`);
11017
+ }
11018
+ if (result.maxQueuedTotal > 64 || result.maxQueuedPerDomain > 32 || result.queueLifetimeSeconds > 900 || result.maxClientWaitSeconds > 900 || result.watchSeconds > 30 || result.maxSampleAgeSeconds > 15) {
11019
+ throw new Error(`${label} exceeds supported controller bounds.`);
11020
+ }
11021
+ if (result.queueLifetimeSeconds < result.maxClientWaitSeconds) {
11022
+ throw new Error(`${label}.queueLifetimeSeconds must cover maxClientWaitSeconds.`);
11023
+ }
11024
+ if (result.maxSampleAgeSeconds < result.sampleIntervalSeconds) {
11025
+ throw new Error(`${label}.maxSampleAgeSeconds must be at least sampleIntervalSeconds.`);
11026
+ }
11027
+ return result;
11028
+ }
11029
+ function parseRecovery(value) {
11030
+ if (value === void 0) return { ...DEFAULT_CAPACITY_RECOVERY };
11031
+ const label = "recovery";
11032
+ const recovery = ensureObject2(value, label);
11033
+ ensureAllowedKeys2(
11034
+ recovery,
11035
+ [
11036
+ "enabled",
11037
+ "maxProcessRestarts",
11038
+ "maxServiceRestarts",
11039
+ "maxCorrectiveActions",
11040
+ "windowSeconds",
11041
+ "observationSeconds",
11042
+ "resumeDwellSeconds"
11043
+ ],
11044
+ label
11045
+ );
11046
+ if (typeof recovery.enabled !== "boolean") {
11047
+ throw new Error(`${label}.enabled must be a boolean.`);
11048
+ }
11049
+ const result = {
11050
+ enabled: recovery.enabled,
11051
+ maxProcessRestarts: parseSafeInteger(
11052
+ recovery.maxProcessRestarts,
11053
+ `${label}.maxProcessRestarts`,
11054
+ 0
11055
+ ),
11056
+ maxServiceRestarts: parseSafeInteger(
11057
+ recovery.maxServiceRestarts,
11058
+ `${label}.maxServiceRestarts`,
11059
+ 0
11060
+ ),
11061
+ maxCorrectiveActions: parsePositiveInteger(
11062
+ recovery.maxCorrectiveActions,
11063
+ `${label}.maxCorrectiveActions`
11064
+ ),
11065
+ windowSeconds: parsePositiveInteger(recovery.windowSeconds, `${label}.windowSeconds`),
11066
+ observationSeconds: parsePositiveInteger(
11067
+ recovery.observationSeconds,
11068
+ `${label}.observationSeconds`
11069
+ ),
11070
+ resumeDwellSeconds: parsePositiveInteger(
11071
+ recovery.resumeDwellSeconds,
11072
+ `${label}.resumeDwellSeconds`
11073
+ )
11074
+ };
11075
+ if (result.maxCorrectiveActions < result.maxProcessRestarts + result.maxServiceRestarts) {
11076
+ throw new Error(
11077
+ `${label}.maxCorrectiveActions must cover maxProcessRestarts and maxServiceRestarts.`
11078
+ );
11079
+ }
11080
+ if (result.maxProcessRestarts > 4 || result.maxServiceRestarts > 2 || result.maxCorrectiveActions > 8 || result.windowSeconds > 3600 || result.observationSeconds > 300 || result.resumeDwellSeconds > 3600) {
11081
+ throw new Error(`${label} exceeds supported controller bounds.`);
11082
+ }
11083
+ return result;
11084
+ }
11085
+ function validateDomainReferences(domains) {
11086
+ const daemonIds = /* @__PURE__ */ new Map();
11087
+ const endpoints = /* @__PURE__ */ new Map();
11088
+ for (const [domainId, domain] of Object.entries(domains)) {
11089
+ if (domain.kind !== "runtime") continue;
11090
+ const previousDaemon = daemonIds.get(domain.daemonId);
11091
+ if (previousDaemon) {
11092
+ throw new Error(
11093
+ `domains.${domainId}.daemonId duplicates runtime domain '${previousDaemon}'.`
11094
+ );
11095
+ }
11096
+ daemonIds.set(domain.daemonId, domainId);
11097
+ const previousEndpoint = endpoints.get(domain.endpoint);
11098
+ if (previousEndpoint) {
11099
+ throw new Error(`domains.${domainId}.endpoint aliases runtime domain '${previousEndpoint}'.`);
11100
+ }
11101
+ endpoints.set(domain.endpoint, domainId);
11102
+ if (!Object.hasOwn(domains, domain.hostDomain)) {
11103
+ throw new Error(
11104
+ `domains.${domainId}.hostDomain references missing domain '${domain.hostDomain}'.`
11105
+ );
11106
+ }
11107
+ const hostDomain = domains[domain.hostDomain];
11108
+ if (hostDomain.kind !== "host") {
11109
+ throw new Error(
11110
+ `domains.${domainId}.hostDomain must reference a host domain, not '${domain.hostDomain}'.`
11111
+ );
11112
+ }
11113
+ const unmanagedAllowanceBytes = hostDomain.adapter === "macos-declared-v1" ? hostDomain.unmanagedAllowanceBytes : 0;
11114
+ const admissibleHostBytes = hostDomain.capacityBytes - hostDomain.protectedHeadroomBytes - unmanagedAllowanceBytes;
11115
+ if (domain.hostChargeCeilingBytes > admissibleHostBytes) {
11116
+ throw new Error(
11117
+ `domains.${domainId}.hostChargeCeilingBytes exceeds admissible capacity of host domain '${domain.hostDomain}'.`
11118
+ );
11119
+ }
11120
+ }
11121
+ }
11122
+ function parseEnrollment(value, index, domains) {
11123
+ const label = `enrollments[${index}]`;
11124
+ const enrollment = ensureObject2(value, label);
11125
+ ensureAllowedKeys2(
11126
+ enrollment,
11127
+ [
11128
+ "repoPath",
11129
+ "gitCommonDir",
11130
+ "workspace",
11131
+ "provider",
11132
+ "providerId",
11133
+ "hostDomain",
11134
+ "runtimeDomain",
11135
+ "profiles",
11136
+ "estimatesDigest",
11137
+ "defaultOperation"
11138
+ ],
11139
+ label
11140
+ );
11141
+ const repoPath = parseCanonicalAbsolutePath(enrollment.repoPath, `${label}.repoPath`);
11142
+ const gitCommonDir = parseCanonicalAbsolutePath(enrollment.gitCommonDir, `${label}.gitCommonDir`);
11143
+ const workspace = parseBoundedString(enrollment.workspace, `${label}.workspace`, 32);
11144
+ if (!WORKSPACE_TOKEN_RE.test(workspace)) {
11145
+ throw new Error(`${label}.workspace must be an existing workspace token.`);
11146
+ }
11147
+ const provider = parseBoundedString(enrollment.provider, `${label}.provider`);
11148
+ if (provider !== "devsy" && provider !== "devpod") {
11149
+ throw new Error(`${label}.provider must be 'devsy' or 'devpod'.`);
11150
+ }
11151
+ const providerId = parseBoundedString(enrollment.providerId, `${label}.providerId`);
11152
+ const hostDomain = parseDomainId(enrollment.hostDomain, `${label}.hostDomain`);
11153
+ const runtimeDomain = parseDomainId(enrollment.runtimeDomain, `${label}.runtimeDomain`);
11154
+ if (!Object.hasOwn(domains, hostDomain)) {
11155
+ throw new Error(`${label}.hostDomain references missing domain '${hostDomain}'.`);
11156
+ }
11157
+ const host = domains[hostDomain];
11158
+ if (host.kind !== "host") {
11159
+ throw new Error(`${label}.hostDomain must reference a host domain.`);
11160
+ }
11161
+ if (!Object.hasOwn(domains, runtimeDomain)) {
11162
+ throw new Error(`${label}.runtimeDomain references missing domain '${runtimeDomain}'.`);
11163
+ }
11164
+ const runtime = domains[runtimeDomain];
11165
+ if (runtime.kind !== "runtime") {
11166
+ throw new Error(`${label}.runtimeDomain must reference a runtime domain.`);
11167
+ }
11168
+ if (runtime.hostDomain !== hostDomain) {
11169
+ throw new Error(
11170
+ `${label}.runtimeDomain '${runtimeDomain}' is bound to host domain '${runtime.hostDomain}', not '${hostDomain}'.`
11171
+ );
11172
+ }
11173
+ const profiles = parseUniqueProfiles(enrollment.profiles, `${label}.profiles`);
11174
+ const estimatesDigest = parseDigest(enrollment.estimatesDigest, `${label}.estimatesDigest`);
11175
+ const defaultOperation = ensureObject2(enrollment.defaultOperation, `${label}.defaultOperation`);
11176
+ ensureAllowedKeys2(
11177
+ defaultOperation,
11178
+ ["hostIncrementBytes", "runtimeIncrementBytes"],
11179
+ `${label}.defaultOperation`
11180
+ );
11181
+ return {
11182
+ repoPath,
11183
+ gitCommonDir,
11184
+ workspace,
11185
+ provider,
11186
+ providerId,
11187
+ hostDomain,
11188
+ runtimeDomain,
11189
+ profiles,
11190
+ estimatesDigest,
11191
+ defaultOperation: {
11192
+ hostIncrementBytes: parseSafeInteger(
11193
+ defaultOperation.hostIncrementBytes,
11194
+ `${label}.defaultOperation.hostIncrementBytes`,
11195
+ 0
11196
+ ),
11197
+ runtimeIncrementBytes: parsePositiveInteger(
11198
+ defaultOperation.runtimeIncrementBytes,
11199
+ `${label}.defaultOperation.runtimeIncrementBytes`
11200
+ )
11201
+ }
11202
+ };
11203
+ }
11204
+ function parseCapacityPolicy(value) {
11205
+ const root = ensureObject2(value, "capacity policy");
11206
+ ensureAllowedKeys2(
11207
+ root,
11208
+ ["version", "revision", "admissions", "scheduling", "recovery", "domains", "enrollments"],
11209
+ "capacity policy"
11210
+ );
11211
+ const version = parsePositiveInteger(root.version, "capacity policy.version");
11212
+ if (version !== 1) {
11213
+ throw new Error("capacity policy.version must be 1.");
11214
+ }
11215
+ const revision = parsePositiveInteger(root.revision, "capacity policy.revision");
11216
+ const admissions = parseBoundedString(root.admissions, "capacity policy.admissions");
11217
+ if (admissions !== "enabled" && admissions !== "paused") {
11218
+ throw new Error("capacity policy.admissions must be 'enabled' or 'paused'.");
11219
+ }
11220
+ const scheduling = parseScheduling(root.scheduling);
11221
+ const recovery = parseRecovery(root.recovery);
11222
+ const domainsValue = ensureObject2(root.domains, "capacity policy.domains");
11223
+ const domainEntries = Object.entries(domainsValue);
11224
+ if (domainEntries.length > MAX_DOMAINS) {
11225
+ throw new Error(`capacity policy.domains exceeds the maximum of ${MAX_DOMAINS} domains.`);
11226
+ }
11227
+ const domains = {};
11228
+ for (const [domainId, domainValue] of domainEntries) {
11229
+ const parsedId = parseDomainId(domainId, "capacity policy.domains key");
11230
+ domains[parsedId] = parseDomain(domainValue, parsedId);
11231
+ }
11232
+ validateDomainReferences(domains);
11233
+ if (!Array.isArray(root.enrollments)) {
11234
+ throw new Error("capacity policy.enrollments must be an array.");
11235
+ }
11236
+ if (root.enrollments.length > MAX_ENROLLMENTS) {
11237
+ throw new Error(
11238
+ `capacity policy.enrollments exceeds the maximum of ${MAX_ENROLLMENTS} enrollments.`
11239
+ );
11240
+ }
11241
+ const enrollments = [];
11242
+ const enrollmentKeys = /* @__PURE__ */ new Set();
11243
+ const providerIds = /* @__PURE__ */ new Map();
11244
+ for (const [index, enrollmentValue] of root.enrollments.entries()) {
11245
+ const enrollment = parseEnrollment(enrollmentValue, index, domains);
11246
+ const enrollmentKey = [enrollment.repoPath, enrollment.gitCommonDir, enrollment.workspace].join(
11247
+ "\0"
11248
+ );
11249
+ if (enrollmentKeys.has(enrollmentKey)) {
11250
+ throw new Error(`enrollments[${index}] duplicates an existing enrollment.`);
11251
+ }
11252
+ enrollmentKeys.add(enrollmentKey);
11253
+ const providerKey = `${enrollment.provider}\0${enrollment.providerId}`;
11254
+ const previousProviderIndex = providerIds.get(providerKey);
11255
+ if (previousProviderIndex !== void 0) {
11256
+ throw new Error(
11257
+ `enrollments[${index}].providerId duplicates enrollments[${previousProviderIndex}].`
11258
+ );
11259
+ }
11260
+ providerIds.set(providerKey, index);
11261
+ enrollments.push(enrollment);
11262
+ }
11263
+ return {
11264
+ version: 1,
11265
+ revision,
11266
+ admissions,
11267
+ scheduling,
11268
+ recovery,
11269
+ domains,
11270
+ enrollments
11271
+ };
11272
+ }
11273
+
11274
+ // src/core/controller-client.ts
11275
+ var import_node_crypto15 = require("crypto");
11276
+ var import_node_fs28 = __toESM(require("fs"));
11277
+ var import_node_net2 = __toESM(require("net"));
11278
+ var import_node_path33 = __toESM(require("path"));
11279
+
11280
+ // src/core/controller-protocol.ts
11281
+ var import_node_path32 = __toESM(require("path"));
11282
+ var CONTROLLER_FRAME_BYTES = 64 * 1024;
11283
+ var CONTROLLER_MAX_OPERATION_REQUEST_BYTES = 32 * 1024;
11284
+ var CONTROLLER_PROFILE_TOKEN_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
11285
+ var CONTROLLER_PROFILE_RE = new RegExp(
11286
+ `^(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)})(?:,(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)}))*$`
11287
+ );
11288
+
11289
+ // src/core/controller-monitor.ts
11290
+ var import_node_crypto16 = require("crypto");
11291
+ var import_node_util6 = require("util");
11292
+
11293
+ // src/core/reliability-output.ts
11294
+ function reliabilityAttention(state) {
11295
+ assertReliabilityState(state);
11296
+ const completeStop = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
11297
+ if (state.desired === "stopped-by-user") return completeStop ? void 0 : "stop-incomplete";
11298
+ if (state.operation?.status === "COMPLETION_UNKNOWN" || state.operation?.status === "INTERRUPTED")
11299
+ return "operation-unknown";
11300
+ if (state.desired === "parked-for-capacity")
11301
+ return completeStop ? "capacity-parked" : "stop-incomplete";
11302
+ if (state.admission === "denied-unadmittable") return "unadmittable";
11303
+ if (state.admission === "waiting") return "capacity-waiting";
11304
+ if (state.phase === "recovering") return "recovering";
11305
+ if (state.phase === "queued" || state.phase === "starting" || state.phase === "verifying")
11306
+ return "starting";
11307
+ return void 0;
11308
+ }
11309
+
11310
+ // src/core/managed-post-start.ts
11311
+ var import_node_child_process20 = require("child_process");
11312
+ var import_node_crypto18 = require("crypto");
11313
+ var import_node_fs29 = __toESM(require("fs"));
11314
+ var import_node_path34 = __toESM(require("path"));
11315
+
11316
+ // src/core/managed-host-ports.ts
11317
+ var import_node_child_process19 = require("child_process");
11318
+ var import_node_crypto17 = require("crypto");
11319
+ var PORT_LOOKUP_TIMEOUT_MS = 1e4;
11320
+ var PORT_LOOKUP_MAX_BUFFER = 64 * 1024;
11321
+ var LOOPBACK_BINDING = /^127\.0\.0\.1:(\d+)$/;
11322
+ function unavailableEntry(declaration, reason2) {
11323
+ return { ...declaration, status: "unavailable", reason: reason2 };
11324
+ }
11325
+ function livePublishedBindings(containerId, target) {
11326
+ const result = (0, import_node_child_process19.spawnSync)("docker", ["port", containerId, `${target}/tcp`], {
11327
+ encoding: "utf-8",
11328
+ timeout: PORT_LOOKUP_TIMEOUT_MS,
11329
+ maxBuffer: PORT_LOOKUP_MAX_BUFFER,
11330
+ ...networkDockerOptions()
11331
+ });
11332
+ if (result.error || result.status === null) return void 0;
11333
+ if (result.status !== 0) return "";
11334
+ return typeof result.stdout === "string" ? result.stdout : void 0;
11335
+ }
11336
+ function parseLoopbackBinding(output) {
11337
+ const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
11338
+ if (lines.length === 0) return { reason: "not-published" };
11339
+ if (lines.length > 1) return { reason: "ambiguous" };
11340
+ const match = LOOPBACK_BINDING.exec(lines[0]);
11341
+ if (!match) return { reason: "not-published" };
11342
+ const port = Number(match[1]);
11343
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return { reason: "not-published" };
11344
+ return { endpoint: { host: "127.0.0.1", port } };
11345
+ }
11346
+ function revalidateLiveOwner(containerId, input, composeProject, composeFiles, service) {
11347
+ try {
11348
+ const current = inspectWorkspaceContainers({ ids: [containerId] });
11349
+ if (current.length !== 1 || current[0].id !== containerId) return "unavailable";
11350
+ if (!hasExactComposeIdentity(current[0], {
11351
+ repoPath: input.repoPath,
11352
+ service,
11353
+ composeProject,
11354
+ composeFiles
11355
+ })) {
11356
+ return "unavailable";
11357
+ }
11358
+ return current[0].state.Running ? "available" : "not-running";
11359
+ } catch {
11360
+ return "unavailable";
11361
+ }
11362
+ }
11363
+ function resolveManagedHostPorts(input) {
11364
+ const report = {};
11365
+ const selectedServices = new Set(input.selectedServices);
11366
+ const selectedKeys = [];
11367
+ for (const [key, declaration] of Object.entries(input.declarations)) {
11368
+ if (!selectedServices.has(declaration.service)) {
11369
+ report[key] = { ...declaration, status: "inactive" };
11370
+ continue;
11371
+ }
11372
+ selectedKeys.push(key);
11373
+ report[key] = { ...declaration, status: "unavailable", reason: "unavailable" };
11374
+ }
11375
+ if (selectedKeys.length === 0) return report;
11376
+ const composeProject = input.composeProject;
11377
+ const composeFiles = input.composeFiles;
11378
+ if (typeof composeProject !== "string" || composeProject.length === 0 || composeFiles === void 0 || composeFiles.length === 0 || composeFiles.some((file) => file.length === 0)) {
11379
+ return report;
11380
+ }
11381
+ if (selectedKeys.some((key) => {
11382
+ const target = input.declarations[key].target;
11383
+ return !Number.isInteger(target) || target < 1 || target > 65535;
11384
+ })) {
11385
+ return report;
11386
+ }
11387
+ const liveContainers = input.containers === void 0;
11388
+ let containers = input.containers;
11389
+ if (containers === void 0) {
11390
+ try {
11391
+ containers = inspectWorkspaceContainers({ composeProject });
11392
+ } catch {
11393
+ return report;
11394
+ }
11395
+ }
11396
+ const publishedBindings = input.publishedBindings ?? livePublishedBindings;
11397
+ for (const key of selectedKeys) {
11398
+ const declaration = input.declarations[key];
11399
+ let owners;
11400
+ try {
11401
+ owners = containers.filter(
11402
+ (container) => hasExactComposeIdentity(container, {
11403
+ repoPath: input.repoPath,
11404
+ service: declaration.service,
11405
+ composeProject,
11406
+ composeFiles
11407
+ })
11408
+ );
11409
+ } catch {
11410
+ report[key] = unavailableEntry(declaration, "unavailable");
11411
+ continue;
11412
+ }
11413
+ if (owners.length === 0) {
11414
+ report[key] = unavailableEntry(declaration, "not-found");
11415
+ continue;
11416
+ }
11417
+ if (owners.length > 1) {
11418
+ report[key] = unavailableEntry(declaration, "ambiguous");
11419
+ continue;
11420
+ }
11421
+ const owner = owners[0];
11422
+ if (!owner.state.Running) {
11423
+ report[key] = unavailableEntry(declaration, "not-running");
11424
+ continue;
11425
+ }
11426
+ let output;
11427
+ try {
11428
+ output = publishedBindings(owner.id, declaration.target);
11429
+ } catch {
11430
+ output = void 0;
11431
+ }
11432
+ if (typeof output !== "string") {
11433
+ report[key] = unavailableEntry(declaration, "unavailable");
11434
+ continue;
11435
+ }
11436
+ const binding = parseLoopbackBinding(output);
11437
+ if (!binding.endpoint) {
11438
+ report[key] = unavailableEntry(declaration, binding.reason ?? "unavailable");
11439
+ continue;
11440
+ }
11441
+ if (liveContainers) {
11442
+ const proof = revalidateLiveOwner(
11443
+ owner.id,
11444
+ input,
11445
+ composeProject,
11446
+ composeFiles,
11447
+ declaration.service
11448
+ );
11449
+ if (proof !== "available") {
11450
+ report[key] = unavailableEntry(declaration, proof);
11451
+ continue;
11452
+ }
11453
+ }
11454
+ report[key] = {
11455
+ ...declaration,
11456
+ status: "available",
11457
+ endpoint: binding.endpoint
11458
+ };
11459
+ }
11460
+ return report;
11461
+ }
11462
+ function managedHostPortDeclarationsIdentity(declarations) {
11463
+ const keys2 = Object.keys(declarations).sort();
11464
+ if (keys2.length === 0) return void 0;
11465
+ const canonical = keys2.map((key) => [key, declarations[key].service, declarations[key].target]);
11466
+ return (0, import_node_crypto17.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
11467
+ }
11468
+ function managedHostPortVariables(report) {
11469
+ return Object.fromEntries(
11470
+ Object.entries(report).map(([key, entry]) => [
11471
+ `DEVROUTER_HOST_PORT_${key.toUpperCase().replaceAll("-", "_")}`,
11472
+ entry.status === "available" && entry.endpoint ? String(entry.endpoint.port) : ""
11473
+ ])
11474
+ );
11475
+ }
11476
+ function managedHostPortIdentity(report) {
11477
+ const keys2 = Object.keys(report).sort();
11478
+ if (keys2.length === 0) return void 0;
11479
+ const canonical = keys2.map((key) => {
11480
+ const entry = report[key];
11481
+ return [key, entry.status === "available" ? entry.endpoint?.port ?? null : entry.status];
11482
+ });
11483
+ return (0, import_node_crypto17.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
11484
+ }
11485
+ function assertManagedHostPortsAvailable(report) {
11486
+ const unavailable = Object.entries(report).filter(([, entry]) => entry.status === "unavailable").map(([key]) => key).sort();
11487
+ if (unavailable.length > 0) {
11488
+ throw new Error(`Selected managed host ports are unavailable: ${unavailable.join(", ")}.`);
11489
+ }
11490
+ }
11491
+
11492
+ // src/core/managed-post-start.ts
11493
+ var MANAGED_MARKER = "devrouter:managed devcontainer";
11494
+ var MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
11495
+ var RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
11496
+ var RUNTIME_VARIANT_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
11497
+ var ADAPTER_WRAPPER = `adapter_snapshot="$1"
11498
+ shift
11499
+ readonly DEVROUTER_PROCESS_HELPER DEVROUTER_PROCESS_ADAPTER_SHA256 DEVROUTER_RUNTIME_VARIANT DEVROUTER_PROCESS_HOST_PORTS_SHA256
11500
+ source "$adapter_snapshot"
11501
+ `;
11502
+ function renderDeliveryScript(targetPath) {
11503
+ if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
11504
+ throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
11505
+ }
11506
+ const temporaryPrefix = import_node_path34.default.posix.basename(targetPath);
11507
+ return `set -eu
11508
+ umask 077
11509
+ runtime_root=/tmp/devrouter
11510
+ runtime_bin="$runtime_root/bin"
11511
+ target="${targetPath}"
11512
+ if [ -L "$runtime_root" ] || [ -L "$runtime_bin" ]; then
11513
+ echo "Refusing symlinked devrouter runtime path." >&2
11514
+ exit 1
11515
+ fi
11516
+ mkdir -p "$runtime_bin"
11517
+ chmod 700 "$runtime_root" "$runtime_bin"
11518
+ temporary="$(mktemp "$runtime_bin/.${temporaryPrefix}.XXXXXX")"
11519
+ trap 'rm -f "$temporary"' EXIT
11520
+ cat > "$temporary"
11521
+ chmod 700 "$temporary"
11522
+ mv -f "$temporary" "$target"
11523
+ trap - EXIT
11524
+ `;
11525
+ }
11526
+ function commandFailure2(result) {
11527
+ return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
11528
+ }
11529
+ function deliverRuntimeFile(containerId, targetPath, contents, label) {
11530
+ const result = (0, import_node_child_process20.spawnSync)(
11531
+ "docker",
11532
+ ["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
11533
+ { input: contents, encoding: "utf-8", ...networkDockerOptions() }
11534
+ );
11535
+ if (result.status !== 0) {
11536
+ const details = commandFailure2(result);
11537
+ throw new Error(`Could not deliver ${label}${details ? `: ${details}` : "."}`);
11538
+ }
11539
+ }
11540
+ function readRegularFileBytes(filePath) {
11541
+ if (!import_node_fs29.default.existsSync(filePath) || !import_node_fs29.default.lstatSync(filePath).isFile()) return void 0;
11542
+ return import_node_fs29.default.readFileSync(filePath);
11543
+ }
11544
+ function readRegularFile(filePath) {
11545
+ return readRegularFileBytes(filePath)?.toString("utf-8");
11546
+ }
11547
+ function adapterFingerprint(adapter) {
11548
+ return (0, import_node_crypto18.createHash)("sha256").update(adapter).digest("hex");
11549
+ }
11550
+ function resolveProcessHelperPath() {
11551
+ const candidates = [
11552
+ import_node_path34.default.resolve(__dirname, "..", "bin", "devrouter-process"),
11553
+ import_node_path34.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
11554
+ ];
11555
+ const helperPath = candidates.find((candidate) => import_node_fs29.default.existsSync(candidate));
11556
+ if (!helperPath) {
11557
+ throw new Error("Could not locate the packaged devrouter-process helper.");
11558
+ }
11559
+ return helperPath;
11560
+ }
11561
+ function resolveManagedPostStartPlan(repoPath) {
11562
+ const adapterPath = import_node_path34.default.join(repoPath, MANAGED_ADAPTER_PATH);
11563
+ const adapterBytes = readRegularFileBytes(adapterPath);
11564
+ const adapterText = adapterBytes?.toString("utf-8");
11565
+ const dockerfilePath = import_node_path34.default.join(repoPath, ".devcontainer", "Dockerfile");
11566
+ const devcontainerPath = import_node_path34.default.join(repoPath, ".devcontainer", "devcontainer.json");
11567
+ const dockerfile = readRegularFile(dockerfilePath) ?? "";
11568
+ const devcontainer = readRegularFile(devcontainerPath) ?? "";
11569
+ const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
11570
+ (value) => value.includes("DEVROUTER_PROCESS_HELPER") || value.includes("devrouter-process")
11571
+ );
11572
+ if (adapterBytes === void 0) {
11573
+ if (devrouterPattern) {
11574
+ throw new Error(
11575
+ "Devrouter lifecycle wiring is incomplete: add the managed post-start adapter or remove the stale devrouter-process references."
11576
+ );
11577
+ }
11578
+ return { kind: "unmanaged" };
11579
+ }
11580
+ const adapter = adapterBytes.toString("utf-8");
11581
+ if (!adapter.includes(MANAGED_MARKER)) {
11582
+ if (devrouterPattern) {
11583
+ throw new Error(
11584
+ "Devrouter-looking post-start adapter is missing the 'devrouter:managed devcontainer' marker. Regenerate or migrate the devcontainer, then retry devrouter ensure."
11585
+ );
11586
+ }
11587
+ return { kind: "unmanaged" };
11588
+ }
11589
+ assertManagedDevcontainerLifecycle(repoPath);
11590
+ if (adapter.includes("DEVROUTER_PROCESS_HELPER")) {
11591
+ return {
11592
+ kind: "runtime",
11593
+ adapterPath: MANAGED_ADAPTER_PATH,
11594
+ adapterSha256: adapterFingerprint(adapterBytes),
11595
+ adapterContents: adapterBytes
11596
+ };
11597
+ }
11598
+ if (dockerfile.includes("devrouter-process") && devcontainer.includes("postStartCommand")) {
11599
+ return { kind: "legacy" };
11600
+ }
11601
+ throw new Error(
11602
+ "Managed post-start must use DEVROUTER_PROCESS_HELPER before removing the legacy image helper and postStartCommand. Regenerate or migrate the devcontainer, then retry devrouter ensure."
11603
+ );
11604
+ }
11605
+ function runManagedPostStart(options) {
11606
+ if (options.plan.kind !== "runtime") return;
11607
+ const runtimeVariant = options.variant ?? "default";
11608
+ if (runtimeVariant.length > 64 || !RUNTIME_VARIANT_PATTERN.test(runtimeVariant)) {
11609
+ throw new Error(
11610
+ "Managed runtime variant must use lowercase alphanumeric segments separated by hyphens and be at most 64 characters."
11611
+ );
11612
+ }
11613
+ const hostPorts = options.hostPorts ?? {};
11614
+ assertManagedHostPortsAvailable(hostPorts);
11615
+ const portEnvironment = managedHostPortVariables(hostPorts);
11616
+ const portIdentity = managedHostPortIdentity(hostPorts) ?? "";
11617
+ const helper = import_node_fs29.default.readFileSync(resolveProcessHelperPath());
11618
+ deliverRuntimeFile(
11619
+ options.container.id,
11620
+ RUNTIME_HELPER_PATH,
11621
+ helper,
11622
+ "the managed process helper"
11623
+ );
11624
+ const runtimeAdapterPath = `/tmp/devrouter/bin/managed-post-start-${options.plan.adapterSha256}`;
11625
+ deliverRuntimeFile(
11626
+ options.container.id,
11627
+ runtimeAdapterPath,
11628
+ options.plan.adapterContents,
11629
+ "the managed post-start adapter"
11630
+ );
11631
+ const started2 = (0, import_node_child_process20.spawnSync)(
11632
+ "docker",
11633
+ [
11634
+ "exec",
11635
+ "--workdir",
11636
+ options.container.workspacePath,
11637
+ "--env",
11638
+ `DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
11639
+ "--env",
11640
+ `DEVROUTER_PROCESS_ADAPTER_SHA256=${options.plan.adapterSha256}`,
11641
+ "--env",
11642
+ `DEVROUTER_PROCESS_HOST_PORTS_SHA256=${portIdentity}`,
11643
+ ...Object.entries(portEnvironment).flatMap(([name, value]) => ["--env", `${name}=${value}`]),
11644
+ "--env",
11645
+ `DEVROUTER_RUNTIME_VARIANT=${runtimeVariant}`,
11646
+ ...options.profile ? ["--env", `DEVROUTER_PROFILE=${options.profile}`] : [],
11647
+ ...options.processes ? [
11648
+ "--env",
11649
+ `DEVROUTER_PROCESS_SET=${options.processes.join(",")}`,
11650
+ "--env",
11651
+ "DEVROUTER_PROCESS_FINGERPRINT_ENV=DEVROUTER_PROFILE,DEVROUTER_PROCESS_SET"
11652
+ ] : [],
11653
+ options.container.id,
11654
+ "bash",
11655
+ "-c",
11656
+ ADAPTER_WRAPPER,
11657
+ options.plan.adapterPath,
11658
+ runtimeAdapterPath
11659
+ ],
11660
+ {
11661
+ stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit",
11662
+ ...networkDockerOptions()
11663
+ }
11664
+ );
11665
+ if (started2.status !== 0) {
11666
+ const details = commandFailure2(started2);
11667
+ throw new Error(`Managed post-start failed${details ? `: ${details}.` : "."}`);
11668
+ }
11669
+ }
11670
+ function assertSafeProcessName(name) {
11671
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
11672
+ throw new Error(`Managed process '${name}' is not a safe exact identifier.`);
11673
+ }
11674
+ }
11675
+ function runManagedProcessAction(options) {
11676
+ assertSafeProcessName(options.name);
11677
+ const result = (0, import_node_child_process20.spawnSync)(
11678
+ "docker",
11679
+ [
11680
+ "exec",
11681
+ "--workdir",
11682
+ options.container.workspacePath,
11683
+ "--env",
11684
+ `DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
11685
+ options.container.id,
11686
+ "bash",
11687
+ "-c",
11688
+ 'exec "$DEVROUTER_PROCESS_HELPER" "$1" --name "$2"',
11689
+ "devrouter-process",
11690
+ options.action,
11691
+ options.name
11692
+ ],
11693
+ {
11694
+ encoding: "utf-8",
11695
+ stdio: options.action === "status" ? ["ignore", "pipe", "pipe"] : ["ignore", 2, "inherit"],
11696
+ ...networkDockerOptions()
11697
+ }
11698
+ );
11699
+ if (options.action === "stop") {
11700
+ if (result.status !== 0) {
11701
+ throw new Error(`Could not stop managed process '${options.name}'.`);
11702
+ }
11703
+ return "stopped";
11704
+ }
11705
+ try {
11706
+ const parsed = JSON.parse(result.stdout.trim());
11707
+ if (parsed.status === "running" || parsed.status === "stopped" || parsed.status === "foreign" || parsed.status === "drifted") {
11708
+ return parsed.status;
11709
+ }
11710
+ } catch {
11711
+ }
11712
+ return "drifted";
11713
+ }
11714
+
11715
+ // src/core/recovery-budget.ts
11716
+ function findRecoveryUnit(incident, unit) {
11717
+ return (incident.units ?? []).find((record5) => record5.key === unit.key);
11718
+ }
11719
+ function restartsFor(kind, limits) {
11720
+ return kind === "service" ? limits.maxServiceRestarts : limits.maxProcessRestarts;
11721
+ }
11722
+ function recoveryBudgetRefusal(input) {
11723
+ const startedAtMs = input.incident.startedAtMs ?? input.nowMs;
11724
+ const elapsed = input.activeElapsedMs ?? Math.max(0, input.nowMs - startedAtMs);
11725
+ if (elapsed > input.limits.windowSeconds * 1e3) return "window-closed";
11726
+ if (input.unit !== void 0) {
11727
+ const record5 = findRecoveryUnit(input.incident, input.unit);
11728
+ if (record5 !== void 0 && record5.actions >= restartsFor(input.unit.kind, input.limits))
11729
+ return "unit-exhausted";
11730
+ }
11731
+ if (input.incident.correctiveActionsTaken >= input.incident.actionLimit)
11732
+ return "budget-exhausted";
11733
+ return void 0;
11734
+ }
11735
+ function claimRecoveryUnit(input) {
11736
+ const reason2 = recoveryBudgetRefusal(input);
11737
+ if (reason2 !== void 0) return { ok: false, reason: reason2 };
11738
+ const units = (input.incident.units ?? []).map((record5) => ({ ...record5 }));
11739
+ if (input.unit !== void 0) {
11740
+ const index = units.findIndex((record5) => record5.key === input.unit?.key);
11741
+ if (index === -1) {
11742
+ if (units.length >= RELIABILITY_MAX_ITEMS) return { ok: false, reason: "unit-exhausted" };
11743
+ units.push({ ...input.unit, actions: 1, lastActionAtMs: input.nowMs });
11744
+ } else {
11745
+ const record5 = units[index];
11746
+ units[index] = {
11747
+ ...record5,
11748
+ actions: record5.actions + 1,
11749
+ lastActionAtMs: input.nowMs
11750
+ };
11751
+ }
11752
+ }
11753
+ return {
11754
+ ok: true,
11755
+ incident: {
11756
+ ...input.incident,
11757
+ startedAtMs: input.incident.startedAtMs ?? input.nowMs,
11758
+ correctiveActionsTaken: input.incident.correctiveActionsTaken + 1,
11759
+ units
11760
+ }
11761
+ };
11762
+ }
11763
+
11764
+ // src/core/reliability-model.ts
11765
+ var INFRASTRUCTURE_VALUES = ["healthy", "failed", "unknown"];
11766
+ var APPLICATION_VALUES = ["verified", "unready", "unverified"];
11767
+ var ADMISSION_VALUES = ["admitted", "waiting", "unknown", "denied-unadmittable"];
11768
+ function record3(value) {
11769
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11770
+ }
11771
+ function oneOf2(value, values) {
11772
+ return typeof value === "string" && values.includes(value);
11773
+ }
11774
+ function uniqueIds2(values) {
11775
+ return new Set(values).size === values.length && values.every(isReliabilityId);
11776
+ }
11777
+ function isFence(value) {
11778
+ return record3(value) && isReliabilityId(value.environmentId) && isReliabilityCounter(value.intentRevision) && isReliabilityCounter(value.runtimeGeneration) && isReliabilityCounter(value.controllerEpoch);
11779
+ }
11780
+ function isConsumer(value) {
11781
+ return record3(value) && isReliabilityId(value.id) && typeof value.pinned === "boolean" && Array.isArray(value.requiredCapabilities) && value.requiredCapabilities.length <= 128 && uniqueIds2(value.requiredCapabilities);
11782
+ }
11783
+ function isObservation(value) {
11784
+ return record3(value) && isReliabilityId(value.capability) && oneOf2(value.infrastructure, INFRASTRUCTURE_VALUES) && oneOf2(value.application, APPLICATION_VALUES) && isReliabilityCounter(value.observedAtMs) && isReliabilityCounter(value.validForMs);
11785
+ }
11786
+ function assertReliabilityEvent(value) {
11787
+ if (!record3(value) || !isFence(value) || typeof value.type !== "string") {
11788
+ throw new Error("Invalid reliability event.");
11789
+ }
11790
+ let valid = false;
11791
+ switch (value.type) {
11792
+ case "request":
11793
+ valid = oneOf2(value.mode, ["start", "attach"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer);
11794
+ break;
11795
+ case "operation-request":
11796
+ valid = oneOf2(value.kind, ["ensure", "exec"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer) && typeof value.runtimeRunning === "boolean" && (value.recoverInterruptedEnsure === void 0 || typeof value.recoverInterruptedEnsure === "boolean");
11797
+ break;
11798
+ case "drained":
11799
+ valid = isReliabilityId(value.operationId);
11800
+ break;
11801
+ case "release":
11802
+ valid = isReliabilityId(value.consumerId);
11803
+ break;
11804
+ case "stop":
11805
+ case "park":
11806
+ case "dispatch":
11807
+ valid = true;
11808
+ break;
11809
+ case "admission":
11810
+ valid = oneOf2(value.result, ADMISSION_VALUES);
11811
+ break;
11812
+ case "resume":
11813
+ valid = typeof value.pressureDwellSatisfied === "boolean" && isReliabilityId(value.operationId);
11814
+ break;
11815
+ case "dispatch-persisted":
10614
11816
  case "launched":
10615
11817
  case "interrupted":
10616
11818
  case "not-started":
@@ -11278,10 +12480,10 @@ function stepReliability(state, event, nowMs) {
11278
12480
  }
11279
12481
 
11280
12482
  // src/core/reliability-worker.ts
11281
- var import_node_child_process19 = require("child_process");
11282
- var import_node_crypto17 = require("crypto");
11283
- var import_node_fs29 = __toESM(require("fs"));
11284
- var import_node_path34 = __toESM(require("path"));
12483
+ var import_node_child_process21 = require("child_process");
12484
+ var import_node_crypto19 = require("crypto");
12485
+ var import_node_fs30 = __toESM(require("fs"));
12486
+ var import_node_path35 = __toESM(require("path"));
11285
12487
 
11286
12488
  // src/core/lifecycle-progress.ts
11287
12489
  var ROLES = {
@@ -11347,11 +12549,11 @@ function workerGroupAbsent(pid) {
11347
12549
  if (process.platform !== "linux") return false;
11348
12550
  let observedZombie = false;
11349
12551
  try {
11350
- for (const entry of import_node_fs29.default.readdirSync("/proc")) {
12552
+ for (const entry of import_node_fs30.default.readdirSync("/proc")) {
11351
12553
  if (!/^\d+$/.test(entry)) continue;
11352
12554
  let stat;
11353
12555
  try {
11354
- stat = import_node_fs29.default.readFileSync(`/proc/${entry}/stat`, "utf8");
12556
+ stat = import_node_fs30.default.readFileSync(`/proc/${entry}/stat`, "utf8");
11355
12557
  } catch (error) {
11356
12558
  if (error.code === "ENOENT") continue;
11357
12559
  return false;
@@ -11371,20 +12573,20 @@ function workerGroupAbsent(pid) {
11371
12573
  }
11372
12574
 
11373
12575
  // src/core/workspace-ensure.ts
11374
- var import_node_child_process30 = require("child_process");
12576
+ var import_node_child_process31 = require("child_process");
11375
12577
  var import_node_fs35 = __toESM(require("fs"));
11376
12578
  var import_node_path40 = __toESM(require("path"));
11377
12579
 
11378
12580
  // src/core/devpod-mutation.ts
11379
- var import_node_child_process20 = require("child_process");
11380
- var import_node_fs30 = __toESM(require("fs"));
11381
- var import_node_path35 = __toESM(require("path"));
11382
- var DEVPOD_MUTATION_LOCK_FILE = import_node_path35.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
12581
+ var import_node_child_process22 = require("child_process");
12582
+ var import_node_fs31 = __toESM(require("fs"));
12583
+ var import_node_path36 = __toESM(require("path"));
12584
+ var DEVPOD_MUTATION_LOCK_FILE = import_node_path36.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
11383
12585
  var DEVPOD_MUTATION_WAIT_MS = 18e5;
11384
12586
  var DevpodStartPostconditionError = class extends Error {
11385
12587
  };
11386
12588
  function withMutationLock2(activity, target, operation) {
11387
- import_node_fs30.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
12589
+ import_node_fs31.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
11388
12590
  return withFileLockSync(
11389
12591
  DEVPOD_MUTATION_LOCK_FILE,
11390
12592
  {
@@ -11397,20 +12599,20 @@ function withMutationLock2(activity, target, operation) {
11397
12599
  operation
11398
12600
  );
11399
12601
  }
11400
- function commandFailure2(result) {
12602
+ function commandFailure3(result) {
11401
12603
  return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
11402
12604
  }
11403
12605
  function runDevpodAction(action, devpodId, force = false, repoPath) {
11404
12606
  const args = action === "delete" ? [action, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action, devpodId];
11405
12607
  claimLifecycleEffect();
11406
12608
  const binding = repoPath ? networkProviderEffectOptions("devpod", devpodId, repoPath) : { args: [] };
11407
- const result = (0, import_node_child_process20.spawnSync)("devpod", [...args, ...binding.args], {
12609
+ const result = (0, import_node_child_process22.spawnSync)("devpod", [...args, ...binding.args], {
11408
12610
  encoding: "utf-8",
11409
12611
  ...binding.env ? { env: binding.env } : {}
11410
12612
  });
11411
12613
  if (result.status !== 0) {
11412
12614
  throw new Error(
11413
- `devpod ${action}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure2(result) || "unknown error"}`
12615
+ `devpod ${action}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
11414
12616
  );
11415
12617
  }
11416
12618
  }
@@ -11545,7 +12747,7 @@ async function startDevpodWorkspace(options) {
11545
12747
  }
11546
12748
  claimLifecycleEffect();
11547
12749
  network?.beforeDispatch?.();
11548
- const result = (0, import_node_child_process20.spawnSync)("devpod", args, {
12750
+ const result = (0, import_node_child_process22.spawnSync)("devpod", args, {
11549
12751
  stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
11550
12752
  env
11551
12753
  });
@@ -11585,8 +12787,8 @@ async function startDevpodWorkspace(options) {
11585
12787
  }
11586
12788
 
11587
12789
  // src/core/host-port-claims.ts
11588
- var import_node_child_process21 = require("child_process");
11589
- var import_node_path36 = __toESM(require("path"));
12790
+ var import_node_child_process23 = require("child_process");
12791
+ var import_node_path37 = __toESM(require("path"));
11590
12792
  var COMPOSE_RENDER_TIMEOUT_MS = 3e4;
11591
12793
  var COMPOSE_RENDER_MAX_BUFFER = 8 * 1024 * 1024;
11592
12794
  var DOCKER_TIMEOUT_MS = 1e4;
@@ -11689,7 +12891,7 @@ function hostPortBindingsConflict(desired, holder) {
11689
12891
  }
11690
12892
  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}}}';
11691
12893
  function runDocker(args) {
11692
- const result = (0, import_node_child_process21.spawnSync)("docker", args, {
12894
+ const result = (0, import_node_child_process23.spawnSync)("docker", args, {
11693
12895
  ...networkDockerOptions(),
11694
12896
  encoding: "utf-8",
11695
12897
  timeout: DOCKER_TIMEOUT_MS,
@@ -11759,7 +12961,7 @@ function listHolderSnapshots() {
11759
12961
  }
11760
12962
  function renderManagedComposeModel(plan, workspace) {
11761
12963
  const fileArgs = plan.composeFiles.flatMap((file) => ["-f", file]);
11762
- const result = (0, import_node_child_process21.spawnSync)(
12964
+ const result = (0, import_node_child_process23.spawnSync)(
11763
12965
  "docker",
11764
12966
  [
11765
12967
  "compose",
@@ -11794,8 +12996,8 @@ function renderManagedComposeModel(plan, workspace) {
11794
12996
  }
11795
12997
  }
11796
12998
  function attributeHolderWorktree(workingDir) {
11797
- if (import_node_path36.default.basename(workingDir) !== ".devcontainer") return void 0;
11798
- return comparableWorkspacePath(import_node_path36.default.dirname(workingDir));
12999
+ if (import_node_path37.default.basename(workingDir) !== ".devcontainer") return void 0;
13000
+ return comparableWorkspacePath(import_node_path37.default.dirname(workingDir));
11799
13001
  }
11800
13002
  function holderAttribution(holder, repoPath) {
11801
13003
  const attribution = {};
@@ -11830,7 +13032,7 @@ function detectHostPortClaimConflicts(options) {
11830
13032
  );
11831
13033
  if (desired.length === 0) return [];
11832
13034
  const holders = listHolderSnapshots();
11833
- const devcontainerDir = import_node_path36.default.join(options.repoPath, ".devcontainer");
13035
+ const devcontainerDir = import_node_path37.default.join(options.repoPath, ".devcontainer");
11834
13036
  const conflicts = [];
11835
13037
  for (const binding of desired) {
11836
13038
  for (const holder of holders) {
@@ -11854,7 +13056,7 @@ function detectHostPortClaimConflicts(options) {
11854
13056
  }
11855
13057
 
11856
13058
  // src/core/http-route-probe.ts
11857
- var import_node_child_process22 = require("child_process");
13059
+ var import_node_child_process24 = require("child_process");
11858
13060
  var DEFAULT_MAX_TIME_SECONDS = 5;
11859
13061
  var PARENT_TIMEOUT_PADDING_MS = 250;
11860
13062
  var CONTRACT_MAX_BUFFER = 1024;
@@ -11887,7 +13089,7 @@ function probeHttpRoute(host, options = {}) {
11887
13089
  args.push("--cacert", CERT_FILE);
11888
13090
  }
11889
13091
  args.push(url);
11890
- const result = (0, import_node_child_process22.spawnSync)("curl", args, { encoding: "utf-8" });
13092
+ const result = (0, import_node_child_process24.spawnSync)("curl", args, { encoding: "utf-8" });
11891
13093
  const stdout = result.stdout?.trim() ?? "";
11892
13094
  const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
11893
13095
  const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
@@ -11906,7 +13108,7 @@ function probeHttpReadinessSync(host, readiness, maxTimeSeconds, repoPath) {
11906
13108
  }
11907
13109
  let result;
11908
13110
  try {
11909
- result = (0, import_node_child_process22.spawnSync)("curl", args, {
13111
+ result = (0, import_node_child_process24.spawnSync)("curl", args, {
11910
13112
  encoding: "utf-8",
11911
13113
  maxBuffer: CONTRACT_MAX_BUFFER,
11912
13114
  timeout: Math.ceil(parentTimeoutMs)
@@ -11990,8 +13192,8 @@ function transportFailure() {
11990
13192
  }
11991
13193
 
11992
13194
  // src/core/managed-host-preparation.ts
11993
- var import_node_child_process23 = require("child_process");
11994
- var import_node_fs31 = __toESM(require("fs"));
13195
+ var import_node_child_process25 = require("child_process");
13196
+ var import_node_fs32 = __toESM(require("fs"));
11995
13197
  var PREPARATION_TIMEOUT_MS = 6e4;
11996
13198
  var CONFIG_READ_ERROR = "Managed host preparation could not read .devrouter.yml.";
11997
13199
  var CONFIG_CHANGED_ERROR = "Managed host preparation changed .devrouter.yml; refusing to continue.";
@@ -12002,7 +13204,7 @@ function runPreparationCommand(argv, cwd) {
12002
13204
  return new Promise((resolve) => {
12003
13205
  let child;
12004
13206
  try {
12005
- child = (0, import_node_child_process23.spawn)(argv[0], argv.slice(1), {
13207
+ child = (0, import_node_child_process25.spawn)(argv[0], argv.slice(1), {
12006
13208
  cwd,
12007
13209
  detached: true,
12008
13210
  shell: false,
@@ -12034,7 +13236,7 @@ function runPreparationCommand(argv, cwd) {
12034
13236
  }
12035
13237
  function readConfigBytes(configPath) {
12036
13238
  try {
12037
- return import_node_fs31.default.readFileSync(configPath);
13239
+ return import_node_fs32.default.readFileSync(configPath);
12038
13240
  } catch {
12039
13241
  return void 0;
12040
13242
  }
@@ -12067,219 +13269,8 @@ async function runManagedHostPreparation(repoPath, config) {
12067
13269
  }
12068
13270
  }
12069
13271
 
12070
- // src/core/managed-post-start.ts
12071
- var import_node_child_process24 = require("child_process");
12072
- var import_node_crypto18 = require("crypto");
12073
- var import_node_fs32 = __toESM(require("fs"));
12074
- var import_node_path37 = __toESM(require("path"));
12075
- var MANAGED_MARKER = "devrouter:managed devcontainer";
12076
- var MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
12077
- var RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
12078
- var ADAPTER_WRAPPER = `adapter_snapshot="$1"
12079
- shift
12080
- readonly DEVROUTER_PROCESS_HELPER DEVROUTER_PROCESS_ADAPTER_SHA256
12081
- source "$adapter_snapshot"
12082
- `;
12083
- function renderDeliveryScript(targetPath) {
12084
- if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
12085
- throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
12086
- }
12087
- const temporaryPrefix = import_node_path37.default.posix.basename(targetPath);
12088
- return `set -eu
12089
- umask 077
12090
- runtime_root=/tmp/devrouter
12091
- runtime_bin="$runtime_root/bin"
12092
- target="${targetPath}"
12093
- if [ -L "$runtime_root" ] || [ -L "$runtime_bin" ]; then
12094
- echo "Refusing symlinked devrouter runtime path." >&2
12095
- exit 1
12096
- fi
12097
- mkdir -p "$runtime_bin"
12098
- chmod 700 "$runtime_root" "$runtime_bin"
12099
- temporary="$(mktemp "$runtime_bin/.${temporaryPrefix}.XXXXXX")"
12100
- trap 'rm -f "$temporary"' EXIT
12101
- cat > "$temporary"
12102
- chmod 700 "$temporary"
12103
- mv -f "$temporary" "$target"
12104
- trap - EXIT
12105
- `;
12106
- }
12107
- function commandFailure3(result) {
12108
- return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
12109
- }
12110
- function deliverRuntimeFile(containerId, targetPath, contents, label) {
12111
- const result = (0, import_node_child_process24.spawnSync)(
12112
- "docker",
12113
- ["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
12114
- { input: contents, encoding: "utf-8", ...networkDockerOptions() }
12115
- );
12116
- if (result.status !== 0) {
12117
- const details = commandFailure3(result);
12118
- throw new Error(`Could not deliver ${label}${details ? `: ${details}` : "."}`);
12119
- }
12120
- }
12121
- function readRegularFileBytes(filePath) {
12122
- if (!import_node_fs32.default.existsSync(filePath) || !import_node_fs32.default.lstatSync(filePath).isFile()) return void 0;
12123
- return import_node_fs32.default.readFileSync(filePath);
12124
- }
12125
- function readRegularFile(filePath) {
12126
- return readRegularFileBytes(filePath)?.toString("utf-8");
12127
- }
12128
- function adapterFingerprint(adapter) {
12129
- return (0, import_node_crypto18.createHash)("sha256").update(adapter).digest("hex");
12130
- }
12131
- function resolveProcessHelperPath() {
12132
- const candidates = [
12133
- import_node_path37.default.resolve(__dirname, "..", "bin", "devrouter-process"),
12134
- import_node_path37.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
12135
- ];
12136
- const helperPath = candidates.find((candidate) => import_node_fs32.default.existsSync(candidate));
12137
- if (!helperPath) {
12138
- throw new Error("Could not locate the packaged devrouter-process helper.");
12139
- }
12140
- return helperPath;
12141
- }
12142
- function resolveManagedPostStartPlan(repoPath) {
12143
- const adapterPath = import_node_path37.default.join(repoPath, MANAGED_ADAPTER_PATH);
12144
- const adapterBytes = readRegularFileBytes(adapterPath);
12145
- const adapterText = adapterBytes?.toString("utf-8");
12146
- const dockerfilePath = import_node_path37.default.join(repoPath, ".devcontainer", "Dockerfile");
12147
- const devcontainerPath = import_node_path37.default.join(repoPath, ".devcontainer", "devcontainer.json");
12148
- const dockerfile = readRegularFile(dockerfilePath) ?? "";
12149
- const devcontainer = readRegularFile(devcontainerPath) ?? "";
12150
- const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
12151
- (value) => value.includes("DEVROUTER_PROCESS_HELPER") || value.includes("devrouter-process")
12152
- );
12153
- if (adapterBytes === void 0) {
12154
- if (devrouterPattern) {
12155
- throw new Error(
12156
- "Devrouter lifecycle wiring is incomplete: add the managed post-start adapter or remove the stale devrouter-process references."
12157
- );
12158
- }
12159
- return { kind: "unmanaged" };
12160
- }
12161
- const adapter = adapterBytes.toString("utf-8");
12162
- if (!adapter.includes(MANAGED_MARKER)) {
12163
- if (devrouterPattern) {
12164
- throw new Error(
12165
- "Devrouter-looking post-start adapter is missing the 'devrouter:managed devcontainer' marker. Regenerate or migrate the devcontainer, then retry devrouter ensure."
12166
- );
12167
- }
12168
- return { kind: "unmanaged" };
12169
- }
12170
- assertManagedDevcontainerLifecycle(repoPath);
12171
- if (adapter.includes("DEVROUTER_PROCESS_HELPER")) {
12172
- return {
12173
- kind: "runtime",
12174
- adapterPath: MANAGED_ADAPTER_PATH,
12175
- adapterSha256: adapterFingerprint(adapterBytes),
12176
- adapterContents: adapterBytes
12177
- };
12178
- }
12179
- if (dockerfile.includes("devrouter-process") && devcontainer.includes("postStartCommand")) {
12180
- return { kind: "legacy" };
12181
- }
12182
- throw new Error(
12183
- "Managed post-start must use DEVROUTER_PROCESS_HELPER before removing the legacy image helper and postStartCommand. Regenerate or migrate the devcontainer, then retry devrouter ensure."
12184
- );
12185
- }
12186
- function runManagedPostStart(options) {
12187
- if (options.plan.kind !== "runtime") return;
12188
- const helper = import_node_fs32.default.readFileSync(resolveProcessHelperPath());
12189
- deliverRuntimeFile(
12190
- options.container.id,
12191
- RUNTIME_HELPER_PATH,
12192
- helper,
12193
- "the managed process helper"
12194
- );
12195
- const runtimeAdapterPath = `/tmp/devrouter/bin/managed-post-start-${options.plan.adapterSha256}`;
12196
- deliverRuntimeFile(
12197
- options.container.id,
12198
- runtimeAdapterPath,
12199
- options.plan.adapterContents,
12200
- "the managed post-start adapter"
12201
- );
12202
- const started2 = (0, import_node_child_process24.spawnSync)(
12203
- "docker",
12204
- [
12205
- "exec",
12206
- "--workdir",
12207
- options.container.workspacePath,
12208
- "--env",
12209
- `DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
12210
- "--env",
12211
- `DEVROUTER_PROCESS_ADAPTER_SHA256=${options.plan.adapterSha256}`,
12212
- ...options.profile ? ["--env", `DEVROUTER_PROFILE=${options.profile}`] : [],
12213
- ...options.processes ? [
12214
- "--env",
12215
- `DEVROUTER_PROCESS_SET=${options.processes.join(",")}`,
12216
- "--env",
12217
- "DEVROUTER_PROCESS_FINGERPRINT_ENV=DEVROUTER_PROFILE,DEVROUTER_PROCESS_SET"
12218
- ] : [],
12219
- options.container.id,
12220
- "bash",
12221
- "-c",
12222
- ADAPTER_WRAPPER,
12223
- options.plan.adapterPath,
12224
- runtimeAdapterPath
12225
- ],
12226
- {
12227
- stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit",
12228
- ...networkDockerOptions()
12229
- }
12230
- );
12231
- if (started2.status !== 0) {
12232
- const details = commandFailure3(started2);
12233
- throw new Error(`Managed post-start failed${details ? `: ${details}.` : "."}`);
12234
- }
12235
- }
12236
- function assertSafeProcessName(name) {
12237
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
12238
- throw new Error(`Managed process '${name}' is not a safe exact identifier.`);
12239
- }
12240
- }
12241
- function runManagedProcessAction(options) {
12242
- assertSafeProcessName(options.name);
12243
- const result = (0, import_node_child_process24.spawnSync)(
12244
- "docker",
12245
- [
12246
- "exec",
12247
- "--workdir",
12248
- options.container.workspacePath,
12249
- "--env",
12250
- `DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
12251
- options.container.id,
12252
- "bash",
12253
- "-c",
12254
- 'exec "$DEVROUTER_PROCESS_HELPER" "$1" --name "$2"',
12255
- "devrouter-process",
12256
- options.action,
12257
- options.name
12258
- ],
12259
- {
12260
- encoding: "utf-8",
12261
- stdio: options.action === "status" ? ["ignore", "pipe", "pipe"] : ["ignore", 2, "inherit"],
12262
- ...networkDockerOptions()
12263
- }
12264
- );
12265
- if (options.action === "stop") {
12266
- if (result.status !== 0) {
12267
- throw new Error(`Could not stop managed process '${options.name}'.`);
12268
- }
12269
- return "stopped";
12270
- }
12271
- try {
12272
- const parsed = JSON.parse(result.stdout.trim());
12273
- if (parsed.status === "running" || parsed.status === "stopped" || parsed.status === "foreign" || parsed.status === "drifted") {
12274
- return parsed.status;
12275
- }
12276
- } catch {
12277
- }
12278
- return "drifted";
12279
- }
12280
-
12281
13272
  // src/core/managed-mount-nesting.ts
12282
- var import_node_child_process25 = require("child_process");
13273
+ var import_node_child_process26 = require("child_process");
12283
13274
  var NESTED_MOUNT_REASON = "no configured mount is nested inside another configured mount";
12284
13275
  var UNREADABLE_MOUNT_REASON = "the container's own mount table could not be read";
12285
13276
  function normalizeDestination(value) {
@@ -12352,7 +13343,7 @@ function classifyManagedMountNesting(configured, effectiveDestinations, containe
12352
13343
  };
12353
13344
  }
12354
13345
  function readContainerMountinfo(containerId, options) {
12355
- const result = (0, import_node_child_process25.spawnSync)("docker", ["exec", containerId, "cat", "/proc/self/mountinfo"], {
13346
+ const result = (0, import_node_child_process26.spawnSync)("docker", ["exec", containerId, "cat", "/proc/self/mountinfo"], {
12356
13347
  encoding: "utf-8",
12357
13348
  timeout: options.timeoutMs ?? 5e3,
12358
13349
  stdio: ["ignore", "pipe", "pipe"],
@@ -12373,8 +13364,9 @@ function recoveryPath(repoPath) {
12373
13364
  if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(repoPath)) return repoPath;
12374
13365
  return `'${repoPath.replaceAll("'", "'\\''")}'`;
12375
13366
  }
12376
- function reliabilityRecovery(reason2, repoPath) {
13367
+ function reliabilityRecovery(reason2, repoPath, variant) {
12377
13368
  const checkout = recoveryPath(repoPath);
13369
+ const variantOption = variant && variant !== "default" ? ` --variant ${variant}` : "";
12378
13370
  switch (reason2) {
12379
13371
  case "stop-incomplete":
12380
13372
  case "operation-unknown":
@@ -12382,12 +13374,12 @@ function reliabilityRecovery(reason2, repoPath) {
12382
13374
  case "start-refused":
12383
13375
  return [
12384
13376
  `Run: devrouter doctor --repo ${checkout} to read the refused fixed host-port claim and its holder`,
12385
- `Stop the holding workspace or change this repository's own published binding, then run: devrouter ensure ${checkout}`
13377
+ `Stop the holding workspace or change this repository's own published binding, then run: devrouter ensure ${checkout}${variantOption}`
12386
13378
  ];
12387
13379
  case "unadmittable":
12388
13380
  return [
12389
13381
  `Run: devrouter stop ${checkout}`,
12390
- `Lower the requested capacity in .devrouter.yml, then run: devrouter ensure ${checkout}`
13382
+ `Lower the requested capacity in .devrouter.yml, then run: devrouter ensure ${checkout}${variantOption}`
12391
13383
  ];
12392
13384
  case "capacity-parked":
12393
13385
  return [
@@ -12396,7 +13388,9 @@ function reliabilityRecovery(reason2, repoPath) {
12396
13388
  `'devrouter ensure' refuses while the journal stays parked; release the intent instead with: devrouter stop ${checkout}`
12397
13389
  ];
12398
13390
  case "capacity-waiting":
12399
- return [`Run: devrouter ensure ${checkout} (still queued until capacity frees up)`];
13391
+ return [
13392
+ `Run: devrouter ensure ${checkout}${variantOption} (still queued until capacity frees up)`
13393
+ ];
12400
13394
  default:
12401
13395
  return [];
12402
13396
  }
@@ -12437,7 +13431,12 @@ function readReliabilityStatus(options) {
12437
13431
  correctiveActionsTaken: record5.state.incident.correctiveActionsTaken,
12438
13432
  actionLimit: record5.state.incident.actionLimit
12439
13433
  } : null,
12440
- ...reason2 ? { attention: { reason: reason2, recovery: reliabilityRecovery(reason2, options.repoPath) } } : {}
13434
+ ...reason2 ? {
13435
+ attention: {
13436
+ reason: reason2,
13437
+ recovery: reliabilityRecovery(reason2, options.repoPath, options.variant)
13438
+ }
13439
+ } : {}
12441
13440
  };
12442
13441
  }
12443
13442
  var EMPTY_RESOURCES = {
@@ -12762,6 +13761,10 @@ function collectManagedRuntimeStatus(options) {
12762
13761
  }
12763
13762
  const drift = [];
12764
13763
  if (stateReadFailed) drift.push("managed runtime state could not be read");
13764
+ if (state && options.variant !== void 0 && (state.variant ?? "default") !== options.variant)
13765
+ drift.push("active runtime variant differs from the selected variant");
13766
+ if (state?.variant && state.variant !== "default" && !managedRuntime.variants?.includes(state.variant))
13767
+ drift.push("active runtime variant is no longer declared");
12765
13768
  if (state?.status === "degraded") drift.push("the last managed transition is degraded");
12766
13769
  if (state) {
12767
13770
  const desiredState = {
@@ -12843,6 +13846,7 @@ function collectManagedRuntimeStatus(options) {
12843
13846
  };
12844
13847
  const reliability = readReliabilityStatus({
12845
13848
  repoPath: options.repoPath,
13849
+ variant: state?.variant,
12846
13850
  ...options.workspace !== void 0 ? { workspace: options.workspace } : {}
12847
13851
  });
12848
13852
  return {
@@ -12850,7 +13854,7 @@ function collectManagedRuntimeStatus(options) {
12850
13854
  status,
12851
13855
  profile: options.profile,
12852
13856
  ...reliability ? { reliability } : {},
12853
- ...state?.profile ? { activeProfile: state.profile } : {},
13857
+ ...state?.profile ? { activeProfile: state.profile, activeVariant: state.variant ?? "default" } : {},
12854
13858
  ...options.workspace !== void 0 ? { workspace: options.workspace } : {},
12855
13859
  ...state?.devpodId ? { devpodId: state.devpodId } : {},
12856
13860
  ...inspection.composeProject ? { composeProject: inspection.composeProject } : {},
@@ -12874,7 +13878,7 @@ function collectManagedRuntimeStatus(options) {
12874
13878
  }
12875
13879
 
12876
13880
  // src/core/network-inventory.ts
12877
- var import_node_child_process26 = require("child_process");
13881
+ var import_node_child_process27 = require("child_process");
12878
13882
  var NetworkInventoryError = class extends Error {
12879
13883
  };
12880
13884
  var MAX_IDS = 4096;
@@ -12886,7 +13890,7 @@ function readDocker(endpoint, args, timeoutMs) {
12886
13890
  const env = { ...process.env };
12887
13891
  delete env.DOCKER_CONTEXT;
12888
13892
  delete env.DOCKER_HOST;
12889
- const result = (0, import_node_child_process26.spawnSync)("docker", ["--host", endpoint, ...args], {
13893
+ const result = (0, import_node_child_process27.spawnSync)("docker", ["--host", endpoint, ...args], {
12890
13894
  env,
12891
13895
  encoding: "utf-8",
12892
13896
  timeout: timeoutMs,
@@ -13050,7 +14054,7 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
13050
14054
  }
13051
14055
 
13052
14056
  // src/core/network-routes.ts
13053
- var import_node_child_process27 = require("child_process");
14057
+ var import_node_child_process28 = require("child_process");
13054
14058
  var import_node_net3 = require("net");
13055
14059
 
13056
14060
  // src/core/network-local-daemon.ts
@@ -13119,7 +14123,7 @@ function parseLinuxNetworkRoutes(output) {
13119
14123
  function collectNetworkRoutes(options) {
13120
14124
  if ((options.platform ?? process.platform) !== "linux" || !hasLocalDockerNetworkNamespace(options.endpoint))
13121
14125
  return { status: "unknown", routes: [] };
13122
- const result = (0, import_node_child_process27.spawnSync)("ip", ["-j", "-4", "route", "show", "table", "all"], {
14126
+ const result = (0, import_node_child_process28.spawnSync)("ip", ["-j", "-4", "route", "show", "table", "all"], {
13123
14127
  encoding: "utf8",
13124
14128
  timeout: 5e3,
13125
14129
  maxBuffer: 1024 * 1024
@@ -13155,10 +14159,10 @@ function inspectLegacyNetworkCapacity(input) {
13155
14159
  }
13156
14160
 
13157
14161
  // src/core/network-managed.ts
13158
- var import_node_crypto21 = require("crypto");
14162
+ var import_node_crypto22 = require("crypto");
13159
14163
 
13160
14164
  // src/core/network-compose.ts
13161
- var import_node_crypto19 = require("crypto");
14165
+ var import_node_crypto20 = require("crypto");
13162
14166
  var SAFE_ENDPOINT_UPPER_BOUND_SEMANTICS = "full-lifecycle-demand";
13163
14167
  function isRecord5(value) {
13164
14168
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -13964,12 +14968,12 @@ function stableValue(value) {
13964
14968
  }
13965
14969
  function fingerprintComposeNetworkModel(input) {
13966
14970
  const projection = normalizedFingerprintProjection(input);
13967
- return (0, import_node_crypto19.createHash)("sha256").update(stableValue(projection), "utf8").digest("hex");
14971
+ return (0, import_node_crypto20.createHash)("sha256").update(stableValue(projection), "utf8").digest("hex");
13968
14972
  }
13969
14973
 
13970
14974
  // src/core/network-compose-files.ts
13971
- var import_node_child_process28 = require("child_process");
13972
- var import_node_crypto20 = require("crypto");
14975
+ var import_node_child_process29 = require("child_process");
14976
+ var import_node_crypto21 = require("crypto");
13973
14977
  var import_node_fs34 = __toESM(require("fs"));
13974
14978
  var import_node_path39 = __toESM(require("path"));
13975
14979
  var import_yaml4 = __toESM(require("yaml"));
@@ -14005,7 +15009,7 @@ function inspectNetworkComposeFiles(input) {
14005
15009
  const env = managedComposeEnvironment(input.workspace);
14006
15010
  delete env.DOCKER_CONTEXT;
14007
15011
  delete env.DOCKER_HOST;
14008
- const result = (0, import_node_child_process28.spawnSync)(
15012
+ const result = (0, import_node_child_process29.spawnSync)(
14009
15013
  "docker",
14010
15014
  [
14011
15015
  "--host",
@@ -14040,7 +15044,7 @@ function prepareNetworkComposeFiles(plan, subnet) {
14040
15044
  const overlay = deriveComposeNetworkOverlay(subnet);
14041
15045
  if (overlay.status !== "ready" || !overlay.overlay) throw new Error("Network subnet is invalid.");
14042
15046
  const overlayPath = import_node_path39.default.join(plan.composeDirectory, OVERLAY);
14043
- const ignored = (0, import_node_child_process28.spawnSync)("git", [
15047
+ const ignored = (0, import_node_child_process29.spawnSync)("git", [
14044
15048
  "-C",
14045
15049
  plan.composeDirectory,
14046
15050
  "check-ignore",
@@ -14070,7 +15074,7 @@ ${JSON.stringify(extended.config, null, 2)}
14070
15074
  ...plan,
14071
15075
  contents,
14072
15076
  composeFiles: [...plan.composeFiles, overlayPath],
14073
- effectiveConfigSha256: (0, import_node_crypto20.createHash)("sha256").update(contents).digest("hex")
15077
+ effectiveConfigSha256: (0, import_node_crypto21.createHash)("sha256").update(contents).digest("hex")
14074
15078
  };
14075
15079
  return {
14076
15080
  plan: result,
@@ -14082,7 +15086,7 @@ ${JSON.stringify(extended.config, null, 2)}
14082
15086
  }
14083
15087
 
14084
15088
  // src/core/network-connected-route.ts
14085
- var import_node_child_process29 = require("child_process");
15089
+ var import_node_child_process30 = require("child_process");
14086
15090
  function bounds(cidr) {
14087
15091
  const canonical = parseIPv4Cidr(cidr);
14088
15092
  if (!canonical) throw new Error("Invalid connected route.");
@@ -14107,7 +15111,7 @@ function inspectClaimedBridge(claim2, networkId) {
14107
15111
  const env = { ...process.env };
14108
15112
  delete env.DOCKER_CONTEXT;
14109
15113
  delete env.DOCKER_HOST;
14110
- const result = (0, import_node_child_process29.spawnSync)(
15114
+ const result = (0, import_node_child_process30.spawnSync)(
14111
15115
  "docker",
14112
15116
  [
14113
15117
  "--host",
@@ -14134,7 +15138,7 @@ function inspectClaimedBridge(claim2, networkId) {
14134
15138
  const [first, last] = bounds(claim2.subnet);
14135
15139
  const [address] = bounds(gateway);
14136
15140
  if (address <= first || address >= last) throw new Error();
14137
- const interfaces = (0, import_node_child_process29.spawnSync)("ip", ["-j", "-d", "-4", "address", "show", "dev", bridge], {
15141
+ const interfaces = (0, import_node_child_process30.spawnSync)("ip", ["-j", "-d", "-4", "address", "show", "dev", bridge], {
14138
15142
  encoding: "utf8",
14139
15143
  timeout: 5e3,
14140
15144
  maxBuffer: 65536
@@ -14388,7 +15392,7 @@ function createManagedNetworkSession(input) {
14388
15392
  "Insufficient endpoint headroom; request an allowed larger network before first allocation."
14389
15393
  );
14390
15394
  const authority = readNetworkOperationAuthority(identity2);
14391
- const fingerprint = (0, import_node_crypto21.createHash)("sha256").update(input.plan().sourceConfigSha256).update(fingerprintComposeNetworkModel(model)).digest("hex");
15395
+ const fingerprint = (0, import_node_crypto22.createHash)("sha256").update(input.plan().sourceConfigSha256).update(fingerprintComposeNetworkModel(model)).digest("hex");
14392
15396
  if (!saved) {
14393
15397
  if (policy.status !== "valid")
14394
15398
  throw new Error("New network allocation requires valid machine policy.");
@@ -14890,7 +15894,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
14890
15894
  target
14891
15895
  });
14892
15896
  if (target.kind === "linked") {
14893
- const workspaceEnv = (0, import_node_child_process30.spawnSync)(
15897
+ const workspaceEnv = (0, import_node_child_process31.spawnSync)(
14894
15898
  "docker",
14895
15899
  ["exec", appContainer.id, "printenv", "WORKSPACE"],
14896
15900
  { encoding: "utf-8", ...networkDockerOptions() }
@@ -14900,7 +15904,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
14900
15904
  `Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
14901
15905
  );
14902
15906
  }
14903
- const devrouterWorkspaceEnv = (0, import_node_child_process30.spawnSync)(
15907
+ const devrouterWorkspaceEnv = (0, import_node_child_process31.spawnSync)(
14904
15908
  "docker",
14905
15909
  ["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
14906
15910
  { encoding: "utf-8", ...networkDockerOptions() }
@@ -14911,7 +15915,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
14911
15915
  );
14912
15916
  }
14913
15917
  }
14914
- const gitCheck = (0, import_node_child_process30.spawnSync)(
15918
+ const gitCheck = (0, import_node_child_process31.spawnSync)(
14915
15919
  "docker",
14916
15920
  [
14917
15921
  "exec",
@@ -15073,6 +16077,9 @@ function assertRepairBaseline(options) {
15073
16077
  };
15074
16078
  const reasons = [];
15075
16079
  if (state.profile !== profile) reasons.push("profile");
16080
+ if ((state.variant ?? "default") !== (options.variant ?? "default")) reasons.push("variant");
16081
+ if (state.hostPortDeclarationsSha256 !== managedHostPortDeclarationsIdentity(config.managedRuntime?.devcontainer.hostPorts ?? {}))
16082
+ reasons.push("host-ports");
15076
16083
  if (state.sourceConfigSha256 !== plan.sourceConfigSha256) reasons.push("source-config");
15077
16084
  if (state.effectiveConfigSha256 !== plan.effectiveConfigSha256) reasons.push("effective-config");
15078
16085
  for (const resource of ["apps", "services", "processes"]) {
@@ -15191,7 +16198,7 @@ function isPrimaryCheckout(repoPath) {
15191
16198
  }
15192
16199
  function openUrls(urls) {
15193
16200
  for (const url of urls) {
15194
- const opened = (0, import_node_child_process30.spawnSync)("open", [url], { encoding: "utf-8" });
16201
+ const opened = (0, import_node_child_process31.spawnSync)("open", [url], { encoding: "utf-8" });
15195
16202
  if (opened.status !== 0) {
15196
16203
  process.stderr.write(`Warning: could not open '${url}'.
15197
16204
  `);
@@ -15262,6 +16269,12 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15262
16269
  target.kind === "primary" ? "" : target.workspace,
15263
16270
  options.profile ?? (options.repair ? previousManagedState?.profile : void 0)
15264
16271
  );
16272
+ const variant = resolveRuntimeVariant(
16273
+ runtime.config,
16274
+ options.variant ?? (options.repair ? previousManagedState?.variant : void 0)
16275
+ );
16276
+ if (variant !== "default" && managedPostStart.kind !== "runtime")
16277
+ throw new Error("Runtime variants require a managed runtime post-start adapter.");
15265
16278
  const managedRuntime = runtime.config.managedRuntime;
15266
16279
  if (options.repair && !managedRuntime)
15267
16280
  throw new Error("Repair requires managedRuntime configuration.");
@@ -15337,6 +16350,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15337
16350
  repoPath,
15338
16351
  ...target.kind === "linked" ? { workspace: target.workspace } : {},
15339
16352
  profile: runtime.profile,
16353
+ variant,
15340
16354
  // A refusal precedes the provider start, so the exact id may not
15341
16355
  // be known yet on a primary checkout.
15342
16356
  devpodId: devpodId ?? "(absent)",
@@ -15366,6 +16380,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15366
16380
  plan: managedPlan,
15367
16381
  config: runtime.config,
15368
16382
  profile: runtime.profile,
16383
+ variant,
15369
16384
  processes: desiredProcesses,
15370
16385
  routes: previousRoutes,
15371
16386
  workspace: managedWorkspaceEnv
@@ -15474,7 +16489,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15474
16489
  transitionPhase = "service-start";
15475
16490
  reportLifecycleProgress("service-start");
15476
16491
  claimLifecycleEffect();
15477
- const result = (0, import_node_child_process30.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
16492
+ const result = (0, import_node_child_process31.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
15478
16493
  ...networkDockerOptions(),
15479
16494
  encoding: "utf-8",
15480
16495
  timeout: DEFAULT_READINESS_TIMEOUT_MS,
@@ -15584,6 +16599,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15584
16599
  devpodId: captureId,
15585
16600
  composeProject: captureProject,
15586
16601
  profile: runtime.profile,
16602
+ variant,
16603
+ hostPortDeclarationsSha256: managedHostPortDeclarationsIdentity(
16604
+ managedRuntime?.devcontainer.hostPorts ?? {}
16605
+ ),
15587
16606
  desired: {
15588
16607
  apps: apps.map((app) => app.name).sort(),
15589
16608
  services: [...capturePlan.desiredProfileServices].sort(),
@@ -15630,6 +16649,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15630
16649
  devpodId,
15631
16650
  composeProject: managedComposeProject,
15632
16651
  profile: runtime.profile,
16652
+ variant,
16653
+ hostPortDeclarationsSha256: managedHostPortDeclarationsIdentity(
16654
+ managedRuntime?.devcontainer.hostPorts ?? {}
16655
+ ),
15633
16656
  desired: {
15634
16657
  apps: apps.map((app) => app.name).sort(),
15635
16658
  services: [...managedPlan.desiredProfileServices].sort(),
@@ -15653,7 +16676,17 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15653
16676
  container,
15654
16677
  quiet: options.quiet,
15655
16678
  profile: runtime.profile,
15656
- ...managedPlan ? { processes: desiredProcesses } : {}
16679
+ variant,
16680
+ ...managedPlan ? {
16681
+ processes: desiredProcesses,
16682
+ hostPorts: resolveManagedHostPorts({
16683
+ repoPath,
16684
+ declarations: managedRuntime?.devcontainer.hostPorts ?? {},
16685
+ selectedServices: managedPlan.desiredServices,
16686
+ composeProject: managedComposeProject,
16687
+ composeFiles: managedPlan.composeFiles
16688
+ })
16689
+ } : {}
15657
16690
  });
15658
16691
  if (managedPlan && managedComposeProject) {
15659
16692
  const processRegistry = runtime.config.managedRuntime?.processes ?? [];
@@ -15746,7 +16779,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15746
16779
  plan: managedPostStart,
15747
16780
  container: recoveredContainer,
15748
16781
  quiet: options.quiet,
15749
- profile: runtime.profile
16782
+ profile: runtime.profile,
16783
+ variant
15750
16784
  });
15751
16785
  recreated = true;
15752
16786
  claimLifecycleEffect();
@@ -15778,6 +16812,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15778
16812
  devpodId,
15779
16813
  composeProject: managedComposeProject,
15780
16814
  profile: runtime.profile,
16815
+ variant,
16816
+ hostPortDeclarationsSha256: managedHostPortDeclarationsIdentity(
16817
+ managedRuntime?.devcontainer.hostPorts ?? {}
16818
+ ),
15781
16819
  desired: {
15782
16820
  apps: apps.map((app) => app.name).sort(),
15783
16821
  services: [...managedPlan.desiredProfileServices].sort(),
@@ -15809,6 +16847,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15809
16847
  workspace: target.workspace,
15810
16848
  config: runtime.config,
15811
16849
  profile: runtime.profile,
16850
+ variant,
15812
16851
  resolvedProfile: runtime.resolvedProfile,
15813
16852
  ...options.repair ? { candidateState } : {}
15814
16853
  }) : void 0;
@@ -15826,6 +16865,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15826
16865
  repoPath,
15827
16866
  workspace: target.workspace,
15828
16867
  profile: runtime.profile,
16868
+ variant,
15829
16869
  devpodId,
15830
16870
  urls,
15831
16871
  recreated,
@@ -16018,13 +17058,32 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
16018
17058
  throw new Error("Managed post-start plan disappeared during rollback.");
16019
17059
  }
16020
17060
  if (previousProcesses.length > 0 && previousManagedState?.status !== "degraded") {
17061
+ if (previousManagedState?.hostPortDeclarationsSha256 !== managedHostPortDeclarationsIdentity(
17062
+ managedRuntimeConfig?.managedRuntime?.devcontainer.hostPorts ?? {}
17063
+ )) {
17064
+ throw new Error(
17065
+ "Host-port declarations changed; restore the retained declarations before repair."
17066
+ );
17067
+ }
16021
17068
  claimLifecycleEffect();
16022
17069
  runManagedPostStart({
16023
17070
  plan: managedPostStartPlan,
16024
17071
  container: managedContainer,
16025
17072
  quiet: options.quiet,
16026
17073
  profile: previousManagedState?.profile ?? "full",
16027
- processes: previousProcesses
17074
+ variant: previousManagedState?.variant ?? "default",
17075
+ processes: previousProcesses,
17076
+ hostPorts: resolveManagedHostPorts({
17077
+ repoPath,
17078
+ declarations: managedRuntimeConfig?.managedRuntime?.devcontainer.hostPorts ?? {},
17079
+ selectedServices: [
17080
+ managedPlan.primaryService,
17081
+ ...managedPlan.baseServices,
17082
+ ...previousServices
17083
+ ],
17084
+ composeProject: managedComposeProject,
17085
+ composeFiles: managedPlan.composeFiles
17086
+ })
16028
17087
  });
16029
17088
  }
16030
17089
  for (const process2 of managedProcessRegistry) {
@@ -16174,7 +17233,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
16174
17233
  ensureNetworkLocked,
16175
17234
  () => network?.endpoint
16176
17235
  );
17236
+ preflightRuntimeVariant(repoPath, options);
16177
17237
  return withLifecycleOperationLock(repoPath, async () => {
17238
+ preflightRuntimeVariant(repoPath, options);
16178
17239
  const preparationConfig = loadRepoConfig(repoPath);
16179
17240
  if (!options.repair)
16180
17241
  recoverUnattachedNetworkReservation(repoPath, Boolean(preparationConfig.managedRuntime));
@@ -16215,7 +17276,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
16215
17276
  linked ? workspace : "",
16216
17277
  requestedOptions.profile
16217
17278
  ).profile;
16218
- if (desiredProfile !== retained.profile) {
17279
+ const desiredVariant = resolveRuntimeVariant(preparationConfig, requestedOptions.variant);
17280
+ if (desiredProfile !== retained.profile && desiredVariant === (retained.variant ?? "default")) {
16219
17281
  const target = resolveRepairTarget(repoPath, linked);
16220
17282
  const baseline = loadRuntimeConfig(repoPath, linked ? workspace : "", retained.profile);
16221
17283
  const plan = inspectManagedDevcontainerConfig({
@@ -16233,6 +17295,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
16233
17295
  plan,
16234
17296
  config: baseline.config,
16235
17297
  profile: baseline.profile,
17298
+ variant: retained.variant ?? "default",
16236
17299
  processes: desiredManagedProcesses(
16237
17300
  baseline.config.managedRuntime,
16238
17301
  baseline.resolvedProfile
@@ -16244,7 +17307,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
16244
17307
  claimLifecycleEffect();
16245
17308
  return ensureLocked();
16246
17309
  }
16247
- options = { ...requestedOptions, repair: true, profile: retained.profile, open: false };
17310
+ options = {
17311
+ ...requestedOptions,
17312
+ repair: true,
17313
+ profile: retained.profile,
17314
+ variant: retained.variant ?? "default",
17315
+ open: false
17316
+ };
16248
17317
  let repaired;
16249
17318
  try {
16250
17319
  repaired = await repairLocked();
@@ -16252,7 +17321,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
16252
17321
  options = requestedOptions;
16253
17322
  }
16254
17323
  claimLifecycleEffect();
16255
- if (desiredProfile !== repaired.profile) return ensureLocked();
17324
+ if (desiredProfile !== repaired.profile || desiredVariant !== (repaired.variant ?? "default"))
17325
+ return ensureLocked();
16256
17326
  if (options.open) openUrls(repaired.urls);
16257
17327
  return repaired;
16258
17328
  });
@@ -16338,6 +17408,33 @@ function reconcileDrained(record5) {
16338
17408
  record5.worker = null;
16339
17409
  }
16340
17410
  }
17411
+ function preflightRuntimeVariant(repoPath, options) {
17412
+ if (options.variant === void 0 && !options.repair) return "default";
17413
+ const workspace = isLinkedWorktree(repoPath) ? readPersistedWorkspace(repoPath) : void 0;
17414
+ const retained = options.repair ? readManagedRuntimeState(repoPath, workspace) : void 0;
17415
+ const variant = resolveRuntimeVariant(
17416
+ loadRepoConfig(repoPath),
17417
+ options.variant ?? retained?.variant
17418
+ );
17419
+ if (options.repair && variant !== (retained?.variant ?? "default"))
17420
+ throw new Error("Repair requires the retained runtime variant.");
17421
+ if (variant !== "default") {
17422
+ if (resolveManagedPostStartPlan(repoPath).kind !== "runtime")
17423
+ throw new Error("Runtime variants require a managed runtime post-start adapter.");
17424
+ const previous = readReliabilityOperation({
17425
+ repoPath,
17426
+ workspace: workspace ?? null,
17427
+ provider: resolveWorkspaceRuntimeOrDefault(repoPath)
17428
+ });
17429
+ if (capacityPolicyTargets(repoPath) || previous?.state.executionPolicy === "capacity-managed")
17430
+ throw new Error("Non-default runtime variants require manual lifecycle admission.");
17431
+ }
17432
+ return variant;
17433
+ }
17434
+ function capacityPolicyTargets(repoPath) {
17435
+ const policy = readCapacityPolicy(import_node_path41.default.join(DEVROUTER_HOME, "controller"));
17436
+ return policy?.admissions === "enabled" && policy.enrollments.some((entry) => entry.repoPath === repoPath);
17437
+ }
16341
17438
  function clearActiveLifecycleStartupWitness() {
16342
17439
  const request = activeWorker;
16343
17440
  if (!request || request.kind === "stop") return;
@@ -16395,12 +17492,46 @@ async function executeLifecycleWorker(request, operation) {
16395
17492
  request.repoPath,
16396
17493
  () => proveInitialManagedDevsyAbsence(request.repoPath)
16397
17494
  );
17495
+ const markStopWorkStarted = () => updateReliabilityOperation(request.identity, (record5) => {
17496
+ if (!matchesFence(record5, request.fence))
17497
+ throw new Error("Lifecycle intent changed before stop work began.");
17498
+ record5.stopWorkStarted = true;
17499
+ });
17500
+ if (request.options.repair && (!retained?.stopBaseline || request.options.delete || request.identity.provider !== "devsy"))
17501
+ throw new Error("Stop recovery requires a retained Devsy baseline and preserves data.");
16398
17502
  if (retained?.stopBaseline) {
16399
- stopBaselineState = retained;
16400
- withMutationLock(
17503
+ stopBaselineState = withMutationLock(
16401
17504
  "Verify retained stop",
16402
17505
  request.repoPath,
16403
- () => proveManagedStop(retained)
17506
+ () => {
17507
+ let needsRecovery = false;
17508
+ if (request.options.repair) {
17509
+ try {
17510
+ proveManagedStop(retained);
17511
+ } catch {
17512
+ needsRecovery = true;
17513
+ }
17514
+ }
17515
+ const candidate = needsRecovery ? recoverManagedStopBaseline(
17516
+ retained,
17517
+ () => {
17518
+ const runtime = loadRuntimeConfig(
17519
+ request.repoPath,
17520
+ request.identity.workspace ?? "",
17521
+ retained.profile
17522
+ );
17523
+ return inspectManagedDevcontainerConfig({
17524
+ repoPath: request.repoPath,
17525
+ config: runtime.config,
17526
+ profile: runtime.resolvedProfile,
17527
+ linked: isLinkedWorktree(request.repoPath)
17528
+ });
17529
+ },
17530
+ markStopWorkStarted
17531
+ ) : retained;
17532
+ proveManagedStop(candidate);
17533
+ return candidate;
17534
+ }
16404
17535
  );
16405
17536
  } else {
16406
17537
  const containers = inspectWorkspaceContainers();
@@ -16418,11 +17549,7 @@ async function executeLifecycleWorker(request, operation) {
16418
17549
  ];
16419
17550
  for (const project of stopProjects) inspectManagedStopContainers(project);
16420
17551
  }
16421
- updateReliabilityOperation(request.identity, (record5) => {
16422
- if (!matchesFence(record5, request.fence))
16423
- throw new Error("Lifecycle intent changed before stop work began.");
16424
- record5.stopWorkStarted = true;
16425
- });
17552
+ markStopWorkStarted();
16426
17553
  } else {
16427
17554
  updateReliabilityOperation(request.identity, (record5) => {
16428
17555
  stepRecord(record5, {
@@ -16627,7 +17754,7 @@ async function devsyExecOutcome(repoPath, command, retainedProof) {
16627
17754
  const launch = () => {
16628
17755
  if (retainedProof) revalidateDevsyExecProof(repoPath, retainedProof);
16629
17756
  claimLifecycleEffect();
16630
- return (0, import_node_child_process31.spawn)("devsy", args, { stdio: "inherit" });
17757
+ return (0, import_node_child_process32.spawn)("devsy", args, { stdio: "inherit" });
16631
17758
  };
16632
17759
  child = retainedProof ? withMutationLock("Execute retained workspace", repoPath, launch) : launch();
16633
17760
  } catch (error) {
@@ -16675,7 +17802,7 @@ function ensureGuidance2(repoPath) {
16675
17802
  return `Run 'devrouter ensure ${repoPath}' first.`;
16676
17803
  }
16677
17804
  function assertRunningDevpod(devpodId, repoPath) {
16678
- const result = (0, import_node_child_process32.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
17805
+ const result = (0, import_node_child_process33.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
16679
17806
  encoding: "utf-8"
16680
17807
  });
16681
17808
  if (result.status !== 0) {
@@ -16722,7 +17849,7 @@ async function devpodExecOutcome(repoPath, command) {
16722
17849
  }
16723
17850
  assertRunningDevpod(devpod.id, repoPath);
16724
17851
  const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
16725
- const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto23.randomUUID)()}__:`;
17852
+ const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto24.randomUUID)()}__:`;
16726
17853
  const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
16727
17854
  const literalCommand = command.map(quotePosixArg).join(" ");
16728
17855
  const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
@@ -16854,7 +17981,7 @@ async function devpodExecOutcome(repoPath, command) {
16854
17981
  let child;
16855
17982
  try {
16856
17983
  claimLifecycleEffect();
16857
- child = (0, import_node_child_process32.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
17984
+ child = (0, import_node_child_process33.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
16858
17985
  } catch (error) {
16859
17986
  settled = true;
16860
17987
  reject(
@@ -16911,7 +18038,7 @@ async function devpodExecOutcome(repoPath, command) {
16911
18038
  }
16912
18039
 
16913
18040
  // src/core/workspace-lifecycle.ts
16914
- var import_node_child_process33 = require("child_process");
18041
+ var import_node_child_process34 = require("child_process");
16915
18042
  var import_node_fs37 = __toESM(require("fs"));
16916
18043
  var import_node_path42 = __toESM(require("path"));
16917
18044