From c0f539745294e0e63ef97db15189f725f9fa65d5 Mon Sep 17 00:00:00 2001 From: J Date: Mon, 15 Jun 2026 17:33:39 +0800 Subject: [PATCH] fix: preserve openclaw gateway token mask Co-authored-by: multica-agent --- .../agents/openclaw-runtime-config.test.ts | 63 +++++++++++++++++++ .../core/agents/openclaw-runtime-config.ts | 17 +++-- 2 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 packages/core/agents/openclaw-runtime-config.test.ts diff --git a/packages/core/agents/openclaw-runtime-config.test.ts b/packages/core/agents/openclaw-runtime-config.test.ts new file mode 100644 index 000000000..ffb5c2bf2 --- /dev/null +++ b/packages/core/agents/openclaw-runtime-config.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { + OPENCLAW_GATEWAY_TOKEN_MASK, + serializeOpenclawRuntimeConfig, +} from "./openclaw-runtime-config"; + +describe("serializeOpenclawRuntimeConfig", () => { + it("keeps the masked gateway token sentinel so the API can preserve the persisted token", () => { + expect( + serializeOpenclawRuntimeConfig({ + mode: "gateway", + gateway: { + host: "gw.internal", + port: 18789, + token: OPENCLAW_GATEWAY_TOKEN_MASK, + tls: true, + }, + }), + ).toEqual({ + mode: "gateway", + gateway: { + host: "gw.internal", + port: 18789, + token: OPENCLAW_GATEWAY_TOKEN_MASK, + tls: true, + }, + }); + }); + + it("omits an empty gateway token so users can clear a persisted token", () => { + expect( + serializeOpenclawRuntimeConfig({ + mode: "gateway", + gateway: { + host: "gw.internal", + port: 18789, + }, + }), + ).toEqual({ + mode: "gateway", + gateway: { + host: "gw.internal", + port: 18789, + }, + }); + }); + + it("passes through a real gateway token value", () => { + expect( + serializeOpenclawRuntimeConfig({ + mode: "gateway", + gateway: { + token: "rotated-secret", + }, + }), + ).toEqual({ + mode: "gateway", + gateway: { + token: "rotated-secret", + }, + }); + }); +}); diff --git a/packages/core/agents/openclaw-runtime-config.ts b/packages/core/agents/openclaw-runtime-config.ts index a4e019825..c0b1190a2 100644 --- a/packages/core/agents/openclaw-runtime-config.ts +++ b/packages/core/agents/openclaw-runtime-config.ts @@ -20,10 +20,9 @@ export interface OpenclawRuntimeConfig { } // Sentinel the API substitutes for a non-empty `gateway.token` on every read. -// When the form re-submits the same sentinel we strip the field client-side -// so the backend's matching preserve hook restores the persisted token -// instead of overwriting it. Mirrors `runtimeConfigGatewayTokenMask` in -// server/internal/handler/agent.go. +// When the form re-submits the same sentinel, the backend's matching +// preserve hook restores the persisted token instead of overwriting it. +// Mirrors `runtimeConfigGatewayTokenMask` in server/internal/handler/agent.go. export const OPENCLAW_GATEWAY_TOKEN_MASK = "***"; // Parse an arbitrary runtime_config payload into the typed schema. Unknown @@ -65,12 +64,10 @@ export function serializeOpenclawRuntimeConfig( if (cfg.gateway.host) gw.host = cfg.gateway.host; if (cfg.gateway.port) gw.port = cfg.gateway.port; if (cfg.gateway.tls) gw.tls = true; - // The mask sentinel must NEVER round-trip back to the server; the - // matching preserve hook on the backend treats its presence as "keep - // the persisted token", but emitting it here would still hit the wire - // and trip any future schema validation. Drop it client-side so the - // payload genuinely omits the field. - if (cfg.gateway.token && cfg.gateway.token !== OPENCLAW_GATEWAY_TOKEN_MASK) { + // The mask sentinel is the explicit "keep persisted token" signal for + // the API. Omitting the field means "clear/no token" for partial + // gateway pins, so the sentinel must survive serialization. + if (cfg.gateway.token) { gw.token = cfg.gateway.token; } if (Object.keys(gw).length > 0) out.gateway = gw;