fix(cron): preserve combined authority

This commit is contained in:
joshavant
2026-08-10 23:48:37 -05:00
committed by Josh Avant
parent bb873c8806
commit 9827ec2a94
2 changed files with 51 additions and 9 deletions
+16 -4
View File
@@ -105,14 +105,26 @@ function resolveAgentRuntimeAuthorityCommitGuard(
}
function combineCronCommitGuards(
...guards: Array<(() => void) | undefined>
): (() => void) | undefined {
const active = guards.filter((guard): guard is () => void => guard !== undefined);
...guards: Array<(() => void | CronRuntimeAuthority) | undefined>
): (() => CronRuntimeAuthority | undefined) | undefined {
const active = guards.filter(
(guard): guard is () => void | CronRuntimeAuthority => guard !== undefined,
);
return active.length > 0
? () => {
let runtimeAuthority: CronRuntimeAuthority | undefined;
for (const guard of active) {
guard();
const candidate = guard();
if (candidate !== undefined) {
// A mutation has one runtime-authority owner. Combining validation
// guards must not silently choose between competing authority caps.
if (runtimeAuthority !== undefined) {
throw new TypeError("multiple cron runtime authorities resolved at commit");
}
runtimeAuthority = candidate;
}
}
return runtimeAuthority;
}
: undefined;
}
@@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createOperationalRunInstanceRef } from "../../agents/admitted-run-context.js";
import type { ChannelPlugin } from "../../channels/plugins/types.public.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { CronRuntimeAuthority } from "../../cron/runtime-authority.js";
import type { CronDelivery, CronJob } from "../../cron/types.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js";
import {
@@ -132,6 +133,7 @@ function setCronValidationTestRegistry(): void {
function createCronContext(currentJobs?: CronJob | CronJob[]) {
const jobs = currentJobs ? (Array.isArray(currentJobs) ? currentJobs : [currentJobs]) : [];
const committedAdds: Partial<CronJob>[] = [];
const committedRuntimeAuthorities: Array<CronRuntimeAuthority | undefined> = [];
const committedUpdates: Array<{ id: string; patch: Partial<CronJob> }> = [];
const update = vi.fn(async (id: string, patch: Partial<CronJob>) => {
committedUpdates.push({ id, patch });
@@ -143,13 +145,19 @@ function createCronContext(currentJobs?: CronJob | CronJob[]) {
});
return {
committedAdds,
committedRuntimeAuthorities,
committedUpdates,
cron: {
add: vi.fn(async (input: Partial<CronJob>, opts?: { commitGuard?: () => void }) => {
opts?.commitGuard?.();
committedAdds.push(input);
return createCronJob({ ...input, id: "cron-1" });
}),
add: vi.fn(
async (
input: Partial<CronJob>,
opts?: { commitGuard?: () => CronRuntimeAuthority | undefined },
) => {
committedRuntimeAuthorities.push(opts?.commitGuard?.());
committedAdds.push(input);
return createCronJob({ ...input, id: "cron-1" });
},
),
update,
updateWithPrecondition: vi.fn(
async (
@@ -1326,6 +1334,28 @@ describe("cron method validation", () => {
revokeCronCreatorAuthorityRunScope(scope);
});
it("preserves creator runtime authority while revalidating delegated authority at commit", async () => {
const runtimeAuthority = {
version: 1 as const,
runtimeId: "codex",
namespace: "codex.apps",
payload: { apps: [{ id: "calendar" }] },
};
const scope = createCronCreatorAuthorityRunScope("run-add-authority");
const grant = mintCronCreatorAuthorityGrant(scope, undefined, runtimeAuthority);
const context = createCronContext();
context.validateAgentRuntimeApprovalAuthority = () => true;
const result = await invokeCron("cron.add", agentTurnCronParams(), {
context,
client: callerClientWithCronCreatorAuthority(grant),
});
expectCronSuccess(result.respond);
expect(context.committedRuntimeAuthorities).toEqual([runtimeAuthority]);
revokeCronCreatorAuthorityRunScope(scope);
});
it("rejects a mismatched cron.add runId without consuming the exact grant", async () => {
const scope = createCronCreatorAuthorityRunScope("run-add");
const grant = mintCronCreatorAuthorityGrant(scope);