fix(coding-agent): pair checkpoint/rewind for restricted sessions too

The !restrictToolNames guard on the pairing blocks was wrong: a restricted
session with tools:[checkpoint] passes isToolAllowed (requestedTools is
defined) but the pairing is skipped, stranding the agent without rewind.
Remove the guard — this is a safety pairing, not a convenience widening.
Added restricted-session tests in both createTools and SDK active-set paths.
This commit is contained in:
Slava Zavadsky
2026-07-28 21:18:43 -04:00
parent 4364cfa5b3
commit fdd46bd971
4 changed files with 41 additions and 4 deletions
+4 -2
View File
@@ -2796,8 +2796,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
// Checkpoint and rewind are a pair: `createTools` auto-includes the sister
// tool in the registry, but an explicit `toolNames` list would otherwise
// drop it from the ACTIVE set — leaving the agent able to checkpoint but
// unable to rewind (or vice versa). Mirror the pairing here.
if (!restrictToolNames && explicitlyRequestedToolNames) {
// unable to rewind (or vice versa). Mirror the pairing here. Unlike the
// manage_skill/learn mirror above, this is a safety pairing — it applies
// to restricted sessions too.
if (explicitlyRequestedToolNames) {
if (builtInToolNames.includes("checkpoint") && !explicitlyRequestedToolNames.includes("rewind")) {
explicitlyRequestedToolNames.push("rewind");
} else if (builtInToolNames.includes("rewind") && !explicitlyRequestedToolNames.includes("checkpoint")) {
+3 -2
View File
@@ -505,8 +505,9 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
// Checkpoint and rewind are a pair: listing one without the other strands
// the agent (it can checkpoint but not rewind, or vice versa). Auto-include
// the sister tool so a one-sided frontmatter `tools:` entry still works.
// Like the AST/auto-learn siblings below, restricted callers own the list.
if (requestedTools && !restrictToolNames && session.settings.get("checkpoint.enabled")) {
// Unlike the AST/auto-learn convenience auto-includes below, this is a
// safety pairing — it applies to restricted sessions too.
if (requestedTools && session.settings.get("checkpoint.enabled")) {
if (requestedTools.includes("checkpoint") && !requestedTools.includes("rewind")) {
requestedTools.push("rewind");
} else if (requestedTools.includes("rewind") && !requestedTools.includes("checkpoint")) {
@@ -122,4 +122,23 @@ describe("createAgentSession auto-learn tool activation", () => {
expect(names).toContain("checkpoint");
expect(names).toContain("rewind");
});
it("activates checkpoint and rewind in a restricted session with one-sided toolNames", async () => {
const { session } = await createAgentSession({
cwd: registryDir,
agentDir: registryDir,
modelRegistry,
sessionManager: SessionManager.inMemory(),
settings: Settings.isolated({ "checkpoint.enabled": true }),
model: getBundledModel("openai", "gpt-4o-mini"),
disableExtensionDiscovery: true,
toolNames: ["checkpoint"],
requireYieldTool: true,
restrictToolNames: true,
});
sessions.push(session);
const names = session.getActiveToolNames();
expect(names).toContain("checkpoint");
expect(names).toContain("rewind");
});
});
@@ -397,6 +397,21 @@ describe("createTools", () => {
expect(names).not.toContain("rewind");
});
it("auto-pairs checkpoint/rewind in a restricted subagent with one-sided list", async () => {
const names = (
await createTools(
createTestSession({
taskDepth: 1,
restrictToolNames: true,
settings: createSettingsWithOverrides({ "checkpoint.enabled": true }),
}),
["checkpoint"],
)
).map(t => t.name);
expect(names).toContain("checkpoint");
expect(names).toContain("rewind");
});
it("HIDDEN_TOOLS contains yield and goal", () => {
expect(Object.keys(HIDDEN_TOOLS).sort()).toEqual(["goal", "yield"]);
});