@askexenow/exe-os 0.9.389 → 0.9.390

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.
@@ -4217,11 +4217,154 @@
4217
4217
  }
4218
4218
  },
4219
4219
  "rolloutStage": "canary"
4220
+ },
4221
+ "0.9.48": {
4222
+ "version": "0.9.48",
4223
+ "releasedAt": "2026-09-22T19:52:27.677Z",
4224
+ "npmVersion": "0.9.389",
4225
+ "notes": "Authenticated config-only canary from stack 0.9.47. Deliver the reviewed SSO edge redirect fix using exe-os v0.9.389 config apply: the signed release covers the exact packaged compose template and every packaged bind-mounted config asset. Preserve every image pin from 0.9.47; no image movement is authorized. Apply only after registry tarball integrity and installed updater bytes are verified, with backups, strict dependency-ordered health checks, and rollback.",
4226
+ "services": {
4227
+ "exe-db": {
4228
+ "image": "update.askexe.com/askexe/pgvector:0.8.0-pg16@sha256:a132765ec351c65111b5b675928a3a0515a466a40f97277329db8b8209ad8bc9",
4229
+ "env": "EXE_DB_IMAGE",
4230
+ "composeService": "exe-db",
4231
+ "required": true,
4232
+ "category": "core",
4233
+ "description": "Postgres (pgvector) — digest-pinned (bug 32d6e02f; was tag-only in 0.9.25, regressed from 0.9.24)"
4234
+ },
4235
+ "exe-os": {
4236
+ "image": "update.askexe.com/askexe/exe-os:v0.9.382@sha256:266a59a482865eab9f79497999acb33c1fe1868ec54ea5cd1ce971c492f11bd6",
4237
+ "env": "EXE_OS_IMAGE_TAG",
4238
+ "composeService": "exe-os",
4239
+ "healthUrl": "http://127.0.0.1:8765/health",
4240
+ "required": true,
4241
+ "category": "core",
4242
+ "description": "exe-os daemon + MCP server"
4243
+ },
4244
+ "crm": {
4245
+ "image": "update.askexe.com/askexe/exe-crm:v0.9.62@sha256:584113f88ce09f5ea3b93b7b5e8960ebb8a00e34adaa205c92d964b46525b7a8",
4246
+ "env": "CRM_IMAGE_TAG",
4247
+ "composeService": "exe-crm",
4248
+ "healthUrl": "http://127.0.0.1:3000/healthz",
4249
+ "category": "app",
4250
+ "gitSha": "60f8f08faa5d8fe86b92a87eda93eb0d051a6353",
4251
+ "description": "Central authentication and recovery with server-validated shared sessions for CRM web and worker."
4252
+ },
4253
+ "wiki": {
4254
+ "image": "update.askexe.com/askexe/exe-wiki:v0.9.37@sha256:8fa22e77c0742fbb690f7b4c8431a186692a20ebd335fb141782df4289dc337f",
4255
+ "env": "WIKI_IMAGE_TAG",
4256
+ "composeService": "exe-wiki",
4257
+ "healthUrl": "http://127.0.0.1:3001/api/ping",
4258
+ "category": "app",
4259
+ "description": "Central hosted authentication and recovery with server-validated shared sessions, membership enforcement, and no product-local credential form.",
4260
+ "gitSha": "713a4370c2b641b8f8b07fd1e7390afa79a9c931"
4261
+ },
4262
+ "gateway": {
4263
+ "image": "update.askexe.com/askexe/exe-gateway:v2026.08.07-c15f093-stamped@sha256:1bc11a2a0ad1f3929d59d2685696f22a72042b9fca9c75e15c02dc7d3c640105",
4264
+ "env": "GATEWAY_IMAGE_TAG",
4265
+ "composeService": "exe-gateway",
4266
+ "healthUrl": "http://127.0.0.1:3100/health",
4267
+ "category": "app",
4268
+ "description": "WhatsApp/messaging gateway — provenance-stamped rebuild of main (outbound message persistence fix); pinned by digest, dated immutable tag.",
4269
+ "gitSha": "c15f093a0f59730bd5c2c73791ff77059dfa7cc1"
4270
+ },
4271
+ "monitorAgent": {
4272
+ "image": "update.askexe.com/askexe/exe-monitor-agent:v2026.08.09-10660eb4-stamped@sha256:29fbd445f79484df04f000603e4ff445e76f0380f86d687a0033e3a0ef7d1b7a",
4273
+ "env": "MONITOR_AGENT_IMAGE_TAG",
4274
+ "composeService": "exe-monitor-agent",
4275
+ "category": "monitoring",
4276
+ "gitSha": "10660eb4abdeffbcba2aad13549aa58e208ac7a3"
4277
+ },
4278
+ "monitorHub": {
4279
+ "image": "update.askexe.com/askexe/exe-monitor-hub:v2026.08.09-10660eb4-stamped@sha256:c9a9218c2713b8dffea7e1cf8c0702a614657d311a7a4e821c6a543bab29bb97",
4280
+ "env": "MONITOR_HUB_IMAGE_TAG",
4281
+ "composeService": "exe-monitor-hub",
4282
+ "healthUrl": "http://127.0.0.1:8090/api/health",
4283
+ "category": "monitoring",
4284
+ "gitSha": "10660eb4abdeffbcba2aad13549aa58e208ac7a3"
4285
+ },
4286
+ "auth": {
4287
+ "image": "update.askexe.com/askexe/exe-auth:v0.6.10@sha256:0260349962d087cd3c33fed1d362029416e82ae9cc60abca7969beac2cb8639f",
4288
+ "env": "AUTH_IMAGE_TAG",
4289
+ "composeService": "exe-auth",
4290
+ "healthUrl": "http://127.0.0.1:3300/",
4291
+ "category": "auth",
4292
+ "description": "GoTrue SSO and privileged license authority bridge; stateless auth proxy.",
4293
+ "gitSha": "29fe7015d90b80c94db3a1e3904852b934071711"
4294
+ },
4295
+ "erp": {
4296
+ "image": "update.askexe.com/askexe/exe-erp:v0.3.17@sha256:ef61b59d94c04189fe2975045a3816d2fecc8efe3027e0ee8c17d0fa2efdbc3b",
4297
+ "env": "ERP_IMAGE_TAG",
4298
+ "composeService": "exe-erp",
4299
+ "healthUrl": "http://127.0.0.1:8069/api/method/ping",
4300
+ "healthHost": "localhost",
4301
+ "required": false,
4302
+ "category": "app",
4303
+ "description": "Enterprise Resource Planning with central hosted authentication and subject-bound organization admission.",
4304
+ "gitSha": "a769ec5e0ae53bc5944715845869715ebbffd63a"
4305
+ },
4306
+ "gotrue": {
4307
+ "image": "update.askexe.com/askexe/gotrue:v2.172.1@sha256:b0dd7aa28dd1f9a8bd31f136c8b912661f701d9b863171e5843cb08f18f49de4",
4308
+ "composeService": "gotrue",
4309
+ "healthUrl": "http://127.0.0.1:9999/health",
4310
+ "category": "infra"
4311
+ },
4312
+ "redis": {
4313
+ "image": "update.askexe.com/askexe/redis:7.4-alpine@sha256:6ab0b6e7381779332f97b8ca76193e45b0756f38d4c0dcda72dbb3c32061ab99",
4314
+ "composeService": "redis",
4315
+ "category": "infra"
4316
+ },
4317
+ "clickhouse": {
4318
+ "image": "update.askexe.com/askexe/clickhouse-server:24.8.4.13-alpine@sha256:88a45f9e328549b2579256c46ee38e5c0e25ae58303d9eb6d9c7ed8d6d2bbf3c",
4319
+ "composeService": "clickhouse",
4320
+ "category": "infra"
4321
+ },
4322
+ "cloudflared": {
4323
+ "image": "update.askexe.com/askexe/cloudflared:2026-07@sha256:ba461b8aa9c042156dbd39c38657fe7431bafa063220eab8d5330a523863da9f",
4324
+ "composeService": "cloudflared",
4325
+ "category": "infra"
4326
+ },
4327
+ "exe-sso-edge": {
4328
+ "image": "update.askexe.com/askexe/nginx:alpine@sha256:20316569d8f81a160065d7d2a5eeffc7ca97d79022462ee255fd23fa103a6b5c",
4329
+ "composeService": "exe-sso-edge",
4330
+ "category": "infra"
4331
+ },
4332
+ "exe-otel-collector": {
4333
+ "image": "update.askexe.com/askexe/otel-collector-contrib:0.114.0@sha256:37fa87091cfaaec7234a27e4e395a40c31c2bfaea97a349a4afef6d9e9681197",
4334
+ "composeService": "exe-otel-collector",
4335
+ "category": "infra"
4336
+ },
4337
+ "dashboard": {
4338
+ "image": "update.askexe.com/askexe/exe-dashboard:v0.9.391@sha256:272f3440fa0f93bd19b3a0b53983c488d90b9731f252a1fb9120c9516566b4a5",
4339
+ "env": "DASHBOARD_IMAGE_TAG",
4340
+ "composeService": "exe-dashboard",
4341
+ "healthUrl": "http://127.0.0.1:3301/health",
4342
+ "required": false,
4343
+ "category": "optional",
4344
+ "description": "Customer dashboard at dashboard.<apex> — credits, usage, API keys, billing",
4345
+ "gitSha": "ce76474b7af9476846549a5ae2eb272dc182e9cf"
4346
+ }
4347
+ },
4348
+ "rolloutStage": "canary",
4349
+ "config": {
4350
+ "composeSha256": "4a648434eef4f194de15cc742684dfd3220be2334035333257dd37faa4828b3d",
4351
+ "assets": {
4352
+ "sso-edge/default.conf.template": "fac3627a51989bbb62b8cba1f2dfd4aab69555b998fd48c0a1a67deb5889f29f",
4353
+ "sso-edge/entrypoint.sh": "2281fc9382ee1f80dbf306a380d165d0afe1fcc37f5b752925e34f5a06419ce9",
4354
+ "sso-edge/error-pages/404.html": "d9a163356479204cf0c200d1cf483a986d3ef4c46c53e02d97bfe128d25e833f",
4355
+ "sso-edge/error-pages/500.html": "82972c5fbb237047f532c95d8757a2bcc696ca9466e70c73debf64e8b58689a7",
4356
+ "sso-edge/error-pages/503.html": "58f73f74cabff1a8faa63b11af36aa643c78ac740c9a31378f8a8ee6aa572cf8",
4357
+ "sso-edge/sso-redirect.conf": "d9e1036ba2129450372caff3f09d5ad8894b3b774ab50fe0807322e0f8896d7b",
4358
+ "erp-nginx/nginx.conf": "918d3e3b9c854c3c2bfefaab48ea4e41c4430dd92429e9f035676783d7136ab7",
4359
+ "observability/otel-collector-config.yaml": "01fcd1192e695455fdf85029e60074bee96ff9565da05f4c01b3428b264fe4d5",
4360
+ "cloudflared/config.yml.example": "c60e28c67d8c13e54d4d15f4a79ed3f4ad8723f6345138b084a1925c871b529a"
4361
+ }
4362
+ }
4220
4363
  }
