@@ -16,7 +16,7 @@ import {
1616import { xaiProfileFromProviderName } from "../config/xai-providers.js" ;
1717import { formatDirectorSystemPrompt } from "../agent/directors/identity.js" ;
1818import { DIRECTOR_REGISTRY } from "../agent/directors/registry.js" ;
19- import type { DirectorId } from "../agent/directors/types.js" ;
19+ import type { DirectorId , DirectorPackage } from "../agent/directors/types.js" ;
2020import { submitOutputDefinition } from "../agent/director.js" ;
2121import {
2222 shellDefinition ,
@@ -265,19 +265,51 @@ export function resolveExecDirectorOverlay(
265265 if ( director === undefined || director === "skywalker" ) {
266266 return { mountFleet : true } ;
267267 }
268- const pkg = DIRECTOR_REGISTRY [ director ] ;
268+ return resolveExecDirectorOverlayForPackage ( DIRECTOR_REGISTRY [ director ] ) ;
269+ }
270+
271+ /**
272+ * Single enforcement point for the exec allowlist. Everything the overlay
273+ * permits — tool_search results, promoter activation, the call gate — flows
274+ * through here, so a tool outside the allow can never become callable.
275+ */
276+ export function isExecOverlayToolAllowed (
277+ overlay : ExecDirectorOverlay ,
278+ name : string ,
279+ ) : boolean {
280+ return (
281+ overlay . advertisedAllow === undefined ||
282+ overlay . advertisedAllow . includes ( name )
283+ ) ;
284+ }
285+
286+ export function resolveExecDirectorOverlayForPackage (
287+ pkg : DirectorPackage ,
288+ ) : ExecDirectorOverlay {
269289 const allow = pkg . tools ?. allow ;
270- const advertisedAllow =
290+ const deny = pkg . tools ?. deny ?? [ ] ;
291+ if ( ( allow === undefined || allow . length === 0 ) && deny . length > 0 ) {
292+ throw new Error (
293+ `Director package "${ pkg . id } " sets tools.deny without tools.allow — ` +
294+ "exec overlays enforce a closed allow list, so a deny-only package " +
295+ "has no list to subtract from. Add tools.allow." ,
296+ ) ;
297+ }
298+ const allowed =
271299 allow !== undefined && allow . length > 0
300+ ? allow . filter ( ( name ) => ! deny . includes ( name ) )
301+ : undefined ;
302+ const advertisedAllow =
303+ allowed !== undefined
272304 ? pkg . spawn . maySpawn
273305 ? [
274- ...allow ,
306+ ...allowed ,
275307 // Exec mounts wait_agents beside the fleet verbs (mountWaitAgents),
276308 // so it stays advertised here even though the package allow omits
277309 // it for TUI/nested mailbox-mail collection.
278- ...( ! allow . includes ( "wait_agents" ) ? [ "wait_agents" ] : [ ] ) ,
310+ ...( ! allowed . includes ( "wait_agents" ) ? [ "wait_agents" ] : [ ] ) ,
279311 ]
280- : allow . filter (
312+ : allowed . filter (
281313 ( name ) =>
282314 ! [
283315 "search_agents" ,
@@ -649,6 +681,11 @@ export async function runExec(config: Config): Promise<ExecResult> {
649681 : { } ) ,
650682 sessionMode,
651683 toolAvailability,
684+ // Closed director overlays unmount tool_search (unless allowed) and
685+ // filter its index, so search cannot surface outside-allow tools.
686+ ...( overlay . advertisedAllow !== undefined
687+ ? { toolSearchAllow : overlay . advertisedAllow }
688+ : { } ) ,
652689 // Exec-primary keeps wait_agents mounted (with an advertised allow):
653690 // headless runs have no mailbox-mail flush, so wait_agents stays the
654691 // collection path here. TUI primary and nested orchestrators omit it.
@@ -839,10 +876,14 @@ export async function runExec(config: Config): Promise<ExecResult> {
839876
840877 // tool_search starts as a no-op promoter; without this, the call gate
841878 // refuses MCP/present/plugin names the result just told the model to
842- // invoke.
879+ // invoke. Under a closed overlay the promoter only activates allowed
880+ // names, so outside-allow tools can never become advertised or callable.
843881 agentToolset . setToolPromoter (
844882 createExecToolPromoter ( {
845- activate : ( names ) => activatedToolNames . activate ( names ) ,
883+ activate : ( names ) =>
884+ activatedToolNames . activate (
885+ names . filter ( ( name ) => isExecOverlayToolAllowed ( overlay , name ) ) ,
886+ ) ,
846887 currentDefinitions : ( ) =>
847888 agentToolset . dynamicRunner . currentDefinitions ( ) ,
848889 computeAdvertised,
0 commit comments