mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
fix(launchd): verify profile updater jobs by metadata (#97264)
Some checks are pending
CI / -5 (push) Blocked by required conditions
CI / macos-swift (push) Blocked by required conditions
CI / ios-build (push) Blocked by required conditions
CI / -6 (push) Blocked by required conditions
CI / check-guards (push) Blocked by required conditions
CI / check-lint (push) Blocked by required conditions
CI / check-prod-types (push) Blocked by required conditions
CI / check-shrinkwrap (push) Blocked by required conditions
CI / check-test-types (push) Blocked by required conditions
CI / check-additional-boundaries-a (push) Blocked by required conditions
CI / check-additional-boundaries-bcd (push) Blocked by required conditions
CI / check-additional-extension-bundled (push) Blocked by required conditions
CI / check-additional-extension-channels (push) Blocked by required conditions
CI / check-additional-extension-package-boundary (push) Blocked by required conditions
CI / runner-admission (push) Waiting to run
CI / preflight (push) Blocked by required conditions
CI / security-fast (push) Blocked by required conditions
CI / ci-timings-summary (push) Blocked by required conditions
CI / skills-python (push) Blocked by required conditions
CI / -4 (push) Blocked by required conditions
Control UI Locale Refresh / Refresh (push) Blocked by required conditions
Control UI Locale Refresh / Commit control UI locale refresh (push) Blocked by required conditions
Native App Locale Refresh / Refresh native hi (push) Waiting to run
Native App Locale Refresh / Refresh native id (push) Waiting to run
Native App Locale Refresh / Refresh native it (push) Waiting to run
Native App Locale Refresh / Refresh native ja-JP (push) Waiting to run
Native App Locale Refresh / Refresh native ko (push) Waiting to run
Native App Locale Refresh / Refresh native nl (push) Waiting to run
CI / check-additional-runtime-topology-architecture (push) Blocked by required conditions
CI / check-session-accessor-boundary (push) Blocked by required conditions
CI / check-session-transcript-reader-boundary (push) Blocked by required conditions
CI / check-docs (push) Blocked by required conditions
CI / pnpm-store-warmup (push) Blocked by required conditions
CI / build-artifacts (push) Blocked by required conditions
CI / native-i18n (push) Blocked by required conditions
CI / (push) Blocked by required conditions
CI / QA Smoke CI (matrix) (push) Blocked by required conditions
CI / QA Smoke CI (telegram 1/2) (push) Blocked by required conditions
CI / QA Smoke CI (telegram 2/2) (push) Blocked by required conditions
CI / QA Smoke CI (push) Blocked by required conditions
CI / -1 (push) Blocked by required conditions
CI / -2 (push) Blocked by required conditions
CI / checks-node-compat-node22 (push) Blocked by required conditions
CI / -3 (push) Blocked by required conditions
CI / check-bundled-channel-config-metadata (push) Blocked by required conditions
CI / check-dependencies (push) Blocked by required conditions
ClawSweeper Dispatch / dispatch (push) Waiting to run
CodeQL / Security High (actions) (push) Waiting to run
CodeQL / Security High (channel-runtime-boundary) (push) Waiting to run
CodeQL / Security High (core-auth-secrets) (push) Waiting to run
CodeQL / Security High (mcp-process-tool-boundary) (push) Waiting to run
CodeQL / Security High (network-ssrf-boundary) (push) Waiting to run
CodeQL / Security High (plugin-trust-boundary) (push) Waiting to run
CodeQL / Security High (process-exec-boundary) (push) Waiting to run
Control UI Locale Refresh / plan (push) Waiting to run
Docs Sync Publish Repo / sync-publish-repo (push) Waiting to run
Docs / docs (push) Waiting to run
Native App Locale Refresh / Refresh native ar (push) Waiting to run
Native App Locale Refresh / Refresh native de (push) Waiting to run
Native App Locale Refresh / Refresh native es (push) Waiting to run
Native App Locale Refresh / Refresh native fa (push) Waiting to run
Native App Locale Refresh / Refresh native fr (push) Waiting to run
Native App Locale Refresh / Refresh native zh-TW (push) Waiting to run
Native App Locale Refresh / Commit native locale refresh (push) Blocked by required conditions
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Waiting to run
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Blocked by required conditions
Plugin Init Scaffold Validation / Validate provider scaffold (push) Waiting to run
Plugin NPM Release / preview_plugins_npm (push) Waiting to run
Plugin NPM Release / Validate release publish approval (push) Blocked by required conditions
Plugin NPM Release / preview_plugin_pack (push) Blocked by required conditions
Native App Locale Refresh / Refresh native pl (push) Waiting to run
Native App Locale Refresh / Refresh native pt-BR (push) Waiting to run
Native App Locale Refresh / Refresh native ru (push) Waiting to run
Native App Locale Refresh / Refresh native sv (push) Waiting to run
Native App Locale Refresh / Refresh native th (push) Waiting to run
Native App Locale Refresh / Refresh native tr (push) Waiting to run
Native App Locale Refresh / Refresh native uk (push) Waiting to run
Native App Locale Refresh / Refresh native vi (push) Waiting to run
Native App Locale Refresh / Refresh native zh-CN (push) Waiting to run
Plugin NPM Release / publish_plugins_npm (push) Blocked by required conditions
Plugin NPM Release / verify_plugins_npm (push) Blocked by required conditions
Website Installer Sync / static (push) Waiting to run
Website Installer Sync / linux-docker (push) Waiting to run
Website Installer Sync / macos-installer (push) Waiting to run
Website Installer Sync / windows-installer (push) Waiting to run
Website Installer Sync / sync-website (push) Blocked by required conditions
Workflow Sanity / no-tabs (push) Waiting to run
Workflow Sanity / actionlint (push) Waiting to run
Workflow Sanity / generated-doc-baselines (push) Waiting to run
Some checks are pending
CI / -5 (push) Blocked by required conditions
CI / macos-swift (push) Blocked by required conditions
CI / ios-build (push) Blocked by required conditions
CI / -6 (push) Blocked by required conditions
CI / check-guards (push) Blocked by required conditions
CI / check-lint (push) Blocked by required conditions
CI / check-prod-types (push) Blocked by required conditions
CI / check-shrinkwrap (push) Blocked by required conditions
CI / check-test-types (push) Blocked by required conditions
CI / check-additional-boundaries-a (push) Blocked by required conditions
CI / check-additional-boundaries-bcd (push) Blocked by required conditions
CI / check-additional-extension-bundled (push) Blocked by required conditions
CI / check-additional-extension-channels (push) Blocked by required conditions
CI / check-additional-extension-package-boundary (push) Blocked by required conditions
CI / runner-admission (push) Waiting to run
CI / preflight (push) Blocked by required conditions
CI / security-fast (push) Blocked by required conditions
CI / ci-timings-summary (push) Blocked by required conditions
CI / skills-python (push) Blocked by required conditions
CI / -4 (push) Blocked by required conditions
Control UI Locale Refresh / Refresh (push) Blocked by required conditions
Control UI Locale Refresh / Commit control UI locale refresh (push) Blocked by required conditions
Native App Locale Refresh / Refresh native hi (push) Waiting to run
Native App Locale Refresh / Refresh native id (push) Waiting to run
Native App Locale Refresh / Refresh native it (push) Waiting to run
Native App Locale Refresh / Refresh native ja-JP (push) Waiting to run
Native App Locale Refresh / Refresh native ko (push) Waiting to run
Native App Locale Refresh / Refresh native nl (push) Waiting to run
CI / check-additional-runtime-topology-architecture (push) Blocked by required conditions
CI / check-session-accessor-boundary (push) Blocked by required conditions
CI / check-session-transcript-reader-boundary (push) Blocked by required conditions
CI / check-docs (push) Blocked by required conditions
CI / pnpm-store-warmup (push) Blocked by required conditions
CI / build-artifacts (push) Blocked by required conditions
CI / native-i18n (push) Blocked by required conditions
CI / (push) Blocked by required conditions
CI / QA Smoke CI (matrix) (push) Blocked by required conditions
CI / QA Smoke CI (telegram 1/2) (push) Blocked by required conditions
CI / QA Smoke CI (telegram 2/2) (push) Blocked by required conditions
CI / QA Smoke CI (push) Blocked by required conditions
CI / -1 (push) Blocked by required conditions
CI / -2 (push) Blocked by required conditions
CI / checks-node-compat-node22 (push) Blocked by required conditions
CI / -3 (push) Blocked by required conditions
CI / check-bundled-channel-config-metadata (push) Blocked by required conditions
CI / check-dependencies (push) Blocked by required conditions
ClawSweeper Dispatch / dispatch (push) Waiting to run
CodeQL / Security High (actions) (push) Waiting to run
CodeQL / Security High (channel-runtime-boundary) (push) Waiting to run
CodeQL / Security High (core-auth-secrets) (push) Waiting to run
CodeQL / Security High (mcp-process-tool-boundary) (push) Waiting to run
CodeQL / Security High (network-ssrf-boundary) (push) Waiting to run
CodeQL / Security High (plugin-trust-boundary) (push) Waiting to run
CodeQL / Security High (process-exec-boundary) (push) Waiting to run
Control UI Locale Refresh / plan (push) Waiting to run
Docs Sync Publish Repo / sync-publish-repo (push) Waiting to run
Docs / docs (push) Waiting to run
Native App Locale Refresh / Refresh native ar (push) Waiting to run
Native App Locale Refresh / Refresh native de (push) Waiting to run
Native App Locale Refresh / Refresh native es (push) Waiting to run
Native App Locale Refresh / Refresh native fa (push) Waiting to run
Native App Locale Refresh / Refresh native fr (push) Waiting to run
Native App Locale Refresh / Refresh native zh-TW (push) Waiting to run
Native App Locale Refresh / Commit native locale refresh (push) Blocked by required conditions
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Waiting to run
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Blocked by required conditions
Plugin Init Scaffold Validation / Validate provider scaffold (push) Waiting to run
Plugin NPM Release / preview_plugins_npm (push) Waiting to run
Plugin NPM Release / Validate release publish approval (push) Blocked by required conditions
Plugin NPM Release / preview_plugin_pack (push) Blocked by required conditions
Native App Locale Refresh / Refresh native pl (push) Waiting to run
Native App Locale Refresh / Refresh native pt-BR (push) Waiting to run
Native App Locale Refresh / Refresh native ru (push) Waiting to run
Native App Locale Refresh / Refresh native sv (push) Waiting to run
Native App Locale Refresh / Refresh native th (push) Waiting to run
Native App Locale Refresh / Refresh native tr (push) Waiting to run
Native App Locale Refresh / Refresh native uk (push) Waiting to run
Native App Locale Refresh / Refresh native vi (push) Waiting to run
Native App Locale Refresh / Refresh native zh-CN (push) Waiting to run
Plugin NPM Release / publish_plugins_npm (push) Blocked by required conditions
Plugin NPM Release / verify_plugins_npm (push) Blocked by required conditions
Website Installer Sync / static (push) Waiting to run
Website Installer Sync / linux-docker (push) Waiting to run
Website Installer Sync / macos-installer (push) Waiting to run
Website Installer Sync / windows-installer (push) Waiting to run
Website Installer Sync / sync-website (push) Blocked by required conditions
Workflow Sanity / no-tabs (push) Waiting to run
Workflow Sanity / actionlint (push) Waiting to run
Workflow Sanity / generated-doc-baselines (push) Waiting to run
* fix(launchd): verify profile updater jobs by metadata * fix(launchd): bind updater proof to job metadata --------- Co-authored-by: Peter Steinberger <peter@steipete.me> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
parent
f6c16d22b4
commit
4f9a371c95
2 changed files with 488 additions and 19 deletions
|
|
@ -96,6 +96,52 @@ function createDefaultLaunchdEnv(): Record<string, string | undefined> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createTestLaunchAgentPlist(params: {
|
||||||
|
label: string;
|
||||||
|
programArguments: string[];
|
||||||
|
environment?: Record<string, string>;
|
||||||
|
}): string {
|
||||||
|
const argsXml = params.programArguments.map((arg) => ` <string>${arg}</string>`).join("\n");
|
||||||
|
const envXml = params.environment
|
||||||
|
? [
|
||||||
|
" <key>EnvironmentVariables</key>",
|
||||||
|
" <dict>",
|
||||||
|
...Object.entries(params.environment).flatMap(([key, value]) => [
|
||||||
|
` <key>${key}</key>`,
|
||||||
|
` <string>${value}</string>`,
|
||||||
|
]),
|
||||||
|
" </dict>",
|
||||||
|
].join("\n")
|
||||||
|
: "";
|
||||||
|
return [
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
|
'<plist version="1.0">',
|
||||||
|
" <dict>",
|
||||||
|
" <key>Label</key>",
|
||||||
|
` <string>${params.label}</string>`,
|
||||||
|
" <key>ProgramArguments</key>",
|
||||||
|
" <array>",
|
||||||
|
argsXml,
|
||||||
|
" </array>",
|
||||||
|
envXml,
|
||||||
|
" </dict>",
|
||||||
|
"</plist>",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLaunchAgentPlist(params: {
|
||||||
|
env: Record<string, string | undefined>;
|
||||||
|
label: string;
|
||||||
|
programArguments: string[];
|
||||||
|
environment?: Record<string, string>;
|
||||||
|
}): void {
|
||||||
|
state.files.set(
|
||||||
|
`${params.env.HOME}/Library/LaunchAgents/${params.label}.plist`,
|
||||||
|
createTestLaunchAgentPlist(params),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function withProcessEnv<T>(
|
async function withProcessEnv<T>(
|
||||||
overrides: Record<string, string | undefined>,
|
overrides: Record<string, string | undefined>,
|
||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
|
|
@ -478,6 +524,7 @@ describe("launchctl list detection", () => {
|
||||||
"- 127 ai.openclaw.update.2026.5.12",
|
"- 127 ai.openclaw.update.2026.5.12",
|
||||||
"- 0 ai.openclaw.manual-update.1717168800",
|
"- 0 ai.openclaw.manual-update.1717168800",
|
||||||
"8142 0 ai.openclaw.update.2026.5.13-beta.1",
|
"8142 0 ai.openclaw.update.2026.5.13-beta.1",
|
||||||
|
"915 0 ai.openclaw.tayoun.update.20260625T201026-0400",
|
||||||
"- 0 ai.openclaw.manual-updater.1717168800",
|
"- 0 ai.openclaw.manual-updater.1717168800",
|
||||||
"- 0 com.example.other",
|
"- 0 com.example.other",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
|
|
@ -516,6 +563,169 @@ describe("launchctl list detection", () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it.runIf(process.platform === "darwin")(
|
||||||
|
"reports profile-scoped updater jobs only when launchd metadata confirms an update command",
|
||||||
|
async () => {
|
||||||
|
const env = createDefaultLaunchdEnv();
|
||||||
|
const updaterLabel = "ai.openclaw.tayoun.update.20260625T201026-0400";
|
||||||
|
const gatewayLikeLabel = "ai.openclaw.dev.team.update.20260625T201026-0400";
|
||||||
|
const nonOpenClawLabel = "ai.openclaw.fake.update.20260625T201026-0400";
|
||||||
|
const prefixedCliLabel = "ai.openclaw.helper.update.20260625T201026-0400";
|
||||||
|
state.listOutput = [
|
||||||
|
`4321 0 ${updaterLabel}`,
|
||||||
|
`9876 0 ${gatewayLikeLabel}`,
|
||||||
|
`2468 0 ${nonOpenClawLabel}`,
|
||||||
|
`1357 0 ${prefixedCliLabel}`,
|
||||||
|
].join("\n");
|
||||||
|
setLaunchAgentPlist({
|
||||||
|
env,
|
||||||
|
label: updaterLabel,
|
||||||
|
programArguments: ["/opt/homebrew/bin/openclaw", "update", "--yes", "--json"],
|
||||||
|
});
|
||||||
|
setLaunchAgentPlist({
|
||||||
|
env,
|
||||||
|
label: gatewayLikeLabel,
|
||||||
|
programArguments: ["/opt/homebrew/bin/openclaw", "gateway", "run"],
|
||||||
|
});
|
||||||
|
setLaunchAgentPlist({
|
||||||
|
env,
|
||||||
|
label: nonOpenClawLabel,
|
||||||
|
programArguments: ["/bin/echo", "update", "--yes"],
|
||||||
|
});
|
||||||
|
setLaunchAgentPlist({
|
||||||
|
env,
|
||||||
|
label: prefixedCliLabel,
|
||||||
|
programArguments: ["/usr/local/bin/openclaw-helper", "update", "--yes"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const jobs = await findStaleOpenClawUpdateLaunchdJobs(env as NodeJS.ProcessEnv);
|
||||||
|
|
||||||
|
expect(jobs).toEqual([
|
||||||
|
{
|
||||||
|
label: updaterLabel,
|
||||||
|
pid: 4321,
|
||||||
|
lastExitStatus: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.runIf(process.platform === "darwin")(
|
||||||
|
"accepts an explicit updater marker when confirming profile-scoped updater jobs",
|
||||||
|
async () => {
|
||||||
|
const env = createDefaultLaunchdEnv();
|
||||||
|
const updaterLabel = "ai.openclaw.tayoun.update.20260625T201026-0400";
|
||||||
|
state.listOutput = `4321 0 ${updaterLabel}`;
|
||||||
|
setLaunchAgentPlist({
|
||||||
|
env,
|
||||||
|
label: updaterLabel,
|
||||||
|
programArguments: ["/opt/homebrew/bin/openclaw", "gateway", "run"],
|
||||||
|
environment: { OPENCLAW_UPDATE_RUN_HANDOFF: "1" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const jobs = await findStaleOpenClawUpdateLaunchdJobs(env as NodeJS.ProcessEnv);
|
||||||
|
|
||||||
|
expect(jobs).toEqual([
|
||||||
|
{
|
||||||
|
label: updaterLabel,
|
||||||
|
pid: 4321,
|
||||||
|
lastExitStatus: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.runIf(process.platform === "darwin")(
|
||||||
|
"unwraps generated environment-wrapper metadata for profile-scoped updater jobs",
|
||||||
|
async () => {
|
||||||
|
const env = createDefaultLaunchdEnv();
|
||||||
|
const label = "ai.openclaw.tayoun.update.20260625T201026-0400";
|
||||||
|
const envDir = "/Users/test/.openclaw-tayoun/service-env";
|
||||||
|
const wrapperPath = `${envDir}/${label}-env-wrapper.sh`;
|
||||||
|
const envFilePath = `${envDir}/${label}.env`;
|
||||||
|
state.listOutput = `4321 0 ${label}`;
|
||||||
|
state.files.set(envFilePath, "export PATH='/opt/homebrew/bin:/usr/bin'\n");
|
||||||
|
setLaunchAgentPlist({
|
||||||
|
env,
|
||||||
|
label,
|
||||||
|
programArguments: [
|
||||||
|
LAUNCH_AGENT_ENV_WRAPPER_SHELL,
|
||||||
|
wrapperPath,
|
||||||
|
envFilePath,
|
||||||
|
"/opt/homebrew/bin/openclaw",
|
||||||
|
"update",
|
||||||
|
"--yes",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const jobs = await findStaleOpenClawUpdateLaunchdJobs(env as NodeJS.ProcessEnv);
|
||||||
|
|
||||||
|
expect(jobs).toEqual([
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
pid: 4321,
|
||||||
|
lastExitStatus: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.runIf(process.platform === "darwin")(
|
||||||
|
"reads the updater marker from a generated environment file",
|
||||||
|
async () => {
|
||||||
|
const env = createDefaultLaunchdEnv();
|
||||||
|
const label = "ai.openclaw.tayoun.update.20260625T201026-0400";
|
||||||
|
const envDir = "/Users/test/.openclaw-tayoun/service-env";
|
||||||
|
const wrapperPath = `${envDir}/${label}-env-wrapper.sh`;
|
||||||
|
const envFilePath = `${envDir}/${label}.env`;
|
||||||
|
state.listOutput = `4321 0 ${label}`;
|
||||||
|
state.files.set(envFilePath, "export OPENCLAW_UPDATE_RUN_HANDOFF='1'\n");
|
||||||
|
setLaunchAgentPlist({
|
||||||
|
env,
|
||||||
|
label,
|
||||||
|
programArguments: [
|
||||||
|
LAUNCH_AGENT_ENV_WRAPPER_SHELL,
|
||||||
|
wrapperPath,
|
||||||
|
envFilePath,
|
||||||
|
"/opt/homebrew/bin/openclaw",
|
||||||
|
"gateway",
|
||||||
|
"run",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const jobs = await findStaleOpenClawUpdateLaunchdJobs(env as NodeJS.ProcessEnv);
|
||||||
|
|
||||||
|
expect(jobs).toEqual([
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
pid: 4321,
|
||||||
|
lastExitStatus: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.runIf(process.platform === "darwin")(
|
||||||
|
"does not use the scanner process marker to confirm other profile-scoped jobs",
|
||||||
|
async () => {
|
||||||
|
const env = {
|
||||||
|
...createDefaultLaunchdEnv(),
|
||||||
|
OPENCLAW_UPDATE_RUN_HANDOFF: "1",
|
||||||
|
};
|
||||||
|
const gatewayLikeLabel = "ai.openclaw.dev.team.update.20260625T201026-0400";
|
||||||
|
state.listOutput = `9876 0 ${gatewayLikeLabel}`;
|
||||||
|
setLaunchAgentPlist({
|
||||||
|
env,
|
||||||
|
label: gatewayLikeLabel,
|
||||||
|
programArguments: ["/opt/homebrew/bin/openclaw", "gateway", "run"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const jobs = await findStaleOpenClawUpdateLaunchdJobs(env as NodeJS.ProcessEnv);
|
||||||
|
|
||||||
|
expect(jobs).toEqual([]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it.runIf(process.platform === "darwin")(
|
it.runIf(process.platform === "darwin")(
|
||||||
"does not report current gateway labels that collide with manual update labels",
|
"does not report current gateway labels that collide with manual update labels",
|
||||||
async () => {
|
async () => {
|
||||||
|
|
@ -653,6 +863,117 @@ describe("launchctl list detection", () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it.runIf(process.platform === "darwin")(
|
||||||
|
"disables current profile-scoped updater launchd jobs only after metadata confirmation",
|
||||||
|
async () => {
|
||||||
|
const env = createDefaultLaunchdEnv();
|
||||||
|
const label = "ai.openclaw.tayoun.update.20260625T201026-0400";
|
||||||
|
setLaunchAgentPlist({
|
||||||
|
env,
|
||||||
|
label,
|
||||||
|
programArguments: ["/usr/local/bin/node", "/opt/openclaw/openclaw.mjs", "update", "--yes"],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
disableCurrentOpenClawUpdateLaunchdJob({
|
||||||
|
...env,
|
||||||
|
LAUNCH_JOB_LABEL: label,
|
||||||
|
}),
|
||||||
|
).resolves.toBe(true);
|
||||||
|
|
||||||
|
const domain = typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501";
|
||||||
|
expect(state.launchctlCalls).toContainEqual(["disable", `${domain}/${label}`]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.runIf(process.platform === "darwin")(
|
||||||
|
"lets a profile-scoped updater self-disarm from launchd runtime metadata",
|
||||||
|
async () => {
|
||||||
|
const env = createDefaultLaunchdEnv();
|
||||||
|
const label = "ai.openclaw.tayoun.update.20260625T201026-0400";
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
disableCurrentOpenClawUpdateLaunchdJob({
|
||||||
|
...env,
|
||||||
|
LAUNCH_JOB_LABEL: label,
|
||||||
|
OPENCLAW_UPDATE_RUN_HANDOFF: "1",
|
||||||
|
}),
|
||||||
|
).resolves.toBe(true);
|
||||||
|
|
||||||
|
const domain = typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501";
|
||||||
|
expect(state.launchctlCalls).toContainEqual(["disable", `${domain}/${label}`]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.runIf(process.platform === "darwin")(
|
||||||
|
"requires plist proof for a configured label preserved by an update handoff",
|
||||||
|
async () => {
|
||||||
|
const env = createDefaultLaunchdEnv();
|
||||||
|
const label = "ai.openclaw.dev.team.update.20260625T201026-0400";
|
||||||
|
setLaunchAgentPlist({
|
||||||
|
env,
|
||||||
|
label,
|
||||||
|
programArguments: ["/opt/homebrew/bin/openclaw", "gateway", "run"],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
disableCurrentOpenClawUpdateLaunchdJob({
|
||||||
|
...env,
|
||||||
|
OPENCLAW_LAUNCHD_LABEL: label,
|
||||||
|
OPENCLAW_UPDATE_RUN_HANDOFF: "1",
|
||||||
|
}),
|
||||||
|
).resolves.toBe(false);
|
||||||
|
|
||||||
|
expect(state.launchctlCalls).toEqual([]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.runIf(process.platform === "darwin")(
|
||||||
|
"disables a configured profile-scoped updater only with confirming plist metadata",
|
||||||
|
async () => {
|
||||||
|
const env = createDefaultLaunchdEnv();
|
||||||
|
const label = "ai.openclaw.tayoun.update.20260625T201026-0400";
|
||||||
|
setLaunchAgentPlist({
|
||||||
|
env,
|
||||||
|
label,
|
||||||
|
programArguments: ["/opt/homebrew/bin/openclaw", "update", "--yes"],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
disableCurrentOpenClawUpdateLaunchdJob({
|
||||||
|
...env,
|
||||||
|
OPENCLAW_LAUNCHD_LABEL: label,
|
||||||
|
OPENCLAW_UPDATE_RUN_HANDOFF: "1",
|
||||||
|
}),
|
||||||
|
).resolves.toBe(true);
|
||||||
|
|
||||||
|
const domain = typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501";
|
||||||
|
expect(state.launchctlCalls).toContainEqual(["disable", `${domain}/${label}`]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.runIf(process.platform === "darwin")(
|
||||||
|
"does not disable profile-scoped gateway labels without updater metadata",
|
||||||
|
async () => {
|
||||||
|
const env = createDefaultLaunchdEnv();
|
||||||
|
const label = "ai.openclaw.tayoun.update.20260625T201026-0400";
|
||||||
|
setLaunchAgentPlist({
|
||||||
|
env,
|
||||||
|
label,
|
||||||
|
programArguments: ["/opt/homebrew/bin/openclaw", "gateway", "run"],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
disableCurrentOpenClawUpdateLaunchdJob({
|
||||||
|
...env,
|
||||||
|
LAUNCH_JOB_LABEL: label,
|
||||||
|
}),
|
||||||
|
).resolves.toBe(false);
|
||||||
|
|
||||||
|
expect(state.launchctlCalls).toEqual([]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it.runIf(process.platform === "darwin")(
|
it.runIf(process.platform === "darwin")(
|
||||||
"does not disable custom gateway launchd labels under the manual-update prefix",
|
"does not disable custom gateway launchd labels under the manual-update prefix",
|
||||||
async () => {
|
async () => {
|
||||||
|
|
@ -705,6 +1026,23 @@ describe("launchctl list detection", () => {
|
||||||
`${domain}/ai.openclaw.manual-update.1717168800`,
|
`${domain}/ai.openclaw.manual-update.1717168800`,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.runIf(process.platform === "darwin")(
|
||||||
|
"does not let the process marker bypass metadata for an explicit profile job",
|
||||||
|
async () => {
|
||||||
|
const env = createDefaultLaunchdEnv();
|
||||||
|
const label = "ai.openclaw.tayoun.update.20260625T201026-0400";
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
disableOpenClawUpdateLaunchdJob(label, {
|
||||||
|
...env,
|
||||||
|
OPENCLAW_UPDATE_RUN_HANDOFF: "1",
|
||||||
|
}),
|
||||||
|
).resolves.toBe(false);
|
||||||
|
|
||||||
|
expect(state.launchctlCalls).toEqual([]);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("launchd bootstrap repair", () => {
|
describe("launchd bootstrap repair", () => {
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,11 @@ const LAUNCH_AGENT_ENV_DIR_NAME = "service-env";
|
||||||
const LAUNCH_AGENT_STDERR_PATH = "/dev/null";
|
const LAUNCH_AGENT_STDERR_PATH = "/dev/null";
|
||||||
const OPENCLAW_UPDATE_LAUNCHD_LABEL_PREFIX = "ai.openclaw.update.";
|
const OPENCLAW_UPDATE_LAUNCHD_LABEL_PREFIX = "ai.openclaw.update.";
|
||||||
const OPENCLAW_MANUAL_UPDATE_LAUNCHD_LABEL_PATTERN = /^ai\.openclaw\.manual-update\.\d+$/;
|
const OPENCLAW_MANUAL_UPDATE_LAUNCHD_LABEL_PATTERN = /^ai\.openclaw\.manual-update\.\d+$/;
|
||||||
|
const OPENCLAW_PROFILE_UPDATE_LAUNCHD_LABEL_PATTERN =
|
||||||
|
/^ai\.openclaw\.[A-Za-z0-9._-]+\.update\.[A-Za-z0-9._-]+$/;
|
||||||
|
const OPENCLAW_DIRECT_CLI_NAMES = new Set(["openclaw", "openclaw.mjs"]);
|
||||||
|
const OPENCLAW_NODE_RUNTIME_NAMES = new Set(["bun", "bun.exe", "node", "node.exe"]);
|
||||||
|
const OPENCLAW_SCRIPT_NAMES = new Set(["openclaw.mjs"]);
|
||||||
const LAUNCH_AGENT_STOP_PORT_RELEASE_TIMEOUT_MS = LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS * 1_000;
|
const LAUNCH_AGENT_STOP_PORT_RELEASE_TIMEOUT_MS = LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS * 1_000;
|
||||||
const LAUNCH_AGENT_STOP_PORT_RELEASE_POLL_MS = 100;
|
const LAUNCH_AGENT_STOP_PORT_RELEASE_POLL_MS = 100;
|
||||||
|
|
||||||
|
|
@ -62,6 +67,11 @@ export type StaleOpenClawUpdateLaunchdJob = {
|
||||||
lastExitStatus?: number;
|
lastExitStatus?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type OpenClawUpdateLaunchdLabelCandidate = {
|
||||||
|
label: string;
|
||||||
|
requiresMetadata: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
function normalizeOpenClawUpdateLaunchdLabel(label: unknown): string | null {
|
function normalizeOpenClawUpdateLaunchdLabel(label: unknown): string | null {
|
||||||
if (typeof label !== "string") {
|
if (typeof label !== "string") {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -75,6 +85,22 @@ function normalizeOpenClawUpdateLaunchdLabel(label: unknown): string | null {
|
||||||
return OPENCLAW_MANUAL_UPDATE_LAUNCHD_LABEL_PATTERN.test(trimmed) ? trimmed : null;
|
return OPENCLAW_MANUAL_UPDATE_LAUNCHD_LABEL_PATTERN.test(trimmed) ? trimmed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeOpenClawUpdateLaunchdLabelCandidate(
|
||||||
|
label: unknown,
|
||||||
|
): OpenClawUpdateLaunchdLabelCandidate | null {
|
||||||
|
const normalized = normalizeOpenClawUpdateLaunchdLabel(label);
|
||||||
|
if (normalized) {
|
||||||
|
return { label: normalized, requiresMetadata: false };
|
||||||
|
}
|
||||||
|
if (typeof label !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = label.trim();
|
||||||
|
return OPENCLAW_PROFILE_UPDATE_LAUNCHD_LABEL_PATTERN.test(trimmed)
|
||||||
|
? { label: trimmed, requiresMetadata: true }
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
function isCurrentGatewayLaunchdLabel(label: string, env: NodeJS.ProcessEnv): boolean {
|
function isCurrentGatewayLaunchdLabel(label: string, env: NodeJS.ProcessEnv): boolean {
|
||||||
const gatewayProfileLabel = resolveGatewayLaunchAgentLabel(env.OPENCLAW_PROFILE);
|
const gatewayProfileLabel = resolveGatewayLaunchAgentLabel(env.OPENCLAW_PROFILE);
|
||||||
if (label === gatewayProfileLabel) {
|
if (label === gatewayProfileLabel) {
|
||||||
|
|
@ -92,19 +118,19 @@ function isCurrentGatewayLaunchdLabel(label: string, env: NodeJS.ProcessEnv): bo
|
||||||
|
|
||||||
function resolveCurrentOpenClawUpdateLaunchdJobLabel(
|
function resolveCurrentOpenClawUpdateLaunchdJobLabel(
|
||||||
env: NodeJS.ProcessEnv = process.env,
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
): string | null {
|
): OpenClawUpdateLaunchdLabelCandidate | null {
|
||||||
for (const label of [
|
for (const label of [
|
||||||
env.LAUNCH_JOB_LABEL,
|
env.LAUNCH_JOB_LABEL,
|
||||||
env.LAUNCH_JOB_NAME,
|
env.LAUNCH_JOB_NAME,
|
||||||
env.XPC_SERVICE_NAME,
|
env.XPC_SERVICE_NAME,
|
||||||
env.OPENCLAW_LAUNCHD_LABEL,
|
env.OPENCLAW_LAUNCHD_LABEL,
|
||||||
]) {
|
]) {
|
||||||
const normalized = normalizeOpenClawUpdateLaunchdLabel(label);
|
const candidate = normalizeOpenClawUpdateLaunchdLabelCandidate(label);
|
||||||
if (normalized) {
|
if (candidate) {
|
||||||
if (isCurrentGatewayLaunchdLabel(normalized, env)) {
|
if (isCurrentGatewayLaunchdLabel(candidate.label, env)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return normalized;
|
return candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -352,7 +378,15 @@ async function execLaunchctl(
|
||||||
export function parseLaunchctlListOpenClawUpdateJobs(
|
export function parseLaunchctlListOpenClawUpdateJobs(
|
||||||
output: string,
|
output: string,
|
||||||
): StaleOpenClawUpdateLaunchdJob[] {
|
): StaleOpenClawUpdateLaunchdJob[] {
|
||||||
const jobs: StaleOpenClawUpdateLaunchdJob[] = [];
|
return parseLaunchctlListOpenClawUpdateJobCandidates(output)
|
||||||
|
.filter((job) => !job.requiresMetadata)
|
||||||
|
.map(({ requiresMetadata: _requiresMetadata, ...job }) => job);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLaunchctlListOpenClawUpdateJobCandidates(
|
||||||
|
output: string,
|
||||||
|
): Array<StaleOpenClawUpdateLaunchdJob & OpenClawUpdateLaunchdLabelCandidate> {
|
||||||
|
const jobs: Array<StaleOpenClawUpdateLaunchdJob & OpenClawUpdateLaunchdLabelCandidate> = [];
|
||||||
for (const rawLine of output.split(/\r?\n/)) {
|
for (const rawLine of output.split(/\r?\n/)) {
|
||||||
const line = rawLine.trim();
|
const line = rawLine.trim();
|
||||||
if (!line) {
|
if (!line) {
|
||||||
|
|
@ -360,14 +394,15 @@ export function parseLaunchctlListOpenClawUpdateJobs(
|
||||||
}
|
}
|
||||||
const parts = line.split(/\s+/);
|
const parts = line.split(/\s+/);
|
||||||
const [pidRaw, statusRaw, ...labelParts] = parts;
|
const [pidRaw, statusRaw, ...labelParts] = parts;
|
||||||
const label = normalizeOpenClawUpdateLaunchdLabel(labelParts.join(" "));
|
const candidate = normalizeOpenClawUpdateLaunchdLabelCandidate(labelParts.join(" "));
|
||||||
if (!label) {
|
if (!candidate) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const pid = pidRaw === "-" ? undefined : parseStrictPositiveInteger(pidRaw ?? "");
|
const pid = pidRaw === "-" ? undefined : parseStrictPositiveInteger(pidRaw ?? "");
|
||||||
const lastExitStatus = parseStrictInteger(statusRaw ?? "");
|
const lastExitStatus = parseStrictInteger(statusRaw ?? "");
|
||||||
jobs.push({
|
jobs.push({
|
||||||
label,
|
label: candidate.label,
|
||||||
|
requiresMetadata: candidate.requiresMetadata,
|
||||||
...(pid !== undefined ? { pid } : {}),
|
...(pid !== undefined ? { pid } : {}),
|
||||||
...(lastExitStatus !== undefined ? { lastExitStatus } : {}),
|
...(lastExitStatus !== undefined ? { lastExitStatus } : {}),
|
||||||
});
|
});
|
||||||
|
|
@ -375,6 +410,49 @@ export function parseLaunchctlListOpenClawUpdateJobs(
|
||||||
return jobs.toSorted((a, b) => a.label.localeCompare(b.label));
|
return jobs.toSorted((a, b) => a.label.localeCompare(b.label));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasOpenClawUpdateLaunchdMarker(env: Record<string, string | undefined> | undefined) {
|
||||||
|
return env?.OPENCLAW_UPDATE_RUN_HANDOFF?.trim() === "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOpenClawUpdateCommandPrefix(programArguments: string[], updateIndex: number): boolean {
|
||||||
|
if (updateIndex === 1) {
|
||||||
|
const cliName = path.basename(programArguments[0] ?? "").toLowerCase();
|
||||||
|
return OPENCLAW_DIRECT_CLI_NAMES.has(cliName);
|
||||||
|
}
|
||||||
|
if (updateIndex !== 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const runtimeName = path.basename(programArguments[0] ?? "").toLowerCase();
|
||||||
|
const entryName = path.basename(programArguments[1] ?? "").toLowerCase();
|
||||||
|
return OPENCLAW_NODE_RUNTIME_NAMES.has(runtimeName) && OPENCLAW_SCRIPT_NAMES.has(entryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOpenClawUpdateProgramArguments(programArguments: string[] | undefined): boolean {
|
||||||
|
if (!Array.isArray(programArguments) || programArguments.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const updateIndex = programArguments.findIndex((arg) => arg.trim() === "update");
|
||||||
|
if (updateIndex < 0 || !programArguments.slice(updateIndex + 1).includes("--yes")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
isOpenClawUpdateCommandPrefix(programArguments, updateIndex) &&
|
||||||
|
!programArguments.some((arg) => arg.trim() === "gateway")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isLaunchdJobConfirmedOpenClawUpdater(params: {
|
||||||
|
label: string;
|
||||||
|
env: NodeJS.ProcessEnv;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
const plistPath = resolveLaunchAgentPlistPathForLabel(params.env, params.label);
|
||||||
|
const command = await readLaunchAgentProgramArgumentsFromFile(plistPath);
|
||||||
|
return (
|
||||||
|
hasOpenClawUpdateLaunchdMarker(command?.environment) ||
|
||||||
|
isOpenClawUpdateProgramArguments(command?.programArguments)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function findStaleOpenClawUpdateLaunchdJobs(
|
export async function findStaleOpenClawUpdateLaunchdJobs(
|
||||||
env: NodeJS.ProcessEnv = process.env,
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
): Promise<StaleOpenClawUpdateLaunchdJob[]> {
|
): Promise<StaleOpenClawUpdateLaunchdJob[]> {
|
||||||
|
|
@ -387,29 +465,82 @@ export async function findStaleOpenClawUpdateLaunchdJobs(
|
||||||
}
|
}
|
||||||
// Never report the active gateway label as stale even when a wrapper exposes
|
// Never report the active gateway label as stale even when a wrapper exposes
|
||||||
// update-like launchd metadata through the current environment.
|
// update-like launchd metadata through the current environment.
|
||||||
return parseLaunchctlListOpenClawUpdateJobs(result.stdout).filter(
|
const jobs: StaleOpenClawUpdateLaunchdJob[] = [];
|
||||||
(job) => !isCurrentGatewayLaunchdLabel(job.label, env),
|
for (const job of parseLaunchctlListOpenClawUpdateJobCandidates(result.stdout)) {
|
||||||
);
|
if (isCurrentGatewayLaunchdLabel(job.label, env)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
job.requiresMetadata &&
|
||||||
|
!(await isLaunchdJobConfirmedOpenClawUpdater({ label: job.label, env }))
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
jobs.push({
|
||||||
|
label: job.label,
|
||||||
|
...(job.pid !== undefined ? { pid: job.pid } : {}),
|
||||||
|
...(job.lastExitStatus !== undefined ? { lastExitStatus: job.lastExitStatus } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return jobs;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function disableOpenClawUpdateLaunchdJob(label: string): Promise<boolean> {
|
async function disableOpenClawUpdateLaunchdJobCandidate(params: {
|
||||||
const normalizedLabel = normalizeOpenClawUpdateLaunchdLabel(label);
|
candidate: OpenClawUpdateLaunchdLabelCandidate;
|
||||||
if (process.platform !== "darwin" || !normalizedLabel) {
|
env: NodeJS.ProcessEnv;
|
||||||
|
trustCurrentEnvMarker: boolean;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const serviceTarget = `${resolveGuiDomain()}/${assertValidLaunchAgentLabel(normalizedLabel)}`;
|
if (
|
||||||
|
params.candidate.requiresMetadata &&
|
||||||
|
!(
|
||||||
|
(params.trustCurrentEnvMarker && hasOpenClawUpdateLaunchdMarker(params.env)) ||
|
||||||
|
(await isLaunchdJobConfirmedOpenClawUpdater({
|
||||||
|
label: params.candidate.label,
|
||||||
|
env: params.env,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const serviceTarget = `${resolveGuiDomain()}/${assertValidLaunchAgentLabel(params.candidate.label)}`;
|
||||||
const result = await execLaunchctl(["disable", serviceTarget]);
|
const result = await execLaunchctl(["disable", serviceTarget]);
|
||||||
return result.code === 0;
|
return result.code === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function disableOpenClawUpdateLaunchdJob(
|
||||||
|
label: string,
|
||||||
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const candidate = normalizeOpenClawUpdateLaunchdLabelCandidate(label);
|
||||||
|
if (!candidate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return await disableOpenClawUpdateLaunchdJobCandidate({
|
||||||
|
candidate,
|
||||||
|
env,
|
||||||
|
trustCurrentEnvMarker: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function disableCurrentOpenClawUpdateLaunchdJob(
|
export async function disableCurrentOpenClawUpdateLaunchdJob(
|
||||||
env: NodeJS.ProcessEnv = process.env,
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const label = resolveCurrentOpenClawUpdateLaunchdJobLabel(env);
|
const candidate = resolveCurrentOpenClawUpdateLaunchdJobLabel(env);
|
||||||
if (!label) {
|
if (!candidate) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return await disableOpenClawUpdateLaunchdJob(label);
|
return await disableOpenClawUpdateLaunchdJobCandidate({
|
||||||
|
candidate,
|
||||||
|
env,
|
||||||
|
// Detached handoffs preserve the configured label, so only launchd-backed
|
||||||
|
// current-process identity may turn the ambient marker into proof.
|
||||||
|
trustCurrentEnvMarker: isCurrentProcessLaunchdServiceLabel(candidate.label, env, {
|
||||||
|
allowConfiguredLabelFallback: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseGatewayPortFromProgramArguments(
|
function parseGatewayPortFromProgramArguments(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue