Skip to content

Commit b2a5966

Browse files
committed
fix(subagent): check requires_tools after all run mounts append
1 parent d008743 commit b2a5966

2 files changed

Lines changed: 63 additions & 23 deletions

File tree

‎src/subagent/run-requires-tools.test.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,44 @@ describe("runSubAgent requires_tools mount echo", () => {
109109
expect((error as Error).message).not.toContain("stale_snapshot");
110110
}, 15_000);
111111

112+
test("stamped manage_tasks survives the echo under a narrow allowlist", async () => {
113+
const cwd = await tmpCwd();
114+
const error = await withFailingInference(async (baseURL) => {
115+
try {
116+
await runSubAgent({
117+
...baseParams(cwd, baseURL),
118+
capabilities: { mode: "allow", tools: ["read_file"] },
119+
requiresTools: ["manage_tasks"],
120+
});
121+
} catch (err) {
122+
return err;
123+
}
124+
throw new Error("runSubAgent did not throw");
125+
});
126+
127+
expect(error).toBeInstanceOf(Error);
128+
expect((error as Error).message).not.toContain("stale_snapshot");
129+
}, 15_000);
130+
131+
test("stamped update_plan alias survives the echo under a narrow allowlist", async () => {
132+
const cwd = await tmpCwd();
133+
const error = await withFailingInference(async (baseURL) => {
134+
try {
135+
await runSubAgent({
136+
...baseParams(cwd, baseURL),
137+
capabilities: { mode: "allow", tools: ["read_file"] },
138+
requiresTools: ["update_plan"],
139+
});
140+
} catch (err) {
141+
return err;
142+
}
143+
throw new Error("runSubAgent did not throw");
144+
});
145+
146+
expect(error).toBeInstanceOf(Error);
147+
expect((error as Error).message).not.toContain("stale_snapshot");
148+
}, 15_000);
149+
112150
test("absent requiresTools leaves the mount path unchanged", async () => {
113151
const cwd = await tmpCwd();
114152
const error = await withFailingInference(async (baseURL) => {

‎src/subagent/run.ts‎

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -772,29 +772,6 @@ async function runSubAgentInner(
772772
if (params.capabilities !== undefined) {
773773
tools = applyCapabilityFilter(tools, params.capabilities);
774774
}
775-
// CL-9476 mount echo: dispatch verified requires_tools pre-spawn, but the
776-
// filter or mount may have shifted since — a stamped tool missing here is
777-
// a stale snapshot, a setup_error that never retries (non-continuable).
778-
// Dispatch-side rejection is the normal path; this throw never fires for
779-
// a dispatch that passed preflight against the same mount.
780-
if (params.requiresTools !== undefined && params.requiresTools.length > 0) {
781-
const mountedNames = tools.map((tool) => tool.definition.name);
782-
const mountedCheck = checkMountedRequiresTools(
783-
params.requiresTools,
784-
mountedNames,
785-
);
786-
if (!mountedCheck.ok) {
787-
throw new Error(
788-
formatCapabilityUnavailable(
789-
{
790-
code: "stale_snapshot",
791-
tool: mountedCheck.missing[0] ?? "unknown",
792-
},
793-
params.directorId ?? params.description,
794-
),
795-
);
796-
}
797-
}
798775
backgroundCollectMounted = tools.some(
799776
(tool) => tool.definition.name === "shell_collect",
800777
);
@@ -1022,6 +999,31 @@ async function runSubAgentInner(
1022999
];
10231000
}
10241001

1002+
// CL-9476 mount echo: dispatch verified requires_tools pre-spawn, but the
1003+
// filter or mount may have shifted since — a stamped tool missing here is
1004+
// a stale snapshot, a setup_error that never retries (non-continuable).
1005+
// Dispatch-side rejection is the normal path. This check runs after ALL
1006+
// mounts (manage_tasks, leaf submit_result/ask_director, fleet verbs), so
1007+
// a dispatch that passed preflight against the same mount never throws.
1008+
if (params.requiresTools !== undefined && params.requiresTools.length > 0) {
1009+
const mountedNames = tools.map((tool) => tool.definition.name);
1010+
const mountedCheck = checkMountedRequiresTools(
1011+
params.requiresTools,
1012+
mountedNames,
1013+
);
1014+
if (!mountedCheck.ok) {
1015+
throw new Error(
1016+
formatCapabilityUnavailable(
1017+
{
1018+
code: "stale_snapshot",
1019+
tool: mountedCheck.missing[0] ?? "unknown",
1020+
},
1021+
params.directorId ?? params.description,
1022+
),
1023+
);
1024+
}
1025+
}
1026+
10251027
tools = wrapAgentToolsWithResultTruncation(tools, {
10261028
getBlobWriter: () => childBlobWriter,
10271029
getContextDir: () => childContextDir,

0 commit comments

Comments
 (0)