fix(cron): keep default toolsAllow marker when edits echo an unchanged list

This commit is contained in:
Ayaan Zaidi
2026-07-05 00:17:31 -07:00
parent e72a2c825d
commit cfdc2b3ff9
3 changed files with 79 additions and 6 deletions

View File

@@ -2,6 +2,7 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createSourceDeliveryPlan } from "../../infra/outbound/source-delivery-plan.js";
import type { SkillSnapshot } from "../../skills/types.js";
import { applyJobPatch } from "../service/jobs.js";
import type { CronDeliveryMode } from "../types.js";
import type { MutableCronSession } from "./run-session-state.js";
import {
@@ -860,6 +861,45 @@ describe("runCronIsolatedAgentTurn message tool policy", () => {
expect(cliRun.toolsAllow).toBeUndefined();
});
it("keeps a cron-tool default toolsAllow marker after a self-edit before CLI execution", async () => {
mockRunCronFallbackPassthrough();
resolveCronDeliveryPlanMock.mockReturnValue(makeAnnounceDeliveryPlan());
isCliProviderMock.mockReturnValue(true);
runCliAgentMock.mockResolvedValue({
payloads: [{ text: "done" }],
meta: { agentMeta: { usage: { input: 10, output: 20 } } },
});
const job = makeMessageToolPolicyJob(
{ mode: "announce", channel: "messagechat", to: "123" },
{
kind: "agentTurn",
message: "send a message",
toolsAllow: ["read", "cron"],
toolsAllowIsDefault: true,
},
);
applyJobPatch(job, {
payload: {
kind: "agentTurn",
message: "send a clearer message",
toolsAllow: ["read", "cron"],
},
});
await runCronIsolatedAgentTurn({
...makeParams(),
job,
});
const cliRun = expectRecordFields(
getMockCallArg(runCliAgentMock, 0, 0, "CLI run"),
{},
"CLI run params",
);
expect(cliRun.toolsAllow).toBeUndefined();
});
it("keeps automatic exec completion notifications when announce delivery is active", async () => {
mockRunCronFallbackPassthrough();
resolveCronDeliveryPlanMock.mockReturnValue(makeAnnounceDeliveryPlan());

View File

@@ -449,6 +449,34 @@ describe("applyJobPatch", () => {
}
});
it("preserves the default toolsAllow flag when a self-edit echoes the default list", () => {
const job = createIsolatedAgentTurnJob("job-tools-default-echo", {
mode: "announce",
channel: "telegram",
});
job.payload = {
kind: "agentTurn",
message: "do it",
toolsAllow: ["exec", "read"],
toolsAllowIsDefault: true,
};
applyJobPatch(job, {
payload: {
kind: "agentTurn",
message: "do it later",
toolsAllow: ["exec", "read"],
},
});
expect(job.payload.kind).toBe("agentTurn");
if (job.payload.kind === "agentTurn") {
expect(job.payload.message).toBe("do it later");
expect(job.payload.toolsAllow).toEqual(["exec", "read"]);
expect(job.payload.toolsAllowIsDefault).toBe(true);
}
});
it("clears agentTurn payload.toolsAllow when patch requests null", () => {
const job = createIsolatedAgentTurnJob("job-tools-clear", {
mode: "announce",

View File

@@ -963,12 +963,17 @@ function applyAgentTurnToolsAllowPatch(
): void {
if (Array.isArray(patch.toolsAllow)) {
payload.toolsAllow = patch.toolsAllow;
// Same-kind edits keep the marker only when the default list is unchanged;
// kind replacements carry the cron-tool-stamped marker into persistence.
if (
patch.toolsAllowIsDefault === true &&
(!existing || (existing.toolsAllowIsDefault === true && toolsAllowEqual(existing, patch)))
) {
// Same-kind edits keep the marker whenever the default-stamped list is
// unchanged — even when the patch omits toolsAllowIsDefault, because the
// cron tool's model-facing schema never sends it. Dropping the marker on an
// echoed list silently reclassifies "default" as an explicit restriction,
// which fail-closes the next run on CLI backends that cannot enforce
// runtime toolsAllow. Kind replacements (no existing payload) still require
// the cron-tool-stamped marker on the patch itself.
const keepDefaultMarker = existing
? existing.toolsAllowIsDefault === true && toolsAllowEqual(existing, patch)
: patch.toolsAllowIsDefault === true;
if (keepDefaultMarker) {
payload.toolsAllowIsDefault = true;
} else {
delete payload.toolsAllowIsDefault;