diff --git a/apps/controller/src/drivers/compute/kubernetes/configuration-rules.ts b/apps/controller/src/drivers/compute/kubernetes/configuration-rules.ts index 554a59c09c..c155e0a63d 100644 --- a/apps/controller/src/drivers/compute/kubernetes/configuration-rules.ts +++ b/apps/controller/src/drivers/compute/kubernetes/configuration-rules.ts @@ -136,13 +136,14 @@ const RUNTIME_VARIABLE = /^(?:HOSTNAME|YARN_VERSION|GIT_SSL_CAINFO)$|_SERVICE_(?:HOST|PORT)(?:_|$)|_PORT(?:_|$)/; // The variable a secret-input value reads as an environment SecretRef, if any: a whole `${NAME}` -// with no fallback, the `$NAME` shorthand or an env SecretRef object. OpenClaw keeps a reference +// with no fallback, the `$NAME` shorthand or an env SecretRef object. OpenClaw trims the value +// first (`parseEnvTemplateSecretRef`), so a padded `${NAME}` counts too. It keeps a reference // inside other text as written, and a fallback replaces an unset name, so neither stops it. function secretInputVariable(value: unknown): string | undefined { const reference = asRecord(value); const name = typeof value === "string" - ? (wholeEnvironmentReferenceName(value) ?? secretInputShorthandName(value)) + ? (wholeEnvironmentReferenceName(value.trim()) ?? secretInputShorthandName(value)) : reference?.source === "env" ? reference.id : undefined; diff --git a/tests/conformance/kubernetes-compute.test.mjs b/tests/conformance/kubernetes-compute.test.mjs index 88723de5e4..7704661a21 100644 --- a/tests/conformance/kubernetes-compute.test.mjs +++ b/tests/conformance/kubernetes-compute.test.mjs @@ -9627,6 +9627,9 @@ test("admission refuses a plugin secret input whose unset variable stops any Gat "codex", ], [withSearchHeader("${RELAY_TOKEN}"), searchHeader, "RELAY_TOKEN", "google"], + // OpenClaw trims a secret-input value before it reads `${NAME}`, so padding still stops the + // Gateway (finding 3046). + [withSearchHeader(" ${RELAY_TOKEN}\n"), searchHeader, "RELAY_TOKEN", "google"], [ withSearchHeader(envRef("relay_token"), { enabled: true }), searchHeader, diff --git a/tests/integration/configuration-controller.test.mjs b/tests/integration/configuration-controller.test.mjs index 72b406197b..0f85bdea3f 100644 --- a/tests/integration/configuration-controller.test.mjs +++ b/tests/integration/configuration-controller.test.mjs @@ -2025,6 +2025,26 @@ test("Configuration save and Kubernetes deployment admission refuse settings pre "google", ), ], + // OpenClaw trims a secret-input value before it reads `${NAME}`, so a padded reference stops + // the Gateway too (finding 3046). Save accepts it here: the header name marks no credential. + [ + { + ...valid, + plugins: { + entries: { + google: { + config: { webSearch: { headers: { "X-Goog-User-Project": " ${SEARCH_HEADER} " } } }, + }, + }, + }, + }, + 400, + unboundPluginSecretRefusal( + "plugins.entries.google.config.webSearch.headers.X-Goog-User-Project", + "SEARCH_HEADER", + "google", + ), + ], ]) { const label = JSON.stringify(values); const saved = await request(context.app, "PATCH", item, { body: { values } });