@@ -24,6 +24,7 @@ import {
2424 stringEnum ,
2525} from "../schema/typebox.js" ;
2626import { CRON_TOOL_DISPLAY_SUMMARY } from "../tool-description-presets.js" ;
27+ import { expandToolGroups , normalizeToolName } from "../tool-policy.js" ;
2728import { setToolTerminalPresentation } from "../tool-terminal-presentation.js" ;
2829import {
2930 type AnyAgentTool ,
@@ -332,6 +333,12 @@ export const CronToolSchema = createCronToolSchema();
332333type CronToolOptions = {
333334 agentSessionKey ?: string ;
334335 currentDeliveryContext ?: DeliveryContext ;
336+ /**
337+ * Effective tool names visible to the caller that created or edited a cron job.
338+ * Isolated cron runs use a fresh session, so agent-origin jobs need this cap
339+ * persisted on agentTurn payloads before the original session policy is lost.
340+ */
341+ creatorToolAllowlist ?: string [ ] ;
335342 selfRemoveOnlyJobId ?: string ;
336343} ;
337344
@@ -366,6 +373,56 @@ function assertNoCronCommandPayload(value: unknown): void {
366373 }
367374}
368375
376+ function normalizeCronToolsAllow ( values : readonly string [ ] ) : string [ ] {
377+ const normalized : string [ ] = [ ] ;
378+ const seen = new Set < string > ( ) ;
379+ for ( const entry of expandToolGroups ( [ ...values ] ) ) {
380+ const toolName = normalizeToolName ( entry ) ;
381+ if ( ! toolName || seen . has ( toolName ) ) {
382+ continue ;
383+ }
384+ seen . add ( toolName ) ;
385+ normalized . push ( toolName ) ;
386+ }
387+ return normalized ;
388+ }
389+
390+ function capCronAgentTurnToolsAllow ( params : {
391+ payload : Record < string , unknown > ;
392+ creatorToolAllowlist : string [ ] ;
393+ } ) : void {
394+ if ( params . payload . kind !== "agentTurn" ) {
395+ return ;
396+ }
397+ const creatorToolsAllow = normalizeCronToolsAllow ( params . creatorToolAllowlist ) ;
398+ const requestedRaw = params . payload . toolsAllow ;
399+ if ( ! Array . isArray ( requestedRaw ) ) {
400+ params . payload . toolsAllow = creatorToolsAllow ;
401+ return ;
402+ }
403+ const requestedToolsAllow = normalizeCronToolsAllow (
404+ requestedRaw . filter ( ( entry ) : entry is string => typeof entry === "string" ) ,
405+ ) ;
406+ if ( requestedToolsAllow . includes ( "*" ) ) {
407+ params . payload . toolsAllow = creatorToolsAllow ;
408+ return ;
409+ }
410+ const creatorAllowSet = new Set ( creatorToolsAllow ) ;
411+ params . payload . toolsAllow = requestedToolsAllow . filter ( ( toolName ) =>
412+ creatorAllowSet . has ( toolName ) ,
413+ ) ;
414+ }
415+
416+ function capCronAgentTurnJobToolsAllow (
417+ value : unknown ,
418+ creatorToolAllowlist : string [ ] | undefined ,
419+ ) : void {
420+ if ( ! creatorToolAllowlist || ! isRecord ( value ) || ! isRecord ( value . payload ) ) {
421+ return ;
422+ }
423+ capCronAgentTurnToolsAllow ( { payload : value . payload , creatorToolAllowlist } ) ;
424+ }
425+
369426function truncateText ( input : string , maxLen : number ) {
370427 if ( input . length <= maxLen ) {
371428 return input ;
@@ -730,6 +787,7 @@ Use jobId canonical; id accepted compat. contextMessages (0-10) adds previous me
730787 normalizeCronJobCreate ( canonicalJob , {
731788 sessionContext : { sessionKey : opts ?. agentSessionKey } ,
732789 } ) ?? canonicalJob ;
790+ capCronAgentTurnJobToolsAllow ( job , opts ?. creatorToolAllowlist ) ;
733791 const cfg = getRuntimeConfig ( ) ;
734792 if ( job && typeof job === "object" ) {
735793 const { mainKey, alias } = resolveMainSessionAlias ( cfg ) ;
@@ -845,6 +903,7 @@ Use jobId canonical; id accepted compat. contextMessages (0-10) adds previous me
845903 assertNoCronCommandPayload ( canonicalPatch ) ;
846904 assertCronDeliveryInputNonBlankFields ( canonicalPatch . delivery ) ;
847905 const patch = normalizeCronJobPatch ( canonicalPatch ) ?? canonicalPatch ;
906+ capCronAgentTurnJobToolsAllow ( patch , opts ?. creatorToolAllowlist ) ;
848907 if ( recoveredFlatPatch && isEmptyRecoveredCronPatch ( patch ) ) {
849908 throw new Error ( "patch required" ) ;
850909 }
0 commit comments