@askexenow/exe-os 0.9.377 → 0.9.378

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.
@@ -27,7 +27,10 @@ REDIS_PASSWORD=CHANGEME_REDIS_PASSWORD
27
27
  # Off-box upload: the daily cron runs `backup.sh --upload-r2`. Without this the
28
28
  # encrypted archive only ever lands on the same disk as live data (disk death =
29
29
  # total loss). R2 creds are reused from the R2_MEDIA_* vars when set.
30
- # EXE_BACKUP_KEY=CHANGEME_EXE_BACKUP_KEY # 256-bit keyencrypts archives at rest
30
+ # EXE_BACKUP_KEY is REQUIRED for off-box uploadbackup.sh refuses to push an
31
+ # unencrypted archive to R2/S3. A generated .env gets a random one; set yours with
32
+ # EXE_BACKUP_KEY=$(openssl rand -base64 32)
33
+ EXE_BACKUP_KEY=CHANGEME_EXE_BACKUP_KEY
31
34
  BACKUP_RETENTION_DAYS=7
32
35
  # Override the DB container name if your topology differs (default: exe-db).
33
36
  # BACKUP_DB_CONTAINER=exe-db
@@ -139,7 +139,8 @@ rm -rf "$ARCHIVE"
139
139
  echo "[backup] Archive: $TARFILE ($(du -h "$TARFILE" | cut -f1))"
140
140
 
141
141
  # 6b. Encrypt before upload — customer data never leaves the VPS unencrypted.
142
- # Uses EXE_BACKUP_KEY from .env (256-bit, generated at setup).
142
+ # Uses EXE_BACKUP_KEY from .env (256-bit, generated by generate-env.ts and
143
+ # backfilled onto existing hosts by tasks/backup-cron.yml).
143
144
  # Without this key, R2 backups are unreadable — even AskExe cannot access them.
144
145
  BACKUP_KEY="${EXE_BACKUP_KEY:-}"
145
146
  if [[ -z "$BACKUP_KEY" && -f "$SCRIPT_DIR/.env" ]]; then
@@ -156,8 +157,19 @@ if [[ -n "$BACKUP_KEY" ]]; then
156
157
  rm -f "$TARFILE"
157
158
  TARFILE="$ENCRYPTED_FILE"
158
159
  echo "[backup] Encrypted: $TARFILE ($(du -h "$TARFILE" | cut -f1))"
160
+ elif $UPLOAD_R2 || [[ -n "$UPLOAD_S3" ]]; then
161
+ # Off-box upload without a key would push the WHOLE archive — pg_dumpall of
162
+ # every database, the stack .env with POSTGRES_PASSWORD and GOTRUE_JWT_SECRET
163
+ # in it, and the WhatsApp auth state — to third-party object storage in
164
+ # plaintext. The deploy playbook installs `backup.sh --upload-r2` as a nightly
165
+ # root cron, so a warn-and-continue here meant every box whose .env never got
166
+ # the key shipped customer data in the clear every night. Refuse. An operator
167
+ # who wants an unencrypted archive can still take one — locally, by running
168
+ # backup.sh with no upload flag.
169
+ fail "encrypt" "EXE_BACKUP_KEY not set — refusing to upload UNENCRYPTED customer data off-box. Set EXE_BACKUP_KEY in /opt/exe-stack/.env (openssl rand -base64 32), or drop --upload-r2/--upload-s3 to keep the archive on this host."
159
170
  else
160
171
  echo "[backup] WARNING: EXE_BACKUP_KEY not set — backup NOT encrypted. Set it in .env for E2E encryption." >&2
172
+ echo "[backup] (an off-box upload would have been refused; local-only archive kept)" >&2
161
173
  fi
162
174
 
163
175
  # 7. Retention — delete old backups (match BOTH .tar.gz and encrypted .tar.gz.enc).
