File size: 6,213 Bytes
eb3f11e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import fs from "node:fs/promises";
import path from "node:path";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { valid as validSemver } from "semver";
import { GATEWAY_CONFIG_SELECTION_ENV_KEYS } from "../../config/gateway-env-selection.js";
import { GATEWAY_SERVICE_SELECTOR_ENV_KEYS } from "../../daemon/constants.js";
import { tempWorkspace } from "../../infra/private-temp-workspace.js";
import { SUPERVISOR_HINT_ENV_VARS } from "../../infra/supervisor-markers.js";
import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js";
import { compareSemverStrings } from "../../infra/update-check.js";
import { parseOpenClawSchemaVersions } from "../../state/openclaw-schema-versions.js";
import { createUpdateProgress } from "./progress.js";
import type { InitializedUpdate } from "./update-command-initialization.js";
import { withUpdateInitializationCleanup } from "./update-command-initialization.js";
import { stagePackageInstallUpdate } from "./update-command-package.js";

type StageParams = Parameters<typeof stagePackageInstallUpdate>[0];
type ArtifactInitialization = Pick<InitializedUpdate, "stagedPackage"> & {
  target: Pick<
    InitializedUpdate["target"],
    | "currentVersion"
    | "targetVersion"
    | "packageTargetSchemaVersions"
    | "packageRuntimeTarget"
    | "downgradeRisk"
    | "packageAlreadyCurrent"
    | "refuseUpdate"
  >;
};

/** Candidate hooks may initialize their own profile, never the profile awaiting admission. */
async function withFreshUpdateArtifact<T>(
  params: StageParams,
  use: (candidate: {
    stage: Awaited<ReturnType<typeof stagePackageInstallUpdate>>;
    manifest: unknown;
  }) => Promise<T>,
): Promise<T> {
  const workspace = await tempWorkspace({
    rootDir: resolvePreferredOpenClawTmpDir(),
    prefix: "openclaw-update-artifact-",
  });
  return await withUpdateInitializationCleanup(
    async () => {
      const home = workspace.dir;
      const state = path.join(home, "state");
      const tmp = path.join(home, "tmp");
      await fs.mkdir(tmp);
      const env = { ...params.installEnv };
      // Child env overlays process.env. Tombstones must survive until spawn.
      for (const key of [
        ...[...GATEWAY_CONFIG_SELECTION_ENV_KEYS].filter(
          (selector) => selector.startsWith("OPENCLAW_") || selector === "PI_CODING_AGENT_DIR",
        ),
        ...GATEWAY_SERVICE_SELECTOR_ENV_KEYS,
        ...SUPERVISOR_HINT_ENV_VARS,
        "STATE_DIRECTORY",
        "NODE_COMPILE_CACHE",
        "OPENCLAW_GATEWAY_SERVICE_PID",
        "OPENCLAW_SERVICE_MARKER",
        "OPENCLAW_SERVICE_KIND",
        "OPENCLAW_UPDATE_RUN_ID",
        "OPENCLAW_UPDATE_RUN_HANDOFF",
        "OPENCLAW_CONTROL_PLANE_UPDATE_SENTINEL_META",
        "OPENCLAW_UPDATE_POST_CORE",
        "OPENCLAW_UPDATE_POST_CORE_CHANNEL",
        "OPENCLAW_UPDATE_POST_CORE_RESULT_PATH",
        "OPENCLAW_UPDATE_POST_CORE_INSTALL_RECORDS_PATH",
        "OPENCLAW_UPDATE_POST_CORE_STARTED_AT_MS",
        "OPENCLAW_UPDATE_POST_CORE_REQUESTED_CHANNEL",
        "OPENCLAW_UPDATE_POST_CORE_SOURCE_CONFIG_PATH",
        "OPENCLAW_DIAGNOSTICS_TIMELINE_PATH",
      ]) {
        env[key] = undefined;
      }
      Object.assign(env, {
        OPENCLAW_HOME: home,
        OPENCLAW_STATE_DIR: state,
        OPENCLAW_CONFIG_PATH: path.join(state, "openclaw.json"),
        TMPDIR: tmp,
        TMP: tmp,
        TEMP: tmp,
        NODE_DISABLE_COMPILE_CACHE: "1",
      });
      const stage = await stagePackageInstallUpdate({ ...params, installEnv: env });
      return await withUpdateInitializationCleanup(
        async () => {
          const manifest: unknown = JSON.parse(
            await fs.readFile(path.join(stage.root, "package.json"), "utf8"),
          );
          return await use({ stage, manifest });
        },
        () => stage.close(),
      );
    },
    async () => {
      await workspace.cleanup();
    },
  );
}

/** Explicit artifacts need their own declaration; a familiar version is not provenance. */
function readFreshUpdateArtifactMetadata(manifest: unknown) {
  if (
    !isRecord(manifest) ||
    typeof manifest.version !== "string" ||
    !validSemver(manifest.version)
  ) {
    return undefined;
  }
  const schemas = isRecord(manifest.openclaw)
    ? parseOpenClawSchemaVersions(manifest.openclaw.schemaVersions)
    : undefined;
  if (!schemas) {
    return undefined;
  }
  return {
    version: manifest.version,
    schemaVersions: schemas,
    nodeEngine:
      isRecord(manifest.engines) && typeof manifest.engines.node === "string"
        ? manifest.engines.node
        : null,
  };
}

/** Bind the inspected artifact to the existing fresh-profile admission flow. */
export async function runFreshUpdateArtifact(
  params: {
    initialization: ArtifactInitialization;
    stageParams: (progress: ReturnType<typeof createUpdateProgress>["progress"]) => StageParams;
    json: boolean;
  },
  run: () => Promise<void>,
): Promise<void> {
  const presentation = createUpdateProgress(!params.json);
  const { initialization } = params;
  const { target } = initialization;
  try {
    return await withFreshUpdateArtifact(
      params.stageParams(presentation.progress),
      async ({ stage, manifest }) => {
        initialization.stagedPackage = stage;
        const metadata = readFreshUpdateArtifactMetadata(manifest);
        if (!metadata) {
          return await target.refuseUpdate(
            "target-metadata-preflight",
            "The selected artifact does not declare its database schema support. Use a compatible artifact with schema metadata, or an exact published --tag.",
          );
        }
        const comparison = compareSemverStrings(target.currentVersion, metadata.version);
        target.downgradeRisk = comparison !== null && comparison > 0;
        target.packageAlreadyCurrent = false;
        target.targetVersion = metadata.version;
        target.packageTargetSchemaVersions = metadata.schemaVersions;
        target.packageRuntimeTarget = {
          version: metadata.version,
          nodeEngine: metadata.nodeEngine,
        };
        return await run();
      },
    );
  } finally {
    presentation.dispose();
  }
}