mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix: doctor skips host services for isolated state (#115922)
* fix(doctor): isolate host service management * fix(doctor): clarify service isolation recovery * test(doctor): isolate service identity fixtures * test(daemon): keep lifecycle fixtures lint-clean * test(daemon): isolate install identity fixtures
This commit is contained in:
committed by
GitHub
parent
6ec3dbd92d
commit
383f8947c1
@@ -87,6 +87,11 @@ const mocks = await vi.hoisted(async () => {
|
||||
|
||||
const { runtimeLogs } = mocks;
|
||||
|
||||
vi.mock("../config/paths.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../config/paths.js")>("../config/paths.js");
|
||||
return { ...actual, isDefaultInstallIdentity: () => true };
|
||||
});
|
||||
|
||||
vi.mock("./daemon-cli/probe.js", () => ({
|
||||
probeGatewayStatus: (opts: unknown) => probeGatewayStatus(opts),
|
||||
}));
|
||||
|
||||
@@ -22,6 +22,12 @@ const serviceMock = vi.hoisted(() => ({
|
||||
readRuntime: vi.fn(async () => ({ status: "stopped" as const })),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/paths.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("../../config/paths.js")>("../../config/paths.js");
|
||||
return { ...actual, isDefaultInstallIdentity: () => true };
|
||||
});
|
||||
|
||||
vi.mock("../../daemon/service.js", () => ({
|
||||
resolveGatewayService: () => serviceMock,
|
||||
}));
|
||||
|
||||
@@ -13,6 +13,7 @@ const resolveNodeStartupTlsEnvironmentMock = vi.hoisted(() => vi.fn());
|
||||
const loadConfigMock = vi.hoisted(() => vi.fn());
|
||||
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
|
||||
const resolveGatewayPortMock = vi.hoisted(() => vi.fn(() => 18789));
|
||||
const isDefaultInstallIdentityMock = vi.hoisted(() => vi.fn(() => true));
|
||||
const replaceConfigFileMock = vi.hoisted(() => vi.fn());
|
||||
const resolveIsNixModeMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const resolveSecretInputRefMock = vi.hoisted(() =>
|
||||
@@ -102,6 +103,7 @@ vi.mock("../../config/mutate.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../config/paths.js", () => ({
|
||||
isDefaultInstallIdentity: isDefaultInstallIdentityMock,
|
||||
resolveGatewayPort: resolveGatewayPortMock,
|
||||
resolveIsNixMode: resolveIsNixModeMock,
|
||||
}));
|
||||
@@ -276,6 +278,7 @@ describe("runDaemonInstall", () => {
|
||||
resolveNodeStartupTlsEnvironmentMock.mockReset();
|
||||
readConfigFileSnapshotMock.mockReset();
|
||||
resolveGatewayPortMock.mockClear();
|
||||
isDefaultInstallIdentityMock.mockReturnValue(true);
|
||||
replaceConfigFileMock.mockReset();
|
||||
resolveIsNixModeMock.mockReset();
|
||||
resolveSecretInputRefMock.mockReset();
|
||||
@@ -362,6 +365,20 @@ describe("runDaemonInstall", () => {
|
||||
expect(installDaemonServiceAndEmitMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("blocks non-default install identities before inspecting host services", async () => {
|
||||
isDefaultInstallIdentityMock.mockReturnValue(false);
|
||||
|
||||
await runDaemonInstall({ json: true });
|
||||
|
||||
expect(actionState.failed[0]?.message).toContain(
|
||||
"service management skipped: non-default state dir or config path",
|
||||
);
|
||||
expect(readConfigFileSnapshotMock).not.toHaveBeenCalled();
|
||||
expect(service.isLoaded).not.toHaveBeenCalled();
|
||||
expect(service.readCommand).not.toHaveBeenCalled();
|
||||
expect(installDaemonServiceAndEmitMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("validates token SecretRef but does not serialize resolved token into service env", async () => {
|
||||
mockResolvedGatewayTokenSecretRef();
|
||||
|
||||
|
||||
@@ -25,10 +25,7 @@ import {
|
||||
isLoopbackHost,
|
||||
resolveGatewayBindHost,
|
||||
} from "../../gateway/net.js";
|
||||
import {
|
||||
formatExternalSupervisorActionRequired,
|
||||
isGatewayExternallySupervised,
|
||||
} from "../../infra/gateway-supervision.js";
|
||||
import { assertGatewayServiceMutationAllowed } from "../../infra/gateway-supervision.js";
|
||||
import {
|
||||
isDangerousHostEnvOverrideVarName,
|
||||
isDangerousHostEnvVarName,
|
||||
@@ -149,10 +146,10 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
if (failIfNixDaemonInstallMode(fail)) {
|
||||
return;
|
||||
}
|
||||
if (isGatewayExternallySupervised()) {
|
||||
fail(
|
||||
`Gateway install blocked: ${formatExternalSupervisorActionRequired("install or rewrite the gateway service")}`,
|
||||
);
|
||||
try {
|
||||
assertGatewayServiceMutationAllowed("install or rewrite the gateway service");
|
||||
} catch (error) {
|
||||
fail(`Gateway install blocked: ${String(error)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -129,9 +129,10 @@ vi.mock("../../config/config.js", () => ({
|
||||
resolveGatewayPort: (cfg?: unknown, env?: unknown) => resolveGatewayPort(cfg, env),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/paths.js", () => ({ isDefaultInstallIdentity: () => true }));
|
||||
|
||||
vi.mock("../../infra/gateway-processes.js", () => ({
|
||||
findVerifiedGatewayListenerPidsOnPortSync: (port: number) =>
|
||||
findVerifiedGatewayListenerPidsOnPortSync(port),
|
||||
findVerifiedGatewayListenerPidsOnPortSync,
|
||||
signalVerifiedGatewayPidSync: (pid: number, signal: "SIGTERM" | "SIGUSR1") =>
|
||||
signalVerifiedGatewayPidSync(pid, signal),
|
||||
formatGatewayPidList: (pids: number[]) => formatGatewayPidList(pids),
|
||||
|
||||
@@ -34,6 +34,7 @@ const resolveGatewayPortMock = vi.hoisted(() => vi.fn(() => 18789));
|
||||
const resolveOpenClawWrapperPathMock = vi.hoisted(() => vi.fn());
|
||||
const formatGatewayServiceStartRepairIssuesMock = vi.hoisted(() => vi.fn());
|
||||
const defaultRuntimeLogMock = vi.hoisted(() => vi.fn());
|
||||
const assertGatewayServiceMutationAllowedMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../commands/daemon-install-helpers.js", () => ({
|
||||
buildGatewayInstallPlan: buildGatewayInstallPlanMock,
|
||||
@@ -64,6 +65,10 @@ vi.mock("../../daemon/service.js", () => ({
|
||||
formatGatewayServiceStartRepairIssues: formatGatewayServiceStartRepairIssuesMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/gateway-supervision.js", () => ({
|
||||
assertGatewayServiceMutationAllowed: assertGatewayServiceMutationAllowedMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../runtime.js", () => ({
|
||||
defaultRuntime: { log: defaultRuntimeLogMock },
|
||||
}));
|
||||
@@ -87,6 +92,7 @@ describe("repairLoadedGatewayServiceForStart", () => {
|
||||
resolveOpenClawWrapperPathMock.mockReset();
|
||||
formatGatewayServiceStartRepairIssuesMock.mockReset();
|
||||
defaultRuntimeLogMock.mockClear();
|
||||
assertGatewayServiceMutationAllowedMock.mockReset();
|
||||
|
||||
resolveGatewayInstallTokenMock.mockResolvedValue({
|
||||
tokenRefConfigured: false,
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
GatewayServiceState,
|
||||
} from "../../daemon/service.js";
|
||||
import { formatGatewayServiceStartRepairIssues } from "../../daemon/service.js";
|
||||
import { assertGatewayServiceMutationAllowed } from "../../infra/gateway-supervision.js";
|
||||
import { parseTcpPort, parseTcpPortFromArgs } from "../../infra/tcp-port.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { mergeInstallInvocationEnv } from "./install.js";
|
||||
@@ -48,6 +49,7 @@ export async function repairLoadedGatewayServiceForStart(
|
||||
warnings?: string[];
|
||||
loaded: boolean;
|
||||
}> {
|
||||
assertGatewayServiceMutationAllowed("repair the gateway service");
|
||||
const { snapshot: configSnapshot, writeOptions: configWriteOptions } =
|
||||
await readConfigFileSnapshotForWrite();
|
||||
const cfg = configSnapshot.valid ? configSnapshot.sourceConfig : configSnapshot.config;
|
||||
|
||||
@@ -35,6 +35,7 @@ const findSystemGatewayServices = vi.hoisted(() =>
|
||||
);
|
||||
const buildGatewayRuntimeHints = vi.hoisted(() => vi.fn((): string[] => []));
|
||||
const formatGatewayRuntimeSummary = vi.hoisted(() => vi.fn((): string | null => null));
|
||||
const isDefaultInstallIdentity = vi.hoisted(() => vi.fn(() => true));
|
||||
|
||||
vi.mock("../config/config.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
|
||||
@@ -44,6 +45,11 @@ vi.mock("../config/config.js", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../config/paths.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../config/paths.js")>("../config/paths.js");
|
||||
return { ...actual, isDefaultInstallIdentity };
|
||||
});
|
||||
|
||||
vi.mock("../daemon/constants.js", () => ({
|
||||
resolveGatewayLaunchAgentLabel: vi.fn(() => "ai.openclaw.gateway"),
|
||||
resolveNodeLaunchAgentLabel: vi.fn(() => "ai.openclaw.node"),
|
||||
@@ -161,6 +167,7 @@ describe("maybeRepairGatewayDaemon", () => {
|
||||
service.readRuntime.mockResolvedValue({ status: "running" });
|
||||
service.readCommand.mockResolvedValue(null);
|
||||
service.restart.mockResolvedValue({ outcome: "completed" });
|
||||
isDefaultInstallIdentity.mockReturnValue(true);
|
||||
readGatewayRestartHandoffSync.mockReturnValue(null);
|
||||
findSystemGatewayServices.mockResolvedValue([]);
|
||||
inspectPortUsage.mockResolvedValue({
|
||||
@@ -287,6 +294,41 @@ describe("maybeRepairGatewayDaemon", () => {
|
||||
await runScheduledGatewayRepairAndExpectVerificationSkipped("Restart gateway service now?");
|
||||
});
|
||||
|
||||
it("skips every service-manager seam for a non-default install identity", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_STATE_DIR: "/tmp/openclaw-copied-state",
|
||||
OPENCLAW_CONFIG_PATH: "/tmp/openclaw-copied-state/openclaw.json",
|
||||
},
|
||||
async () => {
|
||||
isDefaultInstallIdentity.mockReturnValue(false);
|
||||
await runNonInteractiveRepair();
|
||||
},
|
||||
);
|
||||
|
||||
expect(service.isLoaded).not.toHaveBeenCalled();
|
||||
expect(service.readRuntime).not.toHaveBeenCalled();
|
||||
expect(service.readCommand).not.toHaveBeenCalled();
|
||||
expect(service.install).not.toHaveBeenCalled();
|
||||
expect(service.restart).not.toHaveBeenCalled();
|
||||
expect(launchd.repairLaunchAgentBootstrap).not.toHaveBeenCalled();
|
||||
expect(findSystemGatewayServices).not.toHaveBeenCalled();
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
"service management skipped: non-default state dir or config path",
|
||||
"Gateway",
|
||||
);
|
||||
});
|
||||
|
||||
it("still inspects the managed service for the default install identity", async () => {
|
||||
await withEnvAsync(
|
||||
{ OPENCLAW_STATE_DIR: undefined, OPENCLAW_CONFIG_PATH: undefined },
|
||||
runNonInteractiveRepair,
|
||||
);
|
||||
|
||||
expect(service.isLoaded).toHaveBeenCalledTimes(1);
|
||||
expect(service.readRuntime).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reports recent restart handoffs during deep doctor", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(40_000);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { note } from "../../packages/terminal-core/src/note.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { resolveGatewayPort } from "../config/config.js";
|
||||
import { isDefaultInstallIdentity } from "../config/paths.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
@@ -18,6 +19,7 @@ import type { GatewayServiceRuntime } from "../daemon/service-runtime.js";
|
||||
import { describeGatewayServiceRestart, resolveGatewayService } from "../daemon/service.js";
|
||||
import { renderSystemdUnavailableHints } from "../daemon/systemd-hints.js";
|
||||
import { isSystemdUserServiceAvailable } from "../daemon/systemd.js";
|
||||
import { NON_DEFAULT_INSTALL_SERVICE_SKIP_REASON } from "../infra/gateway-supervision.js";
|
||||
import {
|
||||
formatPortDiagnostics,
|
||||
inspectPortConnections,
|
||||
@@ -193,6 +195,10 @@ export async function maybeRepairGatewayDaemon(params: {
|
||||
healthOk: boolean;
|
||||
healthSkipped?: boolean;
|
||||
}) {
|
||||
if (!isDefaultInstallIdentity(process.env)) {
|
||||
note(NON_DEFAULT_INSTALL_SERVICE_SKIP_REASON, "Gateway");
|
||||
return;
|
||||
}
|
||||
if (params.healthOk) {
|
||||
await maybeReportEstablishedGatewayClients({
|
||||
cfg: params.cfg,
|
||||
|
||||
@@ -39,6 +39,7 @@ const mocks = vi.hoisted(() => ({
|
||||
resolveGatewayAuthTokenForService: vi.fn(),
|
||||
resolveGatewayPort: vi.fn(() => 18789),
|
||||
resolveIsNixMode: vi.fn(() => false),
|
||||
isDefaultInstallIdentity: vi.fn(() => true),
|
||||
findExtraGatewayServices: vi.fn().mockResolvedValue([]),
|
||||
renderGatewayServiceCleanupHints: vi.fn().mockReturnValue([]),
|
||||
needsNodeRuntimeMigration: vi.fn(() => false),
|
||||
@@ -53,6 +54,7 @@ const mocks = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("../config/paths.js", () => ({
|
||||
isDefaultInstallIdentity: mocks.isDefaultInstallIdentity,
|
||||
resolveGatewayPort: mocks.resolveGatewayPort,
|
||||
resolveIsNixMode: mocks.resolveIsNixMode,
|
||||
}));
|
||||
@@ -421,6 +423,7 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
||||
vi.clearAllMocks();
|
||||
fsMocks.realpath.mockImplementation(async (value: string) => value);
|
||||
mocks.resolveGatewayPort.mockReturnValue(18789);
|
||||
mocks.isDefaultInstallIdentity.mockReturnValue(true);
|
||||
mocks.readRuntime.mockResolvedValue({ status: "unknown" });
|
||||
mocks.readWindowsStartupFallbackRuntimeForUpdate.mockResolvedValue(null);
|
||||
mocks.needsNodeRuntimeMigration.mockReturnValue(false);
|
||||
@@ -486,6 +489,21 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
||||
expectNoteContaining("adaptive default", "Gateway heap");
|
||||
});
|
||||
|
||||
it("skips service audit and rewrite for a non-default install identity", async () => {
|
||||
mocks.isDefaultInstallIdentity.mockReturnValue(false);
|
||||
|
||||
await runRepair({ gateway: {} });
|
||||
|
||||
expect(mocks.readCommand).not.toHaveBeenCalled();
|
||||
expect(mocks.auditGatewayServiceConfig).not.toHaveBeenCalled();
|
||||
expect(mocks.stage).not.toHaveBeenCalled();
|
||||
expect(mocks.install).not.toHaveBeenCalled();
|
||||
expectNoteContaining(
|
||||
"service management skipped: non-default state dir or config path",
|
||||
"Gateway",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats gateway.auth.token as source of truth for service token repairs", async () => {
|
||||
setupGatewayTokenRepairScenario();
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import { note } from "../../packages/terminal-core/src/note.js";
|
||||
import { replaceConfigFile, type OpenClawConfig } from "../config/config.js";
|
||||
import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
|
||||
import { isDefaultInstallIdentity, resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { formatGatewayHeapLimitReport, inspectGatewayHeapLimit } from "../daemon/gateway-heap.js";
|
||||
import {
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
} from "../daemon/systemd.js";
|
||||
import type { HealthFinding, HealthRepairEffect } from "../flows/health-checks.js";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import { NON_DEFAULT_INSTALL_SERVICE_SKIP_REASON } from "../infra/gateway-supervision.js";
|
||||
import { readWindowsProcessArgsSync } from "../infra/windows-port-pids.js";
|
||||
import { runExec } from "../process/exec.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
@@ -357,6 +358,9 @@ async function filterInactiveExtraGatewayServices(
|
||||
export async function detectExtraGatewayServiceIssues(
|
||||
options: Pick<DoctorOptions, "deep"> = {},
|
||||
): Promise<readonly ExtraGatewayService[]> {
|
||||
if (!isDefaultInstallIdentity(process.env)) {
|
||||
return [];
|
||||
}
|
||||
const detectedExtraServices = await findExtraGatewayServices(process.env, {
|
||||
deep: options.deep,
|
||||
});
|
||||
@@ -538,6 +542,10 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
prompter: DoctorPrompter,
|
||||
options: GatewayServiceConfigRepairOptions = {},
|
||||
): Promise<OpenClawConfig> {
|
||||
if (!isDefaultInstallIdentity(process.env)) {
|
||||
note(NON_DEFAULT_INSTALL_SERVICE_SKIP_REASON, "Gateway");
|
||||
return cfg;
|
||||
}
|
||||
if (resolveIsNixMode(process.env)) {
|
||||
note("Nix mode detected; skip service updates.", "Gateway");
|
||||
return cfg;
|
||||
@@ -934,6 +942,10 @@ export async function maybeScanExtraGatewayServices(
|
||||
runtime: RuntimeEnv,
|
||||
prompter: DoctorPrompter,
|
||||
) {
|
||||
if (!isDefaultInstallIdentity(process.env)) {
|
||||
note(NON_DEFAULT_INSTALL_SERVICE_SKIP_REASON, "Gateway");
|
||||
return;
|
||||
}
|
||||
const extraServices = await detectExtraGatewayServiceIssues(options);
|
||||
if (extraServices.length === 0) {
|
||||
return;
|
||||
|
||||
@@ -11,6 +11,7 @@ const originalServiceRepairPolicy = process.env.OPENCLAW_SERVICE_REPAIR_POLICY;
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
createUpdateProgress: vi.fn(),
|
||||
isDefaultInstallIdentity: vi.fn(() => true),
|
||||
note: vi.fn(),
|
||||
readGatewayServiceState: vi.fn(),
|
||||
restartGatewayService: vi.fn(),
|
||||
@@ -24,6 +25,11 @@ vi.mock("../cli/update-cli/progress.js", () => ({
|
||||
createUpdateProgress: mocks.createUpdateProgress,
|
||||
}));
|
||||
|
||||
vi.mock("../config/paths.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../config/paths.js")>("../config/paths.js");
|
||||
return { ...actual, isDefaultInstallIdentity: mocks.isDefaultInstallIdentity };
|
||||
});
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout: mocks.runCommandWithTimeout,
|
||||
}));
|
||||
@@ -67,6 +73,7 @@ async function runOffer(params?: {
|
||||
beforeEach(async () => {
|
||||
mocks.createUpdateProgress.mockReset();
|
||||
mocks.createUpdateProgress.mockReturnValue({ progress: {}, stop: vi.fn() });
|
||||
mocks.isDefaultInstallIdentity.mockReturnValue(true);
|
||||
mocks.note.mockReset();
|
||||
mocks.readGatewayServiceState.mockReset();
|
||||
mocks.restartGatewayService.mockReset();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/st
|
||||
import { note } from "../../packages/terminal-core/src/note.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { createUpdateProgress } from "../cli/update-cli/progress.js";
|
||||
import { isDefaultInstallIdentity } from "../config/paths.js";
|
||||
import { summarizeGatewayServiceLayout } from "../daemon/service-layout.js";
|
||||
import { readGatewayServiceState, resolveGatewayService } from "../daemon/service.js";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
@@ -61,7 +62,7 @@ const NO_GATEWAY_SERVICE_UPDATE: GatewayServiceUpdatePolicy = {
|
||||
async function inspectGatewayServiceForUpdate(
|
||||
root: string,
|
||||
): Promise<GatewayServiceUpdateInspection> {
|
||||
if (isServiceRepairExternallyManaged()) {
|
||||
if (!isDefaultInstallIdentity(process.env) || isServiceRepairExternallyManaged()) {
|
||||
return NO_GATEWAY_SERVICE_UPDATE;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import {
|
||||
CONFIG_PATH,
|
||||
DEFAULT_GATEWAY_PORT,
|
||||
isDefaultInstallIdentity,
|
||||
isDefaultStateDir,
|
||||
isNixMode,
|
||||
normalizeStateDirEnv,
|
||||
@@ -41,6 +42,45 @@ describe("default state directory", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("default install identity", () => {
|
||||
it("accepts default paths and equivalent explicit overrides", () => {
|
||||
const home = "/home/test";
|
||||
const stateDir = path.join(home, ".openclaw");
|
||||
const configPath = path.join(stateDir, "openclaw.json");
|
||||
|
||||
expect(isDefaultInstallIdentity({ HOME: home }, () => home)).toBe(true);
|
||||
expect(
|
||||
isDefaultInstallIdentity(
|
||||
{ HOME: home, OPENCLAW_STATE_DIR: stateDir, OPENCLAW_CONFIG_PATH: configPath },
|
||||
() => home,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects non-default state or config paths", () => {
|
||||
const home = "/home/test";
|
||||
|
||||
expect(
|
||||
isDefaultInstallIdentity({ HOME: home, OPENCLAW_STATE_DIR: "/tmp/copied-state" }, () => home),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isDefaultInstallIdentity(
|
||||
{ HOME: home, OPENCLAW_CONFIG_PATH: "/tmp/copied-openclaw.json" },
|
||||
() => home,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects process home overrides that relocate the implicit install", () => {
|
||||
const accountHome = "/home/test";
|
||||
|
||||
expect(isDefaultInstallIdentity({ HOME: "/tmp/copied-home" }, () => accountHome)).toBe(false);
|
||||
expect(isDefaultInstallIdentity({ OPENCLAW_HOME: "/tmp/copied-home" }, () => accountHome)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("oauth paths", () => {
|
||||
it("prefers OPENCLAW_OAUTH_DIR over OPENCLAW_STATE_DIR", () => {
|
||||
const env = {
|
||||
|
||||
+37
-4
@@ -36,6 +36,10 @@ function resolveDefaultHomeDir(): string {
|
||||
return resolveRequiredHomeDir(process.env, os.homedir);
|
||||
}
|
||||
|
||||
function resolveSystemAccountHomeDir(): string {
|
||||
return os.userInfo().homedir;
|
||||
}
|
||||
|
||||
/** Build a homedir thunk that respects OPENCLAW_HOME for the given env. */
|
||||
function envHomedir(env: NodeJS.ProcessEnv): () => string {
|
||||
return () => resolveRequiredHomeDir(env, os.homedir);
|
||||
@@ -93,8 +97,8 @@ export function resolveStateDir(
|
||||
return newDir;
|
||||
}
|
||||
|
||||
function normalizeStateDirForComparison(stateDir: string): string {
|
||||
const resolved = path.resolve(stateDir);
|
||||
function normalizePathForComparison(candidate: string): string {
|
||||
const resolved = path.resolve(candidate);
|
||||
try {
|
||||
return fs.realpathSync.native(resolved);
|
||||
} catch {
|
||||
@@ -115,8 +119,37 @@ export function isDefaultStateDir(
|
||||
}
|
||||
const effectiveHomedir = () => resolveRequiredHomeDir(env, homedir);
|
||||
return (
|
||||
normalizeStateDirForComparison(resolveStateDir(env, effectiveHomedir)) ===
|
||||
normalizeStateDirForComparison(newStateDir(effectiveHomedir))
|
||||
normalizePathForComparison(resolveStateDir(env, effectiveHomedir)) ===
|
||||
normalizePathForComparison(newStateDir(effectiveHomedir))
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether host service management belongs to the active default install identity. */
|
||||
export function isDefaultInstallIdentity(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
homedir: () => string = resolveSystemAccountHomeDir,
|
||||
): boolean {
|
||||
const accountHome = resolveRequiredHomeDir({}, homedir);
|
||||
const accountHomedir = () => accountHome;
|
||||
if (
|
||||
normalizePathForComparison(resolveStateDir(env, envHomedir(env))) !==
|
||||
normalizePathForComparison(newStateDir(accountHomedir))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!env.OPENCLAW_CONFIG_PATH?.trim()) {
|
||||
return true;
|
||||
}
|
||||
const defaultConfigEnv = {
|
||||
...env,
|
||||
HOME: accountHome,
|
||||
OPENCLAW_HOME: undefined,
|
||||
OPENCLAW_STATE_DIR: undefined,
|
||||
OPENCLAW_CONFIG_PATH: undefined,
|
||||
};
|
||||
return (
|
||||
normalizePathForComparison(resolveConfigPathCandidate(env, envHomedir(env))) ===
|
||||
normalizePathForComparison(resolveConfigPathCandidate(defaultConfigEnv, accountHomedir))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ import {
|
||||
} from "./service.js";
|
||||
import { createMockGatewayService } from "./service.test-helpers.js";
|
||||
|
||||
vi.mock("../config/paths.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../config/paths.js")>("../config/paths.js");
|
||||
return { ...actual, isDefaultInstallIdentity: () => true };
|
||||
});
|
||||
|
||||
function setPlatform(value: NodeJS.Platform) {
|
||||
mockProcessPlatform(value);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
uiProtocolFreshnessIssueToRepairEffects,
|
||||
} from "../commands/doctor-ui.js";
|
||||
import { collectDisabledCodexPluginRouteIssues } from "../commands/doctor/shared/codex-route-warnings.js";
|
||||
import { isDefaultInstallIdentity } from "../config/paths.js";
|
||||
import type { ConfigValidationIssue, OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveSecretInputRef, type SecretRef } from "../config/types.secrets.js";
|
||||
import type { CronListPageResult } from "../cron/service/list-page-types.js";
|
||||
@@ -932,6 +933,9 @@ const gatewayPlatformNotesCheck: HealthCheck = {
|
||||
description: "Gateway platform notes are captured as structured findings.",
|
||||
source: "doctor",
|
||||
async detect(ctx) {
|
||||
if (!isDefaultInstallIdentity(process.env)) {
|
||||
return [];
|
||||
}
|
||||
const { collectMacGatewayPlatformWarnings } =
|
||||
await import("../commands/doctor-platform-notes.js");
|
||||
const warnings = await collectMacGatewayPlatformWarnings(ctx.cfg);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { note } from "../../packages/terminal-core/src/note.js";
|
||||
import { isDefaultInstallIdentity } from "../config/paths.js";
|
||||
import { NON_DEFAULT_INSTALL_SERVICE_SKIP_REASON } from "../infra/gateway-supervision.js";
|
||||
import { runCoreContributionHealth } from "./doctor-health-contribution-core.js";
|
||||
import type { DoctorHealthFlowContext } from "./doctor-health-contribution-types.js";
|
||||
import {
|
||||
@@ -17,6 +20,10 @@ export async function runClaudeCliHealth(ctx: DoctorHealthFlowContext): Promise<
|
||||
}
|
||||
|
||||
export async function runGatewayServicesHealth(ctx: DoctorHealthFlowContext): Promise<void> {
|
||||
if (!isDefaultInstallIdentity(ctx.env ?? process.env)) {
|
||||
note(NON_DEFAULT_INSTALL_SERVICE_SKIP_REASON, "Gateway");
|
||||
return;
|
||||
}
|
||||
const { maybeRepairGatewayServiceConfig, maybeScanExtraGatewayServices } =
|
||||
await import("../commands/doctor-gateway-services.js");
|
||||
const {
|
||||
@@ -66,6 +73,9 @@ export async function runSecurityHealth(ctx: DoctorHealthFlowContext): Promise<v
|
||||
}
|
||||
|
||||
export async function runWebFetchProxyHealth(ctx: DoctorHealthFlowContext): Promise<void> {
|
||||
if (!isDefaultInstallIdentity(ctx.env ?? process.env)) {
|
||||
return;
|
||||
}
|
||||
const { noteWebFetchProxyDiagnostic } = await import("../commands/doctor-web-fetch-proxy.js");
|
||||
await noteWebFetchProxyDiagnostic({ cfg: ctx.cfg, env: ctx.env ?? process.env });
|
||||
}
|
||||
@@ -98,6 +108,9 @@ export async function runDevicePairingHealth(ctx: DoctorHealthFlowContext): Prom
|
||||
}
|
||||
|
||||
export async function runGatewayDaemonHealth(ctx: DoctorHealthFlowContext): Promise<void> {
|
||||
if (!isDefaultInstallIdentity(ctx.env ?? process.env)) {
|
||||
return;
|
||||
}
|
||||
const { maybeRepairGatewayDaemon } = await import("../commands/doctor-gateway-daemon-flow.js");
|
||||
await maybeRepairGatewayDaemon({
|
||||
cfg: ctx.cfg,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { runDoctorLintChecks } from "./doctor-lint-flow.js";
|
||||
import type { HealthCheck, HealthFinding } from "./health-checks.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
isDefaultInstallIdentity: vi.fn(() => true),
|
||||
maybeRunConfiguredPluginInstallReleaseStep: vi.fn(),
|
||||
registerBundledHealthChecks: vi.fn(),
|
||||
runDoctorHealthRepairs: vi.fn(),
|
||||
@@ -162,6 +163,11 @@ const mocks = vi.hoisted(() => ({
|
||||
getSkillCuratorDoctorWarning: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../config/paths.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../config/paths.js")>("../config/paths.js");
|
||||
return { ...actual, isDefaultInstallIdentity: mocks.isDefaultInstallIdentity };
|
||||
});
|
||||
|
||||
const DOCTOR_GATEWAY_HEALTH_ID = "doctor:gateway-health";
|
||||
|
||||
vi.mock("../commands/doctor/shared/release-configured-plugin-installs.js", () => ({
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/** Gateway startup regression coverage for copied auth stores with omitted SecretRef providers. */
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { resolveDefaultAgentDir } from "../agents/agent-scope-config.js";
|
||||
import {
|
||||
deletePersistedAuthProfileStoreRaw,
|
||||
readPersistedAuthProfileStoreRaw,
|
||||
writePersistedAuthProfileStoreRaw,
|
||||
} from "../agents/auth-profiles/sqlite.js";
|
||||
import { writeConfigFile, type OpenClawConfig } from "../config/config.js";
|
||||
import { resolveAuthProfileSecretOwnerId } from "../secrets/runtime-auth-profile-owner.js";
|
||||
import {
|
||||
clearSecretsRuntimeSnapshot,
|
||||
getActiveSecretsRuntimeSnapshot,
|
||||
} from "../secrets/runtime.js";
|
||||
import { getFreePort, installGatewayTestHooks, startGatewayServer } from "./test-helpers.js";
|
||||
|
||||
installGatewayTestHooks({ scope: "suite" });
|
||||
|
||||
describe("Gateway startup copied auth SecretRef isolation", () => {
|
||||
let server: Awaited<ReturnType<typeof startGatewayServer>> | undefined;
|
||||
let agentDir: string | undefined;
|
||||
|
||||
afterEach(async () => {
|
||||
await server?.close();
|
||||
server = undefined;
|
||||
if (agentDir) {
|
||||
deletePersistedAuthProfileStoreRaw(agentDir);
|
||||
agentDir = undefined;
|
||||
}
|
||||
clearSecretsRuntimeSnapshot();
|
||||
});
|
||||
|
||||
it("starts degraded when copied auth state references an omitted SecretRef provider", async () => {
|
||||
const profileId = "openai:copied";
|
||||
const config: OpenClawConfig = {
|
||||
gateway: { mode: "local", bind: "loopback", auth: { mode: "none" } },
|
||||
agents: { defaults: { model: { primary: "openai/gpt-5.4" } } },
|
||||
auth: { order: { openai: [profileId] } },
|
||||
};
|
||||
agentDir = resolveDefaultAgentDir(config);
|
||||
writePersistedAuthProfileStoreRaw(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: { source: "file", provider: "clawrouter_key", id: "value" },
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
expect(readPersistedAuthProfileStoreRaw(agentDir)).toMatchObject({
|
||||
profiles: { [profileId]: { provider: "openai" } },
|
||||
});
|
||||
await writeConfigFile(config);
|
||||
|
||||
const port = await getFreePort();
|
||||
server = await startGatewayServer(port, { auth: { mode: "none" } });
|
||||
const ready = await fetch(`http://127.0.0.1:${port}/readyz`);
|
||||
|
||||
expect(ready.status).toBe(200);
|
||||
const ownerId = resolveAuthProfileSecretOwnerId({ agentDir, profileId });
|
||||
expect(getActiveSecretsRuntimeSnapshot()?.degradedOwners).toMatchObject([
|
||||
{
|
||||
ownerKind: "account",
|
||||
ownerId,
|
||||
state: "unavailable",
|
||||
reason: "secret provider is not configured",
|
||||
},
|
||||
]);
|
||||
expect(getActiveSecretsRuntimeSnapshot()?.warnings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_OWNER_UNAVAILABLE",
|
||||
path: `${agentDir}.auth-profiles.${profileId}.key`,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
assertGatewayServiceMutationAllowed,
|
||||
formatExternalSupervisorUpdateRequired,
|
||||
isGatewayExternallySupervised,
|
||||
NON_DEFAULT_INSTALL_SERVICE_SKIP_REASON,
|
||||
} from "./gateway-supervision.js";
|
||||
|
||||
// The env variable name is part of the observable contract the messages
|
||||
@@ -34,6 +35,20 @@ describe("gateway supervision", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ OPENCLAW_STATE_DIR: "/tmp/copied-state" },
|
||||
{ OPENCLAW_CONFIG_PATH: "/tmp/copied-openclaw.json" },
|
||||
])("blocks native service mutation for non-default install identity %#", (override) => {
|
||||
expect(() =>
|
||||
assertGatewayServiceMutationAllowed("restart the gateway", {
|
||||
HOME: "/home/operator",
|
||||
...override,
|
||||
}),
|
||||
).toThrow(
|
||||
`${NON_DEFAULT_INSTALL_SERVICE_SKIP_REASON}. Rerun with HOME set to the OS account home and without OPENCLAW_HOME, OPENCLAW_STATE_DIR, or OPENCLAW_CONFIG_PATH overrides to restart the gateway.`,
|
||||
);
|
||||
});
|
||||
|
||||
it("explains why self-update must be delegated", () => {
|
||||
expect(formatExternalSupervisorUpdateRequired()).toContain(
|
||||
"stop the gateway, update and finalize the runtime, then restart it safely",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// Defines gateway lifecycle ownership shared by service, restart, and update paths.
|
||||
import { isDefaultInstallIdentity } from "../config/paths.js";
|
||||
|
||||
const GATEWAY_SUPERVISOR_MODE_ENV = "OPENCLAW_SUPERVISOR_MODE";
|
||||
export const EXTERNAL_SUPERVISOR_UPDATE_REQUIRED_REASON = "external-supervisor-update-required";
|
||||
export const NON_DEFAULT_INSTALL_SERVICE_SKIP_REASON =
|
||||
"service management skipped: non-default state dir or config path";
|
||||
|
||||
type GatewaySupervisorMode = "auto" | "external";
|
||||
|
||||
@@ -35,4 +39,9 @@ export function assertGatewayServiceMutationAllowed(
|
||||
if (isGatewayExternallySupervised(env)) {
|
||||
throw new Error(formatExternalSupervisorActionRequired(action));
|
||||
}
|
||||
if (!isDefaultInstallIdentity(env)) {
|
||||
throw new Error(
|
||||
`${NON_DEFAULT_INSTALL_SERVICE_SKIP_REASON}. Rerun with HOME set to the OS account home and without OPENCLAW_HOME, OPENCLAW_STATE_DIR, or OPENCLAW_CONFIG_PATH overrides to ${action}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@ type SecretRefResolutionCode =
|
||||
| "SECRET_REF_PROVIDER_ERROR"
|
||||
| "SECRET_REF_PROVIDER_CONTRACT";
|
||||
|
||||
type SecretProviderResolutionCode = "SECRET_PROVIDER_INVALID" | "SECRET_PROVIDER_UNAVAILABLE";
|
||||
type SecretProviderResolutionCode =
|
||||
| "SECRET_PROVIDER_INVALID"
|
||||
| "SECRET_PROVIDER_NOT_CONFIGURED"
|
||||
| "SECRET_PROVIDER_UNAVAILABLE";
|
||||
|
||||
export type SecretResolutionFailureReason =
|
||||
| "secret provider failed"
|
||||
|
||||
@@ -151,7 +151,7 @@ function resolveConfiguredProvider(params: {
|
||||
return { source: "env" };
|
||||
}
|
||||
throw providerResolutionError({
|
||||
code: "SECRET_PROVIDER_INVALID",
|
||||
code: "SECRET_PROVIDER_NOT_CONFIGURED",
|
||||
source: ref.source,
|
||||
provider: ref.provider,
|
||||
message: `Secret provider "${ref.provider}" is not configured (ref: ${ref.source}:${ref.provider}:${ref.id}).`,
|
||||
|
||||
@@ -182,4 +182,47 @@ describe("secrets runtime snapshot inline auth-store refs", () => {
|
||||
});
|
||||
expect(profiles?.[healthyProfileId]).toMatchObject({ key: "anthropic-runtime-key" });
|
||||
});
|
||||
|
||||
it("isolates an auth profile whose SecretRef provider is absent from copied config", async () => {
|
||||
const agentDir = "/tmp/openclaw-agent-copied-auth-store";
|
||||
const profileId = "openai:copied";
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
auth: { order: { openai: [profileId] } },
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: [agentDir],
|
||||
allowUnavailableSecretOwners: true,
|
||||
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
loadAuthStore: () =>
|
||||
loadAuthStoreWithProfiles({
|
||||
[profileId]: {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: { source: "file", provider: "clawrouter_key", id: "value" },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(snapshot.degradedOwners).toMatchObject([
|
||||
{
|
||||
ownerKind: "account",
|
||||
ownerId: resolveAuthProfileSecretOwnerId({ agentDir, profileId }),
|
||||
state: "unavailable",
|
||||
reason: "secret provider is not configured",
|
||||
},
|
||||
]);
|
||||
expect(snapshot.warnings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_OWNER_UNAVAILABLE",
|
||||
path: `${agentDir}.auth-profiles.${profileId}.key`,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(snapshot.authStores[0]?.store.profiles[profileId]).toMatchObject({
|
||||
key: undefined,
|
||||
keyRef: { source: "file", provider: "clawrouter_key", id: "value" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
|
||||
export type SecretDegradationReason =
|
||||
| SecretResolutionFailureReason
|
||||
| "secret provider is not configured"
|
||||
| "resolved secret value was invalid"
|
||||
| "secret reference is not allowed for this provider"
|
||||
| "secret reference was not materialized by the active runtime"
|
||||
@@ -108,6 +109,7 @@ export function classifySecretResolutionErrorDegradations(error: unknown): Secre
|
||||
export function redactSecretDegradationReason(reason: string): SecretDegradationReason {
|
||||
switch (reason) {
|
||||
case "secret provider failed":
|
||||
case "secret provider is not configured":
|
||||
case "secret provider policy denied resolution":
|
||||
case "secret provider response violated its contract":
|
||||
case "secret reference is not allowed for this provider":
|
||||
|
||||
@@ -85,6 +85,25 @@ function assignmentOwnerKey(assignment: SecretAssignment): string {
|
||||
return `${getSecretAssignmentSource(assignment)}\0${assignment.ownerKind}\0${assignment.ownerId}`;
|
||||
}
|
||||
|
||||
const AUTH_STORE_PROVIDER_UNCONFIGURED_REASON = "secret provider is not configured" as const;
|
||||
|
||||
function resolveOwnerFailureReason(params: {
|
||||
assignments: SecretAssignment[];
|
||||
error: unknown;
|
||||
fallback: SecretDegradationReason | undefined;
|
||||
}): SecretDegradationReason | undefined {
|
||||
if (params.fallback) {
|
||||
return params.fallback;
|
||||
}
|
||||
const owner = params.assignments[0];
|
||||
return owner &&
|
||||
getSecretAssignmentSource(owner) === "auth-store" &&
|
||||
isProviderScopedSecretResolutionError(params.error) &&
|
||||
params.error.code === "SECRET_PROVIDER_NOT_CONFIGURED"
|
||||
? AUTH_STORE_PROVIDER_UNCONFIGURED_REASON
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function groupAssignmentsByOwner(assignments: SecretAssignment[]): SecretAssignment[][] {
|
||||
const groups = new Map<string, SecretAssignment[]>();
|
||||
for (const assignment of assignments) {
|
||||
@@ -182,11 +201,14 @@ function associateAssignmentFailureOwners(params: {
|
||||
.map(assignmentOwnerKey),
|
||||
),
|
||||
);
|
||||
const reason =
|
||||
const sharedReason =
|
||||
validationFailures.length > 0
|
||||
? "resolved secret value was invalid"
|
||||
: describeSecretResolutionError(params.error);
|
||||
if (!reason) {
|
||||
const authStoreProviderUnconfigured =
|
||||
isProviderScopedSecretResolutionError(params.error) &&
|
||||
params.error.code === "SECRET_PROVIDER_NOT_CONFIGURED";
|
||||
if (!sharedReason && !authStoreProviderUnconfigured) {
|
||||
return;
|
||||
}
|
||||
const owners = groupAssignmentsByOwner(params.assignments).flatMap((assignments) => {
|
||||
@@ -201,6 +223,14 @@ function associateAssignmentFailureOwners(params: {
|
||||
if (!failureMatched) {
|
||||
return [];
|
||||
}
|
||||
const reason = resolveOwnerFailureReason({
|
||||
assignments,
|
||||
error: params.error,
|
||||
fallback: sharedReason,
|
||||
});
|
||||
if (!reason) {
|
||||
return [];
|
||||
}
|
||||
const degradedOwner = createDegradedOwner(assignments, reason);
|
||||
return [
|
||||
{
|
||||
@@ -279,6 +309,14 @@ function associateAssignmentFailureOwners(params: {
|
||||
if (refs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const reason =
|
||||
sharedReason ??
|
||||
(source === "auth-store" && authStoreProviderUnconfigured
|
||||
? AUTH_STORE_PROVIDER_UNCONFIGURED_REASON
|
||||
: undefined);
|
||||
if (!reason) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
ownerKind: owner.ownerKind,
|
||||
@@ -355,10 +393,17 @@ function assertOwnerCanBeIsolated(
|
||||
error: unknown,
|
||||
): SecretDegradationReason {
|
||||
const owner = assignments[0]!;
|
||||
const reason = describeSecretResolutionError(error);
|
||||
const reason = resolveOwnerFailureReason({
|
||||
assignments,
|
||||
error,
|
||||
fallback: describeSecretResolutionError(error),
|
||||
});
|
||||
const isolatableFailure =
|
||||
reason === AUTH_STORE_PROVIDER_UNCONFIGURED_REASON ||
|
||||
(reason !== undefined && isRetryableSecretDegradationReason(reason));
|
||||
if (
|
||||
!reason ||
|
||||
!isRetryableSecretDegradationReason(reason) ||
|
||||
!isolatableFailure ||
|
||||
owner.ownerKind === "unknown" ||
|
||||
owner.requiredForGateway ||
|
||||
owner.disposition === "fail-closed"
|
||||
|
||||
Reference in New Issue
Block a user