@askexenow/exe-os 0.9.411 → 0.9.412

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.
@@ -22,7 +22,7 @@ import {
22
22
  } from "./chunk-EOWJXCYC.js";
23
23
 
24
24
  // src/lib/stack-update.ts
25
- import { execFileSync, spawnSync as spawnSync2 } from "child_process";
25
+ import { execFileSync as execFileSync2, spawnSync as spawnSync2 } from "child_process";
26
26
  import { createHash, createVerify, randomBytes, verify as verifySignature } from "crypto";
27
27
  import { chmodSync, closeSync, copyFileSync, existsSync, lstatSync, mkdirSync, openSync, realpathSync, readdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, utimesSync, writeFileSync } from "fs";
28
28
  import http from "http";
@@ -32,6 +32,183 @@ import path from "path";
32
32
  import { fileURLToPath } from "url";
33
33
  import { parseDocument as parseYamlDocument } from "yaml";
34
34
 
35
+ // src/lib/stack-worker-migration.ts
36
+ import { isScalar, parseDocument } from "yaml";
37
+ var LEGACY_IMAGE = "${EXE_OS_WORKER_IMAGE_TAG:-ghcr.io/askexe/exe-os-worker:v0.9.311}";
38
+ var DISABLED_IMAGE = "${EXE_OS_WORKER_IMAGE_TAG:-}";
39
+ var WORKER = "exe-os-worker";
40
+ function planLegacyWorkerMigration(input) {
41
+ const unchanged = { raw: input.composeRaw, changed: false };
42
+ if (!input.composeRaw.includes(LEGACY_IMAGE)) return unchanged;
43
+ const doc = parseDocument(input.composeRaw, { uniqueKeys: true });
44
+ if (doc.errors.length) throw new Error("Cannot inspect legacy worker migration: invalid Compose YAML");
45
+ const image = doc.getIn(["services", WORKER, "image"], true);
46
+ if (!isScalar(image) || image.value !== LEGACY_IMAGE) return unchanged;
47
+ const refuse = (reason) => {
48
+ throw new Error(`Cannot migrate the legacy embeddings worker: ${reason}. Keep the signed-manifest and digest checks enabled.`);
49
+ };
50
+ const worker = doc.getIn(["services", WORKER]);
51
+ const definition = worker.toJSON();
52
+ if (definition.extends || !Array.isArray(definition.profiles) || definition.profiles.length !== 1 || definition.profiles[0] !== "embeddings") {
53
+ refuse("the worker is not the shipped embeddings-only service");
54
+ }
55
+ const processEnv = input.processEnv ?? process.env;
56
+ const unquote = (value) => value.trim().replace(/\s+#.*$/, "").replace(/^(["'])(.*)\1$/, "$2").trim();
57
+ for (const value of [input.env.get("COMPOSE_PROFILES") ?? "", processEnv.COMPOSE_PROFILES ?? ""]) {
58
+ const profiles = unquote(value).split(",").map((profile) => profile.trim()).filter(Boolean);
59
+ if (profiles.some((profile) => !/^(?:[a-zA-Z0-9][a-zA-Z0-9_.-]*|\*)$/.test(profile))) {
60
+ refuse("enabled profiles could not be determined");
61
+ }
62
+ if (profiles.includes("embeddings") || profiles.includes("*")) refuse("embeddings are enabled");
63
+ }
64
+ if ([input.env.get("EXE_OS_WORKER_IMAGE_TAG") ?? "", processEnv.EXE_OS_WORKER_IMAGE_TAG ?? ""].some((value) => unquote(value))) {
65
+ refuse("an explicit worker image is configured; it must be covered by a signed release");
66
+ }
67
+ if (input.hostServicesUnavailable) refuse("the host's running services could not be observed");
68
+ if (input.hostServices.includes(WORKER)) refuse("the worker exists on the host");
69
+ if (input.overrideRaw) {
70
+ const override = parseDocument(input.overrideRaw, { uniqueKeys: true });
71
+ if (override.errors.length) refuse("the customer override could not be inspected");
72
+ if (override.hasIn(["services", WORKER])) refuse("the customer override defines the worker");
73
+ }
74
+ const targetComposeRaw = typeof input.targetComposeRaw === "function" ? input.targetComposeRaw() : input.targetComposeRaw;
75
+ if (!targetComposeRaw) refuse("the packaged replacement template is missing");
76
+ const target = parseDocument(targetComposeRaw, { uniqueKeys: true });
77
+ if (target.errors.length || target.getIn(["services", WORKER, "image"]) !== DISABLED_IMAGE) {
78
+ refuse("the packaged replacement does not contain the current disabled worker slot");
79
+ }
80
+ if (!image.range) refuse("the legacy image location is unavailable");
81
+ return {
82
+ raw: input.composeRaw.slice(0, image.range[0]) + JSON.stringify(DISABLED_IMAGE) + input.composeRaw.slice(image.range[1]),
83
+ changed: true
84
+ };
85
+ }
86
+
87
+ // src/lib/monitor-rollback.ts
88
+ import { execFileSync } from "child_process";
89
+ var LEGACY_HUB_DIGEST = "sha256:1af31893cc8b1e51892be7d7705fa6c6b3108af8366b1fa8da2879d8ae21ec88";
90
+ var runDocker = (args) => execFileSync("docker", args, {
91
+ encoding: "utf8",
92
+ timeout: 12e4,
93
+ maxBuffer: 1024 * 1024
94
+ });
95
+ function restoreLegacyMonitorOwnership(composeArgs, run = runDocker) {
96
+ const configured = JSON.parse(
97
+ run([...composeArgs, "config", "--format", "json"])
98
+ ).services?.["exe-monitor-hub"];
99
+ if (!configured || typeof configured.image !== "string" || !configured.image.trim())
100
+ return false;
101
+ if (configured.user != null && String(configured.user).trim() && !["65532", "65532:65532"].includes(String(configured.user)))
102
+ return false;
103
+ const ids = run([...composeArgs, "ps", "--all", "--quiet", "exe-monitor-hub"]).trim().split(/\s+/).filter(Boolean);
104
+ if (ids.length === 0) return false;
105
+ if (ids.length !== 1 || !/^[a-f0-9]{12,64}$/.test(ids[0])) {
106
+ throw new Error("Cannot attribute the installed monitor container safely");
107
+ }
108
+ const image = JSON.parse(run(["image", "inspect", configured.image]))[0];
109
+ if (!image?.RepoDigests?.some(
110
+ (ref) => ref.endsWith("@" + LEGACY_HUB_DIGEST)
111
+ ))
112
+ return false;
113
+ if (image.Config?.User !== "65532:65532")
114
+ throw new Error(
115
+ "Legacy monitor runtime identity differs from its verified artifact"
116
+ );
117
+ const hub = JSON.parse(run(["inspect", ids[0]]))[0];
118
+ if (typeof hub?.Id !== "string" || !hub.Id.startsWith(ids[0]))
119
+ throw new Error("Monitor observation changed before recovery");
120
+ const project = hub?.Config?.Labels?.["com.docker.compose.project"];
121
+ const mount = hub?.Mounts?.find(
122
+ (m) => m.Destination === "/app/exe-monitor_data"
123
+ );
124
+ if (!project || hub.Config.Labels["com.docker.compose.service"] !== "exe-monitor-hub" || mount?.Type !== "volume" || typeof mount.Name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(mount.Name)) {
125
+ throw new Error(
126
+ "Legacy monitor recovery requires its managed named data volume"
127
+ );
128
+ }
129
+ const volume = JSON.parse(run(["volume", "inspect", mount.Name]))[0];
130
+ if (volume?.Name !== mount.Name || volume.Labels?.["com.docker.compose.project"] !== project || volume.Labels["com.docker.compose.volume"] !== "monitor_hub_data") {
131
+ throw new Error(
132
+ "Legacy monitor data-volume ownership is not attributable to this stack"
133
+ );
134
+ }
135
+ const db = JSON.parse(run(["inspect", "exe-db"]))[0];
136
+ if (db?.Config?.Labels?.["com.docker.compose.project"] !== project || db.Config.Labels["com.docker.compose.service"] !== "exe-db" || !/^sha256:[a-f0-9]{64}$/.test(db.Image)) {
137
+ throw new Error(
138
+ "Legacy monitor recovery requires this stack's immutable database helper image"
139
+ );
140
+ }
141
+ let helper = JSON.parse(run(["image", "inspect", db.Image]))[0];
142
+ let helperId = db.Image;
143
+ if (!helper?.Os || !helper?.Architecture) {
144
+ const descriptor = db.ImageManifestDescriptor;
145
+ const platform = descriptor?.platform;
146
+ if (!/^sha256:[a-f0-9]{64}$/.test(descriptor?.digest ?? "") || platform?.os !== "linux" || !["amd64", "arm64"].includes(platform.architecture))
147
+ throw new Error("Cannot resolve native recovery helper platform");
148
+ helper = JSON.parse(
149
+ run([
150
+ "image",
151
+ "inspect",
152
+ "--platform",
153
+ `linux/${platform.architecture}`,
154
+ db.Image
155
+ ])
156
+ )[0];
157
+ helperId = descriptor.digest;
158
+ if (helper?.Architecture !== platform.architecture)
159
+ throw new Error("Recovery helper differs from the running DB platform");
160
+ }
161
+ if (helper?.Id !== helperId || helper.Os !== "linux" || !["amd64", "arm64"].includes(helper.Architecture)) {
162
+ throw new Error("Legacy monitor recovery helper metadata is inconsistent");
163
+ }
164
+ const base = [
165
+ "run",
166
+ "--rm",
167
+ "--network",
168
+ "none",
169
+ "--read-only",
170
+ "--user",
171
+ "0:0",
172
+ "--platform",
173
+ `linux/${helper.Architecture}`,
174
+ "--label",
175
+ `com.askexe.monitor-recovery=${project}`
176
+ ];
177
+ const owner = run([
178
+ ...base,
179
+ "--entrypoint",
180
+ "stat",
181
+ "--mount",
182
+ `type=volume,src=${mount.Name},dst=/recovery,readonly`,
183
+ db.Image,
184
+ "-c",
185
+ "%u:%g",
186
+ "/recovery"
187
+ ]).trim();
188
+ if (owner === "65532:65532") return false;
189
+ if (!/^\d+:\d+$/.test(owner))
190
+ throw new Error("Cannot observe legacy monitor data ownership safely");
191
+ run([...composeArgs, "stop", "--timeout", "10", "exe-monitor-hub"]);
192
+ run([
193
+ ...base,
194
+ "--cap-drop",
195
+ "ALL",
196
+ "--cap-add",
197
+ "CHOWN",
198
+ "--cap-add",
199
+ "DAC_OVERRIDE",
200
+ "--entrypoint",
201
+ "chown",
202
+ "--mount",
203
+ `type=volume,src=${mount.Name},dst=/recovery`,
204
+ db.Image,
205
+ "-Rh",
206
+ "65532:65532",
207
+ "/recovery"
208
+ ]);
209
+ return true;
210
+ }
211
+
35
212
  // src/lib/askexe-image-prefixes.ts
36
213
  var ASKEXE_IMAGE_PREFIXES = ["ghcr.io/askexe/", "update.askexe.com/askexe/"];
37
214
  function isAskExeHostedImageRef(trimmedImage) {
@@ -1292,6 +1469,11 @@ async function rollbackStackUpdate(options) {
1292
1469
  if (options.retainCompose && !(scopedServiceNames.length === 1 && lockData?.services)) {
1293
1470
  throw new Error("--retain-compose rollback refused an unscoped restart; restore the selected service from backup manually");
1294
1471
  }
1472
+ if (!options.exec && (scopedServiceNames.length === 0 || scopedServiceNames.includes("monitorHub"))) {
1473
+ if (restoreLegacyMonitorOwnership(composeArgs)) {
1474
+ console.log("[stack-update] Restored legacy monitor data ownership before rollback restart");
1475
+ }
1476
+ }
1295
1477
  if (scopedServiceNames.length > 0 && lockData?.services) {
1296
1478
  const scopedComposeServices = scopedComposeImageServices(
1297
1479
  scopedServiceNames.map((serviceName) => lockData.services[serviceName]?.composeService ?? serviceName),
@@ -1936,6 +2118,7 @@ var MONITOR_IMAGE_TAG_ENV = {
1936
2118
  monitorHub: "MONITOR_HUB_IMAGE_TAG",
1937
2119
  monitorAgent: "MONITOR_AGENT_IMAGE_TAG"
1938
2120
  };
2121
+ var OBSERVED_ROOT_AGENT_DIGEST = "sha256:29fbd445f79484df04f000603e4ff445e76f0380f86d687a0033e3a0ef7d1b7a";
1939
2122
  var STOCK_BESZEL_MONITOR_VERSIONS = /* @__PURE__ */ new Set(["0.9.5"]);
1940
2123
  function monitorImageVersion(imageRef) {
1941
2124
  const noDigest = imageRef.split("@")[0] ?? imageRef;
@@ -1950,6 +2133,7 @@ function isStockBeszelMonitorImage(imageRef) {
1950
2133
  return version !== void 0 && STOCK_BESZEL_MONITOR_VERSIONS.has(version);
1951
2134
  }
1952
2135
  function monitorHealthBinForImage(serviceKey, imageRef) {
2136
+ if (serviceKey === "monitorAgent" && imageRef.endsWith(`@${OBSERVED_ROOT_AGENT_DIGEST}`)) return "/agent";
1953
2137
  const stock = isStockBeszelMonitorImage(imageRef);
1954
2138
  if (serviceKey === "monitorHub") return stock ? "/beszel" : "/app/beszel";
1955
2139
  return stock ? "/agent" : "/app/beszel-agent";
@@ -2202,14 +2386,17 @@ function collectImageAvailabilityIssues(plan) {
2202
2386
  try {
2203
2387
  const result = spawnSync2("docker", ["manifest", "inspect", manifestInspectRef(image), "--verbose"], {
2204
2388
  stdio: ["ignore", "pipe", "pipe"],
2205
- timeout: 3e4
2389
+ // Multi-platform official images can require dozens of child-manifest
2390
+ // requests through the proxy. A valid Redis index took 68s in the
2391
+ // legacy-upgrade rehearsal; 30s misreported it as an absent image.
2392
+ timeout: 12e4
2206
2393
  });
2207
2394
  if (result.status === 0) {
2208
2395
  inspected = true;
2209
2396
  inspectOutput = result.stdout?.toString() ?? "";
2210
2397
  break;
2211
2398
  }
2212
- lastStderr = result.stderr?.toString().trim() ?? "";
2399
+ lastStderr = result.error?.message || result.stderr?.toString().trim() || "manifest inspection failed";
2213
2400
  if (attempt < maxAttempts) {
2214
2401
  const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
2215
2402
  console.log(
@@ -2232,13 +2419,14 @@ function collectImageAvailabilityIssues(plan) {
2232
2419
  try {
2233
2420
  const parsed = JSON.parse(inspectOutput);
2234
2421
  const manifests = Array.isArray(parsed) ? parsed : [parsed];
2422
+ const platformOf = (m) => m.platform ?? m.Descriptor?.platform;
2235
2423
  const hasAmd64 = manifests.some((m) => {
2236
- const platform = m.platform;
2424
+ const platform = platformOf(m);
2237
2425
  return platform?.architecture === "amd64" && platform?.os === "linux";
2238
2426
  });
2239
- if (!hasAmd64 && manifests.length > 0 && manifests[0]?.platform) {
2427
+ if (!hasAmd64 && manifests.some((m) => platformOf(m))) {
2240
2428
  const platforms = manifests.map((m) => {
2241
- const p = m.platform;
2429
+ const p = platformOf(m);
2242
2430
  return `${p?.os ?? "?"}/${p?.architecture ?? "?"}`;
2243
2431
  }).join(", ");
2244
2432
  issues.push({
@@ -3904,6 +4092,9 @@ async function runStackUpdate(options) {
3904
4092
  return withStackUpdateOperationLock(options.envFile, () => runStackUpdateUnlocked(options));
3905
4093
  }
3906
4094
  async function runStackUpdateUnlocked(options) {
4095
+ if (options.rollback && (options.dryRun || options.verifyImages)) {
4096
+ throw new Error("Rollback cannot be combined with a read-only preview; no rollback was performed.");
4097
+ }
3907
4098
  const exec = options.exec ?? defaultExec;
3908
4099
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
3909
4100
  const phaseDurations = [];
@@ -3925,17 +4116,17 @@ async function runStackUpdateUnlocked(options) {
3925
4116
  };
3926
4117
  if (options.rollback) return rollbackStackUpdate(options);
3927
4118
  let preBootstrapBackup;
3928
- if (!options.dryRun && existsSync(options.envFile)) {
4119
+ if (!options.dryRun && !options.verifyImages && existsSync(options.envFile)) {
3929
4120
  const preBootstrapEnvRaw = readFileSync(options.envFile, "utf8");
3930
4121
  preBootstrapBackup = backupStackConfig(path.dirname(options.envFile), options.composeFile, preBootstrapEnvRaw, now);
3931
4122
  }
3932
- if (options.bootstrap !== false) {
4123
+ if (options.bootstrap !== false && !options.dryRun && !options.verifyImages) {
3933
4124
  const report = bootstrapStackHost({ ...options, installDocker: options.installDocker ?? !!options.yes });
3934
4125
  if (!options.dryRun) assertHostReadyForApply(report);
3935
4126
  }
3936
4127
  assertBindMountsExist(path.dirname(options.envFile));
3937
4128
  warnLegacyDaemonVolume(options.volumeExists);
3938
- if (!options.dryRun) {
4129
+ if (!options.dryRun && !options.verifyImages) {
3939
4130
  try {
3940
4131
  const endPhase = startPhase("preflight");
3941
4132
  if (!options.preflight) {
@@ -3962,7 +4153,7 @@ async function runStackUpdateUnlocked(options) {
3962
4153
  const hubUrl = options.monitorHubUrl || parseEnv(readFileSync(options.envFile, "utf8")).get("MONITOR_HUB_URL") || "";
3963
4154
  const pairLicense = options.licenseKey || process.env.EXE_LICENSE_KEY || loadLicense() || "";
3964
4155
  const pairDomain = options.domain || process.env.EXE_STACK_DOMAIN || process.env.CUSTOMER_DOMAIN || "";
3965
- if (!options.retainCompose && hubUrl && pairLicense && pairDomain) {
4156
+ if (!options.dryRun && !options.verifyImages && !options.retainCompose && hubUrl && pairLicense && pairDomain) {
3966
4157
  const envBefore = readFileSync(options.envFile, "utf8");
3967
4158
  const parsedEnvBefore = parseEnv(envBefore);
3968
4159
  const monitorToken = parsedEnvBefore.get("MONITOR_AGENT_TOKEN") ?? "";
@@ -3990,6 +4181,7 @@ async function runStackUpdateUnlocked(options) {
3990
4181
  );
3991
4182
  reportStackManifestSource(manifestLoad);
3992
4183
  const manifest = manifestLoad.manifest;
4184
+ let previewServiceSelection;
3993
4185
  if (options.addService || options.removeService) {
3994
4186
  const effectiveVersion = options.targetVersion ?? manifest.latest;
3995
4187
  const manifestRelease = manifest.stacks[effectiveVersion];
@@ -4013,11 +4205,16 @@ async function runStackUpdateUnlocked(options) {
4013
4205
  currentServices.delete(options.removeService);
4014
4206
  console.log(`[stack-update] Removed service: ${options.removeService}`);
4015
4207
  }
4016
- saveServiceSelection(options.envFile, [...currentServices]);
4017
- console.log(`[stack-update] Saved service selection: ${[...currentServices].join(", ")}`);
4208
+ if (options.dryRun || options.verifyImages) {
4209
+ previewServiceSelection = runScopedServiceSelection([...currentServices], now);
4210
+ console.log(`[stack-update] Planned service selection (not saved): ${[...currentServices].join(", ")}`);
4211
+ } else {
4212
+ saveServiceSelection(options.envFile, [...currentServices]);
4213
+ console.log(`[stack-update] Saved service selection: ${[...currentServices].join(", ")}`);
4214
+ }
4018
4215
  }
4019
4216
  const isRunScoped = Boolean(options.services && options.services.length > 0);
4020
- const serviceSelection = isRunScoped ? runScopedServiceSelection(options.services, now) : loadServiceSelection(options.envFile);
4217
+ const serviceSelection = isRunScoped ? runScopedServiceSelection(options.services, now) : previewServiceSelection ?? loadServiceSelection(options.envFile);
4021
4218
  if (isRunScoped) {
4022
4219
  console.log(
4023
4220
  `[stack-update] Service scope for THIS RUN ONLY: ${options.services.join(", ")} (the managed set in ${serviceSelectionPath(options.envFile)} is unchanged)`
@@ -4094,7 +4291,22 @@ ${lines.join("\n")}`
4094
4291
  `[stack-update] Synced monitor healthcheck bin path(s) into the deploy environment: ${[...processHealthBins.pinned, ...processHealthBins.cleared].join(", ")}`
4095
4292
  );
4096
4293
  }
4097
- assertProductionDeployGate(plan, plannedEnvRaw, composeRaw, {
4294
+ const hostState = listHostComposeServices(
4295
+ composeArgsForStack(options.composeFile, options.envFile),
4296
+ options.composePsCapture ?? defaultComposePsCapture
4297
+ );
4298
+ const overrideFile = composeOverrideFileFor(options.composeFile);
4299
+ const workerMigration = options.retainCompose ? { raw: composeRaw, changed: false } : planLegacyWorkerMigration({
4300
+ composeRaw,
4301
+ targetComposeRaw: stagedCompose.templateFound ? stagedCompose.raw : null,
4302
+ env: parseEnv(plannedEnvRaw),
4303
+ hostServices: hostState.services,
4304
+ hostServicesUnavailable: hostState.unavailable,
4305
+ overrideRaw: existsSync(overrideFile) ? readFileSync(overrideFile, "utf8") : null
4306
+ });
4307
+ const composeRawForGate = workerMigration.raw;
4308
+ if (workerMigration.changed) console.log("[stack-update] Planned configuration migration: clear the unused legacy embeddings-worker image default.");
4309
+ assertProductionDeployGate(plan, plannedEnvRaw, composeRawForGate, {
4098
4310
  breakGlassReason: options.breakGlassReason,
4099
4311
  breakGlassAuditFile: options.breakGlassAuditFile,
4100
4312
  strictDigest: options.strictDigest,
@@ -4139,14 +4351,10 @@ ${lines.join("\n")}`
4139
4351
  );
4140
4352
  }
4141
4353
  }
4142
- const hostState = listHostComposeServices(
4143
- composeArgsForStack(options.composeFile, options.envFile),
4144
- options.composePsCapture ?? defaultComposePsCapture
4145
- );
4146
4354
  assertSignedManifestDeliveryGate({
4147
4355
  release: manifest.stacks[plan.targetVersion],
4148
4356
  targetVersion: plan.targetVersion,
4149
- composeRaw,
4357
+ composeRaw: composeRawForGate,
4150
4358
  envRaw: plannedEnvRaw,
4151
4359
  hostServices: hostState.services,
4152
4360
  hostServicesUnavailable: hostState.unavailable,
@@ -4263,7 +4471,9 @@ ${lines.join("\n")}`
4263
4471
  }
4264
4472
  };
4265
4473
  const containersRunning = plan.changes.length === 0 ? (options.stackContainersRunning ?? areStackContainersRunning)(options.composeFile, options.envFile) : true;
4266
- if (options.dryRun || !options.resume && plan.changes.length === 0 && containersRunning) {
4474
+ const managedTargetPlan = isRunScoped ? createStackUpdatePlan(manifest, envRaw, effectiveTargetVersion, void 0, loadServiceSelection(options.envFile), composeRaw) : fullTargetPlan;
4475
+ const versionOnlyUpgrade = Boolean(options.targetVersion && previousVersion && previousVersion !== plan.targetVersion && managedTargetPlan.changes.length === 0);
4476
+ if (options.dryRun || !options.resume && plan.changes.length === 0 && containersRunning && !versionOnlyUpgrade && !workerMigration.changed) {
4267
4477
  const ssoOutcome = skipSsoEdgeReconcileForRetainScope ? { attempted: false, delivered: true } : await reconcileSsoEdge({ apply: !options.dryRun });
4268
4478
  if (!options.dryRun && !ssoOutcome.delivered) {
4269
4479
  throw new Error(
@@ -4341,6 +4551,7 @@ ${lines.join("\n")}`
4341
4551
  }
4342
4552
  } catch (err) {
4343
4553
  if (options.retainCompose) throw err;
4554
+ if (workerMigration.changed) throw new Error(`Could not install the validated legacy worker migration: ${err instanceof Error ? err.message : String(err)}`);
4344
4555
  const reason = err instanceof Error ? err.message : String(err);
4345
4556
  console.warn(`[stack-update] Could not refresh docker-compose.yml (${reason}); using existing compose.`);
4346
4557
  }
@@ -5292,25 +5503,28 @@ function listHostComposeServices(composeArgs, capture = defaultComposePsCapture)
5292
5503
  if (trimmed.startsWith("[")) {
5293
5504
  try {
5294
5505
  const parsed = JSON.parse(trimmed);
5295
- if (Array.isArray(parsed)) records.push(...parsed);
5506
+ if (!Array.isArray(parsed)) return { services: [], unavailable: true };
5507
+ records.push(...parsed);
5296
5508
  } catch {
5509
+ return { services: [], unavailable: true };
5297
5510
  }
5298
- }
5299
- if (records.length === 0) {
5511
+ } else {
5300
5512
  for (const line of trimmed.split("\n")) {
5301
5513
  const l = line.trim();
5302
5514
  if (!l) continue;
5303
5515
  try {
5304
5516
  records.push(JSON.parse(l));
5305
5517
  } catch {
5518
+ return { services: [], unavailable: true };
5306
5519
  }
5307
5520
  }
5308
5521
  }
5309
- if (records.length === 0) return { services: [], unavailable: true };
5310
5522
  const services = /* @__PURE__ */ new Set();
5311
5523
  for (const obj of records) {
5312
- const service = String(obj.Service ?? "").trim();
5313
- if (service) services.add(service);
5524
+ if (!obj || typeof obj !== "object" || Array.isArray(obj) || typeof obj.Service !== "string" || !obj.Service.trim()) {
5525
+ return { services: [], unavailable: true };
5526
+ }
5527
+ services.add(obj.Service.trim());
5314
5528
  }
5315
5529
  return { services: [...services], unavailable: false };
5316
5530
  }
@@ -5663,16 +5877,16 @@ function httpStatus(urlString, hostHeader) {
5663
5877
  });
5664
5878
  }
5665
5879
  function defaultExec(cmd, args, opts) {
5666
- execFileSync(cmd, args, { stdio: "inherit", cwd: opts?.cwd });
5880
+ execFileSync2(cmd, args, { stdio: "inherit", cwd: opts?.cwd });
5667
5881
  }
5668
5882
  function defaultDockerLogin(creds) {
5669
- execFileSync("docker", ["login", creds.registry, "-u", creds.username, "--password-stdin"], {
5883
+ execFileSync2("docker", ["login", creds.registry, "-u", creds.username, "--password-stdin"], {
5670
5884
  input: creds.password,
5671
5885
  stdio: ["pipe", "inherit", "inherit"]
5672
5886
  });
5673
5887
  }
5674
5888
  function defaultDockerLogout(registry) {
5675
- execFileSync("docker", ["logout", registry], { stdio: "ignore" });
5889
+ execFileSync2("docker", ["logout", registry], { stdio: "ignore" });
5676
5890
  }
5677
5891
  async function defaultFetchText(ref, authToken) {
5678
5892
  const res = await fetch(ref, {
@@ -5895,7 +6109,7 @@ function removeDanglingVolumes(volumes, scope) {
5895
6109
  continue;
5896
6110
  }
5897
6111
  try {
5898
- execFileSync("docker", ["volume", "rm", vol.name], {
6112
+ execFileSync2("docker", ["volume", "rm", vol.name], {
5899
6113
  stdio: ["pipe", "pipe", "pipe"],
5900
6114
  timeout: 3e4
5901
6115
  });
@@ -5963,6 +6177,7 @@ export {
5963
6177
  retainedCompletedDependencies,
5964
6178
  retainedContainerIsHealthy,
5965
6179
  assessRetainedHealthBefore,
6180
+ planLegacyWorkerMigration,
5966
6181
  resolveServiceHealthTimeout,
5967
6182
  serviceSelectionPath,
5968
6183
  loadServiceSelection,
@@ -6042,6 +6257,7 @@ export {
6042
6257
  assertRolloutStageAllowed,
6043
6258
  assertBreakingChangesAllowed,
6044
6259
  verifyManifestImagesAvailable,
6260
+ readStagedComposeTemplate,
6045
6261
  assertRetainedComposePlan,
6046
6262
  verifyPreDeployImages,
6047
6263
  collectImageProvenanceIssues,
@@ -212,7 +212,7 @@ async function main(args) {
212
212
  console.log("[health-gate] Starting rollback...");
213
213
  restorePreDeployBackup();
214
214
  try {
215
- const { rollbackStackUpdate, defaultStackPaths } = await import("./stack-update-XXDRKGGR.js");
215
+ const { rollbackStackUpdate, defaultStackPaths } = await import("./stack-update-EBATTQPP.js");
216
216
  const paths = defaultStackPaths();
217
217
  await rollbackStackUpdate({
218
218
  manifestRef: paths.manifestRef,
@@ -140,7 +140,7 @@ async function assertManifestSignature(manifest, publicKeyPem) {
140
140
  "manifest carries no `signature`. Fleets that enforce EXE_STACK_PUBLIC_KEY reject unsigned manifests, so publishing one would ship a release no host can install. Re-sign with the custodied key: node scripts/sign-stack-manifest.mjs deploy/stack-manifests/v0.9.json"
141
141
  );
142
142
  }
143
- const { verifyStackManifestSignature } = await import("./stack-update-XXDRKGGR.js");
143
+ const { verifyStackManifestSignature } = await import("./stack-update-EBATTQPP.js");
144
144
  try {
145
145
  verifyStackManifestSignature(manifest, publicKeyPem);
146
146
  } catch (err) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "generatedAt": "2026-10-12T18:45:23.939Z",
3
+ "generatedAt": "2026-10-14T20:24:44.425Z",
4
4
  "hashes": {
5
5
  "bug-report-worker.js": "0d9712cf7c03fd8eb3c9dc83e7b17b4df8f396787a9eb49e5108172f2ea741a4",
6
6
  "codex-stop-task-finalizer.js": "6f9fecf94aa63e22c5ed78f29faa6b9f6e6b6dc2c4685fddd6725983dfba818a",
@@ -11,7 +11,7 @@ import {
11
11
  resolveRegistryProxyBuildIdentity,
12
12
  resolveStackManifestChannel,
13
13
  runRegistryProxy
14
- } from "../chunk-54STF27U.js";
14
+ } from "../chunk-W3ABSZ6G.js";
15
15
  import "../chunk-MLKGABMK.js";
16
16
  export {
17
17
  SERVED_VERSION_PATH,
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  writeSignedStackManifest
4
- } from "./chunk-WC44PNZU.js";
5
- import "./chunk-WHUQ44F2.js";
4
+ } from "./chunk-5DLZJ3MY.js";
5
+ import "./chunk-RIKFXHLE.js";
6
6
  import "./chunk-EC5H53YD.js";
7
7
  import "./chunk-YYRI2AGK.js";
8
8
  import "./chunk-L25OAFOU.js";
@@ -120,7 +120,7 @@ var STACK_SERVICE_SPECS = Object.freeze([
120
120
  releaseWorkflowFile: "ci.yml",
121
121
  imageName: "update.askexe.com/askexe/exe-auth",
122
122
  env: "AUTH_IMAGE_TAG",
123
- healthUrl: "http://127.0.0.1:3300/api/health",
123
+ healthUrl: "http://127.0.0.1:3300/",
124
124
  category: "auth",
125
125
  description: "Shared SSO sign-in/sign-up"
126
126
  },
@@ -141,7 +141,7 @@ var STACK_SERVICE_SPECS = Object.freeze([
141
141
  sourceImageName: "ghcr.io/askexe/exe-erp",
142
142
  env: "ERP_IMAGE_TAG",
143
143
  healthUrl: "http://127.0.0.1:8069/api/method/ping",
144
- healthHost: "erp.askexe.com",
144
+ healthHost: "${ERP_SITE_NAME:-erp.localhost}",
145
145
  required: false,
146
146
  category: "app",
147
147
  description: "Enterprise resource planning"
@@ -182,40 +182,16 @@ var STACK_SERVICE_SPECS = Object.freeze([
182
182
  composeServices: ["exe-dashboard"],
183
183
  origin: "askexe-release",
184
184
  deployment: "stack",
185
- // SOURCE IS KNOWN: exe-dashboard is built from THIS repo (AskExe/exe-os),
186
- // build context dashboard-page/, by .github/workflows/dashboard-image.yml.
187
- // Production's sha256:950699473914… came from exe-os commit 1d5b69cffa8d
188
- // (#703, 2026-08-13), confirmed in build run 31714341386.
189
- ghRepo: "AskExe/exe-os",
190
- // …but it is OUTSIDE THE RELEASE LANE: that workflow pushes only
191
- // `:sha-<sha>` and `:main`, sets no org.opencontainers.image.version, and
192
- // neither stack.release.json nor the stack manifest mentions it. Production
193
- // is three builds stale and nothing auto-deploys it.
194
- // FAIL-CLOSED DEFAULT ONLY (bug 61e7096e) — this is stale as of
195
- // 2026-09-08: release-dashboard-image.yml now calls release-service.yml@v1
196
- // and stack.release.json's `components.dashboard` describes it. Leaving
197
- // "none" here is deliberate anyway: a constant like this one is exactly
198
- // what went stale last time, so it must never by itself be trusted to stay
199
- // OPEN — it is trusted only to stay CLOSED. The CLI derives the real
200
- // answer from stack.release.json at run time (`releaseLaneWired` in
201
- // ServiceFacts) and that is what actually decides Guard 2. If the
202
- // descriptor entry is ever removed, this default is what re-closes the
203
- // gate without anyone having to remember to flip a flag back.
204
- releaseLane: "none",
205
- // Wired ahead of the release lane itself (bug e49ac42b): once dashboard is
206
- // flipped into the release lane, its producer is `.github/workflows/
207
- // release-dashboard-image.yml` ("Release dashboard image", id 338670900) —
208
- // a DIFFERENT workflow than exe-os's own "Release stack image"
209
- // (id 274190265), even though both live in this repo, both match
210
- // /release/i, and both can run at the same tag commit during a stack
211
- // release. This is the concrete scenario adversarial review flagged: name
212
- // matching would let exe-os's release stand in for dashboard's own.
185
+ // The standalone repository owns publication after the dashboard extraction.
186
+ // Its producer must pass its own gates; an exe-os release cannot stand in.
187
+ ghRepo: "AskExe/exe-dashboard",
213
188
  releaseWorkflowFile: "release-dashboard-image.yml",
214
- imageName: "ghcr.io/askexe/exe-dashboard",
189
+ imageName: "update.askexe.com/askexe/exe-dashboard",
190
+ sourceImageName: "ghcr.io/askexe/exe-dashboard",
215
191
  env: "DASHBOARD_IMAGE_TAG",
192
+ healthUrl: "http://127.0.0.1:3301/health",
216
193
  category: "app",
217
- description: "Customer dashboard (credits, usage, keys, billing)",
218
- note: "Release lane wiring is now DERIVED from stack.release.json's components.dashboard entry (bug 61e7096e), not asserted here. Dashboard may still fail generation for other, real reasons \u2014 e.g. bug d85b98c7 (the release stamp step refuses to publish from an unclean tree, so no v* dashboard image has ever actually been produced) \u2014 but it will fail with an accurate reason from a later guard, not with a stale claim that the lane doesn't exist."
194
+ description: "Customer dashboard (credits, usage, keys, billing)"
219
195
  },
220
196
  {
221
197
  key: "exe-build",
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  writeSignedStackManifest
4
- } from "./chunk-WC44PNZU.js";
5
- import "./chunk-WHUQ44F2.js";
4
+ } from "./chunk-5DLZJ3MY.js";
5
+ import "./chunk-RIKFXHLE.js";
6
6
  import "./chunk-EC5H53YD.js";
7
7
  import "./chunk-YYRI2AGK.js";
8
8
  import "./chunk-L25OAFOU.js";