From f7e05d0136c70f4d5507e052025751b32a58f943 Mon Sep 17 00:00:00 2001 From: niceysam Date: Thu, 12 Feb 2026 22:55:05 +0900 Subject: [PATCH] fix: exclude maxTokens from config redaction + honor deleteAfterRun on skipped cron jobs (#13342) * fix: exclude maxTokens and token-count fields from config redaction The /token/i regex in SENSITIVE_KEY_PATTERNS falsely matched fields like maxTokens, maxOutputTokens, maxCompletionTokens etc. These are numeric config fields for token counts, not sensitive credentials. Added a whitelist (SENSITIVE_KEY_WHITELIST) that explicitly excludes known token-count field names from redaction. This prevents config corruption when maxTokens gets replaced with __OPENCLAW_REDACTED__ during config round-trips. Fixes #13236 * fix: honor deleteAfterRun for one-shot 'at' jobs with 'skipped' status Previously, deleteAfterRun only triggered when result.status was 'ok'. For one-shot 'at' jobs, a 'skipped' status (e.g. empty heartbeat file) would leave the job in state but disabled, never getting cleaned up. Now deleteAfterRun also triggers on 'skipped' status for 'at' jobs, since a skipped one-shot job has no meaningful retry path. Fixes #13249 * Cron: format timer.ts --------- Co-authored-by: nice03 Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- src/config/redact-snapshot.test.ts | 22 +++++++++++++++++++++- src/config/redact-snapshot.ts | 19 +++++++++++++++++++ src/cron/service/timer.ts | 4 +++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/config/redact-snapshot.test.ts b/src/config/redact-snapshot.test.ts index 8d3b2cfdc78..56774f2cd25 100644 --- a/src/config/redact-snapshot.test.ts +++ b/src/config/redact-snapshot.test.ts @@ -111,6 +111,7 @@ describe("redactConfigSnapshot", () => { it("does not redact maxTokens-style fields", () => { const snapshot = makeSnapshot({ + maxTokens: 16384, models: { providers: { openai: { @@ -124,12 +125,21 @@ describe("redactConfigSnapshot", () => { ], apiKey: "sk-proj-abcdef1234567890ghij", accessToken: "access-token-value-1234567890", + maxTokens: 8192, + maxOutputTokens: 4096, + maxCompletionTokens: 2048, + contextTokens: 128000, + tokenCount: 500, + tokenLimit: 100000, + tokenBudget: 50000, }, }, }, + gateway: { auth: { token: "secret-gateway-token-value" } }, }); const result = redactConfigSnapshot(snapshot); + expect((result.config as Record).maxTokens).toBe(16384); const models = result.config.models as Record; const providerList = (( (models.providers as Record).openai as Record @@ -138,9 +148,19 @@ describe("redactConfigSnapshot", () => { expect(providerList[0]?.contextTokens).toBe(200000); expect(providerList[0]?.maxTokensField).toBe("max_completion_tokens"); - const providers = (models.providers as Record>) ?? {}; + const providers = (models.providers as Record>) ?? {}; expect(providers.openai.apiKey).toBe(REDACTED_SENTINEL); expect(providers.openai.accessToken).toBe(REDACTED_SENTINEL); + expect(providers.openai.maxTokens).toBe(8192); + expect(providers.openai.maxOutputTokens).toBe(4096); + expect(providers.openai.maxCompletionTokens).toBe(2048); + expect(providers.openai.contextTokens).toBe(128000); + expect(providers.openai.tokenCount).toBe(500); + expect(providers.openai.tokenLimit).toBe(100000); + expect(providers.openai.tokenBudget).toBe(50000); + + const gw = result.config.gateway as Record>; + expect(gw.auth.token).toBe(REDACTED_SENTINEL); }); it("preserves hash unchanged", () => { diff --git a/src/config/redact-snapshot.ts b/src/config/redact-snapshot.ts index 29bfb3ef565..a40ac395051 100644 --- a/src/config/redact-snapshot.ts +++ b/src/config/redact-snapshot.ts @@ -8,6 +8,22 @@ import type { ConfigFileSnapshot } from "./types.openclaw.js"; */ export const REDACTED_SENTINEL = "__OPENCLAW_REDACTED__"; +/** + * Non-sensitive field names that happen to match sensitive patterns. + * These are explicitly excluded from redaction. + */ +const SENSITIVE_KEY_WHITELIST = new Set([ + "maxtokens", + "maxoutputtokens", + "maxinputtokens", + "maxcompletiontokens", + "contexttokens", + "totaltokens", + "tokencount", + "tokenlimit", + "tokenbudget", +]); + /** * Patterns that identify sensitive config field names. * Aligned with the UI-hint logic in schema.ts. @@ -15,6 +31,9 @@ export const REDACTED_SENTINEL = "__OPENCLAW_REDACTED__"; const SENSITIVE_KEY_PATTERNS = [/token$/i, /password/i, /secret/i, /api.?key/i]; function isSensitiveKey(key: string): boolean { + if (SENSITIVE_KEY_WHITELIST.has(key.toLowerCase())) { + return false; + } return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key)); } diff --git a/src/cron/service/timer.ts b/src/cron/service/timer.ts index 07490fa7f84..aa94adda2a6 100644 --- a/src/cron/service/timer.ts +++ b/src/cron/service/timer.ts @@ -70,7 +70,9 @@ function applyJobResult( } const shouldDelete = - job.schedule.kind === "at" && result.status === "ok" && job.deleteAfterRun === true; + job.schedule.kind === "at" && + job.deleteAfterRun === true && + (result.status === "ok" || result.status === "skipped"); if (!shouldDelete) { if (job.schedule.kind === "at") {