mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-26 10:13:27 +08:00
feat(gateway): run managed services from standalone artifacts (#183838)
This commit is contained in:
1 parent
bb6a983a9d
commit
cfece29ae8
31 files changed
+374
-113
No files matched your search
@@ -399,6 +399,28 @@ exercise native plugin loading. See [macOS signing](/platforms/mac/signing).
|
||||
|
||||
## Update the complete artifact
|
||||
|
||||
### Managed Gateway service
|
||||
|
||||
Install a complete standalone artifact as the managed Gateway runtime with:
|
||||
|
||||
```sh
|
||||
./standalone-aot/openclaw gateway install --runtime standalone --runtime-path /absolute/path/to/standalone-aot/openclaw
|
||||
```
|
||||
|
||||
The service launches `<executable> gateway ...` directly. It does not add a loose
|
||||
JavaScript entrypoint or Bun flags. Installation probes the executable's embedded
|
||||
runtime-check entry, including SQLite capabilities, and records the exact runtime
|
||||
path. Renaming the executable is supported when its adjacent `manifest.json`
|
||||
names it correctly. Keep that manifest and the complete `runtime/` directory
|
||||
together; older artifacts without the embedded runtime-check entry must be rebuilt.
|
||||
|
||||
Reinstallation and Doctor retain an explicit standalone selection. To switch
|
||||
artifacts, install the newly verified complete directory with the same two flags
|
||||
and `--force`; retain the previous directory until the Gateway and CLI no longer
|
||||
reference it. Desktop applications own this handoff for their bundled artifacts.
|
||||
|
||||
### Artifact replacement
|
||||
|
||||
The standalone executable is not a package-manager or git installation.
|
||||
`openclaw update` refuses to modify it and displays a next step: update the owning
|
||||
desktop application, or rebuild and replace the standalone output.
|
||||
|
||||
@@ -50,7 +50,7 @@ var scripts = {
|
||||
daemonRuntimeProbe: {
|
||||
fileName: "daemon-runtime-probe.mjs",
|
||||
source:
|
||||
'var __getOwnPropNames=Object.getOwnPropertyNames;var __esm=(fn,res,err)=>function __init(){if(err)throw err[0];try{return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res}catch(e){throw err=[e],e}};var GATEWAY_SHUTDOWN_DRAIN_TIMEOUT_MS,GATEWAY_SHUTDOWN_RESERVE_MS,GATEWAY_SUPERVISOR_EXIT_MARGIN_MS,GATEWAY_SHUTDOWN_TIMEOUT_MS,GATEWAY_SERVICE_STOP_TIMEOUT_MS,LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS;var init_gateway_shutdown_budget=__esm({"gateway-shutdown-budget.mjs"(){"use strict";GATEWAY_SHUTDOWN_DRAIN_TIMEOUT_MS=315e3;GATEWAY_SHUTDOWN_RESERVE_MS=1e4;GATEWAY_SUPERVISOR_EXIT_MARGIN_MS=5e3;GATEWAY_SHUTDOWN_TIMEOUT_MS=GATEWAY_SHUTDOWN_DRAIN_TIMEOUT_MS+GATEWAY_SHUTDOWN_RESERVE_MS;GATEWAY_SERVICE_STOP_TIMEOUT_MS=GATEWAY_SHUTDOWN_TIMEOUT_MS+GATEWAY_SUPERVISOR_EXIT_MARGIN_MS;LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS=GATEWAY_SERVICE_STOP_TIMEOUT_MS/1e3}});var parentKey,CLI_RESPAWN_PARENT_ENV,inheritedParent,CLI_PARENT_BOUND_PRELOAD;var init_cli_parent_lifetime=__esm({"cli-parent-lifetime.mjs"(){"use strict";init_gateway_shutdown_budget();parentKey=Symbol.for("openclaw.cliRespawnParent");CLI_RESPAWN_PARENT_ENV="OPENCLAW_CLI_RESPAWN_PARENT";inheritedParent=process.env[CLI_RESPAWN_PARENT_ENV];delete process.env[CLI_RESPAWN_PARENT_ENV];if(inheritedParent===String(process.ppid)){globalThis[parentKey]={pid:process.ppid}}CLI_PARENT_BOUND_PRELOAD=`data:text/javascript,${encodeURIComponent(`\n globalThis[Symbol.for("openclaw.cliRespawnParent")] = { pid: process.ppid };\n process.execArgv = process.execArgv.filter((arg, index, args) =>\n arg !== import.meta.url && !(arg === "--import" && args[index + 1] === import.meta.url));\n`)}`}});var init_node_runtime_env=__esm({"node-runtime-env.mjs"(){"use strict"}});import{readFileSync}from"node:fs";import path from"node:path";import{fileURLToPath}from"node:url";function readStandaloneRuntimeManifest(executable=process.execPath){return readStandaloneManifest({executable})}function projectStandaloneBuildInfo(value){const nonBlank=text=>typeof text==="string"&&text.trim().length>0;const stringList=items=>Array.isArray(items)&&items.every(nonBlank);const result={mode:value.mode,openclaw:{version:value.openclaw.version,...nonBlank(value.openclaw.commit)?{commit:value.openclaw.commit}:{}},bun:{version:value.bun.version}};for(const key of["revision","commit","executableSha256"]){if(nonBlank(value.bun[key])){result.bun[key]=value.bun[key]}}if(value.build!==void 0){const{id,flavor,pluginSetSha256,moduleGraphSha256}=value.build;if(!nonBlank(id)||!["standard","support"].includes(flavor)||!/^[a-f0-9]{64}$/.test(pluginSetSha256)||!/^[a-f0-9]{64}$/.test(moduleGraphSha256)){throw new Error("Invalid standalone build provenance")}result.build={id,flavor,pluginSetSha256,moduleGraphSha256};if(value.build.debugEntry!==void 0){const entry=value.build.debugEntry;if(typeof entry!=="string"||!entry.startsWith("debug/")||entry.includes("\\\\")||entry.split("/").some(part=>!part||part==="."||part==="..")){throw new Error("Invalid standalone debugger entry")}result.build.debugEntry=entry}}if(value.target!==void 0){if(!nonBlank(value.target.platform)||!nonBlank(value.target.arch)){throw new Error("Invalid standalone build target")}result.target={platform:value.target.platform,arch:value.target.arch}}if(value.plugins!==void 0){if(!stringList(value.plugins.embedded)||!stringList(value.plugins.omitted)){throw new Error("Invalid standalone plugin inventory")}result.plugins={embedded:[...value.plugins.embedded],omitted:[...value.plugins.omitted]}}if(value.nativeModules!==void 0){if(!Array.isArray(value.nativeModules)||value.nativeModules.some(item=>!item||!nonBlank(item.name)||!nonBlank(item.version)||!stringList(item.artifacts)||item.qualification!=="unverified"||!nonBlank(item.backend))){throw new Error("Invalid standalone native inventory")}result.nativeModules=value.nativeModules.map(({name,version,artifacts,qualification,backend})=>({name,version,artifacts:[...artifacts],qualification,backend}))}return result}function readStandaloneManifest({executable,runtimeRoot,includeBuild=false}){const location=executable??runtimeRoot;if(!path.isAbsolute(location)){return void 0}const root=path.dirname(location);const manifestPath=path.join(root,"manifest.json");let raw;try{raw=readFileSync(manifestPath,"utf8")}catch(error){if(error?.code==="ENOENT"||error?.code==="ENOTDIR"){return void 0}throw error}let manifest;try{manifest=JSON.parse(raw)}catch(cause){throw new Error(`Invalid standalone runtime manifest: ${manifestPath}`,{cause})}const nonBlank=value=>typeof value==="string"&&value.trim().length>0;if(manifest?.schemaVersion!==1||!["jit","bytecode","aot"].includes(manifest.mode)||!nonBlank(manifest.executable?.path)||!nonBlank(manifest.runtime?.path)||!nonBlank(manifest.openclaw?.version)||!nonBlank(manifest.bun?.version)){throw new Error(`Invalid standalone runtime manifest: ${manifestPath}`)}const executableRelative=path.relative(root,path.resolve(root,manifest.executable.path));const sidecarRelative=path.relative(roLine truncated
|
||||
'var __getOwnPropNames=Object.getOwnPropertyNames;var __esm=(fn,res,err)=>function __init(){if(err)throw err[0];try{return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res}catch(e){throw err=[e],e}};var GATEWAY_SHUTDOWN_DRAIN_TIMEOUT_MS,GATEWAY_SHUTDOWN_RESERVE_MS,GATEWAY_SUPERVISOR_EXIT_MARGIN_MS,GATEWAY_SHUTDOWN_TIMEOUT_MS,GATEWAY_SERVICE_STOP_TIMEOUT_MS,LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS;var init_gateway_shutdown_budget=__esm({"gateway-shutdown-budget.mjs"(){"use strict";GATEWAY_SHUTDOWN_DRAIN_TIMEOUT_MS=315e3;GATEWAY_SHUTDOWN_RESERVE_MS=1e4;GATEWAY_SUPERVISOR_EXIT_MARGIN_MS=5e3;GATEWAY_SHUTDOWN_TIMEOUT_MS=GATEWAY_SHUTDOWN_DRAIN_TIMEOUT_MS+GATEWAY_SHUTDOWN_RESERVE_MS;GATEWAY_SERVICE_STOP_TIMEOUT_MS=GATEWAY_SHUTDOWN_TIMEOUT_MS+GATEWAY_SUPERVISOR_EXIT_MARGIN_MS;LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS=GATEWAY_SERVICE_STOP_TIMEOUT_MS/1e3}});var parentKey,CLI_RESPAWN_PARENT_ENV,inheritedParent,CLI_PARENT_BOUND_PRELOAD;var init_cli_parent_lifetime=__esm({"cli-parent-lifetime.mjs"(){"use strict";init_gateway_shutdown_budget();parentKey=Symbol.for("openclaw.cliRespawnParent");CLI_RESPAWN_PARENT_ENV="OPENCLAW_CLI_RESPAWN_PARENT";inheritedParent=process.env[CLI_RESPAWN_PARENT_ENV];delete process.env[CLI_RESPAWN_PARENT_ENV];if(inheritedParent===String(process.ppid)){globalThis[parentKey]={pid:process.ppid}}CLI_PARENT_BOUND_PRELOAD=`data:text/javascript,${encodeURIComponent(`\n globalThis[Symbol.for("openclaw.cliRespawnParent")] = { pid: process.ppid };\n process.execArgv = process.execArgv.filter((arg, index, args) =>\n arg !== import.meta.url && !(arg === "--import" && args[index + 1] === import.meta.url));\n`)}`}});var init_node_runtime_env=__esm({"node-runtime-env.mjs"(){"use strict"}});import{readFileSync}from"node:fs";import path from"node:path";import{fileURLToPath}from"node:url";function readStandaloneRuntimeManifest(executable=process.execPath){return readStandaloneManifest({executable})}function projectStandaloneBuildInfo(value){const nonBlank=text=>typeof text==="string"&&text.trim().length>0;const stringList=items=>Array.isArray(items)&&items.every(nonBlank);const result={mode:value.mode,openclaw:{version:value.openclaw.version,...nonBlank(value.openclaw.commit)?{commit:value.openclaw.commit}:{}},bun:{version:value.bun.version}};for(const key of["revision","commit","executableSha256"]){if(nonBlank(value.bun[key])){result.bun[key]=value.bun[key]}}if(value.build!==void 0){const{id,flavor,pluginSetSha256,moduleGraphSha256}=value.build;if(!nonBlank(id)||!["standard","support"].includes(flavor)||!/^[a-f0-9]{64}$/.test(pluginSetSha256)||!/^[a-f0-9]{64}$/.test(moduleGraphSha256)){throw new Error("Invalid standalone build provenance")}result.build={id,flavor,pluginSetSha256,moduleGraphSha256};if(value.build.debugEntry!==void 0){const entry=value.build.debugEntry;if(typeof entry!=="string"||!entry.startsWith("debug/")||entry.includes("\\\\")||entry.split("/").some(part=>!part||part==="."||part==="..")){throw new Error("Invalid standalone debugger entry")}result.build.debugEntry=entry}}if(value.target!==void 0){if(!nonBlank(value.target.platform)||!nonBlank(value.target.arch)){throw new Error("Invalid standalone build target")}result.target={platform:value.target.platform,arch:value.target.arch}}if(value.plugins!==void 0){if(!stringList(value.plugins.embedded)||!stringList(value.plugins.omitted)){throw new Error("Invalid standalone plugin inventory")}result.plugins={embedded:[...value.plugins.embedded],omitted:[...value.plugins.omitted]}}if(value.nativeModules!==void 0){if(!Array.isArray(value.nativeModules)||value.nativeModules.some(item=>!item||!nonBlank(item.name)||!nonBlank(item.version)||!stringList(item.artifacts)||item.qualification!=="unverified"||!nonBlank(item.backend))){throw new Error("Invalid standalone native inventory")}result.nativeModules=value.nativeModules.map(({name,version,artifacts,qualification,backend})=>({name,version,artifacts:[...artifacts],qualification,backend}))}return result}function readStandaloneManifest({executable,runtimeRoot,includeBuild=false}){const location=executable??runtimeRoot;if(!path.isAbsolute(location)){return void 0}const root=path.dirname(location);const manifestPath=path.join(root,"manifest.json");let raw;try{raw=readFileSync(manifestPath,"utf8")}catch(error){if(error?.code==="ENOENT"||error?.code==="ENOTDIR"){return void 0}throw error}let manifest;try{manifest=JSON.parse(raw)}catch(cause){throw new Error(`Invalid standalone runtime manifest: ${manifestPath}`,{cause})}const nonBlank=value=>typeof value==="string"&&value.trim().length>0;if(manifest?.schemaVersion!==1||!["jit","bytecode","aot"].includes(manifest.mode)||!nonBlank(manifest.executable?.path)||!nonBlank(manifest.runtime?.path)||!nonBlank(manifest.openclaw?.version)||!nonBlank(manifest.bun?.version)){throw new Error(`Invalid standalone runtime manifest: ${manifestPath}`)}const executableRelative=path.relative(root,path.resolve(root,manifest.executable.path));const sidecarRelative=path.relative(roLine truncated
|
||||
},
|
||||
updateStateSizes: {
|
||||
fileName: "update-state-sizes.mjs",
|
||||
|
||||
@@ -40,13 +40,15 @@ const serviceFile = z
|
||||
.refine((file) => path.isAbsolute(file) && !/[\0\r\n]/.test(file) && isRegularFile(file));
|
||||
const restoreServiceCliSchema = z.strictObject({
|
||||
executable: z.string(),
|
||||
entrypoint: serviceFile.refine(
|
||||
(entry) =>
|
||||
path.basename(entry) === "openclaw.mjs" ||
|
||||
buildGatewayInstallEntrypointCandidates(path.dirname(path.dirname(entry))).includes(
|
||||
path.normalize(entry),
|
||||
),
|
||||
),
|
||||
entrypoint: serviceFile
|
||||
.refine(
|
||||
(entry) =>
|
||||
path.basename(entry) === "openclaw.mjs" ||
|
||||
buildGatewayInstallEntrypointCandidates(path.dirname(path.dirname(entry))).includes(
|
||||
path.normalize(entry),
|
||||
),
|
||||
)
|
||||
.nullable(),
|
||||
sqliteLibrary: serviceFile.nullable(),
|
||||
});
|
||||
|
||||
@@ -66,8 +68,13 @@ export async function resolveRestoreServiceCli(
|
||||
if (expectedRuntimePin === undefined) {
|
||||
throw new Error("requires --expected-runtime-pin.");
|
||||
}
|
||||
if (runtime !== "node" && runtime !== "bun") {
|
||||
throw new Error('requires an explicit --runtime ("node" or "bun").');
|
||||
if (runtime !== "node" && runtime !== "bun" && runtime !== "standalone") {
|
||||
throw new Error('requires an explicit --runtime ("node", "bun", or "standalone").');
|
||||
}
|
||||
if ((runtime === "standalone") !== (serviceCli.entrypoint === null)) {
|
||||
throw new Error(
|
||||
"standalone recovery requires a null entrypoint; Node and Bun require an entry script.",
|
||||
);
|
||||
}
|
||||
if (wrapper !== undefined || installEnv[OPENCLAW_WRAPPER_ENV_KEY]?.trim()) {
|
||||
throw new Error("cannot be combined with a wrapper.");
|
||||
|
||||
@@ -118,7 +118,7 @@ describe("runDaemonInstall", () => {
|
||||
revision: expected.revision,
|
||||
definition: expected.definition,
|
||||
});
|
||||
function recoveryFixture(runtime: "node" | "bun" = "node") {
|
||||
function recoveryFixture(runtime: "node" | "bun" | "standalone" = "node") {
|
||||
const root = tempDirs.make("openclaw-restore-cli-");
|
||||
const executable = path.join(
|
||||
root,
|
||||
@@ -129,8 +129,26 @@ describe("runDaemonInstall", () => {
|
||||
for (const file of [executable, entrypoint, sqliteLibrary]) {
|
||||
fs.writeFileSync(file, "", { mode: 0o755 });
|
||||
}
|
||||
if (runtime === "standalone") {
|
||||
fs.mkdirSync(path.join(root, "runtime"));
|
||||
fs.writeFileSync(
|
||||
path.join(root, "manifest.json"),
|
||||
JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
mode: "aot",
|
||||
executable: { path: "standalone" },
|
||||
runtime: { path: "runtime" },
|
||||
openclaw: { version: "2026.10.25" },
|
||||
bun: { version: "1.4.4" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
pinSnapshotMock.mockReturnValue(expected);
|
||||
return { executable, entrypoint, sqliteLibrary };
|
||||
return {
|
||||
executable,
|
||||
entrypoint,
|
||||
sqliteLibrary,
|
||||
};
|
||||
}
|
||||
const restore = (cli: object, options: Partial<DaemonInstallOptions> = {}) =>
|
||||
runDaemonInstall({
|
||||
@@ -171,13 +189,18 @@ describe("runDaemonInstall", () => {
|
||||
it.each([
|
||||
{ runtime: "node", library: false },
|
||||
{ runtime: "bun", library: true },
|
||||
{ runtime: "standalone", library: false },
|
||||
] as const)(
|
||||
"restores $runtime with retained library=$library and definition custody",
|
||||
async ({ runtime, library }) => {
|
||||
const files = recoveryFixture(runtime);
|
||||
const cli = { ...files, sqliteLibrary: library ? files.sqliteLibrary : null };
|
||||
const cli = {
|
||||
...files,
|
||||
entrypoint: runtime === "standalone" ? null : files.entrypoint,
|
||||
sqliteLibrary: library ? files.sqliteLibrary : null,
|
||||
};
|
||||
process.env.OPENCLAW_SQLITE_LIBRARY = "/installer/libsqlite3.dylib";
|
||||
if (runtime === "bun") {
|
||||
if (runtime !== "node") {
|
||||
const probe = nodeProbeOutput("26.12.1");
|
||||
runExecMock.mockResolvedValue({
|
||||
...probe,
|
||||
@@ -187,19 +210,19 @@ describe("runDaemonInstall", () => {
|
||||
performServiceInstall();
|
||||
await restore(cli, {
|
||||
runtime,
|
||||
...(runtime === "bun" ? { runtimePath: cli.executable } : {}),
|
||||
...(runtime !== "node" ? { runtimePath: cli.executable } : {}),
|
||||
});
|
||||
expect(actionState.failed).toEqual([]);
|
||||
const plan = readFirstInstallPlanArg();
|
||||
expect(plan).toMatchObject({
|
||||
serviceCli: { executable: cli.executable, entrypoint: cli.entrypoint },
|
||||
runtimePath: cli.executable,
|
||||
pinnedRuntimePath: runtime === "bun" ? cli.executable : undefined,
|
||||
pinnedRuntimePath: runtime !== "node" ? cli.executable : undefined,
|
||||
});
|
||||
expectPartialCall(service.install, {
|
||||
runtimePinUpdate: {
|
||||
expected,
|
||||
pin: runtime === "bun" ? { runtime, path: cli.executable } : undefined,
|
||||
pin: runtime !== "node" ? { runtime, path: cli.executable } : undefined,
|
||||
requireDefinitionMatch: true,
|
||||
},
|
||||
env: plan.env,
|
||||
|
||||
@@ -262,9 +262,12 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
fail(formatInvalidConfigPort("gateway.port"));
|
||||
return;
|
||||
}
|
||||
const runtimeRaw = opts.runtime || resolveGatewayDaemonRuntime([pinnedRuntimePath ?? ""]);
|
||||
const runtimeRaw =
|
||||
opts.runtime ||
|
||||
pinSnapshot.pin?.runtime ||
|
||||
resolveGatewayDaemonRuntime([pinnedRuntimePath ?? ""]);
|
||||
if (!isGatewayDaemonRuntime(runtimeRaw)) {
|
||||
fail('Invalid --runtime (use "node" or "bun")');
|
||||
fail('Invalid --runtime (use "node", "bun", or "standalone")');
|
||||
return;
|
||||
}
|
||||
let runtime = runtimeRaw;
|
||||
|
||||
@@ -82,7 +82,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
|
||||
.command("install")
|
||||
.description("Install and start the Gateway service (launchd/systemd/schtasks)")
|
||||
.option("--port <port>", "Gateway port")
|
||||
.option("--runtime <runtime>", "Daemon runtime (node|bun). Default: node")
|
||||
.option("--runtime <runtime>", "Daemon runtime (node|bun|standalone). Default: node")
|
||||
.option("--runtime-path <path>", "Pin an absolute Node/Bun executable path")
|
||||
.addOption(
|
||||
new Option("--expected-runtime-pin <json>", "Require the observed runtime intent").hideHelp(),
|
||||
|
||||
@@ -194,17 +194,23 @@ export async function repairLoadedGatewayServiceForStart(
|
||||
readDaemonRuntimePin({ kind: "gateway", env: installEnv }, params.state.command),
|
||||
);
|
||||
const pinnedRuntime = wrapperPath ? undefined : pinSnapshot.pin?.path;
|
||||
const installedRuntime = resolveGatewayDaemonRuntime(
|
||||
pinnedRuntime ? [pinnedRuntime] : managedCommand?.programArguments,
|
||||
);
|
||||
const installedRuntime =
|
||||
(!wrapperPath && pinSnapshot.pin?.runtime) ||
|
||||
resolveGatewayDaemonRuntime(pinnedRuntime ? [pinnedRuntime] : managedCommand?.programArguments);
|
||||
if (!wrapperPath) {
|
||||
await resolvePinnedDaemonRuntimePath(pinnedRuntime, installedRuntime, installEnv);
|
||||
}
|
||||
const installedRuntimePath =
|
||||
installedRuntime === "bun" ? (pinnedRuntime ?? managedCommand?.programArguments[0]) : undefined;
|
||||
const runtimeInfo = installedRuntimePath
|
||||
? await resolveBunRuntimeInfo(installedRuntimePath)
|
||||
: undefined;
|
||||
installedRuntime === "bun" || installedRuntime === "standalone"
|
||||
? (pinnedRuntime ?? managedCommand?.programArguments[0])
|
||||
: undefined;
|
||||
if (installedRuntime === "standalone" && !pinnedRuntime) {
|
||||
await resolvePinnedDaemonRuntimePath(installedRuntimePath, "standalone", installEnv);
|
||||
}
|
||||
const runtimeInfo =
|
||||
installedRuntimePath && installedRuntime === "bun"
|
||||
? await resolveBunRuntimeInfo(installedRuntimePath)
|
||||
: undefined;
|
||||
if (runtimeInfo?.status === "probe-failed") {
|
||||
throw runtimeInfo.error;
|
||||
}
|
||||
@@ -212,11 +218,12 @@ export async function repairLoadedGatewayServiceForStart(
|
||||
if (runtimeInfo?.status === "unsupported" && runtimeInfo.sqliteSelectionError) {
|
||||
throw new Error(runtimeInfo.sqliteSelectionError);
|
||||
}
|
||||
const runtime = pinnedRuntime
|
||||
? installedRuntime
|
||||
: runtimeInfo?.status === "supported"
|
||||
? "bun"
|
||||
: "node";
|
||||
const runtime =
|
||||
pinnedRuntime || installedRuntime === "standalone"
|
||||
? installedRuntime
|
||||
: runtimeInfo?.status === "supported"
|
||||
? "bun"
|
||||
: "node";
|
||||
|
||||
const tokenResolution = await resolveGatewayInstallToken({
|
||||
config: cfg,
|
||||
@@ -243,7 +250,7 @@ export async function repairLoadedGatewayServiceForStart(
|
||||
env: installEnv,
|
||||
port,
|
||||
runtime,
|
||||
runtimePath: runtime === "bun" ? installedRuntimePath : undefined,
|
||||
runtimePath: runtime === "bun" || runtime === "standalone" ? installedRuntimePath : undefined,
|
||||
pinnedRuntimePath: pinSnapshot.pin?.path,
|
||||
wrapperPath,
|
||||
existingCommand: params.state.command,
|
||||
|
||||
@@ -202,9 +202,12 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) {
|
||||
return;
|
||||
}
|
||||
let pinnedRuntimePath = opts.runtimePath ?? (opts.runtime ? undefined : pinSnapshot.pin?.path);
|
||||
const runtimeRaw = opts.runtime || resolveGatewayDaemonRuntime([pinnedRuntimePath ?? ""]);
|
||||
const runtimeRaw =
|
||||
opts.runtime ||
|
||||
pinSnapshot.pin?.runtime ||
|
||||
resolveGatewayDaemonRuntime([pinnedRuntimePath ?? ""]);
|
||||
if (!isGatewayDaemonRuntime(runtimeRaw)) {
|
||||
fail('Invalid --runtime (use "node" or "bun")');
|
||||
fail('Invalid --runtime (use "node", "bun", or "standalone")');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -261,7 +264,9 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) {
|
||||
});
|
||||
const cliEntrypoint =
|
||||
restoreServiceCli?.entrypoint ??
|
||||
(!devMode && !wrapperPath ? await resolveDurableNodeEntrypoint(installEnv) : undefined);
|
||||
(!devMode && !wrapperPath && runtime !== "standalone"
|
||||
? await resolveDurableNodeEntrypoint(installEnv)
|
||||
: undefined);
|
||||
const { programArguments, workingDirectory } = await resolveNodeProgramArguments({
|
||||
cliEntrypoint,
|
||||
host,
|
||||
|
||||
@@ -175,7 +175,7 @@ export function registerNodeCli(program: Command) {
|
||||
)
|
||||
.option("--share-installed-apps", "Share installed macOS applications with the Gateway")
|
||||
.option("--no-share-installed-apps", "Disable installed application sharing")
|
||||
.option("--runtime <runtime>", "Service runtime (node|bun). Default: node")
|
||||
.option("--runtime <runtime>", "Service runtime (node|bun|standalone). Default: node")
|
||||
.option("--runtime-path <path>", "Pin an absolute Node/Bun executable path")
|
||||
.addOption(
|
||||
new Option("--expected-runtime-pin <json>", "Require the observed runtime intent").hideHelp(),
|
||||
|
||||
@@ -148,7 +148,7 @@ export function registerOnboardRuntimeOptions(
|
||||
.option("--install-daemon", "Install gateway service")
|
||||
.option("--no-install-daemon", "Skip gateway service install")
|
||||
.option("--skip-daemon", "Skip gateway service install")
|
||||
.option("--daemon-runtime <runtime>", "Daemon runtime: node|bun (default: node)")
|
||||
.option("--daemon-runtime <runtime>", "Daemon runtime: node|bun|standalone (default: node)")
|
||||
.option("--skip-channels", "Skip channel setup")
|
||||
.option("--skip-skills", "Skip skills setup")
|
||||
.option("--skip-bootstrap", "Skip creating default agent workspace files")
|
||||
|
||||
@@ -582,7 +582,7 @@ export async function buildGatewayInstallPlan(params: {
|
||||
runtimePath?: string;
|
||||
pinnedRuntimePath?: string;
|
||||
/** Retained CLI to plan for instead of this process's own entrypoint and executable. */
|
||||
serviceCli?: { executable: string; entrypoint: string };
|
||||
serviceCli?: { executable: string; entrypoint: string | null };
|
||||
wrapperPath?: string;
|
||||
platform?: NodeJS.Platform;
|
||||
warn?: DaemonInstallWarnFn;
|
||||
@@ -652,7 +652,7 @@ export async function buildGatewayInstallPlan(params: {
|
||||
runtime,
|
||||
runtimePath,
|
||||
wrapperPath,
|
||||
cliEntrypoint: params.serviceCli?.entrypoint,
|
||||
cliEntrypoint: params.serviceCli?.entrypoint ?? undefined,
|
||||
...(params.existingCommand ? { existingCommand: params.existingCommand } : {}),
|
||||
});
|
||||
await emitNodeRuntimeWarning({
|
||||
@@ -676,7 +676,10 @@ export async function buildGatewayInstallPlan(params: {
|
||||
platform,
|
||||
extraPathDirs: resolveDaemonServicePathDirs({
|
||||
runtimePath,
|
||||
argv: params.serviceCli && [params.serviceCli.executable, params.serviceCli.entrypoint],
|
||||
argv: params.serviceCli && [
|
||||
params.serviceCli.executable,
|
||||
...(params.serviceCli.entrypoint ? [params.serviceCli.entrypoint] : []),
|
||||
],
|
||||
env: serviceInputEnv,
|
||||
platform,
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { isStandaloneRuntimeExecutable } from "../../cli-invocation.mjs";
|
||||
import {
|
||||
resolveBunRuntimeInfo,
|
||||
resolvePinnedDaemonRuntimePath,
|
||||
@@ -65,6 +66,19 @@ export async function resolveDaemonInstallRuntimeInputs(params: {
|
||||
if (params.wrapperPath?.trim()) {
|
||||
return { devMode, runtime: params.runtime, runtimePath: params.runtimePath };
|
||||
}
|
||||
if (params.runtime === "standalone") {
|
||||
const executable =
|
||||
params.pinnedRuntimePath ??
|
||||
params.runtimePath ??
|
||||
(isStandaloneRuntimeExecutable() ? process.execPath : undefined);
|
||||
if (!executable) {
|
||||
throw new Error(
|
||||
"Standalone services require --runtime-path pointing to a complete standalone artifact.",
|
||||
);
|
||||
}
|
||||
const runtimePath = await resolvePinnedDaemonRuntimePath(executable, "standalone", params.env);
|
||||
return { devMode: false, runtime: "standalone", runtimePath };
|
||||
}
|
||||
const pinnedRuntimePath =
|
||||
params.pinnedRuntimePath === undefined
|
||||
? undefined
|
||||
|
||||
@@ -12,6 +12,7 @@ describe("gateway daemon runtime", () => {
|
||||
expect(GATEWAY_DAEMON_RUNTIME_OPTIONS.map((option) => option.value)).toEqual(["node", "bun"]);
|
||||
expect(isGatewayDaemonRuntime("node")).toBe(true);
|
||||
expect(isGatewayDaemonRuntime("bun")).toBe(true);
|
||||
expect(isGatewayDaemonRuntime("standalone")).toBe(true);
|
||||
expect(isGatewayDaemonRuntime(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isStandaloneRuntimeExecutable } from "../../cli-invocation.mjs";
|
||||
import { isBunRuntime } from "../daemon/runtime-binary.js";
|
||||
|
||||
export type GatewayDaemonRuntime = "bun" | "node";
|
||||
export type GatewayDaemonRuntime = "bun" | "node" | "standalone";
|
||||
|
||||
export const DEFAULT_GATEWAY_DAEMON_RUNTIME: GatewayDaemonRuntime = "node";
|
||||
|
||||
@@ -22,12 +23,16 @@ export const GATEWAY_DAEMON_RUNTIME_OPTIONS: Array<{
|
||||
];
|
||||
|
||||
export function isGatewayDaemonRuntime(value: string | undefined): value is GatewayDaemonRuntime {
|
||||
return value === "bun" || value === "node";
|
||||
return value === "bun" || value === "node" || value === "standalone";
|
||||
}
|
||||
|
||||
/** Detects the runtime selected by an installed daemon command. */
|
||||
export function resolveGatewayDaemonRuntime(
|
||||
programArguments: string[] | undefined,
|
||||
): GatewayDaemonRuntime {
|
||||
return isBunRuntime(programArguments?.[0] ?? "") ? "bun" : DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||
const executable = programArguments?.[0] ?? "";
|
||||
if (isStandaloneRuntimeExecutable(executable)) {
|
||||
return "standalone";
|
||||
}
|
||||
return isBunRuntime(executable) ? "bun" : DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||
}
|
||||
@@ -317,15 +317,12 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
const expectedGatewayToken = tokenRefConfigured ? undefined : gatewayTokenResolution.token;
|
||||
const port = resolveGatewayPort(cfg, process.env);
|
||||
const hasInstallWrapper = Boolean(serviceInstallEnv[OPENCLAW_WRAPPER_ENV_KEY]?.trim());
|
||||
const activeRuntimePin = hasInstallWrapper ? undefined : pinSnapshot.pin?.path;
|
||||
const runtimeChoice = hasInstallWrapper
|
||||
? "node"
|
||||
: resolveGatewayDaemonRuntime(
|
||||
activeRuntimePin ? [activeRuntimePin] : managedDefinition.programArguments,
|
||||
);
|
||||
: (pinSnapshot.pin?.runtime ?? resolveGatewayDaemonRuntime(managedDefinition.programArguments));
|
||||
const installedRuntimePath =
|
||||
runtimeChoice === "bun"
|
||||
? (activeRuntimePin ?? managedDefinition.programArguments[0])
|
||||
runtimeChoice === "bun" || runtimeChoice === "standalone"
|
||||
? (pinSnapshot.pin?.path ?? managedDefinition.programArguments[0])
|
||||
: undefined;
|
||||
const expectedPlan = await buildExpectedGatewayServicePlan({
|
||||
cfg,
|
||||
@@ -375,7 +372,7 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
});
|
||||
}
|
||||
const needsNodeRuntime =
|
||||
!hasInstallWrapper && !activeRuntimePin && needsNodeRuntimeMigration(audit.issues);
|
||||
!hasInstallWrapper && !pinSnapshot.pin && needsNodeRuntimeMigration(audit.issues);
|
||||
// Unusable runtimes and version-managed Node services migrate through a concrete system Node.
|
||||
const systemNodeInfo = needsNodeRuntime
|
||||
? await resolveSystemNodeInfo({ env: process.env, platform })
|
||||
|
||||
@@ -3,6 +3,7 @@ import { runtimeFixture } from "../cli/update-cli/update-command-fixtures.test-s
|
||||
import * as runtimePaths from "../daemon/runtime-paths.js";
|
||||
import type { DaemonRuntimePinSnapshot } from "../daemon/runtime-pin-types.js";
|
||||
import { resolveDaemonInstallRuntimeInputs } from "./daemon-install-plan.shared.js";
|
||||
import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
|
||||
import { resolveGatewaySetupRuntime } from "./gateway-setup-runtime.js";
|
||||
|
||||
const readPin = vi.hoisted(() => vi.fn<() => DaemonRuntimePinSnapshot>());
|
||||
@@ -61,7 +62,7 @@ it.each([
|
||||
vi.spyOn(runtimePaths, "resolveBunRuntimeInfo").mockResolvedValue(
|
||||
runtimeFixture("1.4.3", "3.53.4"),
|
||||
);
|
||||
const select = vi.fn(async (runtime: "node" | "bun") => runtime);
|
||||
const select = vi.fn(async (runtime: GatewayDaemonRuntime) => runtime);
|
||||
const selection = await resolveGatewaySetupRuntime({
|
||||
env: {},
|
||||
existingCommand: null,
|
||||
|
||||
@@ -52,7 +52,7 @@ export function validateOnboardingChoiceOptions(
|
||||
return reject('Invalid --flow. Use "quickstart", "advanced", "manual", or "import".');
|
||||
}
|
||||
if (opts.daemonRuntime !== undefined && !isGatewayDaemonRuntime(opts.daemonRuntime)) {
|
||||
return reject('Invalid --daemon-runtime. Use "node" or "bun".');
|
||||
return reject('Invalid --daemon-runtime. Use "node", "bun", or "standalone".');
|
||||
}
|
||||
if (opts.nodeManager !== undefined && !isNodeManagerChoice(opts.nodeManager)) {
|
||||
return reject('Invalid --node-manager. Use "npm", "pnpm", or "bun".');
|
||||
|
||||
@@ -72,6 +72,18 @@ function nodeArgs(options: Partial<Parameters<typeof resolveNodeProgramArguments
|
||||
const gatewayTail = ["gateway", "--port", "18789"];
|
||||
const nodeTail = ["node", "run", "--host", "gateway.example", "--port", "18789"];
|
||||
describe("resolveGatewayProgramArguments", () => {
|
||||
it("runs a standalone service directly without a loose entrypoint or runtime flags", async () => {
|
||||
process.argv = [originalExecPath, "/unavailable/source/entry.ts"];
|
||||
const runtimePath = "/opt/immutable-runtime/openclaw";
|
||||
await expect(gatewayArgs({ runtime: "standalone", runtimePath, dev: true })).resolves.toEqual({
|
||||
programArguments: [runtimePath, ...gatewayTail],
|
||||
});
|
||||
await expect(nodeArgs({ runtime: "standalone", runtimePath })).resolves.toEqual({
|
||||
programArguments: [runtimePath, ...nodeTail],
|
||||
});
|
||||
expect(fsMocks.access).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("installs the requested start-mode override once", async () => {
|
||||
const entryPath = path.resolve("/opt/openclaw/dist/index.js");
|
||||
mockEntrypoint(entryPath);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolveOpenClawCliInvocation } from "../../cli-invocation.mjs";
|
||||
import { SUPPORTED_NODE_VERSIONS } from "../../node-version.mjs";
|
||||
import type { GatewayDaemonRuntime } from "../commands/daemon-runtime.js";
|
||||
import { resolveBrewOpenClawPath } from "../infra/brew.js";
|
||||
@@ -209,6 +210,16 @@ async function resolveCliProgramArguments(params: {
|
||||
}
|
||||
const runtimePath = params.runtimePath;
|
||||
|
||||
if (params.runtime === "standalone") {
|
||||
const invocation = resolveOpenClawCliInvocation({
|
||||
execPath: runtimePath,
|
||||
entryPath: null,
|
||||
args: params.args,
|
||||
execArgv: [],
|
||||
});
|
||||
return { programArguments: [invocation.command, ...invocation.args] };
|
||||
}
|
||||
|
||||
if (params.dev) {
|
||||
const repoRoot = resolveRepoRootForDev();
|
||||
const devCliPath = path.join(repoRoot, "src", "entry.ts");
|
||||
|
||||
+55
-17
@@ -1,19 +1,23 @@
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
import { getEmbeddedStandaloneRuntimeRoot } from "../../cli-invocation.mjs";
|
||||
import {
|
||||
getEmbeddedStandaloneRuntimeRoot,
|
||||
isStandaloneRuntimeExecutable,
|
||||
resolveStandaloneSidecarRoot,
|
||||
} from "../../cli-invocation.mjs";
|
||||
import { nodeRuntimeFailure, type SqliteCapabilities } from "../../node-sqlite.mjs";
|
||||
import { SUPPORTED_NODE_VERSIONS } from "../../node-version.mjs";
|
||||
import { createRuntimeProcessScriptFile } from "../../runtime-process-scripts.generated.mjs";
|
||||
import type { GatewayDaemonRuntime } from "../commands/daemon-runtime.js";
|
||||
import { isMissingPathError } from "../infra/errno.js";
|
||||
import {
|
||||
isSupportedBunVersion,
|
||||
isSupportedNodeVersion,
|
||||
parseSemver,
|
||||
} from "../infra/runtime-guard.js";
|
||||
import { prepareRuntimeProcessScriptInvocation } from "../infra/runtime-process-url.js";
|
||||
import { isSqliteWalResetSafeVersion } from "../infra/sqlite-runtime-version.js";
|
||||
import { resolveStableNodePath } from "../infra/stable-node-path.js";
|
||||
import { getWindowsProgramFilesRoots } from "../infra/windows-install-roots.js";
|
||||
@@ -154,20 +158,25 @@ type SystemNodeInfo = RuntimeInfo & { path: string };
|
||||
|
||||
async function resolveRuntimeInfo(
|
||||
runtimePath: string,
|
||||
runtime: "node" | "bun",
|
||||
runtime: GatewayDaemonRuntime,
|
||||
env: Record<string, string | undefined>,
|
||||
timeoutMs = RUNTIME_PROBE_TIMEOUT_MS,
|
||||
): Promise<RuntimeInfo> {
|
||||
const label = runtime === "node" ? "Node" : "Bun";
|
||||
const label = runtime === "node" ? "Node" : runtime === "bun" ? "Bun" : "Standalone OpenClaw";
|
||||
const probeRuntime = runtime === "standalone" ? "bun" : runtime;
|
||||
let cwd: string | undefined;
|
||||
try {
|
||||
cwd = process.cwd();
|
||||
const script = createRuntimeProcessScriptFile("daemonRuntimeProbe");
|
||||
const script = prepareRuntimeProcessScriptInvocation("daemonRuntimeProbe", runtimePath);
|
||||
let stdout: string;
|
||||
try {
|
||||
({ stdout } = await runExec(
|
||||
runtimePath,
|
||||
[fileURLToPath(script.url), runtime, getEmbeddedStandaloneRuntimeRoot() ?? ""],
|
||||
[
|
||||
...script.argv.slice(1),
|
||||
probeRuntime,
|
||||
resolveStandaloneSidecarRoot(runtimePath) ?? getEmbeddedStandaloneRuntimeRoot() ?? "",
|
||||
],
|
||||
{
|
||||
baseEnv: buildRuntimeProbeEnv(env),
|
||||
logOutput: false,
|
||||
@@ -181,13 +190,13 @@ async function resolveRuntimeInfo(
|
||||
if (!isRecord(parsed)) {
|
||||
throw new Error("Runtime check returned invalid output");
|
||||
}
|
||||
const version = parsed[`${runtime}Version`];
|
||||
const version = parsed[`${probeRuntime}Version`];
|
||||
const sqliteVersion = parsed.sqliteVersion;
|
||||
const sqliteSelectionError = parsed.sqliteSelectionError;
|
||||
const sqliteLibraryPath = parsed.sqliteLibraryPath;
|
||||
const probe = parsed.sqliteProbe;
|
||||
if (
|
||||
!(typeof version === "string" || (runtime === "bun" && version === null)) ||
|
||||
!(typeof version === "string" || (probeRuntime === "bun" && version === null)) ||
|
||||
(runtime === "node" && typeof version === "string" && !parseSemver(version)) ||
|
||||
!(typeof sqliteVersion === "string" || sqliteVersion === null) ||
|
||||
!(typeof sqliteSelectionError === "string" || sqliteSelectionError == null) ||
|
||||
@@ -253,6 +262,14 @@ export function resolveBunRuntimeInfo(
|
||||
return resolveRuntimeInfo(bunPath, "bun", env);
|
||||
}
|
||||
|
||||
export function resolveStandaloneRuntimeInfo(
|
||||
executable: string,
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
timeoutMs = RUNTIME_PROBE_TIMEOUT_MS,
|
||||
) {
|
||||
return resolveRuntimeInfo(executable, "standalone", env, timeoutMs);
|
||||
}
|
||||
|
||||
/** Probes a recorded Node executable without inheriting service preloads or secrets. */
|
||||
export function resolveNodeRuntimeInfo(
|
||||
nodePath: string,
|
||||
@@ -266,15 +283,17 @@ export function resolveNodeRuntimeInfo(
|
||||
export async function resolveRecordedDaemonRuntime(
|
||||
runtimePath: string | undefined,
|
||||
env: Record<string, string | undefined>,
|
||||
): Promise<(RuntimeInfo & { runtime: "node" | "bun"; path: string }) | undefined> {
|
||||
): Promise<(RuntimeInfo & { runtime: GatewayDaemonRuntime; path: string }) | undefined> {
|
||||
if (!runtimePath) {
|
||||
return undefined;
|
||||
}
|
||||
const runtime = isBunRuntime(runtimePath)
|
||||
? "bun"
|
||||
: isNodeRuntime(runtimePath)
|
||||
? "node"
|
||||
: undefined;
|
||||
const runtime = isStandaloneRuntimeExecutable(runtimePath)
|
||||
? "standalone"
|
||||
: isBunRuntime(runtimePath)
|
||||
? "bun"
|
||||
: isNodeRuntime(runtimePath)
|
||||
? "node"
|
||||
: undefined;
|
||||
if (!runtime) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -474,7 +493,7 @@ export async function resolvePreferredBunPath(
|
||||
/** Validate an operator pin without falling back to another executable. */
|
||||
export async function resolvePinnedDaemonRuntimePath(
|
||||
input: string | undefined,
|
||||
runtime: "node" | "bun",
|
||||
runtime: GatewayDaemonRuntime,
|
||||
env: Record<string, string | undefined>,
|
||||
): Promise<string | undefined> {
|
||||
if (input === undefined) {
|
||||
@@ -492,11 +511,30 @@ export async function resolvePinnedDaemonRuntimePath(
|
||||
throw new Error("--runtime-path must be an absolute executable path.");
|
||||
}
|
||||
const runtimePath = paths.normalize(value);
|
||||
const standaloneRoot = resolveStandaloneSidecarRoot(runtimePath);
|
||||
const matchesRuntime =
|
||||
runtime === "node" ? isNodeRuntime(runtimePath) : isBunRuntime(runtimePath);
|
||||
runtime === "standalone"
|
||||
? standaloneRoot !== undefined
|
||||
: standaloneRoot !== undefined
|
||||
? false
|
||||
: runtime === "node"
|
||||
? isNodeRuntime(runtimePath)
|
||||
: isBunRuntime(runtimePath);
|
||||
if (!matchesRuntime) {
|
||||
throw new Error(`--runtime-path must name a ${runtime} executable: ${runtimePath}`);
|
||||
}
|
||||
if (runtime === "standalone") {
|
||||
try {
|
||||
if (!standaloneRoot || !(await fs.stat(standaloneRoot)).isDirectory()) {
|
||||
throw new Error("not a directory");
|
||||
}
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`Pinned standalone runtime directory is unavailable: ${standaloneRoot ?? runtimePath}. Restore the complete standalone artifact.`,
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!(await fs.stat(runtimePath)).isFile()) {
|
||||
throw new Error("not a regular file");
|
||||
|
||||
@@ -21,29 +21,36 @@ beforeEach(() => {
|
||||
afterEach(() => vi.unstubAllEnvs());
|
||||
|
||||
describe("transactional runtime pin state", () => {
|
||||
it("keeps intent local with PostgreSQL selected and the shared database unavailable", async () => {
|
||||
await withOpenClawTestState({ label: "runtime-pin-local" }, async (state) => {
|
||||
fs.writeFileSync(
|
||||
state.statePath("openclaw.json"),
|
||||
JSON.stringify({
|
||||
database: {
|
||||
engine: "postgres",
|
||||
postgres: { connection: "postgres://unreachable.invalid/openclaw" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
const scope = { kind: "gateway" as const, env: state.env };
|
||||
await withDaemonRuntimePinMigration(state.env, () => undefined);
|
||||
const expected = readDaemonRuntimePin(scope, command);
|
||||
commitDaemonRuntimePin(scope, { expected, pin }, command);
|
||||
expect(readDaemonRuntimePin(scope, command).pin).toEqual(pin);
|
||||
expect(fs.existsSync(state.statePath("state", "openclaw.sqlite"))).toBe(false);
|
||||
if (process.platform !== "win32") {
|
||||
expect(fs.statSync(resolveDaemonRuntimePinStorePath(state.env)).mode & 0o777).toBe(0o600);
|
||||
expect(fs.statSync(state.statePath("control")).mode & 0o777).toBe(0o700);
|
||||
}
|
||||
});
|
||||
});
|
||||
it.each(["node", "standalone"] as const)(
|
||||
"keeps %s intent local with PostgreSQL selected and the shared database unavailable",
|
||||
async (runtime) => {
|
||||
await withOpenClawTestState({ label: "runtime-pin-local" }, async (state) => {
|
||||
fs.writeFileSync(
|
||||
state.statePath("openclaw.json"),
|
||||
JSON.stringify({
|
||||
database: {
|
||||
engine: "postgres",
|
||||
postgres: { connection: "postgres://unreachable.invalid/openclaw" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
const scope = { kind: "gateway" as const, env: state.env };
|
||||
await withDaemonRuntimePinMigration(state.env, () => undefined);
|
||||
const selectedPath = runtime === "standalone" ? "/runtime tools/openclaw" : runtimePath;
|
||||
const selectedCommand =
|
||||
runtime === "standalone" ? { programArguments: [selectedPath, "gateway"] } : command;
|
||||
const expected = readDaemonRuntimePin(scope, selectedCommand);
|
||||
const selectedPin = { runtime, path: selectedPath };
|
||||
commitDaemonRuntimePin(scope, { expected, pin: selectedPin }, selectedCommand);
|
||||
expect(readDaemonRuntimePin(scope, selectedCommand).pin).toEqual(selectedPin);
|
||||
expect(fs.existsSync(state.statePath("state", "openclaw.sqlite"))).toBe(false);
|
||||
if (process.platform !== "win32") {
|
||||
expect(fs.statSync(resolveDaemonRuntimePinStorePath(state.env)).mode & 0o777).toBe(0o600);
|
||||
expect(fs.statSync(state.statePath("control")).mode & 0o777).toBe(0o700);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("observes another writer's committed changes and rejects stale writes", async () => {
|
||||
await withOpenClawTestState({ label: "runtime-pin-current" }, async (state) => {
|
||||
|
||||
@@ -21,7 +21,10 @@ import {
|
||||
} from "./service-types.js";
|
||||
import { resolveSystemdServiceName } from "./systemd-service-files.js";
|
||||
|
||||
const pinSchema = z.object({ runtime: z.enum(["node", "bun"]), path: z.string().min(1) });
|
||||
const pinSchema = z.object({
|
||||
runtime: z.enum(["node", "bun", "standalone"]),
|
||||
path: z.string().min(1),
|
||||
});
|
||||
const recordSchema = z.object({ version: z.literal(1), pin: pinSchema, definition: z.string() });
|
||||
|
||||
type PinScope = { kind: "gateway" | "node"; env: NodeJS.ProcessEnv };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Dependency-free runtime intent contracts shared by planners and native service writers. */
|
||||
type DaemonRuntimePin = { runtime: "node" | "bun"; path: string };
|
||||
type DaemonRuntimePin = { runtime: "node" | "bun" | "standalone"; path: string };
|
||||
|
||||
export type DaemonRuntimePinSnapshot = {
|
||||
revision: string;
|
||||
|
||||
@@ -2,6 +2,8 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
|
||||
import { resolveDaemonInstallRuntimeInputs } from "../commands/daemon-install-plan.shared.js";
|
||||
import { resolveGatewayDaemonRuntime } from "../commands/daemon-runtime.js";
|
||||
import { resolvePinnedDaemonRuntimePath } from "./runtime-paths.js";
|
||||
import { buildNodeServiceEnvironment, buildServiceEnvironment } from "./service-env.js";
|
||||
|
||||
@@ -25,6 +27,61 @@ const probe = (file: string, env: NodeJS.ProcessEnv = {}) =>
|
||||
resolvePinnedDaemonRuntimePath(file, "node", env);
|
||||
|
||||
describe.skipIf(process.platform === "win32")("runtime pin executable boundary", () => {
|
||||
it("uses standalone identity even when the executable is named bun", async () => {
|
||||
const file = runtimeFixture(
|
||||
`
|
||||
const path = require("node:path");
|
||||
const root = path.join(path.dirname(process.argv[1]), "runtime");
|
||||
if (process.argv[2] !== path.join(root, "dist/infra/daemon-runtime-probe.entry.js") ||
|
||||
process.argv[3] !== "bun" || process.argv[4] !== root ||
|
||||
process.env.BUN_BE_BUN || process.env.NODE_OPTIONS || process.env.FIXTURE_SECRET) process.exit(9);
|
||||
process.stdout.write(JSON.stringify({
|
||||
bunVersion: "1.4.4", sqliteVersion: "3.53.4",
|
||||
sqliteProbe: { available: true, version: "3.53.4", text: true, blob: true, json: true }
|
||||
}));
|
||||
`,
|
||||
"bun",
|
||||
);
|
||||
const root = path.dirname(file);
|
||||
fs.mkdirSync(path.join(root, "runtime"));
|
||||
fs.writeFileSync(
|
||||
path.join(root, "manifest.json"),
|
||||
JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
mode: "aot",
|
||||
executable: { path: "bun" },
|
||||
runtime: { path: "runtime" },
|
||||
openclaw: { version: "2026.10.25" },
|
||||
bun: { version: "1.4.4" },
|
||||
}),
|
||||
);
|
||||
const env = {
|
||||
NODE_OPTIONS: "--require=/nonexistent/fixture.js",
|
||||
BUN_BE_BUN: "1",
|
||||
FIXTURE_SECRET: "synthetic",
|
||||
};
|
||||
expect(resolveGatewayDaemonRuntime([file, "gateway"])).toBe("standalone");
|
||||
await expect(resolvePinnedDaemonRuntimePath(file, "bun", env)).rejects.toThrow(
|
||||
/must name a bun/,
|
||||
);
|
||||
await expect(
|
||||
resolveDaemonInstallRuntimeInputs({
|
||||
runtime: "standalone",
|
||||
pinnedRuntimePath: file,
|
||||
devMode: true,
|
||||
env,
|
||||
}),
|
||||
).resolves.toEqual({ devMode: false, runtime: "standalone", runtimePath: file });
|
||||
fs.rmdirSync(path.join(root, "runtime"));
|
||||
await expect(resolvePinnedDaemonRuntimePath(file, "standalone", env)).rejects.toThrow(
|
||||
/Restore the complete standalone artifact/,
|
||||
);
|
||||
fs.unlinkSync(path.join(root, "manifest.json"));
|
||||
await expect(resolvePinnedDaemonRuntimePath(file, "standalone", env)).rejects.toThrow(
|
||||
/must name a standalone/,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses a bounded sanitized probe without inheriting secrets or Node preloads", async () => {
|
||||
const file = runtimeFixture(
|
||||
"if (process.env.FIXTURE_SECRET || process.env.NODE_OPTIONS) process.exit(9);\n" +
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isStandaloneRuntimeExecutable } from "../../cli-invocation.mjs";
|
||||
import { SUPPORTED_NODE_VERSIONS } from "../../node-version.mjs";
|
||||
import { isBunRuntime, isNodeRuntime } from "./runtime-binary.js";
|
||||
import {
|
||||
@@ -5,6 +6,7 @@ import {
|
||||
isVersionManagedNodePath,
|
||||
resolveBunRuntimeInfo,
|
||||
resolveNodeRuntimeInfo,
|
||||
resolveStandaloneRuntimeInfo,
|
||||
resolveSystemNodePath,
|
||||
} from "./runtime-paths.js";
|
||||
import * as runtimePins from "./runtime-pin-state.js";
|
||||
@@ -14,6 +16,7 @@ import { normalizeServicePathEntry } from "./service-path-policy.js";
|
||||
export const SERVICE_RUNTIME_AUDIT_CODES = {
|
||||
gatewayRuntimeBun: "gateway-runtime-bun",
|
||||
gatewayRuntimeNode: "gateway-runtime-node",
|
||||
gatewayRuntimeStandalone: "gateway-runtime-standalone",
|
||||
gatewayRuntimeProbeFailed: "gateway-runtime-probe-failed",
|
||||
gatewayRuntimeNodeVersionManager: "gateway-runtime-node-version-manager",
|
||||
gatewayRuntimeNodeSystemMissing: "gateway-runtime-node-system-missing",
|
||||
@@ -32,36 +35,43 @@ export async function auditGatewayRuntime(
|
||||
}
|
||||
|
||||
const bun = isBunRuntime(execPath);
|
||||
if (!bun && !isNodeRuntime(execPath)) {
|
||||
const standalone = isStandaloneRuntimeExecutable(execPath);
|
||||
if (!standalone && !bun && !isNodeRuntime(execPath)) {
|
||||
return undefined;
|
||||
}
|
||||
const runtime = bun
|
||||
? await resolveBunRuntimeInfo(execPath)
|
||||
: await resolveNodeRuntimeInfo(execPath, env, timeoutMs);
|
||||
const runtime = standalone
|
||||
? await resolveStandaloneRuntimeInfo(execPath, env, timeoutMs)
|
||||
: bun
|
||||
? await resolveBunRuntimeInfo(execPath)
|
||||
: await resolveNodeRuntimeInfo(execPath, env, timeoutMs);
|
||||
if (runtime.status !== "supported") {
|
||||
issues.push({
|
||||
code:
|
||||
runtime.status === "probe-failed"
|
||||
? SERVICE_RUNTIME_AUDIT_CODES.gatewayRuntimeProbeFailed
|
||||
: bun
|
||||
? SERVICE_RUNTIME_AUDIT_CODES.gatewayRuntimeBun
|
||||
: SERVICE_RUNTIME_AUDIT_CODES.gatewayRuntimeNode,
|
||||
: standalone
|
||||
? SERVICE_RUNTIME_AUDIT_CODES.gatewayRuntimeStandalone
|
||||
: bun
|
||||
? SERVICE_RUNTIME_AUDIT_CODES.gatewayRuntimeBun
|
||||
: SERVICE_RUNTIME_AUDIT_CODES.gatewayRuntimeNode,
|
||||
message:
|
||||
runtime.status === "probe-failed"
|
||||
? `Gateway service ${bun ? "Bun" : "Node"} runtime check failed.`
|
||||
: bun
|
||||
? "Gateway service uses an unsupported Bun runtime; Bun 1.4.3+ with WAL-reset-safe node:sqlite is required."
|
||||
: (runtime.capabilityError ?? "Gateway service Node failed its capability check."),
|
||||
? `Gateway service ${standalone ? "standalone OpenClaw" : bun ? "Bun" : "Node"} runtime check failed.`
|
||||
: standalone
|
||||
? "Gateway standalone runtime failed its capability check; replace the complete artifact."
|
||||
: bun
|
||||
? "Gateway service uses an unsupported Bun runtime; Bun 1.4.3+ with WAL-reset-safe node:sqlite is required."
|
||||
: (runtime.capabilityError ?? "Gateway service Node failed its capability check."),
|
||||
detail:
|
||||
runtime.status === "probe-failed"
|
||||
? runtime.error.message
|
||||
: bun && runtime.sqliteSelectionError
|
||||
: (bun || standalone) && runtime.sqliteSelectionError
|
||||
? `${execPath}: ${runtime.sqliteSelectionError}`
|
||||
: execPath,
|
||||
level: "recommended",
|
||||
});
|
||||
}
|
||||
if (bun) {
|
||||
if (bun || standalone) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ function buildCommonServiceEnvironment(
|
||||
// Match the installing shell's probed SQLite library; wrappers hide the runtime.
|
||||
if (
|
||||
platform === "darwin" &&
|
||||
(runtime === "bun" || normalizeOptionalString(env.OPENCLAW_WRAPPER))
|
||||
(runtime === "bun" || runtime === "standalone" || normalizeOptionalString(env.OPENCLAW_WRAPPER))
|
||||
) {
|
||||
const library = normalizeOptionalString(env.OPENCLAW_SQLITE_LIBRARY);
|
||||
const prefix = normalizeOptionalString(env.HOMEBREW_PREFIX);
|
||||
|
||||
@@ -9,6 +9,28 @@ const dirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("summarizeGatewayServiceLayout", () => {
|
||||
it("identifies the standalone executable as the entrypoint even when it is named bun", async () => {
|
||||
const root = dirs.make("standalone-service-layout-");
|
||||
const executable = path.join(root, "bun");
|
||||
await fs.writeFile(executable, "");
|
||||
await fs.writeFile(
|
||||
path.join(root, "manifest.json"),
|
||||
JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
mode: "aot",
|
||||
executable: { path: "bun" },
|
||||
runtime: { path: "runtime" },
|
||||
openclaw: { version: "2026.10.25" },
|
||||
bun: { version: "1.4.4" },
|
||||
}),
|
||||
);
|
||||
await expect(
|
||||
summarizeGatewayServiceLayout({
|
||||
programArguments: [executable, "gateway", "--port", "19876"],
|
||||
}),
|
||||
).resolves.toHaveProperty("entrypoint", executable);
|
||||
});
|
||||
|
||||
it("preserves optional physical fields and lexical source paths on probe failure", async () => {
|
||||
vi.spyOn(fs, "realpath").mockRejectedValue(
|
||||
Object.assign(new Error("denied"), { code: "EACCES" }),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { isStandaloneRuntimeExecutable } from "../../cli-invocation.mjs";
|
||||
import { quoteCliArg } from "../cli/quote-cli-arg.js";
|
||||
import { consumeRootCommandOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js";
|
||||
import { pathExists } from "../infra/fs-safe.js";
|
||||
@@ -48,6 +49,7 @@ export async function resolveGatewayServiceInstallationRefreshRoot(params: {
|
||||
!root ||
|
||||
!command ||
|
||||
isBunRuntime(command.programArguments[0] ?? "") ||
|
||||
isStandaloneRuntimeExecutable(command.programArguments[0] ?? "") ||
|
||||
state.loadState.status === "unknown" ||
|
||||
(state.runtime?.status !== "running" && state.runtime?.status !== "stopped") ||
|
||||
(process.platform === "linux" && !isSystemdManagerUid(managerUid)) ||
|
||||
@@ -118,7 +120,9 @@ function resolveSystemdScopeFromServicePath(
|
||||
}
|
||||
|
||||
export function resolveServiceEntrypointIndex(args: readonly string[]): number | undefined {
|
||||
const { position: script } = resolveRuntimeScriptPosition(args);
|
||||
const { position: script } = isStandaloneRuntimeExecutable(args[0] ?? "")
|
||||
? { position: { kind: "not-runtime" } }
|
||||
: resolveRuntimeScriptPosition(args);
|
||||
if (typeof script !== "number" && script.kind === "other") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Generated by scripts/generate-runtime-script-assets.mjs; do not edit.
|
||||
export const DAEMON_RUNTIME_PROBE_SOURCE =
|
||||
'var __getOwnPropNames=Object.getOwnPropertyNames;var __esm=(fn,res,err)=>function __init(){if(err)throw err[0];try{return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res}catch(e){throw err=[e],e}};var GATEWAY_SHUTDOWN_DRAIN_TIMEOUT_MS,GATEWAY_SHUTDOWN_RESERVE_MS,GATEWAY_SUPERVISOR_EXIT_MARGIN_MS,GATEWAY_SHUTDOWN_TIMEOUT_MS,GATEWAY_SERVICE_STOP_TIMEOUT_MS,LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS;var init_gateway_shutdown_budget=__esm({"gateway-shutdown-budget.mjs"(){"use strict";GATEWAY_SHUTDOWN_DRAIN_TIMEOUT_MS=315e3;GATEWAY_SHUTDOWN_RESERVE_MS=1e4;GATEWAY_SUPERVISOR_EXIT_MARGIN_MS=5e3;GATEWAY_SHUTDOWN_TIMEOUT_MS=GATEWAY_SHUTDOWN_DRAIN_TIMEOUT_MS+GATEWAY_SHUTDOWN_RESERVE_MS;GATEWAY_SERVICE_STOP_TIMEOUT_MS=GATEWAY_SHUTDOWN_TIMEOUT_MS+GATEWAY_SUPERVISOR_EXIT_MARGIN_MS;LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS=GATEWAY_SERVICE_STOP_TIMEOUT_MS/1e3}});var parentKey,CLI_RESPAWN_PARENT_ENV,inheritedParent,CLI_PARENT_BOUND_PRELOAD;var init_cli_parent_lifetime=__esm({"cli-parent-lifetime.mjs"(){"use strict";init_gateway_shutdown_budget();parentKey=Symbol.for("openclaw.cliRespawnParent");CLI_RESPAWN_PARENT_ENV="OPENCLAW_CLI_RESPAWN_PARENT";inheritedParent=process.env[CLI_RESPAWN_PARENT_ENV];delete process.env[CLI_RESPAWN_PARENT_ENV];if(inheritedParent===String(process.ppid)){globalThis[parentKey]={pid:process.ppid}}CLI_PARENT_BOUND_PRELOAD=`data:text/javascript,${encodeURIComponent(`\n globalThis[Symbol.for("openclaw.cliRespawnParent")] = { pid: process.ppid };\n process.execArgv = process.execArgv.filter((arg, index, args) =>\n arg !== import.meta.url && !(arg === "--import" && args[index + 1] === import.meta.url));\n`)}`}});var init_node_runtime_env=__esm({"node-runtime-env.mjs"(){"use strict"}});import{readFileSync}from"node:fs";import path from"node:path";import{fileURLToPath}from"node:url";function readStandaloneRuntimeManifest(executable=process.execPath){return readStandaloneManifest({executable})}function projectStandaloneBuildInfo(value){const nonBlank=text=>typeof text==="string"&&text.trim().length>0;const stringList=items=>Array.isArray(items)&&items.every(nonBlank);const result={mode:value.mode,openclaw:{version:value.openclaw.version,...nonBlank(value.openclaw.commit)?{commit:value.openclaw.commit}:{}},bun:{version:value.bun.version}};for(const key of["revision","commit","executableSha256"]){if(nonBlank(value.bun[key])){result.bun[key]=value.bun[key]}}if(value.build!==void 0){const{id,flavor,pluginSetSha256,moduleGraphSha256}=value.build;if(!nonBlank(id)||!["standard","support"].includes(flavor)||!/^[a-f0-9]{64}$/.test(pluginSetSha256)||!/^[a-f0-9]{64}$/.test(moduleGraphSha256)){throw new Error("Invalid standalone build provenance")}result.build={id,flavor,pluginSetSha256,moduleGraphSha256};if(value.build.debugEntry!==void 0){const entry=value.build.debugEntry;if(typeof entry!=="string"||!entry.startsWith("debug/")||entry.includes("\\\\")||entry.split("/").some(part=>!part||part==="."||part==="..")){throw new Error("Invalid standalone debugger entry")}result.build.debugEntry=entry}}if(value.target!==void 0){if(!nonBlank(value.target.platform)||!nonBlank(value.target.arch)){throw new Error("Invalid standalone build target")}result.target={platform:value.target.platform,arch:value.target.arch}}if(value.plugins!==void 0){if(!stringList(value.plugins.embedded)||!stringList(value.plugins.omitted)){throw new Error("Invalid standalone plugin inventory")}result.plugins={embedded:[...value.plugins.embedded],omitted:[...value.plugins.omitted]}}if(value.nativeModules!==void 0){if(!Array.isArray(value.nativeModules)||value.nativeModules.some(item=>!item||!nonBlank(item.name)||!nonBlank(item.version)||!stringList(item.artifacts)||item.qualification!=="unverified"||!nonBlank(item.backend))){throw new Error("Invalid standalone native inventory")}result.nativeModules=value.nativeModules.map(({name,version,artifacts,qualification,backend})=>({name,version,artifacts:[...artifacts],qualification,backend}))}return result}function readStandaloneManifest({executable,runtimeRoot,includeBuild=false}){const location=executable??runtimeRoot;if(!path.isAbsolute(location)){return void 0}const root=path.dirname(location);const manifestPath=path.join(root,"manifest.json");let raw;try{raw=readFileSync(manifestPath,"utf8")}catch(error){if(error?.code==="ENOENT"||error?.code==="ENOTDIR"){return void 0}throw error}let manifest;try{manifest=JSON.parse(raw)}catch(cause){throw new Error(`Invalid standalone runtime manifest: ${manifestPath}`,{cause})}const nonBlank=value=>typeof value==="string"&&value.trim().length>0;if(manifest?.schemaVersion!==1||!["jit","bytecode","aot"].includes(manifest.mode)||!nonBlank(manifest.executable?.path)||!nonBlank(manifest.runtime?.path)||!nonBlank(manifest.openclaw?.version)||!nonBlank(manifest.bun?.version)){throw new Error(`Invalid standalone runtime manifest: ${manifestPath}`)}const executableRelative=path.relative(root,path.resolve(root,manifest.executable.path));const sidecarRelative=path.relative(root,pLine truncated
|
||||
'var __getOwnPropNames=Object.getOwnPropertyNames;var __esm=(fn,res,err)=>function __init(){if(err)throw err[0];try{return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res}catch(e){throw err=[e],e}};var GATEWAY_SHUTDOWN_DRAIN_TIMEOUT_MS,GATEWAY_SHUTDOWN_RESERVE_MS,GATEWAY_SUPERVISOR_EXIT_MARGIN_MS,GATEWAY_SHUTDOWN_TIMEOUT_MS,GATEWAY_SERVICE_STOP_TIMEOUT_MS,LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS;var init_gateway_shutdown_budget=__esm({"gateway-shutdown-budget.mjs"(){"use strict";GATEWAY_SHUTDOWN_DRAIN_TIMEOUT_MS=315e3;GATEWAY_SHUTDOWN_RESERVE_MS=1e4;GATEWAY_SUPERVISOR_EXIT_MARGIN_MS=5e3;GATEWAY_SHUTDOWN_TIMEOUT_MS=GATEWAY_SHUTDOWN_DRAIN_TIMEOUT_MS+GATEWAY_SHUTDOWN_RESERVE_MS;GATEWAY_SERVICE_STOP_TIMEOUT_MS=GATEWAY_SHUTDOWN_TIMEOUT_MS+GATEWAY_SUPERVISOR_EXIT_MARGIN_MS;LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS=GATEWAY_SERVICE_STOP_TIMEOUT_MS/1e3}});var parentKey,CLI_RESPAWN_PARENT_ENV,inheritedParent,CLI_PARENT_BOUND_PRELOAD;var init_cli_parent_lifetime=__esm({"cli-parent-lifetime.mjs"(){"use strict";init_gateway_shutdown_budget();parentKey=Symbol.for("openclaw.cliRespawnParent");CLI_RESPAWN_PARENT_ENV="OPENCLAW_CLI_RESPAWN_PARENT";inheritedParent=process.env[CLI_RESPAWN_PARENT_ENV];delete process.env[CLI_RESPAWN_PARENT_ENV];if(inheritedParent===String(process.ppid)){globalThis[parentKey]={pid:process.ppid}}CLI_PARENT_BOUND_PRELOAD=`data:text/javascript,${encodeURIComponent(`\n globalThis[Symbol.for("openclaw.cliRespawnParent")] = { pid: process.ppid };\n process.execArgv = process.execArgv.filter((arg, index, args) =>\n arg !== import.meta.url && !(arg === "--import" && args[index + 1] === import.meta.url));\n`)}`}});var init_node_runtime_env=__esm({"node-runtime-env.mjs"(){"use strict"}});import{readFileSync}from"node:fs";import path from"node:path";import{fileURLToPath}from"node:url";function readStandaloneRuntimeManifest(executable=process.execPath){return readStandaloneManifest({executable})}function projectStandaloneBuildInfo(value){const nonBlank=text=>typeof text==="string"&&text.trim().length>0;const stringList=items=>Array.isArray(items)&&items.every(nonBlank);const result={mode:value.mode,openclaw:{version:value.openclaw.version,...nonBlank(value.openclaw.commit)?{commit:value.openclaw.commit}:{}},bun:{version:value.bun.version}};for(const key of["revision","commit","executableSha256"]){if(nonBlank(value.bun[key])){result.bun[key]=value.bun[key]}}if(value.build!==void 0){const{id,flavor,pluginSetSha256,moduleGraphSha256}=value.build;if(!nonBlank(id)||!["standard","support"].includes(flavor)||!/^[a-f0-9]{64}$/.test(pluginSetSha256)||!/^[a-f0-9]{64}$/.test(moduleGraphSha256)){throw new Error("Invalid standalone build provenance")}result.build={id,flavor,pluginSetSha256,moduleGraphSha256};if(value.build.debugEntry!==void 0){const entry=value.build.debugEntry;if(typeof entry!=="string"||!entry.startsWith("debug/")||entry.includes("\\\\")||entry.split("/").some(part=>!part||part==="."||part==="..")){throw new Error("Invalid standalone debugger entry")}result.build.debugEntry=entry}}if(value.target!==void 0){if(!nonBlank(value.target.platform)||!nonBlank(value.target.arch)){throw new Error("Invalid standalone build target")}result.target={platform:value.target.platform,arch:value.target.arch}}if(value.plugins!==void 0){if(!stringList(value.plugins.embedded)||!stringList(value.plugins.omitted)){throw new Error("Invalid standalone plugin inventory")}result.plugins={embedded:[...value.plugins.embedded],omitted:[...value.plugins.omitted]}}if(value.nativeModules!==void 0){if(!Array.isArray(value.nativeModules)||value.nativeModules.some(item=>!item||!nonBlank(item.name)||!nonBlank(item.version)||!stringList(item.artifacts)||item.qualification!=="unverified"||!nonBlank(item.backend))){throw new Error("Invalid standalone native inventory")}result.nativeModules=value.nativeModules.map(({name,version,artifacts,qualification,backend})=>({name,version,artifacts:[...artifacts],qualification,backend}))}return result}function readStandaloneManifest({executable,runtimeRoot,includeBuild=false}){const location=executable??runtimeRoot;if(!path.isAbsolute(location)){return void 0}const root=path.dirname(location);const manifestPath=path.join(root,"manifest.json");let raw;try{raw=readFileSync(manifestPath,"utf8")}catch(error){if(error?.code==="ENOENT"||error?.code==="ENOTDIR"){return void 0}throw error}let manifest;try{manifest=JSON.parse(raw)}catch(cause){throw new Error(`Invalid standalone runtime manifest: ${manifestPath}`,{cause})}const nonBlank=value=>typeof value==="string"&&value.trim().length>0;if(manifest?.schemaVersion!==1||!["jit","bytecode","aot"].includes(manifest.mode)||!nonBlank(manifest.executable?.path)||!nonBlank(manifest.runtime?.path)||!nonBlank(manifest.openclaw?.version)||!nonBlank(manifest.bun?.version)){throw new Error(`Invalid standalone runtime manifest: ${manifestPath}`)}const executableRelative=path.relative(root,path.resolve(root,manifest.executable.path));const sidecarRelative=path.relative(root,pLine truncated
|
||||
@@ -18,6 +18,7 @@ export function createRuntimeProcessEntrypoints(currentModuleUrl: string) {
|
||||
}
|
||||
|
||||
return {
|
||||
daemonRuntimeProbe: runtimeProcessEntrypoint("infra/daemon-runtime-probe.entry"),
|
||||
pluginToolsServe: runtimeProcessEntrypoint("mcp/plugin-tools-serve"),
|
||||
openclawToolsServe: runtimeProcessEntrypoint("mcp/openclaw-tools-serve"),
|
||||
repositoryPublicationRestore: runtimeProcessEntrypoint(
|
||||
@@ -702,6 +703,7 @@ export const runtimeProcessScriptEntrypoints = {
|
||||
kind: "file",
|
||||
fileName: "daemon-runtime-probe.mjs",
|
||||
materialize: true,
|
||||
runtimeEntrypoint: "daemonRuntimeProbe",
|
||||
},
|
||||
updateStateSizes: {
|
||||
sourceModule: "src/infra/update-state-sizes.generated.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { isEmbeddedStandaloneModule } from "../../cli-invocation.mjs";
|
||||
import { isEmbeddedStandaloneModule, resolveStandaloneSidecarRoot } from "../../cli-invocation.mjs";
|
||||
import { createRuntimeProcessScriptFile } from "../../runtime-process-scripts.generated.mjs";
|
||||
import { runtimeProcessEntrypoints } from "./runtime-process-entrypoints.js";
|
||||
import {
|
||||
@@ -58,7 +58,7 @@ export function resolveRuntimeScriptEntrypointUrl(
|
||||
return pathToFileURL(path.join(directory, runtimeProcessScriptEntrypoints[name].fileName));
|
||||
}
|
||||
|
||||
/** Embedded self children use the image dispatcher; foreign runtimes need a real file. */
|
||||
/** Standalone images use declared child entries; ordinary runtimes need a physical script. */
|
||||
export function prepareRuntimeProcessScriptInvocation(
|
||||
name: Parameters<typeof createRuntimeProcessScriptFile>[0],
|
||||
execPath: string,
|
||||
@@ -71,6 +71,13 @@ export function prepareRuntimeProcessScriptInvocation(
|
||||
return { argv: [execPath, ...resolveRuntimeWorkerArgv(url, execPath)], dispose() {} };
|
||||
}
|
||||
}
|
||||
if ("runtimeEntrypoint" in entry) {
|
||||
const root = resolveStandaloneSidecarRoot(execPath);
|
||||
if (root !== undefined) {
|
||||
const child = runtimeProcessEntrypoints[entry.runtimeEntrypoint];
|
||||
return { argv: [execPath, path.join(root, "dist", child.distWorkerPath)], dispose() {} };
|
||||
}
|
||||
}
|
||||
const file = createRuntimeProcessScriptFile(name, options);
|
||||
return { argv: [execPath, fileURLToPath(file.url)], dispose: () => file.dispose() };
|
||||
}
|
||||
Reference in new issue
Block a user