openmates 0.25.0-alpha.1 → 0.25.0

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.
@@ -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();
@@ -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);
@@ -36710,14 +36782,34 @@ async function serverUpdate(client2, rest, flags) {
36710
36782
  }
36711
36783
  const installPath = resolveServerPath(flags);
36712
36784
  const dryRun = flags["dry-run"] === true;
36713
- if (!dryRun) {
36714
- ensureGitWorkDirEnv(installPath);
36715
- ensureRuntimeMetricsDirectory(installPath);
36716
- }
36717
36785
  const config = loadConfigForInstallPath(installPath);
36718
36786
  const role = getServerRole(flags, config);
36719
36787
  const withOverrides = config?.composeProfile === "full";
36720
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
+ }
36721
36813
  const deploymentMode = getInstallDeploymentMode(installPath, config);
36722
36814
  const caddyPlan = caddyUpdatePlan(installPath, role, config, flags);
36723
36815
  const releaseUpdateLock = dryRun ? () => void 0 : acquireServerUpdateLock(installPath);
@@ -36742,7 +36834,7 @@ async function serverUpdate(client2, rest, flags) {
36742
36834
  if (installMode === "image") {
36743
36835
  const currentTag = getImageTagFromEnv(installPath, config);
36744
36836
  const requestedTarget = resolveTargetImageTag(flags, currentTag, getPackageVersion());
36745
- const target = requestedTarget.channel === "stable" ? { ...requestedTarget, tag: await resolveStableImageTag() } : requestedTarget;
36837
+ const target = guardedTarget ?? (requestedTarget.channel === "stable" ? { ...requestedTarget, tag: await resolveStableImageTag() } : requestedTarget);
36746
36838
  const templateRef = templateRefForImageTag(target.tag, getPackageVersion());
36747
36839
  const sourceLinks = targetSourceLinks(target.tag, templateRef);
36748
36840
  const safetyPlan = planUpdate({ role, selectedServices, dryRun, skipBackup: flags["skip-backup"] === true, continuous: false, missingRequiredSecrets: missingOrUnverifiedSecrets });
@@ -36763,6 +36855,7 @@ async function serverUpdate(client2, rest, flags) {
36763
36855
  secretPreflight,
36764
36856
  providerKeyReminders: secretPreflight.emptySecretEnvKeys,
36765
36857
  sourceLinks,
36858
+ reuseCompletedSetup,
36766
36859
  blocked: safetyPlan.blocked,
36767
36860
  blockReason: safetyPlan.blockReason,
36768
36861
  dryRun
@@ -36786,7 +36879,7 @@ async function serverUpdate(client2, rest, flags) {
36786
36879
  if (secretPreflight.emptySecretEnvKeys.length) {
36787
36880
  console.log(` Provider keys: add ${secretPreflight.emptySecretEnvKeys.join(", ")} to activate those providers`);
36788
36881
  }
36789
- 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`);
36790
36883
  }
36791
36884
  return;
36792
36885
  }
@@ -36826,11 +36919,23 @@ async function serverUpdate(client2, rest, flags) {
36826
36919
  } else {
36827
36920
  console.error("Skipping image pull because OPENMATES_SKIP_IMAGE_PULL=1.");
36828
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
+ }
36829
36933
  writeServerUpdateStatus(installPath, role, { status: "in_progress", targetImageTag: target.tag, sourceLinks, providerKeyReminders: secretPreflight.emptySecretEnvKeys, step: "up" });
36830
- const upArgs2 = appendSelectedServices(
36831
- [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"],
36934
+ const upArgs2 = imageUpdateUpArgs(
36935
+ composeArgs(installPath, withOverrides, installMode, role),
36832
36936
  selectedServices,
36833
- filterRequested
36937
+ filterRequested,
36938
+ reuseCompletedSetup
36834
36939
  );
36835
36940
  code2 = await runInteractive("docker", upArgs2, installPath);
36836
36941
  if (code2 !== 0) throw new Error(`Docker image restart failed with exit code ${code2}.`);
@@ -38095,6 +38200,7 @@ Command Options:
38095
38200
  --exclude <csv> Update all role services except selected services
38096
38201
  --image-tag <tag> Image mode: update to a specific prebuilt image tag
38097
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
38098
38204
  --continuous Run continuously in foreground, or use with install-service
38099
38205
  --interval <min> Foreground continuous update interval (default: 30)
38100
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-4KYSLHVG.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-4KYSLHVG.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.1",
3
+ "version": "0.25.0",
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