From c254ebfbef4c17ce681303699ef738c1875b9bf1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 11 Apr 2026 01:42:51 +0100 Subject: [PATCH] fix(ci): align protocol and cron gates --- .../OpenClawProtocol/GatewayModels.swift | 12 ++++++++ .../OpenClawProtocol/GatewayModels.swift | 12 ++++++++ src/cron/service/store.test.ts | 28 +++++++++++++++++++ src/cron/service/store.ts | 15 +++++++++- src/cron/session-target.ts | 6 +++- src/gateway/server-methods/cron.ts | 10 +++++-- test/vitest-projects-config.test.ts | 4 +-- 7 files changed, 80 insertions(+), 7 deletions(-) diff --git a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift index 0f5a95f2918..020aa8920dc 100644 --- a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift @@ -1893,6 +1893,7 @@ public struct ConfigApplyParams: Codable, Sendable { public let raw: String public let basehash: String? public let sessionkey: String? + public let deliverycontext: [String: AnyCodable]? public let note: String? public let restartdelayms: Int? @@ -1900,12 +1901,14 @@ public struct ConfigApplyParams: Codable, Sendable { raw: String, basehash: String?, sessionkey: String?, + deliverycontext: [String: AnyCodable]?, note: String?, restartdelayms: Int?) { self.raw = raw self.basehash = basehash self.sessionkey = sessionkey + self.deliverycontext = deliverycontext self.note = note self.restartdelayms = restartdelayms } @@ -1914,6 +1917,7 @@ public struct ConfigApplyParams: Codable, Sendable { case raw case basehash = "baseHash" case sessionkey = "sessionKey" + case deliverycontext = "deliveryContext" case note case restartdelayms = "restartDelayMs" } @@ -1923,6 +1927,7 @@ public struct ConfigPatchParams: Codable, Sendable { public let raw: String public let basehash: String? public let sessionkey: String? + public let deliverycontext: [String: AnyCodable]? public let note: String? public let restartdelayms: Int? @@ -1930,12 +1935,14 @@ public struct ConfigPatchParams: Codable, Sendable { raw: String, basehash: String?, sessionkey: String?, + deliverycontext: [String: AnyCodable]?, note: String?, restartdelayms: Int?) { self.raw = raw self.basehash = basehash self.sessionkey = sessionkey + self.deliverycontext = deliverycontext self.note = note self.restartdelayms = restartdelayms } @@ -1944,6 +1951,7 @@ public struct ConfigPatchParams: Codable, Sendable { case raw case basehash = "baseHash" case sessionkey = "sessionKey" + case deliverycontext = "deliveryContext" case note case restartdelayms = "restartDelayMs" } @@ -4313,17 +4321,20 @@ public struct ChatEvent: Codable, Sendable { public struct UpdateRunParams: Codable, Sendable { public let sessionkey: String? + public let deliverycontext: [String: AnyCodable]? public let note: String? public let restartdelayms: Int? public let timeoutms: Int? public init( sessionkey: String?, + deliverycontext: [String: AnyCodable]?, note: String?, restartdelayms: Int?, timeoutms: Int?) { self.sessionkey = sessionkey + self.deliverycontext = deliverycontext self.note = note self.restartdelayms = restartdelayms self.timeoutms = timeoutms @@ -4331,6 +4342,7 @@ public struct UpdateRunParams: Codable, Sendable { private enum CodingKeys: String, CodingKey { case sessionkey = "sessionKey" + case deliverycontext = "deliveryContext" case note case restartdelayms = "restartDelayMs" case timeoutms = "timeoutMs" diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index 0f5a95f2918..020aa8920dc 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -1893,6 +1893,7 @@ public struct ConfigApplyParams: Codable, Sendable { public let raw: String public let basehash: String? public let sessionkey: String? + public let deliverycontext: [String: AnyCodable]? public let note: String? public let restartdelayms: Int? @@ -1900,12 +1901,14 @@ public struct ConfigApplyParams: Codable, Sendable { raw: String, basehash: String?, sessionkey: String?, + deliverycontext: [String: AnyCodable]?, note: String?, restartdelayms: Int?) { self.raw = raw self.basehash = basehash self.sessionkey = sessionkey + self.deliverycontext = deliverycontext self.note = note self.restartdelayms = restartdelayms } @@ -1914,6 +1917,7 @@ public struct ConfigApplyParams: Codable, Sendable { case raw case basehash = "baseHash" case sessionkey = "sessionKey" + case deliverycontext = "deliveryContext" case note case restartdelayms = "restartDelayMs" } @@ -1923,6 +1927,7 @@ public struct ConfigPatchParams: Codable, Sendable { public let raw: String public let basehash: String? public let sessionkey: String? + public let deliverycontext: [String: AnyCodable]? public let note: String? public let restartdelayms: Int? @@ -1930,12 +1935,14 @@ public struct ConfigPatchParams: Codable, Sendable { raw: String, basehash: String?, sessionkey: String?, + deliverycontext: [String: AnyCodable]?, note: String?, restartdelayms: Int?) { self.raw = raw self.basehash = basehash self.sessionkey = sessionkey + self.deliverycontext = deliverycontext self.note = note self.restartdelayms = restartdelayms } @@ -1944,6 +1951,7 @@ public struct ConfigPatchParams: Codable, Sendable { case raw case basehash = "baseHash" case sessionkey = "sessionKey" + case deliverycontext = "deliveryContext" case note case restartdelayms = "restartDelayMs" } @@ -4313,17 +4321,20 @@ public struct ChatEvent: Codable, Sendable { public struct UpdateRunParams: Codable, Sendable { public let sessionkey: String? + public let deliverycontext: [String: AnyCodable]? public let note: String? public let restartdelayms: Int? public let timeoutms: Int? public init( sessionkey: String?, + deliverycontext: [String: AnyCodable]?, note: String?, restartdelayms: Int?, timeoutms: Int?) { self.sessionkey = sessionkey + self.deliverycontext = deliverycontext self.note = note self.restartdelayms = restartdelayms self.timeoutms = timeoutms @@ -4331,6 +4342,7 @@ public struct UpdateRunParams: Codable, Sendable { private enum CodingKeys: String, CodingKey { case sessionkey = "sessionKey" + case deliverycontext = "deliveryContext" case note case restartdelayms = "restartDelayMs" case timeoutms = "timeoutMs" diff --git a/src/cron/service/store.test.ts b/src/cron/service/store.test.ts index 4bbf1ded22f..214d68936dc 100644 --- a/src/cron/service/store.test.ts +++ b/src/cron/service/store.test.ts @@ -161,4 +161,32 @@ describe("cron service store seam coverage", () => { const after = await fs.readFile(storePath, "utf8"); expect(after).toBe(before); }); + + it("loads persisted jobs with unsafe custom session ids so run paths can fail closed", async () => { + const { storePath } = await makeStorePath(); + + await writeSingleJobStore(storePath, { + id: "unsafe-session-target-job", + name: "unsafe session target job", + enabled: true, + createdAtMs: STORE_TEST_NOW - 60_000, + updatedAtMs: STORE_TEST_NOW - 60_000, + schedule: { kind: "every", everyMs: 60_000 }, + sessionTarget: "session:../../outside", + wakeMode: "now", + payload: { kind: "agentTurn", message: "ping" }, + state: {}, + }); + + const state = createStoreTestState(storePath); + + await ensureLoaded(state, { skipRecompute: true }); + + const job = findJobOrThrow(state, "unsafe-session-target-job"); + expect(job.sessionTarget).toBe("session:../../outside"); + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ storePath, jobId: "unsafe-session-target-job" }), + expect.stringContaining("invalid persisted sessionTarget"), + ); + }); }); diff --git a/src/cron/service/store.ts b/src/cron/service/store.ts index 40071ae7642..2e8b19af454 100644 --- a/src/cron/service/store.ts +++ b/src/cron/service/store.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import { normalizeCronJobIdentityFields } from "../normalize-job-identity.js"; import { normalizeCronJobInput } from "../normalize.js"; +import { isInvalidCronSessionTargetIdError } from "../session-target.js"; import { loadCronStore, saveCronStore } from "../store.js"; import type { CronJob } from "../types.js"; import { recomputeNextRuns } from "./jobs.js"; @@ -38,7 +39,19 @@ export async function ensureLoaded( for (const [index, job] of jobs.entries()) { const raw = job as unknown as Record; const { legacyJobIdIssue } = normalizeCronJobIdentityFields(raw); - const normalized = normalizeCronJobInput(raw); + let normalized: Record | null; + try { + normalized = normalizeCronJobInput(raw); + } catch (error) { + if (!isInvalidCronSessionTargetIdError(error)) { + throw error; + } + normalized = null; + state.deps.log.warn( + { storePath: state.deps.storePath, jobId: typeof raw.id === "string" ? raw.id : undefined }, + "cron: job has invalid persisted sessionTarget; run openclaw doctor --fix to repair", + ); + } const hydrated = normalized && typeof normalized === "object" ? (normalized as unknown as CronJob) : job; jobs[index] = hydrated; diff --git a/src/cron/session-target.ts b/src/cron/session-target.ts index 51fddad87a0..f592db264af 100644 --- a/src/cron/session-target.ts +++ b/src/cron/session-target.ts @@ -1,4 +1,8 @@ -const INVALID_CRON_SESSION_TARGET_ID_ERROR = "invalid cron sessionTarget session id"; +export const INVALID_CRON_SESSION_TARGET_ID_ERROR = "invalid cron sessionTarget session id"; + +export function isInvalidCronSessionTargetIdError(error: unknown): boolean { + return error instanceof Error && error.message === INVALID_CRON_SESSION_TARGET_ID_ERROR; +} export function assertSafeCronSessionTargetId(sessionId: string): string { const trimmed = sessionId.trim(); diff --git a/src/gateway/server-methods/cron.ts b/src/gateway/server-methods/cron.ts index edcc66a66c8..a62e523faf0 100644 --- a/src/gateway/server-methods/cron.ts +++ b/src/gateway/server-methods/cron.ts @@ -4,6 +4,7 @@ import { readCronRunLogEntriesPageAll, resolveCronRunLogPath, } from "../../cron/run-log.js"; +import { isInvalidCronSessionTargetIdError } from "../../cron/session-target.js"; import type { CronJobCreate, CronJobPatch } from "../../cron/types.js"; import { validateScheduleTimestamp } from "../../cron/validate-timestamp.js"; import { formatErrorMessage } from "../../infra/errors.js"; @@ -250,9 +251,12 @@ export const cronHandlers: GatewayRequestHandlers = { try { result = await context.cron.enqueueRun(jobId, p.mode ?? "force"); } catch (error) { - const message = formatErrorMessage(error); - if (message === "invalid cron sessionTarget session id") { - respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, message)); + if (isInvalidCronSessionTargetIdError(error)) { + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, formatErrorMessage(error)), + ); return; } throw error; diff --git a/test/vitest-projects-config.test.ts b/test/vitest-projects-config.test.ts index d4907631525..a5adf59942b 100644 --- a/test/vitest-projects-config.test.ts +++ b/test/vitest-projects-config.test.ts @@ -24,11 +24,11 @@ describe("projects vitest config", () => { expect(createCommandsVitestConfig().test.pool).toBe("threads"); expect(createPluginSdkLightVitestConfig().test.pool).toBe("threads"); expect(createUnitFastVitestConfig().test.pool).toBe("threads"); - expect(createContractsVitestConfig().test.pool).toBe("threads"); }); - it("keeps the contracts lane on the non-isolated runner by default", () => { + it("keeps the contracts lane on the non-isolated fork runner by default", () => { const config = createContractsVitestConfig(); + expect(config.test.pool).toBe("forks"); expect(config.test.isolate).toBe(false); expect(normalizeConfigPath(config.test.runner)).toBe("test/non-isolated-runner.ts"); });