@@ -225,6 +225,17 @@ export function generateEnv(options: GenerateEnvOptions): string {
225
225
  "",
226
226
  `REDIS_PASSWORD=${randomSecret(RANDOM_SECRET_32)}`,
227
227
  "",
228
+ "# --- Backups ---",
229
+ "# The nightly cron installed by deploy.yml runs `backup.sh --upload-r2`, which",
230
+ "# pushes the archive to third-party object storage. That archive contains a",
231
+ "# pg_dumpall of every database, THIS .env (POSTGRES_PASSWORD, GOTRUE_JWT_SECRET),",
232
+ "# and the WhatsApp auth state — so it is encrypted with AES-256 under the key",
233
+ "# below before it leaves the host, and backup.sh REFUSES to upload without one.",
234
+ "# Generated here rather than left commented out: an unset key used to mean a",
235
+ "# silent plaintext upload every night. Keep a copy — losing it makes every",
236
+ "# off-box backup unrecoverable, by design (not even AskExe can read them).",
237
+ `EXE_BACKUP_KEY=${randomSecret(RANDOM_SECRET_32)}`,
238
+ "",
228
239
  "# --- GoTrue (shared auth) ---",
229
240
  `GOTRUE_JWT_SECRET=${randomSecret(RANDOM_SECRET_48)}`,
230
241
  "GOTRUE_API_PORT=9999",
@@ -530,7 +541,10 @@ export function generateExampleEnv(): string {
530
541
  "# Off-box upload: the daily cron runs `backup.sh --upload-r2`. Without this the",
531
542
  "# encrypted archive only ever lands on the same disk as live data (disk death =",
532
543
  "# total loss). R2 creds are reused from the R2_MEDIA_* vars when set.",
533
- "# EXE_BACKUP_KEY=CHANGEME_EXE_BACKUP_KEY # 256-bit keyencrypts archives at rest",
544
+ "# EXE_BACKUP_KEY is REQUIRED for off-box uploadbackup.sh refuses to push an",
545
+ "# unencrypted archive to R2/S3. A generated .env gets a random one; set yours with",
546
+ "# EXE_BACKUP_KEY=$(openssl rand -base64 32)",
547
+ "EXE_BACKUP_KEY=CHANGEME_EXE_BACKUP_KEY",
534
548
  "BACKUP_RETENTION_DAYS=7",
535
549
  "# Override the DB container name if your topology differs (default: exe-db).",
536
550
  "# BACKUP_DB_CONTAINER=exe-db",
@@ -35,6 +35,20 @@ check_http() {
35
35
  fi
36
36
  }
37
37
 
38
+ # --- Data-access boundary ---
39
+ # This script NEVER reads customer rows. It runs as a root-owned host health
40
+ # check on the customer's own VPS and is restricted, deliberately, to two kinds
41
+ # of query against the stack Postgres container:
42
+ # 1. liveness — `SELECT 1`, which touches no relation at all;
43
+ # 2. catalog — pg_class / pg_namespace metadata about whether the wiki's
44
+ # tables EXIST in the schema the container is pointed at.
45
+ # It previously ran `count(*)` over <schema>.users and <schema>.workspace_documents.
46
+ # That is a scan of customer tables by a host cron, outside the data-access
47
+ # boundary the repo requires for customer data, and it was flagged as such in
48
+ # review of #922. Catalog metadata answers the question the check actually asks
49
+ # ("is the wiki pointed at a schema that has its tables?") without reading a
50
+ # single row. Everything that escapes these functions is a fixed status token
51
+ # (ok / fail:<reason> / skip) written to a root-owned JSONL.
38
52
  check_postgres() {
39
53
  local result
40
54
  result=$(docker exec exe-db psql -U "${POSTGRES_USER:-exe}" -d "${POSTGRES_DB:-exedb}" \
@@ -99,8 +113,12 @@ NOW=$(ts)
99
113
  # and no error anywhere — silent-empty, which is the whole failure mode. Checking it
100
114
  # here bounds how long that can go unnoticed to one cron interval.
101
115
  # Reads the LIVE container env, not .env: a compose override can rewrite DATABASE_URL.
116
+ # Data-access note: CATALOG METADATA ONLY — no customer row is read (see the
117
+ # boundary note above check_postgres). This cannot be done through the wiki's
118
+ # own HTTP API — the wiki serves 200 on an empty schema, which IS the failure
119
+ # mode being detected.
102
120
  check_wiki_schema() {
103
- local url schema counts users docs
121
+ local url schema rows
104
122
  url=$(docker inspect exe-wiki --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null \
105
123
  | sed -n 's/^DATABASE_URL=//p')
106
124
  [[ -z "$url" ]] && { echo "skip"; return; }
@@ -108,16 +126,47 @@ check_wiki_schema() {
108
126
  [[ -z "$schema" ]] && schema="public"
109
127
  # Only ever interpolate a bare identifier into SQL.
110
128
  [[ ! "$schema" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] && { echo "fail:bad-schema-name"; return; }
111
- counts=$(docker exec exe-db psql -U "${POSTGRES_USER:-exe}" -d "${POSTGRES_DB:-exedb}" -tA -F' ' -c \
112
- "SELECT COALESCE((SELECT count(*) FROM ${schema}.users), 0),
113
- COALESCE((SELECT count(*) FROM ${schema}.workspace_documents), 0)" 2>/dev/null)
114
- [[ -z "$counts" ]] && { echo "skip"; return; }
115
- users=$(echo "$counts" | awk '{print $1}')
116
- docs=$(echo "$counts" | awk '{print $2}')
117
- if [[ "${users:-0}" -gt 0 || "${docs:-0}" -gt 0 ]]; then
129
+ # `to_regnamespace`/`to_regclass` return NULL instead of erroring on a name
130
+ # that does not exist, so a wrong schema comes back as a clean "missing"
131
+ # rather than as an aborted query indistinguishable from "psql is down".
132
+ # Identifiers are lowercased first: unquoted SQL folds case, but the string
133
+ # form fed to to_regnamespace does not, so `?schema=Wiki` would otherwise
134
+ # look missing when it is fine.
135
+ schema=$(printf '%s' "$schema" | tr '[:upper:]' '[:lower:]')
136
+ rows=$(docker exec exe-db psql -U "${POSTGRES_USER:-exe}" -d "${POSTGRES_DB:-exedb}" -tA -F' ' -c \
137
+ "SELECT t.tbl,
138
+ CASE WHEN c.oid IS NULL THEN 'missing' ELSE 'present' END,
139
+ COALESCE(c.reltuples::bigint, -1)
140
+ FROM (VALUES ('users'), ('workspace_documents')) AS t(tbl)
141
+ LEFT JOIN pg_class c
142
+ ON c.relname = t.tbl
143
+ AND c.relkind IN ('r', 'p')
144
+ AND c.relnamespace = to_regnamespace('${schema}')
145
+ ORDER BY t.tbl" 2>/dev/null)
146
+ # Two rows are always returned when the query ran at all; anything else means
147
+ # the container or psql is unavailable, which check_postgres already reports.
148
+ [[ "$(printf '%s\n' "$rows" | grep -c .)" -ne 2 ]] && { echo "skip"; return; }
149
+
150
+ local missing est max_est=-1
151
+ missing=$(printf '%s\n' "$rows" | awk '$2 == "missing" { print $1 }' | paste -sd, -)
152
+ if [[ -n "$missing" ]]; then
153
+ # The wrong-schema failure mode: the container is pointed somewhere its
154
+ # tables were never created. This is the signal bug 2a8d3003 is about.
155
+ echo "fail:missing-tables-$schema($missing)"
156
+ return
157
+ fi
158
+ while read -r _ _ est; do
159
+ [[ "${est:-0}" -gt "$max_est" ]] && max_est="$est"
160
+ done < <(printf '%s\n' "$rows")
161
+ if [[ "$max_est" -gt 0 ]]; then
118
162
  echo "ok:$schema"
119
163
  else
120
- echo "fail:empty-schema-$schema"
164
+ # reltuples is a PLANNER ESTIMATE, and it is 0 or -1 on a table that has
165
+ # simply never been ANALYZEd — which is exactly the state of a freshly
166
+ # restored or freshly migrated wiki that DOES have rows. Reporting "empty"
167
+ # here would fire the critical DO-NOT-RESTORE alert below on a healthy
168
+ # host, so an unknown estimate is reported as unknown, never as a failure.
169
+ echo "unknown:$schema-stats-stale"
121
170
  fi
122
171
  }
123
172
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "verified_at": "2026-09-02T16:27:58.480Z",
4
- "manifest_sha256": "ece576114533d5eefd5825e465bb966df86e4e85d74e520fe6c9796f7f3d763f",
5
- "package_version": "0.9.377"
3
+ "verified_at": "2026-09-02T17:30:10.063Z",
4
+ "manifest_sha256": "2ac28000eca0fef6a9f2dd65b7297c98868732c8a9cbcd1ff178fc30e2119ccb",
5
+ "package_version": "0.9.378"
6
6
  }
package/dist/bin/cli.js CHANGED
@@ -709,14 +709,14 @@ ${JSON.stringify(stormAlert, null, 2)}`);
709
709
  const { runStackUpdateCli } = await import("./stack-update.js");
710
710
  await runStackUpdateCli(args.slice(1));
711
711
  } else if (args[0] === "release") {
712
- const { parseReleaseArgs, runStackRelease } = await import("../stack-release-MY6U2QIO.js");
712
+ const { parseReleaseArgs, runStackRelease } = await import("../stack-release-JFB3NJLO.js");
713
713
  const releaseFlags = parseReleaseArgs(args.slice(1));
714
714
  await runStackRelease(releaseFlags);
715
715
  } else if (args[0] === "stack-promote") {
716
- const { runStackPromoteCli } = await import("../stack-promote-OP2URGFF.js");
716
+ const { runStackPromoteCli } = await import("../stack-promote-Y4JXWM6R.js");
717
717
  await runStackPromoteCli(args.slice(1));
718
718
  } else if (args[0] === "stack-generate-manifest") {
719
- const { runStackGenerateManifestCli } = await import("../stack-generate-manifest-7WJZWSPK.js");
719
+ const { runStackGenerateManifestCli } = await import("../stack-generate-manifest-BR3GZESE.js");
720
720
  process.exit(await runStackGenerateManifestCli(args.slice(1)));
721
721
  } else if (args[0] === "repo-truth") {
722
722
  const { runRepoTruthCli } = await import("./exe-repo-truth.js");
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  registryProxyOptionsFromEnv,
4
4
  runRegistryProxy
5
- } from "../chunk-FCMQVEVL.js";
5
+ } from "../chunk-A3P3CLE6.js";
6
6
  import {
7
7
  isMainModule
8
8
  } from "../chunk-6Y4B3QF6.js";
@@ -27,8 +27,8 @@ import {
27
27
  serviceSelectionPath,
28
28
  verifyManifestImagesAvailable,
29
29
  warnUnmanagedManifestServices
30
- } from "../chunk-TT5BBVLX.js";
31
- import "../chunk-GHR4ZPJ7.js";
30
+ } from "../chunk-5IUXUD3X.js";
31
+ import "../chunk-EC5H53YD.js";
32
32
  import {
33
33
  runVerifyStack
34
34
  } from "../chunk-PTY2N23C.js";
@@ -43,7 +43,7 @@ import {
43
43
  import {
44
44
  logResult,
45
45
  runHealthGate
46
- } from "../chunk-JRMHGO5B.js";
46
+ } from "../chunk-U3GM5MYE.js";
47
47
  import "../chunk-L25OAFOU.js";
48
48
  import "../chunk-W5MCOZTW.js";
49
49
  import "../chunk-FTZET5YX.js";
@@ -8,7 +8,7 @@ import {
8
8
  logResult,
9
9
  main,
10
10
  runHealthGate
11
- } from "../chunk-JRMHGO5B.js";
11
+ } from "../chunk-U3GM5MYE.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-02T16:27:58.225Z",
3
+ "generated_at": "2026-09-02T17:30:09.805Z",
4
4
  "files": [
5
5
  {
6
6
  "path": "active-agent-BOBBUJ74.js",
@@ -179,7 +179,7 @@
179
179
  },
180
180
  {
181
181
  "path": "bin/cli.js",
182
- "sha256": "6eeb0981cbf05a14ade008e2a90ceff9eaeccd8e8ed12ae6e21bfff0d105764d",
182
+ "sha256": "ff0ebe61eb90170c55e5e3b19df328a051f73a6a4bd40233e168b3e9e9a22ac1",
183
183
  "bytes": 65011
184
184
  },
185
185
  {
@@ -524,7 +524,7 @@
524
524
  },
525
525
  {
526
526
  "path": "bin/registry-proxy.js",
527
- "sha256": "210ef9c6dd138c23e7e0a9c7e51c666e428d0a8a22dfe401e875d06985027cea",
527
+ "sha256": "2d38e5b49eb5749a62453f23c9d84fb3b0fb7f2bce3922a7eafefe295a020648",
528
528
  "bytes": 1885
529
529
  },
530
530
  {
@@ -544,7 +544,7 @@
544
544
  },
545
545
  {
546
546
  "path": "bin/stack-update.js",
547
- "sha256": "30c3ff8677417684e8c8208b372717fe94fed940f7c9e6dd9ef1fac26635350b",
547
+ "sha256": "e0ff5ef9a72ab3afb5e26e9b52cbe4bdb14649d40a9b892a484de419b55ab1b0",
548
548
  "bytes": 57247
549
549
  },
550
550
  {
@@ -564,7 +564,7 @@
564
564
  },
565
565
  {
566
566
  "path": "bin/vps-health-gate.js",
567
- "sha256": "ea15238d388d7b94f937282d05a5a5b82cff60562ef04818983dd5e49c0989be",
567
+ "sha256": "c5489e1f08d2f20572bd17a3ef584abc4472cb387514f992c044f9d5c47037af",
568
568
  "bytes": 333
569
569
  },
570
570
  {
@@ -647,6 +647,11 @@
647
647
  "sha256": "0aae97bac703b5f5102ff180a9b279efb0f0e6b208cb4292f83d353ff47924e7",
648
648
  "bytes": 3642
649
649
  },
650
+ {
651
+ "path": "chunk-3M3N7D7O.js",
652
+ "sha256": "da17bad4260b6af38836556ef7b2698b9ec8c66ec3633c70558c8bd8c8d17f53",
653
+ "bytes": 3250
654
+ },
650
655
  {
651
656
  "path": "chunk-3OEVDGIY.js",
652
657
  "sha256": "ed3690ce4d418043eb922810ed6b67ed626119ff677ef351def6e0bf1e6016d7",
@@ -747,6 +752,11 @@
747
752
  "sha256": "19a9706b415bb25b177a37e1192d9df7d2cfc60cf3c0d997fc5d87c5001e59b1",
748
753
  "bytes": 6076
749
754
  },
755
+ {
756
+ "path": "chunk-5IUXUD3X.js",
757
+ "sha256": "9d553f894e977b53724f71de8a6d9f0a8aec2f48a0700bbb19d245bd60cd660a",
758
+ "bytes": 186260
759
+ },
750
760
  {
751
761
  "path": "chunk-5MOYSU5A.js",
752
762
  "sha256": "c2e2367cffa3e57666f0307f2374f6ca8cbf2f07ba45b7361d680e8271be90ec",
@@ -762,11 +772,6 @@
762
772
  "sha256": "80bfcc6daf45ce066dc277b635bdeba44e42387f0c3b4cfee22287bac913612a",
763
773
  "bytes": 20507
764
774
  },
765
- {
766
- "path": "chunk-5QKLWJ4Z.js",
767
- "sha256": "5ee3b527880c81a19faf07ab497b6c58260a795b5fb753313015ff6029e049cf",
768
- "bytes": 3250
769
- },
770
775
  {
771
776
  "path": "chunk-5UBWBXLF.js",
772
777
  "sha256": "a36075b55ed774c96edb71e5ed68e0aad3c2cc91c82807d374ab3762081ab601",
@@ -867,6 +872,11 @@
867
872
  "sha256": "9ffc6029453a6cecb1164230dbe11515f25d93db4e7d7568a2a9b363ff6689f2",
868
873
  "bytes": 12110
869
874
  },
875
+ {
876
+ "path": "chunk-A3P3CLE6.js",
877
+ "sha256": "24bc185c48cdc2736374cf83a594eeecd488987e19f539bbfc2fde38985b2146",
878
+ "bytes": 30665
879
+ },
870
880
  {
871
881
  "path": "chunk-A7JYF4RG.js",
872
882
  "sha256": "3517b5704e13c31d180df1840da5a4ddf3b55cbad396daa963e783bb9c8f6846",
@@ -1047,6 +1057,11 @@
1047
1057
  "sha256": "0391139d25845faff65e341cf4edf95bb48a2976fbc2a99f35dd47f661cfdb1f",
1048
1058
  "bytes": 21845
1049
1059
  },
1060
+ {
1061
+ "path": "chunk-EC5H53YD.js",
1062
+ "sha256": "31542497d924523e81c2fe63027b0a008d756ce0682592f0de592649a5188302",
1063
+ "bytes": 3928
1064
+ },
1050
1065
  {
1051
1066
  "path": "chunk-ECGTESAP.js",
1052
1067
  "sha256": "80fa0c3b882a1c739530913a08ab4c46e89c76c665ef45084be4baa8fe187283",
@@ -1087,11 +1102,6 @@
1087
1102
  "sha256": "50cfb3eeca20703519c19f97c16bca1578e6d41bf068adca7d62ea37e0cab4be",
1088
1103
  "bytes": 3242
1089
1104
  },
1090
- {
1091
- "path": "chunk-FCMQVEVL.js",
1092
- "sha256": "8b74587c379962e637612221d6677b16ea88405bc7c22f0a0a8c63229b4fcf7e",
1093
- "bytes": 30665
1094
- },
1095
1105
  {
1096
1106
  "path": "chunk-FNEZ4BWT.js",
1097
1107
  "sha256": "03b95f888cd6e804bdfe00e94a9ed83aac34a59e3bf28cd1c3995b5e273a3532",
@@ -1142,11 +1152,6 @@
1142
1152
  "sha256": "6d3b5ff73e66f76bcdcad9cc898bd85904e6ffc3c27bb340055eae1a4d28fe81",
1143
1153
  "bytes": 1155
1144
1154
  },
1145
- {
1146
- "path": "chunk-GHR4ZPJ7.js",
1147
- "sha256": "6f9dc5236c64d9ff74cd8247cc7667d9dcd01c4a5062f2b84a6289b5c589685a",
1148
- "bytes": 2490
1149
- },
1150
1155
  {
1151
1156
  "path": "chunk-GJV3WDWM.js",
1152
1157
  "sha256": "59017c5a02fd07899282dfaaad148e33de6fa003a9b6bdae78b60fdd9d3acc9f",
@@ -1307,11 +1312,6 @@
1307
1312
  "sha256": "533a919e763433ed5e882f31ee225e3a75ae8a643d2936a17767722a6f52fe7d",
1308
1313
  "bytes": 3708
1309
1314
  },
1310
- {
1311
- "path": "chunk-JRMHGO5B.js",
1312
- "sha256": "2322ed24b906fe73faa32d00dfb0dc171094e6611e446b95289c804105426258",
1313
- "bytes": 7587
1314
- },
1315
1315
  {
1316
1316
  "path": "chunk-JSTTPYL3.js",
1317
1317
  "sha256": "acc037274d4e9a5a5705b3f24c44a891dd55f37e428173d29f2d9f582809a24e",
@@ -1772,16 +1772,16 @@
1772
1772
  "sha256": "c044678bdb81f58be175ce56eec162b1f678b19509eafa8893e3beac7def6f72",
1773
1773
  "bytes": 2633
1774
1774
  },
1775
- {
1776
- "path": "chunk-TT5BBVLX.js",
1777
- "sha256": "5d609d7dc2f4979fb6e8c6326eee56ff9d9c8cf60b92bc252df0c1356b2955e2",
1778
- "bytes": 186260
1779
- },
1780
1775
  {
1781
1776
  "path": "chunk-TWMWHDOY.js",
1782
1777
  "sha256": "d1a4b120060e1bda1c09919ff863fbb281c71f017d89b31f47e8df07587814c0",
1783
1778
  "bytes": 2488
1784
1779
  },
1780
+ {
1781
+ "path": "chunk-U3GM5MYE.js",
1782
+ "sha256": "7760a15dba0485ca567c96c9c1e0c3bf59c9bbdc9223869336a7481fd3a25019",
1783
+ "bytes": 7587
1784
+ },
1785
1785
  {
1786
1786
  "path": "chunk-UBMZ3YQU.js",
1787
1787
  "sha256": "340c9342ed76ef63e08ecb48573e8b76ad912f640876b0d50152839f747b6078",
@@ -2464,7 +2464,7 @@
2464
2464
  },
2465
2465
  {
2466
2466
  "path": "hooks/manifest.json",
2467
- "sha256": "aafff5a963888bc061f2b3ce1bdbedc27cc8097b090b0876624bdeb67e0bc28d",
2467
+ "sha256": "935211c99bdd95f9dd65e1a3126e66b8366d3e3e2e4c021dd0e299d98f397e1f",
2468
2468
  "bytes": 1841
2469
2469
  },
2470
2470
  {
@@ -2754,7 +2754,7 @@
2754
2754
  },
2755
2755
  {
2756
2756
  "path": "lib/registry-proxy.js",
2757
- "sha256": "69a2584b0bf78c3dbdba042a0394f4bef3bb7331669e665e62152990d315a1e1",
2757
+ "sha256": "bde5270fe62007134099d85e5c7ae1e8500c6c844204bcf8662ed946fe3b56d9",
2758
2758
  "bytes": 527
2759
2759
  },
2760
2760
  {
@@ -3188,9 +3188,9 @@
3188
3188
  "bytes": 433
3189
3189
  },
3190
3190
  {
3191
- "path": "resolve-image-labels-ZI6UBW5W.js",
3192
- "sha256": "6bd9adeeb3053a1dde6946549776b6799d29f64f188ef57941381eaa6cc0dc0d",
3193
- "bytes": 275
3191
+ "path": "resolve-image-labels-N27XXTUG.js",
3192
+ "sha256": "d3f89a38f106f08cb7f736894a6629e6ce1bf2ae8bcb03f0bf9b9fcf2269bcc5",
3193
+ "bytes": 333
3194
3194
  },
3195
3195
  {
3196
3196
  "path": "restore-proof-HR4TXI3X.js",
@@ -3293,23 +3293,23 @@
3293
3293
  "bytes": 111
3294
3294
  },
3295
3295
  {
3296
- "path": "stack-generate-manifest-7WJZWSPK.js",
3297
- "sha256": "7ba8f753a1f780fe8916e440584656098002ecf7d689056708be04ba98a4de65",
3296
+ "path": "stack-generate-manifest-BR3GZESE.js",
3297
+ "sha256": "65b10366eb91c3f157ef01682b81f22c8ea0ec80c0436aef31661b8a86b29047",
3298
3298
  "bytes": 42703
3299
3299
  },
3300
3300
  {
3301
- "path": "stack-promote-OP2URGFF.js",
3302
- "sha256": "37ac150a4f2b428aa3fb50dd8028e69b16814e61ab6980ae2e3ec407e0310d1a",
3301
+ "path": "stack-promote-Y4JXWM6R.js",
3302
+ "sha256": "4e4f5cae4da628657e7045b26a68895885f232fb2e0a43890592227766caa06e",
3303
3303
  "bytes": 7395
3304
3304
  },
3305
3305
  {
3306
- "path": "stack-release-MY6U2QIO.js",
3307
- "sha256": "aba7c9b54baea1cb4dede11736e36679639610dfb114070e251a03ca597d1c73",
3306
+ "path": "stack-release-JFB3NJLO.js",
3307
+ "sha256": "fcc7c556b206f0da831d25a5f15184c656097dd532528488d5ce92e6dc79e1ec",
3308
3308
  "bytes": 57491
3309
3309
  },
3310
3310
  {
3311
- "path": "stack-update-QSHSLUTZ.js",
3312
- "sha256": "dd8465cdd58a08ff6eb6579b3f4acdfb5e22acb4835a842db9583a806e9bdcff",
3311
+ "path": "stack-update-SWRZLQVQ.js",
3312
+ "sha256": "09bb94cf4e2a6cc99d7c7d112d0de93573cefbe3e1e2faba42af820f0f910f08",
3313
3313
  "bytes": 6185
3314
3314
  },
3315
3315
  {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  canonicalizeStackManifest
3
- } from "./chunk-TT5BBVLX.js";
3
+ } from "./chunk-5IUXUD3X.js";
4
4
 
5
5
  // src/lib/stack-manifest-write.ts
6
6
  import { readFileSync, writeFileSync } from "fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readImageLabel
3
- } from "./chunk-GHR4ZPJ7.js";
3
+ } from "./chunk-EC5H53YD.js";
4
4
  import {
5
5
  clearDeployStalenessAlert,
6
6
  deployStalenessAlertPath,
@@ -136,7 +136,7 @@ async function assertManifestSignature(manifest, publicKeyPem) {
136
136
  "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"
137
137
  );
138
138
  }
139
- const { verifyStackManifestSignature } = await import("./stack-update-QSHSLUTZ.js");
139
+ const { verifyStackManifestSignature } = await import("./stack-update-SWRZLQVQ.js");
140
140
  try {
141
141
  verifyStackManifestSignature(manifest, publicKeyPem);
142
142
  } catch (err) {
@@ -1,6 +1,16 @@
1
1
  // scripts/lib/resolve-image-labels.mjs
2
2
  import { spawnSync } from "child_process";
3
3
  var defaultRunner = (cmd, args) => spawnSync(cmd, args, { encoding: "utf8", timeout: 6e4 });
4
+ var ImageLabelResolutionError = class extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "ImageLabelResolutionError";
8
+ }
9
+ };
10
+ var INDEX_MEDIA_TYPES = /* @__PURE__ */ new Set([
11
+ "application/vnd.oci.image.index.v1+json",
12
+ "application/vnd.docker.distribution.manifest.list.v2+json"
13
+ ]);
4
14
  function repositoryOf(ref) {
5
15
  if (typeof ref !== "string") return ref;
6
16
  const at = ref.indexOf("@");
@@ -24,13 +34,39 @@ function amd64ChildDigest(rawJson) {
24
34
  const digest = pick?.digest;
25
35
  return typeof digest === "string" && digest.length > 0 ? digest : void 0;
26
36
  }
27
- function resolvePlatformRef(imageRef, { runner } = {}) {
37
+ function resolvePlatformRef(imageRef, { runner, failClosed = false } = {}) {
28
38
  const run = runner ?? defaultRunner;
29
39
  const res = run("docker", ["buildx", "imagetools", "inspect", imageRef, "--raw"]);
30
- if (!res || res.status !== 0) return imageRef;
40
+ if (!res || res.status !== 0) {
41
+ if (failClosed) {
42
+ throw new ImageLabelResolutionError(
43
+ `registry manifest fetch failed for ${imageRef} (docker buildx imagetools inspect --raw exited ${res ? res.status : "spawn-failed"}) \u2014 cannot resolve to a platform manifest; refusing to read labels at index level (bug 8415a222)`
44
+ );
45
+ }
46
+ return imageRef;
47
+ }
31
48
  const raw = (res.stdout ?? "").toString();
32
- const childDigest = amd64ChildDigest(raw);
33
- if (!childDigest) return imageRef;
49
+ let doc;
50
+ try {
51
+ doc = JSON.parse(raw);
52
+ } catch {
53
+ if (failClosed) {
54
+ throw new ImageLabelResolutionError(
55
+ `registry returned an unparseable manifest for ${imageRef} \u2014 cannot determine index-vs-manifest; refusing to guess (bug 8415a222)`
56
+ );
57
+ }
58
+ return imageRef;
59
+ }
60
+ const childDigest = amd64ChildDigest(doc);
61
+ if (!childDigest) {
62
+ const isIndex = Boolean(doc && Array.isArray(doc.manifests)) || typeof doc?.mediaType === "string" && INDEX_MEDIA_TYPES.has(doc.mediaType);
63
+ if (isIndex && failClosed) {
64
+ throw new ImageLabelResolutionError(
65
+ `${imageRef} is a multi-arch index with no linux/amd64 child manifest \u2014 no platform manifest exists whose config carries the labels (bug 8415a222)`
66
+ );
67
+ }
68
+ return imageRef;
69
+ }
34
70
  return `${repositoryOf(imageRef)}@${childDigest}`;
35
71
  }
36
72
  function readLabelOff(platformRef, label, { runner } = {}) {
@@ -51,8 +87,8 @@ function readLabelOff(platformRef, label, { runner } = {}) {
51
87
  }
52
88
  return void 0;
53
89
  }
54
- function resolveImageLabels(imageRef, labels, { runner } = {}) {
55
- const platformRef = resolvePlatformRef(imageRef, { runner });
90
+ function resolveImageLabels(imageRef, labels, { runner, failClosed = false } = {}) {
91
+ const platformRef = resolvePlatformRef(imageRef, { runner, failClosed });
56
92
  const out = {};
57
93
  for (const label of labels) out[label] = readLabelOff(platformRef, label, { runner });
58
94
  return out;
@@ -62,6 +98,7 @@ function readImageLabel(imageRef, label, opts = {}) {
62
98
  }
63
99
 
64
100
  export {
101
+ ImageLabelResolutionError,
65
102
  repositoryOf,
66
103
  amd64ChildDigest,
67
104
  resolvePlatformRef,
@@ -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-QSHSLUTZ.js");
215
+ const { rollbackStackUpdate, defaultStackPaths } = await import("./stack-update-SWRZLQVQ.js");
216
216
  const paths = defaultStackPaths();
217
217
  await rollbackStackUpdate({
218
218
  manifestRef: paths.manifestRef,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "generatedAt": "2026-09-02T16:27:58.137Z",
3
+ "generatedAt": "2026-09-02T17:30:09.713Z",
4
4
  "hashes": {
5
5
  "bug-report-worker.js": "8191d9e9ad67127e44969bb4671d21f3ac603dcc8dd978960f1e50b215eac475",
6
6
  "codex-stop-task-finalizer.js": "1c7ebddd870afd357eeab64172a88163ea02957eff527a5e13c5dd24e2f1a678",
@@ -7,7 +7,7 @@ import {
7
7
  registryProxyOptionsFromEnv,
8
8
  resolveRegistryProxyBuildIdentity,
9
9
  runRegistryProxy
10
- } from "../chunk-FCMQVEVL.js";
10
+ } from "../chunk-A3P3CLE6.js";
11
11
  import "../chunk-MLKGABMK.js";
12
12
  export {
13
13
  STACK_RELEASE_PUBLISH_PATH,
@@ -1,12 +1,14 @@
1
1
  import {
2
+ ImageLabelResolutionError,
2
3
  amd64ChildDigest,
3
4
  readImageLabel,
4
5
  repositoryOf,
5
6
  resolveImageLabels,
6
7
  resolvePlatformRef
7
- } from "./chunk-GHR4ZPJ7.js";
8
+ } from "./chunk-EC5H53YD.js";
8
9
  import "./chunk-MLKGABMK.js";
9
10
  export {
11
+ ImageLabelResolutionError,
10
12
  amd64ChildDigest,
11
13
  readImageLabel,
12
14
  repositoryOf,
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  writeSignedStackManifest
4
- } from "./chunk-5QKLWJ4Z.js";
5
- import "./chunk-TT5BBVLX.js";
6
- import "./chunk-GHR4ZPJ7.js";
4
+ } from "./chunk-3M3N7D7O.js";
5
+ import "./chunk-5IUXUD3X.js";
6
+ import "./chunk-EC5H53YD.js";
7
7
  import "./chunk-C5HUMAA7.js";
8
8
  import "./chunk-L25OAFOU.js";
9
9
  import "./chunk-Y2HD2YB7.js";
@@ -710,7 +710,7 @@ async function resolveImageDigest(ref) {
710
710
  return match?.[1];
711
711
  }
712
712
  async function readProvenanceLabels(ref) {
713
- const { resolveImageLabels } = await import("./resolve-image-labels-ZI6UBW5W.js");
713
+ const { resolveImageLabels } = await import("./resolve-image-labels-N27XXTUG.js");
714
714
  return resolveImageLabels(ref, [
715
715
  "org.opencontainers.image.version",
716
716
  "org.opencontainers.image.revision",
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  writeSignedStackManifest
4
- } from "./chunk-5QKLWJ4Z.js";
5
- import "./chunk-TT5BBVLX.js";
6
- import "./chunk-GHR4ZPJ7.js";
4
+ } from "./chunk-3M3N7D7O.js";
5
+ import "./chunk-5IUXUD3X.js";
6
+ import "./chunk-EC5H53YD.js";
7
7
  import "./chunk-C5HUMAA7.js";
8
8
  import "./chunk-L25OAFOU.js";
9
9
  import "./chunk-Y2HD2YB7.js";
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  writeSignedStackManifest
4
- } from "./chunk-5QKLWJ4Z.js";
5
- import "./chunk-TT5BBVLX.js";
6
- import "./chunk-GHR4ZPJ7.js";
4
+ } from "./chunk-3M3N7D7O.js";
5
+ import "./chunk-5IUXUD3X.js";
6
+ import "./chunk-EC5H53YD.js";
7
7
  import {
8
8
  assertPreflightGate,
9
9
  runPreflightOnManifest
@@ -105,8 +105,8 @@ import {
105
105
  verifyStackManifestSignature,
106
106
  waitForHttpOk,
107
107
  warnUnmanagedManifestServices
108
- } from "./chunk-TT5BBVLX.js";
109
- import "./chunk-GHR4ZPJ7.js";
108
+ } from "./chunk-5IUXUD3X.js";
109
+ import "./chunk-EC5H53YD.js";
110
110
  import "./chunk-C5HUMAA7.js";
111
111
  import "./chunk-L25OAFOU.js";
112
112
  import "./chunk-Y2HD2YB7.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askexenow/exe-os",
3
- "version": "0.9.377",
3
+ "version": "0.9.378",
4
4
  "description": "AI employee operating system — persistent memory, task management, and multi-agent coordination for Claude Code.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -1,21 +1,17 @@
1
1
  {
2
- "current": "0.9.377",
2
+ "current": "0.9.378",
3
3
  "notes": {
4
- "0.9.377": {
5
- "version": "0.9.377",
4
+ "0.9.378": {
5
+ "version": "0.9.378",
6
6
  "date": "2026-09-02",
7
7
  "features": [],
8
8
  "fixes": [
9
- "fetch full history+tags in publish checkouts for the ref-provenance guard (#1029)",
10
- "exempt /auth/logout from the SSO gate in crm/wiki/dashboard vhosts (bug 96f8b2b2) (#1027)",
11
- "sso-edge erp vhost preserves Host + websocket upgrade headers (#1024)",
12
- "refuse to push a tag stamped with a non-AskExe identity (#1021)"
9
+ "wire backup + uptime crons into deploy.yml, with the data boundary closed (re-land, bug 6c1893f0) (#1028)",
10
+ "G2 label resolution dereferences the OCI index fail-closed, never reads labels at index level (bug 8415a222) (#1023)"
13
11
  ],
14
12
  "security": [],
15
13
  "other": [
16
- "bump to v0.9.377 (#1031)",
17
- "cross-arch builds use the docker driver (Rosetta), not bundled qemu (#1030)",
18
- "migrate release/x64 lanes off build-my onto the local ci-linux VM (#1026)"
14
+ "bump to v0.9.378 (#1032)"
19
15
  ],
20
16
  "migration_notes": []
21
17
  },
@@ -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.377",
8
- "image": "ghcr.io/askexe/exe-os:v0.9.377",
7
+ "version": "0.9.378",
8
+ "image": "ghcr.io/askexe/exe-os:v0.9.378",
9
9
  "imageEnv": "EXE_OS_IMAGE_TAG",
10
10
  "imageDigestTodo": "TODO(bug 32d6e02f), NARROWED by bug 81ca8903. The top-level `image` is tag-only BY CONSTRUCTION and that is correct: it names the tag the release run is about to build, whose digest does not exist until the push (see the 'Cross-check built digest against the manifest pin' step in .github/workflows/release-stack-image.yml). Demanding a digest here is unsatisfiable, which is why this TODO kept surviving. The one genuinely resolvable case it also covered — components.erp, built in a DIFFERENT repo and therefore already sitting in the registry — is now resolved and digest-pinned below. What remains open is ONLY components.dashboard, and not for the reason this TODO claimed: ghcr.io/askexe/exe-dashboard:v0.9.372 DOES NOT EXIST. .github/workflows/release-dashboard-image.yml has produced exactly one run in its lifetime (id 32405074431, 2026-08-20, tag v0.9.374) and that run FAILED, so the dashboard release lane has never emitted a single v* tag — the only refs in ghcr.io/askexe/exe-dashboard are the floating :main and :sha-<sha> dev refs from dashboard-image.yml. The dashboard pin is therefore a PHANTOM, not an unresolved digest, and it is deliberately left tag-only here rather than silently repointed at a :main digest, because choosing which dashboard bytes constitute a release is a release decision, not a lint fix. FAIL-CLOSED still holds and is enforced where it bites: scripts/publish-stack-release.mjs verifyServicePin() refuses to publish any tag-only app image into the customer manifest.",
11
11
  "components": {
@@ -16,7 +16,7 @@
16
16
  "note": "Bug 81ca8903: was `ghcr.io/askexe/exe-erp:v0.9.312` — a tag that has never existed. ERP is versioned on its own 0.x line; v0.9.312 is an EXE-OS version number that bumpVersions() stamped onto this component because rebaseImageTag() rebased every tag-only component image to the exe-os release version regardless of which repo built it. That rebase is now scoped (same commit) so it can no longer forge tags for independently-versioned components. Repinned to the real, current ERP release v0.3.4, digest resolved live from ghcr.io/askexe/exe-erp on 2026-09-01. Digest-pinning is also what makes this entry self-defending: rebaseImageTag() returns any `@sha256:`-bearing ref untouched. Prior intent for the record (bug 790794e8) was v0.2.0-final8, the first ERP image shipping the realtime/socketio entrypoint (exe-erp PR #15); final3/final7 crash-looped exe-erp-websocket on a missing socketio.js. v0.3.4 supersedes it."
17
17
  },
18
18
  "dashboard": {
19
- "image": "ghcr.io/askexe/exe-dashboard:v0.9.377",
19
+ "image": "ghcr.io/askexe/exe-dashboard:v0.9.378",
20
20
  "env": "DASHBOARD_IMAGE_TAG",
21
21
  "composeService": "exe-dashboard",
22
22
  "versionLockstep": true,
@@ -110,5 +110,5 @@
110
110
  },
111
111
  "deploymentScope": "customer",
112
112
  "highGhostStack": "0.9.9",
113
- "sourceVersion": "0.9.377"
113
+ "sourceVersion": "0.9.378"
114
114
  }