mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(daemon): preserve envfile auth provenance
This commit is contained in:
@@ -39,6 +39,15 @@ vi.mock("../daemon/runtime-paths.js", () => ({
|
||||
vi.mock("../daemon/service-audit.js", () => ({
|
||||
auditGatewayServiceConfig: mocks.auditGatewayServiceConfig,
|
||||
needsNodeRuntimeMigration: vi.fn(() => false),
|
||||
readEmbeddedGatewayToken: (
|
||||
command: {
|
||||
environment?: Record<string, string>;
|
||||
environmentValueSources?: Record<string, "inline" | "file">;
|
||||
} | null,
|
||||
) =>
|
||||
command?.environmentValueSources?.OPENCLAW_GATEWAY_TOKEN === "file"
|
||||
? undefined
|
||||
: command?.environment?.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined,
|
||||
SERVICE_AUDIT_CODES: {
|
||||
gatewayEntrypointMismatch: "gateway-entrypoint-mismatch",
|
||||
},
|
||||
@@ -299,6 +308,49 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("does not persist EnvironmentFile-backed service tokens into config", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_GATEWAY_TOKEN: undefined,
|
||||
CLAWDBOT_GATEWAY_TOKEN: undefined,
|
||||
},
|
||||
async () => {
|
||||
mocks.readCommand.mockResolvedValue({
|
||||
programArguments: gatewayProgramArguments,
|
||||
environment: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "env-file-token",
|
||||
},
|
||||
environmentValueSources: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "file",
|
||||
},
|
||||
});
|
||||
mocks.auditGatewayServiceConfig.mockResolvedValue({
|
||||
ok: false,
|
||||
issues: [],
|
||||
});
|
||||
mocks.buildGatewayInstallPlan.mockResolvedValue({
|
||||
programArguments: gatewayProgramArguments,
|
||||
workingDirectory: "/tmp",
|
||||
environment: {},
|
||||
});
|
||||
mocks.install.mockResolvedValue(undefined);
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
gateway: {},
|
||||
};
|
||||
|
||||
await runRepair(cfg);
|
||||
|
||||
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
|
||||
expect(mocks.buildGatewayInstallPlan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: cfg,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maybeScanExtraGatewayServices", () => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { renderSystemNodeWarning, resolveSystemNodeInfo } from "../daemon/runtim
|
||||
import {
|
||||
auditGatewayServiceConfig,
|
||||
needsNodeRuntimeMigration,
|
||||
readEmbeddedGatewayToken,
|
||||
SERVICE_AUDIT_CODES,
|
||||
} from "../daemon/service-audit.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
@@ -230,7 +231,7 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
command,
|
||||
expectedGatewayToken,
|
||||
});
|
||||
const serviceToken = command.environment?.OPENCLAW_GATEWAY_TOKEN?.trim();
|
||||
const serviceToken = readEmbeddedGatewayToken(command);
|
||||
if (tokenRefConfigured && serviceToken) {
|
||||
audit.issues.push({
|
||||
code: SERVICE_AUDIT_CODES.gatewayTokenMismatch,
|
||||
@@ -316,7 +317,7 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
if (!repair) {
|
||||
return;
|
||||
}
|
||||
const serviceEmbeddedToken = command.environment?.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined;
|
||||
const serviceEmbeddedToken = readEmbeddedGatewayToken(command);
|
||||
const gatewayTokenForRepair = expectedGatewayToken ?? serviceEmbeddedToken;
|
||||
const configuredGatewayToken =
|
||||
typeof cfg.gateway?.auth?.token === "string"
|
||||
|
||||
@@ -126,6 +126,30 @@ describe("auditGatewayServiceConfig", () => {
|
||||
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenMismatch),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not treat EnvironmentFile-backed tokens as embedded", async () => {
|
||||
const audit = await auditGatewayServiceConfig({
|
||||
env: { HOME: "/tmp" },
|
||||
platform: "linux",
|
||||
expectedGatewayToken: "new-token",
|
||||
command: {
|
||||
programArguments: ["/usr/bin/node", "gateway"],
|
||||
environment: {
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin",
|
||||
OPENCLAW_GATEWAY_TOKEN: "old-token",
|
||||
},
|
||||
environmentValueSources: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "file",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenEmbedded),
|
||||
).toBe(false);
|
||||
expect(
|
||||
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenMismatch),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkTokenDrift", () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ export type GatewayServiceCommand = {
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
environment?: Record<string, string>;
|
||||
environmentValueSources?: Record<string, "inline" | "file">;
|
||||
sourcePath?: string;
|
||||
} | null;
|
||||
|
||||
@@ -209,7 +210,7 @@ function auditGatewayToken(
|
||||
issues: ServiceConfigIssue[],
|
||||
expectedGatewayToken?: string,
|
||||
) {
|
||||
const serviceToken = command?.environment?.OPENCLAW_GATEWAY_TOKEN?.trim();
|
||||
const serviceToken = readEmbeddedGatewayToken(command);
|
||||
if (!serviceToken) {
|
||||
return;
|
||||
}
|
||||
@@ -232,6 +233,16 @@ function auditGatewayToken(
|
||||
});
|
||||
}
|
||||
|
||||
export function readEmbeddedGatewayToken(command: GatewayServiceCommand): string | undefined {
|
||||
if (!command) {
|
||||
return undefined;
|
||||
}
|
||||
if (command.environmentValueSources?.OPENCLAW_GATEWAY_TOKEN === "file") {
|
||||
return undefined;
|
||||
}
|
||||
return command.environment?.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined;
|
||||
}
|
||||
|
||||
function getPathModule(platform: NodeJS.Platform) {
|
||||
return platform === "win32" ? path.win32 : path.posix;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export type GatewayServiceCommandConfig = {
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
environment?: Record<string, string>;
|
||||
environmentValueSources?: Record<string, "inline" | "file">;
|
||||
sourcePath?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -460,7 +460,7 @@ describe("readSystemdServiceExecStart", () => {
|
||||
expect(readFileSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("lets inline Environment override EnvironmentFile values", async () => {
|
||||
it("lets EnvironmentFile override inline Environment values", async () => {
|
||||
vi.spyOn(fs, "readFile").mockImplementation(async (pathname) => {
|
||||
const pathValue = pathLikeToString(pathname);
|
||||
if (pathValue.endsWith("/openclaw-gateway.service")) {
|
||||
@@ -478,7 +478,8 @@ describe("readSystemdServiceExecStart", () => {
|
||||
});
|
||||
|
||||
const command = await readSystemdServiceExecStart({ HOME: "/home/test" });
|
||||
expect(command?.environment?.OPENCLAW_GATEWAY_TOKEN).toBe("inline-token");
|
||||
expect(command?.environment?.OPENCLAW_GATEWAY_TOKEN).toBe("env-file-token");
|
||||
expect(command?.environmentValueSources?.OPENCLAW_GATEWAY_TOKEN).toBe("file");
|
||||
});
|
||||
|
||||
it("ignores missing optional EnvironmentFile entries", async () => {
|
||||
@@ -598,6 +599,10 @@ describe("readSystemdServiceExecStart", () => {
|
||||
OPENCLAW_GATEWAY_TOKEN: "quoted token",
|
||||
OPENCLAW_GATEWAY_PASSWORD: "quoted-password", // pragma: allowlist secret
|
||||
});
|
||||
expect(command?.environmentValueSources).toEqual({
|
||||
OPENCLAW_GATEWAY_TOKEN: "file",
|
||||
OPENCLAW_GATEWAY_PASSWORD: "file",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export async function readSystemdServiceExecStart(
|
||||
const content = await fs.readFile(unitPath, "utf8");
|
||||
let execStart = "";
|
||||
let workingDirectory = "";
|
||||
const environment: Record<string, string> = {};
|
||||
const inlineEnvironment: Record<string, string> = {};
|
||||
const environmentFileSpecs: string[] = [];
|
||||
for (const rawLine of content.split("\n")) {
|
||||
const line = rawLine.trim();
|
||||
@@ -80,7 +80,7 @@ export async function readSystemdServiceExecStart(
|
||||
const raw = line.slice("Environment=".length).trim();
|
||||
const parsed = parseSystemdEnvAssignment(raw);
|
||||
if (parsed) {
|
||||
environment[parsed.key] = parsed.value;
|
||||
inlineEnvironment[parsed.key] = parsed.value;
|
||||
}
|
||||
} else if (line.startsWith("EnvironmentFile=")) {
|
||||
const raw = line.slice("EnvironmentFile=".length).trim();
|
||||
@@ -98,14 +98,21 @@ export async function readSystemdServiceExecStart(
|
||||
unitPath,
|
||||
});
|
||||
const mergedEnvironment = {
|
||||
...environmentFromFiles,
|
||||
...environment,
|
||||
...inlineEnvironment,
|
||||
...environmentFromFiles.environment,
|
||||
};
|
||||
const mergedEnvironmentSources = {
|
||||
...buildEnvironmentValueSources(inlineEnvironment, "inline"),
|
||||
...buildEnvironmentValueSources(environmentFromFiles.environment, "file"),
|
||||
};
|
||||
const programArguments = parseSystemdExecStart(execStart);
|
||||
return {
|
||||
programArguments,
|
||||
...(workingDirectory ? { workingDirectory } : {}),
|
||||
...(Object.keys(mergedEnvironment).length > 0 ? { environment: mergedEnvironment } : {}),
|
||||
...(Object.keys(mergedEnvironmentSources).length > 0
|
||||
? { environmentValueSources: mergedEnvironmentSources }
|
||||
: {}),
|
||||
sourcePath: unitPath,
|
||||
};
|
||||
} catch {
|
||||
@@ -113,6 +120,13 @@ export async function readSystemdServiceExecStart(
|
||||
}
|
||||
}
|
||||
|
||||
function buildEnvironmentValueSources(
|
||||
environment: Record<string, string>,
|
||||
source: "inline" | "file",
|
||||
): Record<string, "inline" | "file"> {
|
||||
return Object.fromEntries(Object.keys(environment).map((key) => [key, source]));
|
||||
}
|
||||
|
||||
function expandSystemdSpecifier(input: string, env: GatewayServiceEnv): string {
|
||||
// Support the common unit-specifier used in user services.
|
||||
return input.replaceAll("%h", toPosixPath(resolveHomeDir(env)));
|
||||
@@ -165,10 +179,10 @@ async function resolveSystemdEnvironmentFiles(params: {
|
||||
environmentFileSpecs: string[];
|
||||
env: GatewayServiceEnv;
|
||||
unitPath: string;
|
||||
}): Promise<Record<string, string>> {
|
||||
}): Promise<{ environment: Record<string, string> }> {
|
||||
const resolved: Record<string, string> = {};
|
||||
if (params.environmentFileSpecs.length === 0) {
|
||||
return resolved;
|
||||
return { environment: resolved };
|
||||
}
|
||||
const unitDir = path.posix.dirname(params.unitPath);
|
||||
for (const specRaw of params.environmentFileSpecs) {
|
||||
@@ -193,7 +207,7 @@ async function resolveSystemdEnvironmentFiles(params: {
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
return { environment: resolved };
|
||||
}
|
||||
|
||||
export type SystemdServiceInfo = {
|
||||
|
||||
Reference in New Issue
Block a user