4221
4364
  },
4222
4365
  "signature": {
4223
4366
  "keyId": "exe-stack-1",
4224
4367
  "alg": "ed25519",
4225
- "signature": "X/Du82ErExS0sQRLflix8CLUhHLEqGC6QAYxsUT0o/1HUPOhJhM4KiY64sB/zXN9eK6EcYOxtFjyyn2Lyq0nAg=="
4368
+ "signature": "tyRpYIwHzjT4eixgdAqouFG/9rNSBi9QG0lwOkDgCYlxlr7FHePfIW/2s/DiR35f4eUsIJjJWUJltbhe8Nv8Dw=="
4226
4369
  }
4227
4370
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "verified_at": "2026-09-22T20:51:50.033Z",
4
- "manifest_sha256": "3405a6349892bd3f95a497ecce5aa6d6cf25376f6dd35a7eb24a82e50c463d6f",
5
- "package_version": "0.9.389"
3
+ "verified_at": "2026-09-23T01:55:53.298Z",
4
+ "manifest_sha256": "adc75c50c1588490f5f4f0fdeadc2197fe248394ad1dd56f5a4766693ad7af93",
5
+ "package_version": "0.9.390"
6
6
  }
package/dist/bin/cli.js CHANGED
@@ -792,14 +792,14 @@ ${JSON.stringify(stormAlert, null, 2)}`);
792
792
  const { runStackUpdateCli } = await import("./stack-update.js");
793
793
  await runStackUpdateCli(args.slice(1));
794
794
  } else if (args[0] === "release") {
795
- const { parseReleaseArgs, runStackRelease } = await import("../stack-release-2CXFYVWM.js");
795
+ const { parseReleaseArgs, runStackRelease } = await import("../stack-release-RQ2DYYD5.js");
796
796
  const releaseFlags = parseReleaseArgs(args.slice(1));
797
797
  await runStackRelease(releaseFlags);
798
798
  } else if (args[0] === "stack-promote") {
799
- const { runStackPromoteCli } = await import("../stack-promote-6N23OGZY.js");
799
+ const { runStackPromoteCli } = await import("../stack-promote-DFXJDHVA.js");
800
800
  await runStackPromoteCli(args.slice(1));
801
801
  } else if (args[0] === "stack-generate-manifest") {
802
- const { runStackGenerateManifestCli } = await import("../stack-generate-manifest-AXI55MEQ.js");
802
+ const { runStackGenerateManifestCli } = await import("../stack-generate-manifest-VWNDIMZ2.js");
803
803
  process.exit(await runStackGenerateManifestCli(args.slice(1)));
804
804
  } else if (args[0] === "repo-truth") {
805
805
  const { runRepoTruthCli } = await import("./exe-repo-truth.js");
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  registryProxyOptionsFromEnv,
4
4
  runRegistryProxy
5
- } from "../chunk-PZR54BEI.js";
5
+ } from "../chunk-YI4D6E75.js";
6
6
  import {
7
7
  isMainModule
8
8
  } from "../chunk-6Y4B3QF6.js";
@@ -35,8 +35,9 @@ import {
35
35
  serviceSelectionPath,
36
36
  verifyComposeServicesRunning,
37
37
  verifyManifestImagesAvailable,
38
- warnUnmanagedManifestServices
39
- } from "../chunk-UWVXQEQQ.js";
38
+ warnUnmanagedManifestServices,
39
+ withStackUpdateOperationLock
40
+ } from "../chunk-DZPSBA4B.js";
40
41
  import "../chunk-EC5H53YD.js";
41
42
  import {
42
43
  runVerifyStack
@@ -52,7 +53,7 @@ import {
52
53
  import {
53
54
  logResult,
54
55
  runHealthGate
55
- } from "../chunk-J4UVI652.js";
56
+ } from "../chunk-WQN2XK4V.js";
56
57
  import "../chunk-L25OAFOU.js";
57
58
  import "../chunk-RT57D7C4.js";
58
59
  import "../chunk-FTZET5YX.js";
@@ -566,7 +567,7 @@ if (process.argv[1]?.endsWith("preflight.ts") || process.argv[1]?.endsWith("pref
566
567
  // src/lib/stack-config-update.ts
567
568
  import { spawnSync as spawnSync2 } from "child_process";
568
569
  import { createHash } from "crypto";
569
- import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync2, readdirSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
570
+ import { chmodSync, chownSync, existsSync as existsSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync2, readdirSync, renameSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
570
571
  import { tmpdir } from "os";
571
572
  import path2 from "path";
572
573
  function computeConfigDigest(raw) {
@@ -587,6 +588,130 @@ function collectConfigAssetDigests(configDir, subdirs = CONFIG_ASSET_DIRS) {
587
588
  return digests;
588
589
  }
589
590
  var CONFIG_ASSET_DIRS = ["sso-edge", "erp-nginx", "observability", "cloudflared"];
591
+ function assertSafeAssetRelativePath(rel) {
592
+ if (!rel || path2.isAbsolute(rel) || rel.includes("\\") || path2.posix.normalize(rel) !== rel || rel.startsWith("../") || rel.includes("/../")) {
593
+ throw new Error(`Targeted config asset path is not a canonical relative path: ${rel}`);
594
+ }
595
+ const root = rel.split("/")[0];
596
+ if (!CONFIG_ASSET_DIRS.includes(root)) {
597
+ throw new Error(`Targeted config asset is outside the canonical asset roots: ${rel}`);
598
+ }
599
+ }
600
+ function assertNoSymlinkPath(root, rel, requireLeaf) {
601
+ let current = root;
602
+ for (const [index, part] of rel.split("/").entries()) {
603
+ current = path2.join(current, part);
604
+ if (!existsSync2(current)) {
605
+ if (requireLeaf || index < rel.split("/").length - 1) throw new Error(`Targeted config asset path is missing: ${current}`);
606
+ continue;
607
+ }
608
+ const st = lstatSync2(current);
609
+ if (st.isSymbolicLink()) throw new Error(`Targeted config asset path traverses a symlink: ${current}`);
610
+ if (index === rel.split("/").length - 1) {
611
+ if (!st.isFile()) throw new Error(`Targeted config asset is not a regular file: ${current}`);
612
+ if (st.nlink !== 1) throw new Error(`Targeted config asset has unexpected hard links: ${current}`);
613
+ } else if (!st.isDirectory()) {
614
+ throw new Error(`Targeted config asset parent is not a directory: ${current}`);
615
+ }
616
+ }
617
+ }
618
+ function validateTargetedAssetPatches(input) {
619
+ if (input.config.composeSha256 || input.config.assets) {
620
+ throw new Error("Signed config assetPatches are mutually exclusive with composeSha256/assets");
621
+ }
622
+ const entries = Object.entries(input.config.assetPatches ?? {});
623
+ if (entries.length === 0) throw new Error("Signed config assetPatches is empty");
624
+ const normalized = /* @__PURE__ */ new Set();
625
+ return entries.sort(([a], [b]) => a.localeCompare(b)).map(([rel, patch]) => {
626
+ assertSafeAssetRelativePath(rel);
627
+ const norm = path2.posix.normalize(rel);
628
+ if (normalized.has(norm)) throw new Error(`Duplicate normalized targeted asset path: ${rel}`);
629
+ normalized.add(norm);
630
+ if (!/^[a-f0-9]{64}$/.test(patch.fromSha256) || !/^[a-f0-9]{64}$/.test(patch.toSha256)) {
631
+ throw new Error(`Targeted config asset hashes must be lowercase sha256: ${rel}`);
632
+ }
633
+ if (patch.fromSha256 === patch.toSha256) throw new Error(`Targeted config asset transition must change bytes: ${rel}`);
634
+ if (patch.mode !== void 0 && (!Number.isInteger(patch.mode) || patch.mode < 0 || patch.mode > 4095)) {
635
+ throw new Error(`Targeted config asset mode is outside 0000..7777: ${rel}`);
636
+ }
637
+ const consumers = [...new Set(patch.consumers)].sort();
638
+ if (consumers.length === 0 || consumers.length !== patch.consumers.length || consumers.some((v) => !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(v))) {
639
+ throw new Error(`Targeted config asset consumers are invalid or duplicated: ${rel}`);
640
+ }
641
+ const source = path2.join(input.packagedRoot, rel);
642
+ const target = path2.join(input.liveRoot, rel);
643
+ assertNoSymlinkPath(input.packagedRoot, rel, true);
644
+ assertNoSymlinkPath(input.liveRoot, rel, true);
645
+ const sourceHash = computeConfigDigest(readFileSync2(source));
646
+ const targetHash = computeConfigDigest(readFileSync2(target));
647
+ if (sourceHash !== patch.toSha256) throw new Error(`Packaged targeted asset does not match signed target hash: ${rel}`);
648
+ if (targetHash !== patch.fromSha256 && targetHash !== patch.toSha256) throw new Error(`Live targeted asset does not match signed base or target hash: ${rel}`);
649
+ return {
650
+ rel,
651
+ packagedRoot: input.packagedRoot,
652
+ liveRoot: input.liveRoot,
653
+ source,
654
+ target,
655
+ ...patch,
656
+ consumers,
657
+ state: targetHash === patch.toSha256 ? "to" : "from"
658
+ };
659
+ });
660
+ }
661
+ function assertTargetedAssetConsumers(patches, renderedConfigJson, liveRoot) {
662
+ const selected = /* @__PURE__ */ new Set();
663
+ for (const patch of patches) {
664
+ const observed = servicesMountingPaths(renderedConfigJson, liveRoot, [patch.rel]).sort();
665
+ const expected = [...patch.consumers].sort();
666
+ if (JSON.stringify(observed) !== JSON.stringify(expected)) {
667
+ throw new Error(
668
+ `Targeted config asset consumer mismatch for ${patch.rel}: signed [${expected.join(", ")}], live compose [${observed.join(", ")}]`
669
+ );
670
+ }
671
+ for (const service of observed) selected.add(service);
672
+ }
673
+ return [...selected].sort();
674
+ }
675
+ function atomicReplaceTargetedAsset(patch) {
676
+ assertNoSymlinkPath(patch.packagedRoot, patch.rel, true);
677
+ assertNoSymlinkPath(patch.liveRoot, patch.rel, true);
678
+ const sourceBytes = readFileSync2(patch.source);
679
+ if (computeConfigDigest(sourceBytes) !== patch.toSha256) throw new Error(`Packaged targeted asset changed before apply: ${patch.rel}`);
680
+ const liveHash = computeConfigDigest(readFileSync2(patch.target));
681
+ if (liveHash !== patch.fromSha256 && liveHash !== patch.toSha256) throw new Error(`Live targeted asset changed before apply: ${patch.rel}`);
682
+ const prior = statSync(patch.target);
683
+ const temporary = `${patch.target}.exe-targeted-${process.pid}`;
684
+ let created = false;
685
+ try {
686
+ writeFileSync2(temporary, sourceBytes, { mode: 384, flag: "wx" });
687
+ created = true;
688
+ chownSync(temporary, prior.uid, prior.gid);
689
+ const expectedMode = patch.mode ?? prior.mode & 4095;
690
+ chmodSync(temporary, expectedMode);
691
+ if ((statSync(temporary).mode & 4095) !== expectedMode) {
692
+ throw new Error(`Targeted config asset mode could not be applied: ${patch.rel}`);
693
+ }
694
+ renameSync(temporary, patch.target);
695
+ } finally {
696
+ if (created) rmSync2(temporary, { force: true });
697
+ }
698
+ }
699
+ function atomicRestoreAsset(target, prior) {
700
+ const temporary = `${target}.exe-rollback-${process.pid}`;
701
+ let created = false;
702
+ try {
703
+ writeFileSync2(temporary, prior.bytes, { mode: 384, flag: "wx" });
704
+ created = true;
705
+ chownSync(temporary, prior.uid, prior.gid);
706
+ chmodSync(temporary, prior.mode);
707
+ if ((statSync(temporary).mode & 4095) !== prior.mode) {
708
+ throw new Error(`Targeted config rollback mode could not be applied: ${target}`);
709
+ }
710
+ renameSync(temporary, target);
711
+ } finally {
712
+ if (created) rmSync2(temporary, { force: true });
713
+ }
714
+ }
590
715
  function collectConfigAssetModes(configDir, subdirs = CONFIG_ASSET_DIRS) {
591
716
  const modes = {};
592
717
  for (const subdir of subdirs) {
@@ -1042,6 +1167,9 @@ function defaultComposeCapture(args) {
1042
1167
  return result.stdout ?? "";
1043
1168
  }
1044
1169
  async function runStackConfigUpdate(options) {
1170
+ return withStackUpdateOperationLock(options.envFile, () => runStackConfigUpdateUnlocked(options));
1171
+ }
1172
+ async function runStackConfigUpdateUnlocked(options) {
1045
1173
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
1046
1174
  const exec = options.exec ?? ((cmd, args) => {
1047
1175
  const result2 = spawnSync2(cmd, args, { stdio: "inherit", timeout: 6e5 });
@@ -1062,19 +1190,41 @@ async function runStackConfigUpdate(options) {
1062
1190
  );
1063
1191
  }
1064
1192
  const { release, stackVersion, stackVersionSource } = await resolveSignedRelease(options);
1065
- const stagedComposeRaw = readFileSync2(templatePath, "utf8");
1066
- const stagedAssetDigests = collectConfigAssetDigests(path2.dirname(templatePath));
1067
- assertStagedConfigMatchesManifest({
1068
- release,
1069
- composeRaw: stagedComposeRaw,
1070
- assetDigests: stagedAssetDigests,
1071
- // Applying bytes requires the signed release to cover every byte this mode
1072
- // can write. Planning may inspect legacy releases, but apply never does.
1073
- strict: options.dryRun ? options.requireConfigDigest ?? process.env.EXE_STACK_REQUIRE_CONFIG_DIGEST === "1" : true
1074
- });
1075
1193
  const currentComposeRaw = readFileSync2(options.composeFile, "utf8");
1076
1194
  const currentEnvRaw = readFileSync2(options.envFile, "utf8");
1077
- const envPlan = planConfigOnlyEnv({ stagedComposeRaw, envRaw: currentEnvRaw, release });
1195
+ const targeted = release?.config?.assetPatches ? validateTargetedAssetPatches({
1196
+ config: release.config,
1197
+ packagedRoot: path2.dirname(templatePath),
1198
+ liveRoot: configDir
1199
+ }) : void 0;
1200
+ if (targeted && !options.dryRun && !options.auditUrl) {
1201
+ throw new Error("Targeted config delivery requires an authenticated audit URL");
1202
+ }
1203
+ const targetedControlHashes = targeted ? new Map([
1204
+ [options.composeFile, computeConfigDigest(currentComposeRaw)],
1205
+ [options.envFile, computeConfigDigest(currentEnvRaw)],
1206
+ ...options.lockFile && existsSync2(options.lockFile) ? [[options.lockFile, computeConfigDigest(readFileSync2(options.lockFile))]] : []
1207
+ ]) : void 0;
1208
+ const assertTargetedControlFilesUnchanged = () => {
1209
+ for (const [file, expected] of targetedControlHashes ?? []) {
1210
+ if (!existsSync2(file) || computeConfigDigest(readFileSync2(file)) !== expected) {
1211
+ throw new Error(`Targeted config delivery refused: immutable control file changed during the run: ${file}`);
1212
+ }
1213
+ }
1214
+ };
1215
+ const stagedComposeRaw = targeted ? currentComposeRaw : readFileSync2(templatePath, "utf8");
1216
+ const stagedAssetDigests = targeted ? Object.fromEntries(targeted.map((p) => [p.rel, p.toSha256])) : collectConfigAssetDigests(path2.dirname(templatePath));
1217
+ if (!targeted) {
1218
+ assertStagedConfigMatchesManifest({
1219
+ release,
1220
+ composeRaw: stagedComposeRaw,
1221
+ assetDigests: stagedAssetDigests,
1222
+ // Applying bytes requires the signed release to cover every byte this mode
1223
+ // can write. Planning may inspect legacy releases, but apply never does.
1224
+ strict: options.dryRun ? options.requireConfigDigest ?? process.env.EXE_STACK_REQUIRE_CONFIG_DIGEST === "1" : true
1225
+ });
1226
+ }
1227
+ const envPlan = targeted ? { derived: {}, missingImagePins: [], unresolvable: [] } : planConfigOnlyEnv({ stagedComposeRaw, envRaw: currentEnvRaw, release });
1078
1228
  let adopted = {};
1079
1229
  if (envPlan.missingImagePins.length > 0 && options.adoptDeployedPins) {
1080
1230
  const deployedImages = parseDeployedImages(capture([...composeArgs, "ps", "--format", "json"]));
@@ -1098,13 +1248,40 @@ async function runStackConfigUpdate(options) {
1098
1248
  const beforeHashes = parseComposeConfigHashes(capture([...composeArgs, "config", "--hash=*"]));
1099
1249
  const beforeImages = parseComposeImages(capture([...composeArgs, "config", "--format", "json"]));
1100
1250
  const beforeConfigJson = capture([...composeArgs, "config", "--format", "json"]);
1251
+ let deployedBeforeJson = capture([...composeArgs, ...DEPLOYED_IMAGE_PS_ARGS]);
1252
+ const assertTargetedRuntimeGates = () => {
1253
+ if (!targeted) return;
1254
+ const consumers = assertTargetedAssetConsumers(targeted, beforeConfigJson, configDir);
1255
+ assertRollbackConfigMatchesDeployed(
1256
+ beforeHashes,
1257
+ parseDeployedConfigHashes(deployedBeforeJson),
1258
+ consumers
1259
+ );
1260
+ assertNoPendingImageMovement(
1261
+ beforeImages,
1262
+ parseDeployedImages(deployedBeforeJson),
1263
+ consumers
1264
+ );
1265
+ };
1266
+ assertTargetedRuntimeGates();
1101
1267
  const hostAssetDigests = collectConfigAssetDigests(configDir);
1268
+ const hostAssetModes = collectConfigAssetModes(configDir);
1269
+ const stagedAssetModes = targeted ? Object.fromEntries(targeted.map((patch) => [patch.rel, patch.mode ?? hostAssetModes[patch.rel]])) : collectConfigAssetModes(path2.dirname(templatePath));
1102
1270
  const assetChanges = diffConfigAssets(stagedAssetDigests, hostAssetDigests, {
1103
- packaged: collectConfigAssetModes(path2.dirname(templatePath)),
1104
- host: collectConfigAssetModes(configDir)
1271
+ packaged: stagedAssetModes,
1272
+ host: hostAssetModes
1105
1273
  });
1274
+ for (const patch of targeted?.filter((p) => p.state === "to") ?? []) {
1275
+ if (!assetChanges.some((change) => change.rel === patch.rel)) {
1276
+ assetChanges.push({ rel: patch.rel, kind: "changed", reason: "bytes" });
1277
+ }
1278
+ }
1106
1279
  const composeChanged = stagedComposeRaw !== currentComposeRaw;
1107
1280
  if (!composeChanged && assetChanges.length === 0) {
1281
+ if (targeted) {
1282
+ assertTargetedAssetConsumers(targeted, beforeConfigJson, configDir);
1283
+ assertTargetedControlFilesUnchanged();
1284
+ }
1108
1285
  const envVerdict = collectRenderedEnvDrift(
1109
1286
  beforeConfigJson,
1110
1287
  readDeployedServiceEnv(capture, capture([...composeArgs, ...DEPLOYED_IMAGE_PS_ARGS]))
@@ -1135,6 +1312,12 @@ async function runStackConfigUpdate(options) {
1135
1312
  };
1136
1313
  }
1137
1314
  console.log(`[stack-config-update] Compose template: ${composeChanged ? "REFRESH" : "already current"}`);
1315
+ if (targeted) {
1316
+ console.log("[stack-config-update] Signed targeted asset-only transition; compose, env and lock are immutable:");
1317
+ for (const patch of targeted) {
1318
+ console.log(` - ${patch.rel}: ${patch.fromSha256} \u2192 ${patch.toSha256}; consumers: ${patch.consumers.join(", ")}`);
1319
+ }
1320
+ }
1138
1321
  if (assetChanges.length > 0) {
1139
1322
  console.log(`[stack-config-update] Config assets to deliver (${assetChanges.length}):`);
1140
1323
  for (const a of assetChanges) {
@@ -1166,6 +1349,12 @@ async function runStackConfigUpdate(options) {
1166
1349
  const psJson = capture([...composeArgs, ...DEPLOYED_IMAGE_PS_ARGS]);
1167
1350
  const envVerdict = collectRenderedEnvDrift(afterConfigJson, readDeployedServiceEnv(capture, psJson));
1168
1351
  const assetServices = assetChanges.length > 0 ? servicesMountingPaths(afterConfigJson, configDir, assetChanges.map((a) => a.rel)) : [];
1352
+ if (targeted) {
1353
+ const authenticatedConsumers = assertTargetedAssetConsumers(targeted, afterConfigJson, configDir);
1354
+ if (JSON.stringify([...assetServices].sort()) !== JSON.stringify(authenticatedConsumers)) {
1355
+ throw new Error("Targeted config plan selected consumers outside the signed consumer set");
1356
+ }
1357
+ }
1169
1358
  const wouldRecreate = [.../* @__PURE__ */ new Set([...changeSet.changed, ...assetServices])].sort();
1170
1359
  const pending = collectPendingImageMovements(
1171
1360
  parseComposeImages(afterConfigJson),
@@ -1216,7 +1405,16 @@ async function runStackConfigUpdate(options) {
1216
1405
  rmSync2(scratch, { recursive: true, force: true });
1217
1406
  }
1218
1407
  }
1219
- const deployedBeforeJson = capture([...composeArgs, ...DEPLOYED_IMAGE_PS_ARGS]);
1408
+ if (targeted) validateTargetedAssetPatches({
1409
+ config: release.config,
1410
+ packagedRoot: path2.dirname(templatePath),
1411
+ liveRoot: configDir
1412
+ });
1413
+ assertTargetedControlFilesUnchanged();
1414
+ if (targeted) {
1415
+ deployedBeforeJson = capture([...composeArgs, ...DEPLOYED_IMAGE_PS_ARGS]);
1416
+ assertTargetedRuntimeGates();
1417
+ }
1220
1418
  const backup = backupStackConfig(stackDir, options.composeFile, currentEnvRaw, now);
1221
1419
  console.log(`[stack-config-update] Backed up compose + env to ${backup.updateBackupDir}`);
1222
1420
  const assetRollback = /* @__PURE__ */ new Map();
@@ -1224,8 +1422,13 @@ async function runStackConfigUpdate(options) {
1224
1422
  const addedRels = [];
1225
1423
  for (const change of assetChanges) {
1226
1424
  const target = path2.join(configDir, change.rel);
1227
- const priorMode = existsSync2(target) ? statSync(target).mode & 511 : void 0;
1228
- const prior = priorMode === void 0 ? null : { bytes: readFileSync2(target), mode: priorMode };
1425
+ const priorStat = existsSync2(target) ? statSync(target) : void 0;
1426
+ const prior = priorStat === void 0 ? null : {
1427
+ bytes: readFileSync2(target),
1428
+ mode: priorStat.mode & 4095,
1429
+ uid: priorStat.uid,
1430
+ gid: priorStat.gid
1431
+ };
1229
1432
  assetRollback.set(change.rel, prior);
1230
1433
  if (prior === null) {
1231
1434
  addedRels.push(change.rel);
@@ -1260,8 +1463,8 @@ ${addedRels.map((r) => `${r}
1260
1463
  return false;
