openmates 0.25.0-alpha.0 → 0.25.0-alpha.2

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.
@@ -32647,7 +32647,7 @@ var ROLE_DEFINITIONS = {
32647
32647
  dataBearing: true,
32648
32648
  requiredServices: ["api", "cms", "cms-database", "cache", "vault", "vault-setup", "cms-setup"],
32649
32649
  optionalServices: [...CORE_WORKER_SERVICES, "admin-sidecar", "webapp", "openobserve", "promtail", "prometheus", "cadvisor", "node-exporter", "alertmanager"],
32650
- healthChecks: ["http://localhost:8000/health"],
32650
+ healthChecks: ["http://127.0.0.1:8000/health"],
32651
32651
  templatePath: "templates/core/docker-compose.selfhost.yml",
32652
32652
  composeFile: "backend/core/docker-compose.selfhost.yml"
32653
32653
  },
@@ -32655,7 +32655,7 @@ var ROLE_DEFINITIONS = {
32655
32655
  dataBearing: true,
32656
32656
  requiredServices: ["app-uploads", "clamav", "vault", "vault-setup", "admin-sidecar"],
32657
32657
  optionalServices: [],
32658
- healthChecks: ["http://localhost:8000/health"],
32658
+ healthChecks: ["http://127.0.0.1:8000/health"],
32659
32659
  templatePath: "templates/upload/docker-compose.yml",
32660
32660
  composeFile: "backend/upload/docker-compose.yml"
32661
32661
  },
@@ -32663,7 +32663,7 @@ var ROLE_DEFINITIONS = {
32663
32663
  dataBearing: false,
32664
32664
  requiredServices: ["preview", "admin-sidecar"],
32665
32665
  optionalServices: ["cache"],
32666
- healthChecks: ["http://localhost:8080/health"],
32666
+ healthChecks: ["http://127.0.0.1:8080/health"],
32667
32667
  templatePath: "templates/preview/docker-compose.preview.yml",
32668
32668
  composeFile: "backend/preview/docker-compose.preview.yml"
32669
32669
  }
@@ -33831,15 +33831,37 @@ async function readRuntimeIncidentState(installDir, role) {
33831
33831
  throw error;
33832
33832
  }
33833
33833
  }
33834
- async function writeRuntimeIncidentState(installDir, role, state) {
33834
+ async function runtimeIncidentStateOwner(installDir, effectiveUid = process.geteuid?.() ?? process.getuid?.()) {
33835
+ if (effectiveUid !== 0) return null;
33836
+ const rootDir = path.join(installDir, ".openmates");
33837
+ const stateDir2 = path.join(rootDir, "runtime-health");
33838
+ const [rootInfo, stateInfo] = await Promise.all([fs.lstat(rootDir), fs.lstat(stateDir2)]);
33839
+ for (const info of [rootInfo, stateInfo]) {
33840
+ if (!info.isDirectory() || info.isSymbolicLink() || (info.mode & 511) !== 448) {
33841
+ throw new Error("Runtime health state requires private installer-owned directories.");
33842
+ }
33843
+ }
33844
+ if (rootInfo.uid !== stateInfo.uid || rootInfo.gid !== stateInfo.gid) {
33845
+ throw new Error("Runtime health state directories have conflicting owners.");
33846
+ }
33847
+ return { uid: stateInfo.uid, gid: stateInfo.gid };
33848
+ }
33849
+ async function writeRuntimeIncidentState(installDir, role, state, effectiveUid = process.geteuid?.() ?? process.getuid?.()) {
33835
33850
  const stateDir2 = path.join(installDir, ".openmates", "runtime-health");
33836
33851
  const statePath = path.join(stateDir2, `${role}.json`);
33837
- const temporaryPath = `${statePath}.${process.pid}.tmp`;
33852
+ const temporaryPath = `${statePath}.${randomUUID13()}.tmp`;
33838
33853
  await fs.mkdir(stateDir2, { recursive: true, mode: 448 });
33839
- await fs.writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}
33840
- `, { mode: 384 });
33841
- await fs.rename(temporaryPath, statePath);
33842
- await fs.chmod(statePath, 384);
33854
+ const owner = await runtimeIncidentStateOwner(installDir, effectiveUid);
33855
+ try {
33856
+ await fs.writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}
33857
+ `, { flag: "wx", mode: 384 });
33858
+ if (owner && (owner.uid !== 0 || owner.gid !== 0)) await fs.chown(temporaryPath, owner.uid, owner.gid);
33859
+ await fs.rename(temporaryPath, statePath);
33860
+ await fs.chmod(statePath, 384);
33861
+ } catch (error) {
33862
+ await fs.rm(temporaryPath, { force: true }).catch(() => void 0);
33863
+ throw error;
33864
+ }
33843
33865
  }
