fix(ci): align protocol and cron gates

This commit is contained in:
Peter Steinberger
2026-04-11 01:42:51 +01:00
parent d014567246
commit c254ebfbef
7 changed files with 80 additions and 7 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"),
);
});
});

View File

@@ -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<string, unknown>;
const { legacyJobIdIssue } = normalizeCronJobIdentityFields(raw);
const normalized = normalizeCronJobInput(raw);
let normalized: Record<string, unknown> | 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;

View File

@@ -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();

View File

@@ -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;

View File

@@ -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");
});