fix: allow gateway service commands for named profiles (#116314)

* fix: gateway service commands refuse a named profile or relocated OPENCLAW_HOME

- Resolve the default install identity against the canonical state directory
  for the active OpenClaw home and profile instead of the unprofiled OS
  account default.
- `--profile <name>` / `--dev` project `.openclaw-<profile>` state and config
  paths, so every named profile was classified as isolated state and refused
  `install`, `start`, `stop`, `restart`, `uninstall`, Doctor service repair,
  and self-update service handling.
- `OPENCLAW_HOME` relocates all OpenClaw path defaults and is documented for
  running as a dedicated service user; a relocated home is now an install
  identity. `HOME` alone still is not.
- An `OPENCLAW_STATE_DIR` or `OPENCLAW_CONFIG_PATH` pointing outside those
  canonical paths is still treated as isolated state.
- Recovery guidance in the refusal message now names the paths that must match.

Verified: focused vitest shards for the changed suites plus the daemon, CLI,
and doctor suites that consume the identity check; tsgo core and core-test
lanes; oxlint; docs format, MDX, link, and map checks.

* fix(gateway): keep relocated homes isolated

* fix(config): validate service profile identity

* fix(daemon): enforce named-profile service ownership

* fix(update): reject drifted service selectors before probes

* test(windows): prove scheduled task lifecycle

* test(windows): harden scheduled task proof cleanup

* test(windows): bind lifecycle proof to checkout

* test(windows): normalize cleanup exit status

* test(windows): verify effective task privilege

* test(windows): protect scheduled task proof roots

* test(windows): prove listener-owned task lifecycle

* test(windows): fix scheduled task proof contracts

* test(windows): remove redundant mock coercions

* test(windows): measure fallback before task probes

* test(windows): prove scheduled task process origin

* fix(gateway): preserve unmanaged restart fallback

* test(gateway): cover denied restart ownership

* test(gateway): keep restart helper types private

* test(gateway): classify lifecycle helpers as test code

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Sasan
2026-07-31 23:28:39 -04:00
committed by GitHub
parent 4376387791
commit 6938f7dddb
31 changed files with 2520 additions and 117 deletions
@@ -38,9 +38,13 @@ import {
} from "./update-command-post-core.js";
import { POST_PLUGIN_DOCTOR_EXECUTION_FAILED_REASON } from "./update-command-post-plugin-validation.js";
import {
assertGatewayServiceManagementAllowedForUpdate,
GatewayServiceUpdateOwnershipError,
gatewayServiceCommandUsesRoot,
isGatewayServiceManagementAllowedForUpdate,
maybeRestartService,
maybeRestartServiceAfterFailedMutableUpdate,
resolveGatewayServiceManagementBlockMessageForUpdate,
resolvePostUpdateServiceStateReadEnv,
resolveUpdatedGatewayRestartPort,
restoreWindowsTaskAutoStartOrExit,
@@ -340,18 +344,30 @@ export async function finishUpdate(params: {
let refreshGatewayServiceEnv = false;
let gatewayServiceEnv: NodeJS.ProcessEnv | undefined;
let skipLegacyServiceRestart = false;
const serviceStateReadEnv = resolvePostUpdateServiceStateReadEnv({
updateMode: resultWithPostUpdate.mode,
processEnv: process.env,
preManagedServiceEnv: params.preManagedServiceStop?.serviceEnv,
});
const serviceMutationAllowed =
params.preManagedServiceStop?.serviceMutationAllowed !== false &&
isGatewayServiceManagementAllowedForUpdate(process.env) &&
isGatewayServiceManagementAllowedForUpdate(serviceStateReadEnv);
const serviceMutationSkipMessage =
params.shouldRestart && !serviceMutationAllowed
? (params.preManagedServiceStop?.serviceMutationSkipMessage ??
resolveGatewayServiceManagementBlockMessageForUpdate(process.env) ??
resolveGatewayServiceManagementBlockMessageForUpdate(serviceStateReadEnv))
: undefined;
let gatewayPort = resolveUpdatedGatewayRestartPort({
config: restartConfigSnapshot.valid ? restartConfigSnapshot.config : undefined,
processEnv: process.env,
});
if (params.shouldRestart) {
if (params.shouldRestart && serviceMutationAllowed) {
try {
const serviceState = await readGatewayServiceState(resolveGatewayService(), {
env: resolvePostUpdateServiceStateReadEnv({
updateMode: resultWithPostUpdate.mode,
processEnv: process.env,
preManagedServiceEnv: params.preManagedServiceStop?.serviceEnv,
}),
env: serviceStateReadEnv,
validateEnvBeforeStatusRead: assertGatewayServiceManagementAllowedForUpdate,
});
const serviceMatchesUpdateRoot =
(await gatewayServiceCommandUsesRoot({
@@ -399,7 +415,12 @@ export async function finishUpdate(params: {
// ownership authorizes rewriting the service definition.
refreshGatewayServiceEnv = serviceOwnershipConfirmed;
}
} catch {
} catch (err) {
if (err instanceof GatewayServiceUpdateOwnershipError) {
defaultRuntime.error(err.message);
defaultRuntime.exit(1);
return;
}
// Ignore errors during pre-check; fallback to standard restart
}
}
@@ -420,7 +441,7 @@ export async function finishUpdate(params: {
return;
}
const restartOk = await maybeRestartService({
shouldRestart: params.shouldRestart,
shouldRestart: params.shouldRestart && serviceMutationAllowed,
result: resultWithPostUpdate,
opts: params.opts,
refreshServiceEnv: refreshGatewayServiceEnv,
@@ -432,6 +453,7 @@ export async function finishUpdate(params: {
skipLegacyServiceRestart,
requireRunningServiceAfterRestart:
resultWithPostUpdate.mode === "git" && params.preManagedServiceStop?.stopped === true,
serviceMutationSkipMessage,
timeoutMs: params.updateStepTimeoutMs,
});
if (!restartOk) {