fix(gateway): land #28428 from @l0cka

Landed from contributor PR #28428 by @l0cka.

Co-authored-by: Daniel Alkurdi <danielalkurdi@gmail.com>
This commit is contained in:
Peter Steinberger
2026-03-07 22:49:50 +00:00
parent e83094e63f
commit 265367d99b
26 changed files with 289 additions and 165 deletions

View File

@@ -78,12 +78,15 @@ describe("auditGatewayServiceConfig", () => {
},
},
});
expect(
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenEmbedded),
).toBe(true);
expect(
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenMismatch),
).toBe(true);
});
it("does not flag gateway token mismatch when service token matches config token", async () => {
it("flags embedded service token even when it matches config token", async () => {
const audit = await auditGatewayServiceConfig({
env: { HOME: "/tmp" },
platform: "linux",
@@ -96,6 +99,29 @@ describe("auditGatewayServiceConfig", () => {
},
},
});
expect(
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenEmbedded),
).toBe(true);
expect(
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenMismatch),
).toBe(false);
});
it("does not flag token issues when service token is not 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",
},
},
});
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);
@@ -143,10 +169,9 @@ describe("checkTokenDrift", () => {
expect(result?.message).toContain("differs from service token");
});
it("detects drift when config has token but service has no token", () => {
it("returns null when config has token but service has no token", () => {
const result = checkTokenDrift({ serviceToken: undefined, configToken: "new-token" });
expect(result).not.toBeNull();
expect(result?.code).toBe(SERVICE_AUDIT_CODES.gatewayTokenDrift);
expect(result).toBeNull();
});
it("returns null when service has token but config does not", () => {

View File

@@ -35,6 +35,7 @@ export const SERVICE_AUDIT_CODES = {
gatewayPathMissing: "gateway-path-missing",
gatewayPathMissingDirs: "gateway-path-missing-dirs",
gatewayPathNonMinimal: "gateway-path-nonminimal",
gatewayTokenEmbedded: "gateway-token-embedded",
gatewayTokenMismatch: "gateway-token-mismatch",
gatewayRuntimeBun: "gateway-runtime-bun",
gatewayRuntimeNodeVersionManager: "gateway-runtime-node-version-manager",
@@ -208,19 +209,25 @@ function auditGatewayToken(
issues: ServiceConfigIssue[],
expectedGatewayToken?: string,
) {
const expectedToken = expectedGatewayToken?.trim();
if (!expectedToken) {
const serviceToken = command?.environment?.OPENCLAW_GATEWAY_TOKEN?.trim();
if (!serviceToken) {
return;
}
const serviceToken = command?.environment?.OPENCLAW_GATEWAY_TOKEN?.trim();
if (serviceToken === expectedToken) {
issues.push({
code: SERVICE_AUDIT_CODES.gatewayTokenEmbedded,
message: "Gateway service embeds OPENCLAW_GATEWAY_TOKEN and should be reinstalled.",
detail: "Run `openclaw gateway install --force` to remove embedded service token.",
level: "recommended",
});
const expectedToken = expectedGatewayToken?.trim();
if (!expectedToken || serviceToken === expectedToken) {
return;
}
issues.push({
code: SERVICE_AUDIT_CODES.gatewayTokenMismatch,
message:
"Gateway service OPENCLAW_GATEWAY_TOKEN does not match gateway.auth.token in openclaw.json",
detail: serviceToken ? "service token is stale" : "service token is missing",
detail: "service token is stale",
level: "recommended",
});
}
@@ -360,21 +367,15 @@ export function checkTokenDrift(params: {
serviceToken: string | undefined;
configToken: string | undefined;
}): ServiceConfigIssue | null {
const { serviceToken, configToken } = params;
const serviceToken = params.serviceToken?.trim() || undefined;
const configToken = params.configToken?.trim() || undefined;
// Normalise both tokens before comparing: service-file parsers (systemd,
// launchd) can return values with trailing newlines or whitespace that
// cause a false-positive mismatch against the config value.
const normService = serviceToken?.trim() || undefined;
const normConfig = configToken?.trim() || undefined;
// No drift if both are undefined/empty
if (!normService && !normConfig) {
// Tokenless service units are canonical; no drift to report.
if (!serviceToken) {
return null;
}
// Drift: config has token, service has different or no token
if (normConfig && normService !== normConfig) {
if (configToken && serviceToken !== configToken) {
return {
code: SERVICE_AUDIT_CODES.gatewayTokenDrift,
message:

View File

@@ -264,7 +264,6 @@ describe("buildServiceEnvironment", () => {
const env = buildServiceEnvironment({
env: { HOME: "/home/user" },
port: 18789,
token: "secret",
});
expect(env.HOME).toBe("/home/user");
if (process.platform === "win32") {
@@ -273,7 +272,7 @@ describe("buildServiceEnvironment", () => {
expect(env.PATH).toContain("/usr/bin");
}
expect(env.OPENCLAW_GATEWAY_PORT).toBe("18789");
expect(env.OPENCLAW_GATEWAY_TOKEN).toBe("secret");
expect(env.OPENCLAW_GATEWAY_TOKEN).toBeUndefined();
expect(env.OPENCLAW_SERVICE_MARKER).toBe("openclaw");
expect(env.OPENCLAW_SERVICE_KIND).toBe("gateway");
expect(typeof env.OPENCLAW_SERVICE_VERSION).toBe("string");

View File

@@ -245,11 +245,10 @@ export function buildMinimalServicePath(options: BuildServicePathOptions = {}):
export function buildServiceEnvironment(params: {
env: Record<string, string | undefined>;
port: number;
token?: string;
launchdLabel?: string;
platform?: NodeJS.Platform;
}): Record<string, string | undefined> {
const { env, port, token, launchdLabel } = params;
const { env, port, launchdLabel } = params;
const platform = params.platform ?? process.platform;
const sharedEnv = resolveSharedServiceEnvironmentFields(env, platform);
const profile = env.OPENCLAW_PROFILE;
@@ -260,7 +259,6 @@ export function buildServiceEnvironment(params: {
...buildCommonServiceEnvironment(env, sharedEnv),
OPENCLAW_PROFILE: profile,
OPENCLAW_GATEWAY_PORT: String(port),
OPENCLAW_GATEWAY_TOKEN: token,
OPENCLAW_LAUNCHD_LABEL: resolvedLaunchdLabel,
OPENCLAW_SYSTEMD_UNIT: systemdUnit,
OPENCLAW_WINDOWS_TASK_NAME: resolveGatewayWindowsTaskName(profile),