33844
33866
  function isPrivateAddress(address) {
33845
33867
  const normalized = address.toLowerCase();
@@ -34159,7 +34181,7 @@ function selectExpectedServerApiUrl(input) {
34159
34181
  if (input.allowExplicitOverride && input.explicitApiUrl?.trim()) {
34160
34182
  return input.explicitApiUrl;
34161
34183
  }
34162
- return input.configuredApiUrl || "http://localhost:8000";
34184
+ return input.configuredApiUrl || "http://127.0.0.1:8000";
34163
34185
  }
34164
34186
  function mergeQuickServerTestUpdateStatus(current, outcome) {
34165
34187
  const { role: _role, updated_at: _updatedAt, ...status } = current;
@@ -34736,6 +34758,56 @@ function lifecycleServiceSelection(role, flags, config) {
34736
34758
  }
34737
34759
  return { services: [], requested: false };
34738
34760
  }
34761
+ function validateReuseCompletedSetupScope(input) {
34762
+ if (input.role !== "core" || input.installMode !== "image" || typeof input.servicesFlag !== "string" || input.excludeFlag !== void 0) {
34763
+ throw new Error("--reuse-completed-setup requires a core image-mode update with explicit --services and no --exclude.");
34764
+ }
34765
+ if (!input.selectedServices.length || input.selectedServices.some((service) => service !== "api" && service !== "prometheus" && !service.endsWith("-worker"))) {
34766
+ throw new Error("--reuse-completed-setup allows only api, worker, and prometheus services; exclude setup and infrastructure services.");
34767
+ }
34768
+ if (!/^sha-[a-f0-9]{40}$/i.test(input.targetTag)) {
34769
+ throw new Error("--reuse-completed-setup requires an immutable --image-tag sha-<40 hex characters>.");
34770
+ }
34771
+ }
34772
+ function validateReuseCompletedSetup(input) {
34773
+ validateReuseCompletedSetupScope(input);
34774
+ const expectedImage = `${input.registry.replace(/\/$/, "")}/openmates-cms-setup:${input.targetTag}`;
34775
+ if (input.setup.image !== expectedImage || input.setup.status !== "exited" || input.setup.exitCode !== 0) {
34776
+ throw new Error(`--reuse-completed-setup requires cms-setup exited successfully with target image ${expectedImage}.`);
34777
+ }
34778
+ for (const service of ["cms", "cms-database", "cache", "vault"]) {
34779
+ const state = input.dependencies[service];
34780
+ if (!state || state.status !== "running" || state.health !== void 0 && state.health !== "healthy") {
34781
+ throw new Error(`--reuse-completed-setup requires running, healthy ${service}.`);
34782
+ }
34783
+ }
34784
+ }
34785
+ function imageUpdateUpArgs(baseArgs, selectedServices, filterRequested, reuseCompletedSetup) {
34786
+ if (reuseCompletedSetup && (!filterRequested || selectedServices.length === 0)) {
34787
+ throw new Error("--reuse-completed-setup requires explicitly selected services.");
34788
+ }
34789
+ return appendSelectedServices([...baseArgs, "up", "-d", ...reuseCompletedSetup ? ["--no-deps"] : []], selectedServices, filterRequested);
34790
+ }
34791
+ function inspectReuseCompletedSetupContainer(installPath, withOverrides, service, includeStopped) {
34792
+ const compose = composeArgs(installPath, withOverrides, "image", "core", false);
34793
+ const ps = spawnSync("docker", [...compose, "ps", ...includeStopped ? ["-a"] : [], "-q", service], { cwd: installPath, encoding: "utf8", timeout: 5e3 });
34794
+ const ids = ps.status === 0 ? ps.stdout.trim().split(/\s+/).filter(Boolean) : [];
34795
+ if (ids.length !== 1) throw new Error(`--reuse-completed-setup could not identify one ${service} container.`);
34796
+ const inspect = (format) => {
34797
+ const result = spawnSync("docker", ["inspect", "--format", format, ids[0]], { cwd: installPath, encoding: "utf8", timeout: 5e3 });
34798
+ if (result.status !== 0) throw new Error(`--reuse-completed-setup could not inspect ${service}.`);
34799
+ return result.stdout.trim();
34800
+ };
34801
+ const image = JSON.parse(inspect("{{json .Config.Image}}"));
34802
+ const state = JSON.parse(inspect("{{json .State}}"));
34803
+ return { image, status: state.Status, exitCode: state.ExitCode, health: state.Health?.Status };
34804
+ }
34805
+ function verifyReuseCompletedSetup(installPath, withOverrides, input) {
34806
+ validateReuseCompletedSetupScope(input);
34807
+ const setup = inspectReuseCompletedSetupContainer(installPath, withOverrides, "cms-setup", true);
34808
+ const dependencies = Object.fromEntries(["cms", "cms-database", "cache", "vault"].map((service) => [service, inspectReuseCompletedSetupContainer(installPath, withOverrides, service, false)]));
34809
+ validateReuseCompletedSetup({ ...input, setup, dependencies });
34810
+ }
34739
34811
  function shouldPullImages() {
34740
34812
  return process.env.OPENMATES_SKIP_IMAGE_PULL !== "1";
34741
34813
  }
@@ -34798,10 +34870,10 @@ ${renderFeatureOverrides(overrides)}`;
34798
34870
  function featureKind(featureId) {
34799
34871
  return featureId.split(":", 1)[0] || "unknown";
34800
34872
  }
34801
- function composeArgs(installPath, withOverrides, installMode = getInstallMode(installPath), role = "core") {
34873
+ function composeArgs(installPath, withOverrides, installMode = getInstallMode(installPath), role = "core", materializeCloudNoWebapp = true) {
34802
34874
  const config = loadConfigForInstallPath(installPath);
34803
34875
  const deploymentMode = getInstallDeploymentMode(installPath, config);
34804
- ensureOfficialCloudNoWebappComposeFile(installPath, deploymentMode, role);
34876
+ if (materializeCloudNoWebapp) ensureOfficialCloudNoWebappComposeFile(installPath, deploymentMode, role);
34805
34877
  const env = readEnvMap(installPath);
34806
34878
  const overlayPath = env.OPENMATES_CLOUD_OVERLAY_PATH || config?.openMatesCloudOverlayPath || void 0;
34807
34879
  const resolvedOverlayPath = overlayPath ?? defaultOpenMatesCloudOverlayPath(installPath);
@@ -34935,7 +35007,7 @@ function firstCsvValue(value) {
34935
35007
  }
34936
35008
  function deriveSelfHostCliUrls(envContent) {
34937
35009
  return {
34938
- apiUrl: firstCsvValue(getEnvVar(envContent, "VITE_API_URL")) || "http://localhost:8000",
35010
+ apiUrl: firstCsvValue(getEnvVar(envContent, "VITE_API_URL")) || "http://127.0.0.1:8000",
34939
35011
  appUrl: firstCsvValue(getEnvVar(envContent, "PRODUCTION_URL")) || "http://localhost:5173"
34940
35012
  };
34941
35013
  }
@@ -34958,6 +35030,14 @@ function packagedCaddyTemplatePath(role) {
34958
35030
  function packagedCoreAlertmanagerTemplatePath() {
34959
35031
  return join15(dirname8(new URL(import.meta.url).pathname), "..", "templates", "core", "monitoring", "alertmanager", "alertmanager.yml");
34960
35032
  }
35033
+ function ensureCoreAlertmanagerRuntimeFile(installPath) {
35034
+ const templatePath = packagedCoreAlertmanagerTemplatePath();
35035
+ if (!existsSync13(templatePath)) throw new Error(`Packaged Alertmanager config not found: ${templatePath}`);
35036
+ const runtimePath = join15(installPath, CORE_ALERTMANAGER_CONFIG_FILE);
35037
+ if (fileHash(templatePath) === fileHash(runtimePath)) return;
35038
+ mkdirSync13(dirname8(runtimePath), { recursive: true });
35039
+ copyFileSync2(templatePath, runtimePath);
35040
+ }
34961
35041
  function ensureCorePrometheusRuntimeFiles(installPath) {
34962
35042
  const packagedDir = join15(dirname8(new URL(import.meta.url).pathname), "..", "templates", "core", "monitoring", "prometheus");
34963
35043
  const runtimeDir = join15(installPath, CORE_PROMETHEUS_CONFIG_DIR);
@@ -35029,11 +35109,7 @@ async function writeImageModeRuntimeFiles(installPath, imageTag, role) {
35029
35109
  const promtailConfigPath = join15(installPath, CORE_PROMTAIL_CONFIG_FILE);
35030
35110
  mkdirSync13(dirname8(promtailConfigPath), { recursive: true });
35031
35111
  writeFileSync8(promtailConfigPath, SELFHOST_PROMTAIL_CONFIG_TEMPLATE);
35032
- const alertmanagerTemplatePath = packagedCoreAlertmanagerTemplatePath();
35033
- if (!existsSync13(alertmanagerTemplatePath)) throw new Error(`Packaged Alertmanager config not found: ${alertmanagerTemplatePath}`);
35034
- const alertmanagerConfigPath = join15(installPath, CORE_ALERTMANAGER_CONFIG_FILE);
35035
- mkdirSync13(dirname8(alertmanagerConfigPath), { recursive: true });
35036
- copyFileSync2(alertmanagerTemplatePath, alertmanagerConfigPath);
35112
+ ensureCoreAlertmanagerRuntimeFile(installPath);
35037
35113
  }
35038
35114
  writeFileSync8(join15(vaultConfigDir, "vault.hcl"), VAULT_CONFIG_TEMPLATE);
35039
35115
  ensureImageRuntimeConfig(installPath);
@@ -35081,7 +35157,7 @@ async function checkUrl(url) {
35081
35157
  }
35082
35158
  async function waitForServerHealth(installPath, role = "core", options = {}) {
35083
35159
  if (role === "upload" || role === "preview") {
35084
- const healthUrl = role === "upload" ? "http://localhost:8000/health" : "http://localhost:8080/health";
35160
+ const healthUrl = planServerRuntime({ role }).healthChecks[0];
35085
35161
  const deadline2 = Date.now() + UPDATE_HEALTH_TIMEOUT_MS;
35086
35162
  while (Date.now() < deadline2) {
35087
35163
  if (await checkUrl(healthUrl)) return;
@@ -36706,14 +36782,34 @@ async function serverUpdate(client2, rest, flags) {
36706
36782
  }
36707
36783
  const installPath = resolveServerPath(flags);
36708
36784
  const dryRun = flags["dry-run"] === true;
36709
- if (!dryRun) {
36710
- ensureGitWorkDirEnv(installPath);
36711
- ensureRuntimeMetricsDirectory(installPath);
36712
- }
36713
36785
  const config = loadConfigForInstallPath(installPath);
36714
36786
  const role = getServerRole(flags, config);
36715
36787
  const withOverrides = config?.composeProfile === "full";
36716
36788
  const installMode = getInstallMode(installPath, config);
36789
+ const reuseCompletedSetup = flags["reuse-completed-setup"] === true;
36790
+ if (flags["reuse-completed-setup"] !== void 0 && !reuseCompletedSetup) {
36791
+ throw new Error("--reuse-completed-setup does not take a value.");
36792
+ }
36793
+ let guardedTarget;
36794
+ if (reuseCompletedSetup) {
36795
+ const selection = lifecycleServiceSelection(role, flags, config);
36796
+ const requested = resolveTargetImageTag(flags, getImageTagFromEnv(installPath, config), getPackageVersion());
36797
+ guardedTarget = requested.channel === "stable" ? { ...requested, tag: await resolveStableImageTag() } : requested;
36798
+ requireDocker();
36799
+ verifyReuseCompletedSetup(installPath, withOverrides, {
36800
+ role,
36801
+ installMode,
36802
+ servicesFlag: flags.services,
36803
+ excludeFlag: flags.exclude,
36804
+ selectedServices: selection.services,
36805
+ targetTag: guardedTarget.tag,
36806
+ registry: DEFAULT_IMAGE_REGISTRY
36807
+ });
36808
+ }
36809
+ if (!dryRun) {
36810
+ ensureGitWorkDirEnv(installPath);
36811
+ ensureRuntimeMetricsDirectory(installPath);
36812
+ }
36717
36813
  const deploymentMode = getInstallDeploymentMode(installPath, config);
36718
36814
  const caddyPlan = caddyUpdatePlan(installPath, role, config, flags);
36719
36815
  const releaseUpdateLock = dryRun ? () => void 0 : acquireServerUpdateLock(installPath);
@@ -36738,7 +36834,7 @@ async function serverUpdate(client2, rest, flags) {
36738
36834
  if (installMode === "image") {
36739
36835
  const currentTag = getImageTagFromEnv(installPath, config);
36740
36836
  const requestedTarget = resolveTargetImageTag(flags, currentTag, getPackageVersion());
36741
- const target = requestedTarget.channel === "stable" ? { ...requestedTarget, tag: await resolveStableImageTag() } : requestedTarget;
36837
+ const target = guardedTarget ?? (requestedTarget.channel === "stable" ? { ...requestedTarget, tag: await resolveStableImageTag() } : requestedTarget);
36742
36838
  const templateRef = templateRefForImageTag(target.tag, getPackageVersion());
36743
36839
  const sourceLinks = targetSourceLinks(target.tag, templateRef);
36744
36840
  const safetyPlan = planUpdate({ role, selectedServices, dryRun, skipBackup: flags["skip-backup"] === true, continuous: false, missingRequiredSecrets: missingOrUnverifiedSecrets });
@@ -36759,6 +36855,7 @@ async function serverUpdate(client2, rest, flags) {
36759
36855
  secretPreflight,
36760
36856
  providerKeyReminders: secretPreflight.emptySecretEnvKeys,
36761
36857
  sourceLinks,
36858
+ reuseCompletedSetup,
36762
36859
  blocked: safetyPlan.blocked,
36763
36860
  blockReason: safetyPlan.blockReason,
36764
36861
  dryRun
@@ -36782,7 +36879,7 @@ async function serverUpdate(client2, rest, flags) {
36782
36879
  if (secretPreflight.emptySecretEnvKeys.length) {
36783
36880
  console.log(` Provider keys: add ${secretPreflight.emptySecretEnvKeys.join(", ")} to activate those providers`);
36784
36881
  }
36785
- console.log(" Commands: refresh compose, docker compose pull, docker compose up -d, health checks");
36882
+ console.log(` Commands: refresh compose, docker compose pull, docker compose up -d${reuseCompletedSetup ? " --no-deps" : ""}, health checks`);
36786
36883
  }
36787
36884
  return;
36788
36885
  }
@@ -36822,11 +36919,23 @@ async function serverUpdate(client2, rest, flags) {
36822
36919
  } else {
36823
36920
  console.error("Skipping image pull because OPENMATES_SKIP_IMAGE_PULL=1.");
36824
36921
  }
36922
+ if (reuseCompletedSetup) {
36923
+ verifyReuseCompletedSetup(installPath, withOverrides, {
36924
+ role,
36925
+ installMode,
36926
+ servicesFlag: flags.services,
36927
+ excludeFlag: flags.exclude,
36928
+ selectedServices,
36929
+ targetTag: target.tag,
36930
+ registry: DEFAULT_IMAGE_REGISTRY
36931
+ });
36932
+ }
36825
36933
  writeServerUpdateStatus(installPath, role, { status: "in_progress", targetImageTag: target.tag, sourceLinks, providerKeyReminders: secretPreflight.emptySecretEnvKeys, step: "up" });
36826
- const upArgs2 = appendSelectedServices(
36827
- [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"],
36934
+ const upArgs2 = imageUpdateUpArgs(
36935
+ composeArgs(installPath, withOverrides, installMode, role),
36828
36936
  selectedServices,
36829
- filterRequested
36937
+ filterRequested,
36938
+ reuseCompletedSetup
36830
36939
  );
36831
36940
  code2 = await runInteractive("docker", upArgs2, installPath);
36832
36941
  if (code2 !== 0) throw new Error(`Docker image restart failed with exit code ${code2}.`);
@@ -38091,6 +38200,7 @@ Command Options:
38091
38200
  --exclude <csv> Update all role services except selected services
38092
38201
  --image-tag <tag> Image mode: update to a specific prebuilt image tag
38093
38202
  --channel <name> Image mode: update using stable/main or dev channel tags
38203
+ --reuse-completed-setup Core image mode: reuse verified sha-<40 hex> cms-setup with explicit --services
38094
38204
  --continuous Run continuously in foreground, or use with install-service
38095
38205
  --interval <min> Foreground continuous update interval (default: 30)
38096
38206
  install-service --continuous --channel <name> --window <window>
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  serializeToYaml,
20
20
  shouldRequireTrustedAccountGuard,
21
21
  waitForWorkflowRun
22
- } from "./chunk-VEUFTIP4.js";
22
+ } from "./chunk-TUVCWVER.js";
23
23
  import "./chunk-IWKP55ZB.js";
24
24
  import "./chunk-BBKJZ23O.js";
25
25
  export {
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ import {
43
43
  selectAssistantMessagesForSpeech,
44
44
  serializeToYaml,
45
45
  summarizeAssistantSpeech
46
- } from "./chunk-VEUFTIP4.js";
46
+ } from "./chunk-TUVCWVER.js";
47
47
  import "./chunk-IWKP55ZB.js";
48
48
  import "./chunk-BBKJZ23O.js";
49
49
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmates",
3
- "version": "0.25.0-alpha.0",
3
+ "version": "0.25.0-alpha.2",
4
4
  "description": "OpenMates CLI and SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -296,6 +296,7 @@ services:
296
296
  <<: *openmates-worker-env
297
297
  CELERY_QUEUES: app_ai
298
298
  CELERY_AUTOSCALE_MAX: ${APP_AI_WORKER_CONCURRENCY:-${CELERY_AUTOSCALE_MAX:-3}}
299
+ CELERY_METRICS_PORT: "9102"
299
300
  command: >
300
301
  sh -c "chown -R celeryuser:celeryuser /vault-data && gosu celeryuser python -m celery -A backend.core.api.app.tasks.celery_config worker --loglevel=info --queues=app_ai --concurrency=$${CELERY_AUTOSCALE_MAX} --max-tasks-per-child=50 --max-memory-per-child=600000 --prefetch-multiplier=1"
301
302
 
@@ -307,6 +308,7 @@ services:
307
308
  <<: *openmates-worker-env
308
309
  CELERY_QUEUES: app_images
309
310
  CELERY_AUTOSCALE_MAX: ${APP_IMAGES_WORKER_CONCURRENCY:-${CELERY_AUTOSCALE_MAX:-3}}
311
+ CELERY_METRICS_PORT: "9103"
310
312
  command: >
311
313
  sh -c "chown -R celeryuser:celeryuser /vault-data && gosu celeryuser python -m celery -A backend.core.api.app.tasks.celery_config worker --loglevel=info --queues=app_images --concurrency=$${CELERY_AUTOSCALE_MAX} --max-tasks-per-child=50 --max-memory-per-child=600000 --prefetch-multiplier=1"
312
314
 
@@ -340,6 +342,7 @@ services:
340
342
  environment:
341
343
  <<: *openmates-worker-env
342
344
  CELERY_QUEUES: app_pdf
345
+ CELERY_METRICS_PORT: "9104"
343
346
  command: >
344
347
  sh -c "chown -R celeryuser:celeryuser /vault-data && gosu celeryuser python -m celery -A backend.core.api.app.tasks.celery_config worker --loglevel=info --queues=app_pdf --concurrency=2 --max-tasks-per-child=10 --max-memory-per-child=1000000 --prefetch-multiplier=1"
345
348