1261
1464
  }
1262
1465
  };
1263
- const composeRestored = step(`restore ${options.composeFile}`, () => writeFileSync2(options.composeFile, currentComposeRaw));
1264
- const envRestored = step(`restore ${options.envFile}`, () => writeFileSync2(options.envFile, currentEnvRaw));
1466
+ const composeRestored = targeted ? step(`verify unchanged ${options.composeFile}`, assertTargetedControlFilesUnchanged) : step(`restore ${options.composeFile}`, () => writeFileSync2(options.composeFile, currentComposeRaw));
1467
+ const envRestored = targeted ? composeRestored : step(`restore ${options.envFile}`, () => writeFileSync2(options.envFile, currentEnvRaw));
1265
1468
  const unsafeServices = /* @__PURE__ */ new Set();
1266
1469
  for (const [rel, prior] of assetRollback) {
1267
1470
  const restored = step(`restore asset ${rel}`, () => {
@@ -1270,8 +1473,9 @@ ${addedRels.map((r) => `${r}
1270
1473
  rmSync2(target, { force: true });
1271
1474
  return;
1272
1475
  }
1273
- writeFileSync2(target, prior.bytes);
1274
- chmodSync(target, prior.mode);
1476
+ if (targeted) atomicRestoreAsset(target, prior);
1477
+ else writeFileSync2(target, prior.bytes);
1478
+ if (!targeted) chmodSync(target, prior.mode);
1275
1479
  });
1276
1480
  if (!restored) {
1277
1481
  for (const service of servicesMountingPaths(beforeConfigJson, configDir, [rel])) {
@@ -1354,8 +1558,10 @@ ${addedRels.map((r) => `${r}
1354
1558
  const target = path2.join(configDir, change.rel);
1355
1559
  const source = path2.join(path2.dirname(templatePath), change.rel);
1356
1560
  mkdirSync2(path2.dirname(target), { recursive: true });
1357
- writeFileSync2(target, readFileSync2(source));
1358
- chmodSync(target, statSync(source).mode & 511);
1561
+ const targetedPatch = targeted?.find((p) => p.rel === change.rel);
1562
+ if (targetedPatch) atomicReplaceTargetedAsset(targetedPatch);
1563
+ else writeFileSync2(target, readFileSync2(source));
1564
+ if (!targetedPatch) chmodSync(target, statSync(source).mode & 511);
1359
1565
  }
1360
1566
  const afterConfigJson = capture([...composeArgs, "config", "--format", "json"]);
1361
1567
  const afterImages = parseComposeImages(afterConfigJson);
@@ -1381,6 +1587,12 @@ ${addedRels.map((r) => `${r}
1381
1587
  for (const d of envVerdict.drift) console.log(` - ${d.service}: ${d.missingKeys.join(", ")}`);
1382
1588
  }
1383
1589
  const assetServices = assetChanges.length > 0 ? servicesMountingPaths(afterConfigJson, configDir, assetChanges.map((a) => a.rel)) : [];
1590
+ if (targeted) {
1591
+ const authenticatedConsumers = assertTargetedAssetConsumers(targeted, afterConfigJson, configDir);
1592
+ if (JSON.stringify([...assetServices].sort()) !== JSON.stringify(authenticatedConsumers)) {
1593
+ throw new Error("Targeted config apply selected consumers outside the signed consumer set");
1594
+ }
1595
+ }
1384
1596
  const orphanAssets = assetChanges.length > 0 && assetServices.length === 0;
1385
1597
  if (orphanAssets) {
1386
1598
  console.warn(
@@ -1436,6 +1648,24 @@ ${addedRels.map((r) => `${r}
1436
1648
  }
1437
1649
  result = { status: "applied", stackVersion, stackVersionSource, changed: changeSet.changed, added: [], removed: changeSet.removed, envDrift: envVerdict.drift, envUnverified: envVerdict.unverified, assets: assetChanges, backup };
1438
1650
  }
1651
+ if (targeted) {
1652
+ assertTargetedControlFilesUnchanged();
1653
+ const consumers = assertTargetedAssetConsumers(targeted, beforeConfigJson, configDir);
1654
+ const deployedAfterJson = capture([...composeArgs, ...DEPLOYED_IMAGE_PS_ARGS]);
1655
+ assertNoPendingImageMovement(beforeImages, parseDeployedImages(deployedAfterJson), consumers);
1656
+ assertRollbackConfigMatchesDeployed(beforeHashes, parseDeployedConfigHashes(deployedAfterJson), consumers);
1657
+ await postConfigAudit(options, "succeeded", stackVersion, void 0, {
1658
+ changedServices: result.changed,
1659
+ configAssets: targeted.map((p) => ({
1660
+ path: p.rel,
1661
+ fromSha256: p.fromSha256,
1662
+ toSha256: p.toSha256,
1663
+ consumers: p.consumers
1664
+ })),
1665
+ backupDir: result.backup?.updateBackupDir,
1666
+ targetedAssetOnly: true
1667
+ }, true);
1668
+ }
1439
1669
  } catch (err) {
1440
1670
  const original = err instanceof Error ? err : new Error(String(err));
1441
1671
  const rollback = await restore();
@@ -1458,18 +1688,23 @@ ${addedRels.map((r) => `${r}
1458
1688
  }
1459
1689
  throw original;
1460
1690
  }
1461
- await postConfigAudit(options, "succeeded", stackVersion, void 0, {
1462
- changedServices: result.changed,
1463
- removedFromTemplate: result.removed,
1464
- // Keys only — never values.
1465
- renderedEnvKeysAdded: result.envDrift,
1466
- configAssets: result.assets,
1467
- backupDir: result.backup?.updateBackupDir
1468
- });
1691
+ if (!targeted) {
1692
+ await postConfigAudit(options, "succeeded", stackVersion, void 0, {
1693
+ changedServices: result.changed,
1694
+ removedFromTemplate: result.removed,
1695
+ // Keys only — never values.
1696
+ renderedEnvKeysAdded: result.envDrift,
1697
+ configAssets: result.assets,
1698
+ backupDir: result.backup?.updateBackupDir
1699
+ });
1700
+ }
1469
1701
  return result;
1470
1702
  }
1471
- async function postConfigAudit(options, status, stackVersion, error, metadata) {
1472
- if (!options.auditUrl) return;
1703
+ async function postConfigAudit(options, status, stackVersion, error, metadata, required = false) {
1704
+ if (!options.auditUrl) {
1705
+ if (required) throw new Error("Targeted config delivery requires an authenticated audit URL");
1706
+ return;
1707
+ }
1473
1708
  const postJson = options.postJson ?? defaultPostJson;
1474
1709
  try {
1475
1710
  await postJson(options.auditUrl, {
@@ -1483,6 +1718,7 @@ async function postConfigAudit(options, status, stackVersion, error, metadata) {
1483
1718
  }, options.manifestAuthToken);
1484
1719
  } catch (err) {
1485
1720
  console.warn(`[stack-config-update] deploy audit failed: ${err instanceof Error ? err.message : String(err)}`);
1721
+ if (required) throw err;
1486
1722
  }
1487
1723
  }
1488
1724
  function resolvePackagedComposeDir() {
@@ -8,7 +8,7 @@ import {
8
8
  logResult,
9
9
  main,
10
10
  runHealthGate
11
- } from "../chunk-J4UVI652.js";
11
+ } from "../chunk-WQN2XK4V.js";
12
12
  import "../chunk-MLKGABMK.js";
13
13
  export {
14
14
  checkCRM,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 2,
3
- "generated_at": "2026-09-22T20:51:49.983Z",
3
+ "generated_at": "2026-09-23T01:55:53.241Z",
4
4
  "files": [
5
5
  {
6
6
  "path": "active-agent-RZ24KSJN.js",
@@ -179,7 +179,7 @@
179
179
  },
180
180
  {
181
181
  "path": "bin/cli.js",
182
- "sha256": "05f8a09a0f14f21df25173acd1a7eac0fa290596d5d7ebdbb03f90c022e3bf80",
182
+ "sha256": "f28c012345311069b7b0cf118ffd90f721a35bc1caae17389c4973866a1e631e",
183
183
  "bytes": 69528
184
184
  },
185
185
  {
@@ -524,7 +524,7 @@
524
524
  },
525
525
  {
526
526
  "path": "bin/registry-proxy.js",
527
- "sha256": "66475d895940f4aa154f442ec1edf5037c38c384d92d8c8073b81566eccfa781",
527
+ "sha256": "f6863591b7274a3e5762805442d0d398b0245d2da9a8c8db3cd53be89dda9a5f",
528
528
  "bytes": 1885
529
529
  },
530
530
  {
@@ -549,8 +549,8 @@
549
549
  },
550
550
  {
551
551
  "path": "bin/stack-update.js",
552
- "sha256": "c19f626a648a8466562b3908436973f5bfd34a444b1b2e5e17a7c75250cdc049",
553
- "bytes": 112940
552
+ "sha256": "ac357db942b991e27bd9ecfa8396bfbceec2a636472d993247ea04e6b1e8026e",
553
+ "bytes": 124422
554
554
  },
555
555
  {
556
556
  "path": "bin/update.js",
@@ -569,7 +569,7 @@
569
569
  },
570
570
  {
571
571
  "path": "bin/vps-health-gate.js",
572
- "sha256": "d05d9a67c0834f36724579acfe5497d5393f6711e7fbb57222ab0d039524411c",
572
+ "sha256": "6f1523300863a2955319c458138533e788a090a976ba5e4e23d0bace31617480",
573
573
  "bytes": 333
574
574
  },
575
575
  {
@@ -1087,6 +1087,11 @@
1087
1087
  "sha256": "cf3ce4cf6c9cdba17a8a5e3d9f0c78ae42f1cb72f1a06608cc8da1fb27f06a76",
1088
1088
  "bytes": 3292
1089
1089
  },
1090
+ {
1091
+ "path": "chunk-DZPSBA4B.js",
1092
+ "sha256": "10ad560a524f111988594bc6b060e31f7eff292836561b19d520595b77c60662",
1093
+ "bytes": 217772
1094
+ },
1090
1095
  {
1091
1096
  "path": "chunk-E23LNUWJ.js",
1092
1097
  "sha256": "6d692be7ac73bf481e7013c4b948b70acd8282234f645d562457754afaa7a02e",
@@ -1242,6 +1247,11 @@
1242
1247
  "sha256": "0e8678ecd10db44714ef9a25fcab116b0cb3f86b6de9e384ac5bf2effb0dc3c0",
1243
1248
  "bytes": 2405
1244
1249
  },
1250
+ {
1251
+ "path": "chunk-HJNQAEZV.js",
1252
+ "sha256": "0ca99c9f7c52613ebee3db4f210a65094c9afd7b3abceb09ba4638c216ce9078",
1253
+ "bytes": 3766
1254
+ },
1245
1255
  {
1246
1256
  "path": "chunk-HME6ITKV.js",
1247
1257
  "sha256": "d820c2d874d6841b5c9c1186ba98c24aa25d559b9be05a29ae2373e7f70e7960",
@@ -1302,11 +1312,6 @@
1302
1312
  "sha256": "c0b3a6372de1ceaaa0ee8077beab90baddb26d44c0fbf933543b963927819e16",
1303
1313
  "bytes": 57011
1304
1314
  },
1305
- {
1306
- "path": "chunk-J4UVI652.js",
1307
- "sha256": "3f9abccde5d49dcc844f16c00b04dd9c4166c72c09da21aba1d77c13c48e833e",
1308
- "bytes": 7587
1309
- },
1310
1315
  {
1311
1316
  "path": "chunk-J6KMGEJW.js",
1312
1317
  "sha256": "4ed2938b62c86ebc41d12ac03a4217103a1ae035764b21707a6f4dc301df3d5d",
@@ -1582,11 +1587,6 @@
1582
1587
  "sha256": "3dbde846e7f1326185966deb9f0b3f9a476e5eee8185707e841e6e032c4e3cb3",
1583
1588
  "bytes": 3317
1584
1589
  },
1585
- {
1586
- "path": "chunk-PZR54BEI.js",
1587
- "sha256": "a4d5d925118925edec323fd27179e708a905b6967c0b5fa17906b4907e8bfaec",
1588
- "bytes": 40844
1589
- },
1590
1590
  {
1591
1591
  "path": "chunk-Q3V7K4ME.js",
1592
1592
  "sha256": "da852c37b5a75dd01c94885d382a0e940438d0c1c2c4eacef5b354edf5e6468e",
@@ -1742,11 +1742,6 @@
1742
1742
  "sha256": "032609f5317492d3118b5e8c3d7392dccc7b7b628996f78167c0d108cde68130",
1743
1743
  "bytes": 13671
1744
1744
  },
1745
- {
1746
- "path": "chunk-SLMKLEU6.js",
1747
- "sha256": "dbcd057bc0b493c6aa1912c794ff7177a6e045c978dfd05370427cb3e8ee94ff",
1748
- "bytes": 3766
1749
- },
1750
1745
  {
1751
1746
  "path": "chunk-SNHYZYGZ.js",
1752
1747
  "sha256": "f0b853b3055ffc0f869ade14fa735d7e7457c6d77c78f58244bcaa9c0b8a2b36",
@@ -1857,11 +1852,6 @@
1857
1852
  "sha256": "ae7f759300bb873272d6570df8f4e898f1cb08a7411543a35ce80e3f00f0bb43",
1858
1853
  "bytes": 7964
1859
1854
  },
1860
- {
1861
- "path": "chunk-UWVXQEQQ.js",
1862
- "sha256": "349cfc7abc14b5a1a1abbc73aa756368f6644e43e0257d5949b927f7027cd6e4",
1863
- "bytes": 216205
1864
- },
1865
1855
  {
1866
1856
  "path": "chunk-V4TZI6EO.js",
1867
1857
  "sha256": "697ecdef6991c061baab1e9759bc14aecd28631954b2edfdb721c98c72d41c82",
@@ -1932,6 +1922,11 @@
1932
1922
  "sha256": "4a905397d309c73ed4ad5d1ec488309541be3eec2fc6021d275f433ca92cc7a9",
1933
1923
  "bytes": 2265
1934
1924
  },
1925
+ {
1926
+ "path": "chunk-WQN2XK4V.js",
1927
+ "sha256": "802b24d979abc8934743af0537c96194aed4b4965ecec08b64c39cc58121427e",
1928
+ "bytes": 7587
1929
+ },
1935
1930
  {
1936
1931
  "path": "chunk-WT4RI36X.js",
1937
1932
  "sha256": "7ce4fd5dee79504398f6549a7fadeceafaadaea2cd94ef9bcc310280306147c6",
@@ -2007,6 +2002,11 @@
2007
2002
  "sha256": "176e0aa963a006bc1b64eef84c5ec6eeb4c80ab70ee8dddeb578f4448e48a769",
2008
2003
  "bytes": 19304
2009
2004
  },
2005
+ {
2006
+ "path": "chunk-YI4D6E75.js",
2007
+ "sha256": "5d05ebae4399895f6857bcc6f3cba70bcdee9d2113faf2b6d5e522d647b10ab2",
2008
+ "bytes": 40844
2009
+ },
2010
2010
  {
2011
2011
  "path": "chunk-YMRQIENV.js",
2012
2012
  "sha256": "5590051acf78d41e8e1901a3293246b3fceadd28b972b2ff895889df80fbdab9",
@@ -2539,7 +2539,7 @@
2539
2539
  },
2540
2540
  {
2541
2541
  "path": "hooks/manifest.json",
2542
- "sha256": "9d226f76a97ba6f06dac48c634b73ec2e6625b1b5bd45a4cd541ce8939b666cb",
2542
+ "sha256": "d14c91374638ec9d80f35caf6f5666a7e39718c256396130c8817e1f7a6dfdec",
2543
2543
  "bytes": 1841
2544
2544
  },
2545
2545
  {
@@ -2839,7 +2839,7 @@
2839
2839
  },
2840
2840
  {
2841
2841
  "path": "lib/registry-proxy.js",
2842
- "sha256": "d575fb83615a1f7a22b1b9bc6c6ef9d96d093e3d690c13f9536dcacc455dcab5",
2842
+ "sha256": "988a25b33ecca19b40cbdb9111bdc500003c1503951fc39c043a68a73d7e8f49",
2843
2843
  "bytes": 749
2844
2844
  },
2845
2845
  {
@@ -3413,8 +3413,8 @@
3413
3413
  "bytes": 111
3414
3414
  },
3415
3415
  {
3416
- "path": "stack-generate-manifest-AXI55MEQ.js",
3417
- "sha256": "bfed7a6f7cbba732e9060537ee1cc1b129bdc1662456d9aea9bd5b2672693f42",
3416
+ "path": "stack-generate-manifest-VWNDIMZ2.js",
3417
+ "sha256": "4add4b1efd40afc672c5548604be13f6cb631d2804f43081278604529054ccef",
3418
3418
  "bytes": 56960
3419
3419
  },
3420
3420
  {
@@ -3423,19 +3423,19 @@
3423
3423
  "bytes": 4761
3424
3424
  },
3425
3425
  {
3426
- "path": "stack-promote-6N23OGZY.js",
3427
- "sha256": "78e1589c72312681e95f44a643e0c68ba8414587324f43464a26332ae191ff5d",
3426
+ "path": "stack-promote-DFXJDHVA.js",
3427
+ "sha256": "d401050d81af3fa38a53044a3e7e51956d79bbf6b3e6f042b026a1d979ee42ce",
3428
3428
  "bytes": 7425
3429
3429
  },
3430
3430
  {
3431
- "path": "stack-release-2CXFYVWM.js",
3432
- "sha256": "f7bb67a4defdc1dcc9f5f2bedf2c743a8cc671271b98c94fca59322be2d15673",
3431
+ "path": "stack-release-RQ2DYYD5.js",
3432
+ "sha256": "5b3f61e3c60c6ad2bf17d13a93b8925cb9eb8f1e0638aa63ec7314aab1286f1a",
3433
3433
  "bytes": 117550
3434
3434
  },
3435
3435
  {
3436
- "path": "stack-update-U53FBVCQ.js",
3437
- "sha256": "e025823418b831fe8237ae2204777695ef7b37dc8960d2e01e94c6658c7803ad",
3438
- "bytes": 6839
3436
+ "path": "stack-update-MVGCN3C4.js",
3437
+ "sha256": "00460b684d4ee48912250328a7b1d90231a120b66e60e7024f03b43c49382257",
3438
+ "bytes": 6967
3439
3439
  },
3440
3440
  {
3441
3441
  "path": "stalled-dispatch-escalation-MHLYH2PT.js",
@@ -22,8 +22,8 @@ import {
22
22
 
23
23
  // src/lib/stack-update.ts
24
24
  import { execFileSync, spawnSync as spawnSync2 } from "child_process";
25
- import { createVerify, randomBytes, verify as verifySignature } from "crypto";
26
- import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
25
+ import { createHash, createVerify, randomBytes, verify as verifySignature } from "crypto";
26
+ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, realpathSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
27
27
  import http from "http";
28
28
  import https from "https";
29
29
  import net from "net";
@@ -3142,7 +3142,44 @@ function isFatalLiveArtifactFailure(result) {
3142
3142
  if (!result.verdicts) return true;
3143
3143
  return (result.verdicts.disagrees ?? 0) > 0;
3144
3144
  }
3145
+ function stackUpdateOperationLockPort(envFile) {
3146
+ let ancestor = path.resolve(envFile);
3147
+ const suffix = [];
3148
+ while (!existsSync(ancestor)) {
3149
+ const parent = path.dirname(ancestor);
3150
+ if (parent === ancestor) throw new Error(`Cannot canonicalize stack update path: ${envFile}`);
3151
+ suffix.unshift(path.basename(ancestor));
3152
+ ancestor = parent;
3153
+ }
3154
+ const canonical = path.join(realpathSync(ancestor), ...suffix);
3155
+ const digest = createHash("sha256").update(canonical).digest();
3156
+ return 4e4 + digest.readUInt32BE(0) % 2e4;
3157
+ }
3158
+ async function withStackUpdateOperationLock(envFile, operation) {
3159
+ const port = stackUpdateOperationLockPort(envFile);
3160
+ const server = net.createServer();
3161
+ server.on("connection", (socket) => socket.destroy());
3162
+ await new Promise((resolve, reject) => {
3163
+ const onError = (err) => {
3164
+ server.close();
3165
+ reject(err.code === "EADDRINUSE" ? new Error(`Another stack update operation holds 127.0.0.1:${port}; refusing concurrent mutation`) : err);
3166
+ };
3167
+ server.once("error", onError);
3168
+ server.listen({ host: "127.0.0.1", port, exclusive: true }, () => {
3169
+ server.off("error", onError);
3170
+ resolve();
3171
+ });
3172
+ });
3173
+ try {
3174
+ return await operation();
3175
+ } finally {
3176
+ await new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
3177
+ }
3178
+ }
3145
3179
  async function runStackUpdate(options) {
3180
+ return withStackUpdateOperationLock(options.envFile, () => runStackUpdateUnlocked(options));
3181
+ }
3182
+ async function runStackUpdateUnlocked(options) {
3146
3183
  const exec = options.exec ?? defaultExec;
3147
3184
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
3148
3185
  const phaseDurations = [];
@@ -4845,6 +4882,8 @@ export {
4845
4882
  hardenHost,
4846
4883
  LIVE_ARTIFACT_MATCHES_MANIFEST_CHECK,
4847
4884
  isFatalLiveArtifactFailure,
4885
+ stackUpdateOperationLockPort,
4886
+ withStackUpdateOperationLock,
4848
4887
  runStackUpdate,
4849
4888
  readCurrentStackVersion,
4850
4889
  listAvailableVersions,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  canonicalizeStackManifest
3
- } from "./chunk-UWVXQEQQ.js";
3
+ } from "./chunk-DZPSBA4B.js";
4
4
 
5
5
  // src/lib/stack-manifest-write.ts
6
6
  import { readFileSync, statSync, writeFileSync } from "fs";
@@ -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-U53FBVCQ.js");
215
+ const { rollbackStackUpdate, defaultStackPaths } = await import("./stack-update-MVGCN3C4.js");
216
216
  const paths = defaultStackPaths();
217
217
  await rollbackStackUpdate({
218
218
  manifestRef: paths.manifestRef,
@@ -138,7 +138,7 @@ async function assertManifestSignature(manifest, publicKeyPem) {
138
138
  "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"
139
139
  );
140
140
  }
141
- const { verifyStackManifestSignature } = await import("./stack-update-U53FBVCQ.js");
141
+ const { verifyStackManifestSignature } = await import("./stack-update-MVGCN3C4.js");
142
142
  try {
143
143
  verifyStackManifestSignature(manifest, publicKeyPem);
144
144
  } catch (err) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "generatedAt": "2026-09-22T20:51:49.799Z",
3
+ "generatedAt": "2026-09-23T01:55:53.027Z",
4
4
  "hashes": {
5
5
  "bug-report-worker.js": "e114760c0e7c5c3637bfa9a9041a4390760d541dcdacb9c7d751c9fc1d8311af",
6
6
  "codex-stop-task-finalizer.js": "1a465be64326faca8789b113ec2a1d6b8f9020d97dd1ac0a01fec30447d8b2eb",
@@ -11,7 +11,7 @@ import {
11
11
  resolveRegistryProxyBuildIdentity,
12
12
  resolveStackManifestChannel,
13
13
  runRegistryProxy
14
- } from "../chunk-PZR54BEI.js";
14
+ } from "../chunk-YI4D6E75.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-SLMKLEU6.js";
5
- import "./chunk-UWVXQEQQ.js";
4
+ } from "./chunk-HJNQAEZV.js";
5
+ import "./chunk-DZPSBA4B.js";
6
6
  import "./chunk-EC5H53YD.js";
7
7
  import "./chunk-B7WVYLAQ.js";
8
8
  import "./chunk-L25OAFOU.js";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  writeSignedStackManifest
4
- } from "./chunk-SLMKLEU6.js";
5
- import "./chunk-UWVXQEQQ.js";
4
+ } from "./chunk-HJNQAEZV.js";
5
+ import "./chunk-DZPSBA4B.js";
6
6
  import "./chunk-EC5H53YD.js";
7
7
  import "./chunk-B7WVYLAQ.js";
8
8
  import "./chunk-L25OAFOU.js";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  writeSignedStackManifest
4
- } from "./chunk-SLMKLEU6.js";
5
- import "./chunk-UWVXQEQQ.js";
4
+ } from "./chunk-HJNQAEZV.js";
5
+ import "./chunk-DZPSBA4B.js";
6
6
  import "./chunk-EC5H53YD.js";
7
7
  import {
8
8
  assertPreflightGate,
@@ -2028,7 +2028,7 @@ async function warmProxyCache(imageRef) {
2028
2028
  async function resolveImageGitShaLabel(ref, dryRun) {
2029
2029
  if (!ref || dryRun) return void 0;
2030
2030
  try {
2031
- const { readProvenanceLabels } = await import("./stack-generate-manifest-AXI55MEQ.js");
2031
+ const { readProvenanceLabels } = await import("./stack-generate-manifest-VWNDIMZ2.js");
2032
2032
  const labels = await readProvenanceLabels(ref);
2033
2033
  return labels["org.exe.git_sha"] ?? labels["org.opencontainers.image.revision"];
2034
2034
  } catch (err) {
@@ -103,6 +103,7 @@ import {
103
103
  saveServiceSelection,
104
104
  serviceSelectionPath,
105
105
  snapshotComposeImageDefaults,
106
+ stackUpdateOperationLockPort,
106
107
  stripImageDigest,
107
108
  syncImagePinsWithProcessEnv,
108
109
  unmanagedManifestServices,
@@ -115,8 +116,9 @@ import {
115
116
  verifySsoGatedIngressRouting,
116
117
  verifyStackManifestSignature,
117
118
  waitForHttpOk,
118
- warnUnmanagedManifestServices
119
- } from "./chunk-UWVXQEQQ.js";
119
+ warnUnmanagedManifestServices,
120
+ withStackUpdateOperationLock
121
+ } from "./chunk-DZPSBA4B.js";
120
122
  import "./chunk-EC5H53YD.js";
121
123
  import "./chunk-B7WVYLAQ.js";
122
124
  import "./chunk-L25OAFOU.js";
@@ -234,6 +236,7 @@ export {
234
236
  saveServiceSelection,
235
237
  serviceSelectionPath,
236
238
  snapshotComposeImageDefaults,
239
+ stackUpdateOperationLockPort,
237
240
  stripImageDigest,
238
241
  syncImagePinsWithProcessEnv,
239
242
  unmanagedManifestServices,
@@ -246,5 +249,6 @@ export {
246
249
  verifySsoGatedIngressRouting,
247
250
  verifyStackManifestSignature,
248
251
  waitForHttpOk,
249
- warnUnmanagedManifestServices
252
+ warnUnmanagedManifestServices,
253
+ withStackUpdateOperationLock
250
254
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askexenow/exe-os",
3
- "version": "0.9.389",
3
+ "version": "0.9.390",
4
4
  "description": "AI employee operating system \u2014 persistent memory, task management, and multi-agent coordination for Claude Code.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -1,15 +1,20 @@
1
1
  {
2
- "current": "0.9.389",
2
+ "current": "0.9.390",
3
3
  "notes": {
4
- "0.9.389": {
5
- "version": "0.9.389",
6
- "date": "2026-09-22",
7
- "features": [],
4
+ "0.9.390": {
5
+ "version": "0.9.390",
6
+ "date": "2026-09-23",
7
+ "features": [
8
+ "add signed targeted assets, kernel locking, rollback, and regressions"
9
+ ],
8
10
  "fixes": [
9
- "record integrity after orphan pruning"
11
+ "serialize aliases and preflight targeted audit"
10
12
  ],
11
13
  "security": [],
12
- "other": [],
14
+ "other": [
15
+ "prepare updater v0.9.390",
16
+ "signed config-only canary v0.9.48"
17
+ ],
13
18
  "migration_notes": []
14
19
  },
15
20
  "0.9.371": {
@@ -4,8 +4,8 @@
4
4
  "repo": "AskExe/exe-os",
5
5
  "service": "exe-os",
6
6
  "packageName": "@askexenow/exe-os",
7
- "version": "0.9.389",
8
- "image": "ghcr.io/askexe/exe-os:v0.9.389",
7
+ "version": "0.9.390",
8
+ "image": "ghcr.io/askexe/exe-os:v0.9.390",
9
9
  "imageEnv": "EXE_OS_IMAGE_TAG",
10
10
  "imageDigestTodo": "The top-level image names the forthcoming exe-os build and receives its digest after publication. Independently released components, including dashboard, retain verified digest pins; stack publication validates every service pin.",
11
11
  "components": {
@@ -110,5 +110,5 @@
110
110
  },
111
111
  "deploymentScope": "customer",
112
112
  "highGhostStack": "0.9.9",
113
- "sourceVersion": "0.9.389"
113
+ "sourceVersion": "0.9.390"
114
114
  }