2026-02-02 08:26:42 -08:00
"""
Cron job management tools for Hermes Agent .
2026-03-14 12:21:50 -07:00
Expose a single compressed action - oriented tool to avoid schema / context bloat .
Compatibility wrappers remain for direct Python callers and legacy tests .
2026-02-02 08:26:42 -08:00
"""
import json
import os
Harden agent attack surface: scan writes to memory, skills, cron, and context files
The security scanner (skills_guard.py) was only wired into the hub install path.
All other write paths to persistent state — skills created by the agent, memory
entries, cron prompts, and context files — bypassed it entirely. This closes
those gaps:
- file_operations: deny-list blocks writes to ~/.ssh, ~/.aws, ~/.hermes/.env, etc.
- code_execution_tool: filter secret env vars from sandbox child process
- skill_manager_tool: wire scan_skill() into create/edit/patch/write_file with rollback
- skills_guard: add "agent-created" trust level (same policy as community)
- memory_tool: scan content for injection/exfil before system prompt injection
- prompt_builder: scan AGENTS.md, .cursorrules, SOUL.md for prompt injection
- cronjob_tools: scan cron prompts for critical threats before scheduling
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:43:15 -05:00
import re
2026-02-02 08:26:42 -08:00
import sys
from pathlib import Path
2026-03-14 19:18:10 -07:00
from typing import Any , Dict , List , Optional
2026-03-14 12:21:50 -07:00
# Import from cron module (will be available when properly installed)
2026-02-02 08:26:42 -08:00
sys . path . insert ( 0 , str ( Path ( __file__ ) . parent . parent ) )
2026-03-14 12:21:50 -07:00
from cron . jobs import (
create_job ,
get_job ,
list_jobs ,
parse_schedule ,
pause_job ,
remove_job ,
resume_job ,
trigger_job ,
update_job ,
)
2026-02-02 08:26:42 -08:00
Harden agent attack surface: scan writes to memory, skills, cron, and context files
The security scanner (skills_guard.py) was only wired into the hub install path.
All other write paths to persistent state — skills created by the agent, memory
entries, cron prompts, and context files — bypassed it entirely. This closes
those gaps:
- file_operations: deny-list blocks writes to ~/.ssh, ~/.aws, ~/.hermes/.env, etc.
- code_execution_tool: filter secret env vars from sandbox child process
- skill_manager_tool: wire scan_skill() into create/edit/patch/write_file with rollback
- skills_guard: add "agent-created" trust level (same policy as community)
- memory_tool: scan content for injection/exfil before system prompt injection
- prompt_builder: scan AGENTS.md, .cursorrules, SOUL.md for prompt injection
- cronjob_tools: scan cron prompts for critical threats before scheduling
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:43:15 -05:00
# ---------------------------------------------------------------------------
# Cron prompt scanning — critical-severity patterns only, since cron prompts
# run in fresh sessions with full tool access.
# ---------------------------------------------------------------------------
_CRON_THREAT_PATTERNS = [
2026-02-26 13:55:54 +03:00
( r ' ignore \ s+(?: \ w+ \ s+)*(?:previous|all|above|prior) \ s+(?: \ w+ \ s+)*instructions ' , " prompt_injection " ) ,
Harden agent attack surface: scan writes to memory, skills, cron, and context files
The security scanner (skills_guard.py) was only wired into the hub install path.
All other write paths to persistent state — skills created by the agent, memory
entries, cron prompts, and context files — bypassed it entirely. This closes
those gaps:
- file_operations: deny-list blocks writes to ~/.ssh, ~/.aws, ~/.hermes/.env, etc.
- code_execution_tool: filter secret env vars from sandbox child process
- skill_manager_tool: wire scan_skill() into create/edit/patch/write_file with rollback
- skills_guard: add "agent-created" trust level (same policy as community)
- memory_tool: scan content for injection/exfil before system prompt injection
- prompt_builder: scan AGENTS.md, .cursorrules, SOUL.md for prompt injection
- cronjob_tools: scan cron prompts for critical threats before scheduling
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:43:15 -05:00
( r ' do \ s+not \ s+tell \ s+the \ s+user ' , " deception_hide " ) ,
( r ' system \ s+prompt \ s+override ' , " sys_prompt_override " ) ,
( r ' disregard \ s+(your|all|any) \ s+(instructions|rules|guidelines) ' , " disregard_rules " ) ,
( r ' curl \ s+[^ \ n]* \ $ \ { ? \ w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API) ' , " exfil_curl " ) ,
( r ' wget \ s+[^ \ n]* \ $ \ { ? \ w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API) ' , " exfil_wget " ) ,
( r ' cat \ s+[^ \ n]*( \ .env|credentials| \ .netrc| \ .pgpass) ' , " read_secrets " ) ,
( r ' authorized_keys ' , " ssh_backdoor " ) ,
( r ' /etc/sudoers|visudo ' , " sudoers_mod " ) ,
( r ' rm \ s+-rf \ s+/ ' , " destructive_root_rm " ) ,
]
_CRON_INVISIBLE_CHARS = {
' \u200b ' , ' \u200c ' , ' \u200d ' , ' \u2060 ' , ' \ufeff ' ,
' \u202a ' , ' \u202b ' , ' \u202c ' , ' \u202d ' , ' \u202e ' ,
}
def _scan_cron_prompt ( prompt : str ) - > str :
""" Scan a cron prompt for critical threats. Returns error string if blocked, else empty. """
for char in _CRON_INVISIBLE_CHARS :
if char in prompt :
return f " Blocked: prompt contains invisible unicode U+ { ord ( char ) : 04X } (possible injection). "
for pattern , pid in _CRON_THREAT_PATTERNS :
if re . search ( pattern , prompt , re . IGNORECASE ) :
return f " Blocked: prompt matches threat pattern ' { pid } ' . Cron prompts must not contain injection or exfiltration payloads. "
return " "
2026-03-14 12:21:50 -07:00
def _origin_from_env ( ) - > Optional [ Dict [ str , str ] ] :
2026-02-02 19:01:51 -08:00
origin_platform = os . getenv ( " HERMES_SESSION_PLATFORM " )
origin_chat_id = os . getenv ( " HERMES_SESSION_CHAT_ID " )
if origin_platform and origin_chat_id :
2026-03-14 12:21:50 -07:00
return {
2026-02-02 19:01:51 -08:00
" platform " : origin_platform ,
" chat_id " : origin_chat_id ,
" chat_name " : os . getenv ( " HERMES_SESSION_CHAT_NAME " ) ,
2026-03-14 06:06:44 +03:00
" thread_id " : os . getenv ( " HERMES_SESSION_THREAD_ID " ) ,
2026-02-02 19:01:51 -08:00
}
2026-03-14 12:21:50 -07:00
return None
def _repeat_display ( job : Dict [ str , Any ] ) - > str :
times = ( job . get ( " repeat " ) or { } ) . get ( " times " )
completed = ( job . get ( " repeat " ) or { } ) . get ( " completed " , 0 )
if times is None :
return " forever "
if times == 1 :
return " once " if completed == 0 else " 1/1 "
return f " { completed } / { times } " if completed else f " { times } times "
2026-03-14 19:18:10 -07:00
def _canonical_skills ( skill : Optional [ str ] = None , skills : Optional [ Any ] = None ) - > List [ str ] :
if skills is None :
raw_items = [ skill ] if skill else [ ]
elif isinstance ( skills , str ) :
raw_items = [ skills ]
else :
raw_items = list ( skills )
normalized : List [ str ] = [ ]
for item in raw_items :
text = str ( item or " " ) . strip ( )
if text and text not in normalized :
normalized . append ( text )
return normalized
2026-03-14 22:22:31 -07:00
def _normalize_optional_job_value ( value : Optional [ Any ] , * , strip_trailing_slash : bool = False ) - > Optional [ str ] :
if value is None :
return None
text = str ( value ) . strip ( )
if strip_trailing_slash :
text = text . rstrip ( " / " )
return text or None
2026-03-14 12:21:50 -07:00
def _format_job ( job : Dict [ str , Any ] ) - > Dict [ str , Any ] :
prompt = job . get ( " prompt " , " " )
2026-03-14 19:18:10 -07:00
skills = _canonical_skills ( job . get ( " skill " ) , job . get ( " skills " ) )
2026-03-14 12:21:50 -07:00
return {
" job_id " : job [ " id " ] ,
" name " : job [ " name " ] ,
2026-03-14 19:18:10 -07:00
" skill " : skills [ 0 ] if skills else None ,
" skills " : skills ,
2026-03-14 12:21:50 -07:00
" prompt_preview " : prompt [ : 100 ] + " ... " if len ( prompt ) > 100 else prompt ,
2026-03-14 22:22:31 -07:00
" model " : job . get ( " model " ) ,
" provider " : job . get ( " provider " ) ,
" base_url " : job . get ( " base_url " ) ,
2026-03-14 12:21:50 -07:00
" schedule " : job . get ( " schedule_display " ) ,
" repeat " : _repeat_display ( job ) ,
" deliver " : job . get ( " deliver " , " local " ) ,
" next_run_at " : job . get ( " next_run_at " ) ,
" last_run_at " : job . get ( " last_run_at " ) ,
" last_status " : job . get ( " last_status " ) ,
" enabled " : job . get ( " enabled " , True ) ,
" state " : job . get ( " state " , " scheduled " if job . get ( " enabled " , True ) else " paused " ) ,
" paused_at " : job . get ( " paused_at " ) ,
" paused_reason " : job . get ( " paused_reason " ) ,
2026-02-02 08:26:42 -08:00
}
2026-03-14 12:21:50 -07:00
def cronjob (
action : str ,
job_id : Optional [ str ] = None ,
prompt : Optional [ str ] = None ,
schedule : Optional [ str ] = None ,
name : Optional [ str ] = None ,
repeat : Optional [ int ] = None ,
deliver : Optional [ str ] = None ,
include_disabled : bool = False ,
skill : Optional [ str ] = None ,
2026-03-14 19:18:10 -07:00
skills : Optional [ List [ str ] ] = None ,
2026-03-14 22:22:31 -07:00
model : Optional [ str ] = None ,
provider : Optional [ str ] = None ,
base_url : Optional [ str ] = None ,
2026-03-14 12:21:50 -07:00
reason : Optional [ str ] = None ,
task_id : str = None ,
) - > str :
""" Unified cron job management tool. """
del task_id # unused but kept for handler signature compatibility
2026-02-02 08:26:42 -08:00
try :
2026-03-14 12:21:50 -07:00
normalized = ( action or " " ) . strip ( ) . lower ( )
if normalized == " create " :
if not schedule :
return json . dumps ( { " success " : False , " error " : " schedule is required for create " } , indent = 2 )
2026-03-14 19:18:10 -07:00
canonical_skills = _canonical_skills ( skill , skills )
if not prompt and not canonical_skills :
return json . dumps ( { " success " : False , " error " : " create requires either prompt or at least one skill " } , indent = 2 )
2026-03-14 12:21:50 -07:00
if prompt :
scan_error = _scan_cron_prompt ( prompt )
if scan_error :
return json . dumps ( { " success " : False , " error " : scan_error } , indent = 2 )
job = create_job (
prompt = prompt or " " ,
schedule = schedule ,
name = name ,
repeat = repeat ,
deliver = deliver ,
origin = _origin_from_env ( ) ,
2026-03-14 19:18:10 -07:00
skills = canonical_skills ,
2026-03-14 22:22:31 -07:00
model = _normalize_optional_job_value ( model ) ,
provider = _normalize_optional_job_value ( provider ) ,
base_url = _normalize_optional_job_value ( base_url , strip_trailing_slash = True ) ,
2026-03-14 12:21:50 -07:00
)
return json . dumps (
{
" success " : True ,
" job_id " : job [ " id " ] ,
" name " : job [ " name " ] ,
" skill " : job . get ( " skill " ) ,
2026-03-14 19:18:10 -07:00
" skills " : job . get ( " skills " , [ ] ) ,
2026-03-14 12:21:50 -07:00
" schedule " : job [ " schedule_display " ] ,
" repeat " : _repeat_display ( job ) ,
" deliver " : job . get ( " deliver " , " local " ) ,
" next_run_at " : job [ " next_run_at " ] ,
" job " : _format_job ( job ) ,
" message " : f " Cron job ' { job [ ' name ' ] } ' created. " ,
} ,
indent = 2 ,
)
if normalized == " list " :
jobs = [ _format_job ( job ) for job in list_jobs ( include_disabled = include_disabled ) ]
return json . dumps ( { " success " : True , " count " : len ( jobs ) , " jobs " : jobs } , indent = 2 )
if not job_id :
return json . dumps ( { " success " : False , " error " : f " job_id is required for action ' { normalized } ' " } , indent = 2 )
job = get_job ( job_id )
if not job :
return json . dumps (
{ " success " : False , " error " : f " Job with ID ' { job_id } ' not found. Use cronjob(action= ' list ' ) to inspect jobs. " } ,
indent = 2 ,
)
if normalized == " remove " :
removed = remove_job ( job_id )
if not removed :
return json . dumps ( { " success " : False , " error " : f " Failed to remove job ' { job_id } ' " } , indent = 2 )
return json . dumps (
{
" success " : True ,
" message " : f " Cron job ' { job [ ' name ' ] } ' removed. " ,
" removed_job " : {
" id " : job_id ,
" name " : job [ " name " ] ,
" schedule " : job . get ( " schedule_display " ) ,
} ,
} ,
indent = 2 ,
)
if normalized == " pause " :
updated = pause_job ( job_id , reason = reason )
return json . dumps ( { " success " : True , " job " : _format_job ( updated ) } , indent = 2 )
if normalized == " resume " :
updated = resume_job ( job_id )
return json . dumps ( { " success " : True , " job " : _format_job ( updated ) } , indent = 2 )
if normalized in { " run " , " run_now " , " trigger " } :
updated = trigger_job ( job_id )
return json . dumps ( { " success " : True , " job " : _format_job ( updated ) } , indent = 2 )
if normalized == " update " :
updates : Dict [ str , Any ] = { }
if prompt is not None :
scan_error = _scan_cron_prompt ( prompt )
if scan_error :
return json . dumps ( { " success " : False , " error " : scan_error } , indent = 2 )
updates [ " prompt " ] = prompt
if name is not None :
updates [ " name " ] = name
if deliver is not None :
updates [ " deliver " ] = deliver
2026-03-14 19:18:10 -07:00
if skills is not None or skill is not None :
canonical_skills = _canonical_skills ( skill , skills )
updates [ " skills " ] = canonical_skills
updates [ " skill " ] = canonical_skills [ 0 ] if canonical_skills else None
2026-03-14 22:22:31 -07:00
if model is not None :
updates [ " model " ] = _normalize_optional_job_value ( model )
if provider is not None :
updates [ " provider " ] = _normalize_optional_job_value ( provider )
if base_url is not None :
updates [ " base_url " ] = _normalize_optional_job_value ( base_url , strip_trailing_slash = True )
2026-03-14 12:21:50 -07:00
if repeat is not None :
2026-03-23 14:35:43 +01:00
# Normalize: treat 0 or negative as None (infinite)
normalized_repeat = None if repeat < = 0 else repeat
2026-03-14 12:21:50 -07:00
repeat_state = dict ( job . get ( " repeat " ) or { } )
2026-03-23 14:35:43 +01:00
repeat_state [ " times " ] = normalized_repeat
2026-03-14 12:21:50 -07:00
updates [ " repeat " ] = repeat_state
if schedule is not None :
parsed_schedule = parse_schedule ( schedule )
updates [ " schedule " ] = parsed_schedule
updates [ " schedule_display " ] = parsed_schedule . get ( " display " , schedule )
if job . get ( " state " ) != " paused " :
updates [ " state " ] = " scheduled "
updates [ " enabled " ] = True
if not updates :
return json . dumps ( { " success " : False , " error " : " No updates provided. " } , indent = 2 )
updated = update_job ( job_id , updates )
return json . dumps ( { " success " : True , " job " : _format_job ( updated ) } , indent = 2 )
return json . dumps ( { " success " : False , " error " : f " Unknown cron action ' { action } ' " } , indent = 2 )
2026-02-02 08:26:42 -08:00
except Exception as e :
2026-03-14 12:21:50 -07:00
return json . dumps ( { " success " : False , " error " : str ( e ) } , indent = 2 )
2026-02-02 08:26:42 -08:00
2026-03-14 12:21:50 -07:00
# ---------------------------------------------------------------------------
# Compatibility wrappers
# ---------------------------------------------------------------------------
2026-02-02 08:26:42 -08:00
2026-03-14 12:21:50 -07:00
def schedule_cronjob (
prompt : str ,
schedule : str ,
name : Optional [ str ] = None ,
repeat : Optional [ int ] = None ,
deliver : Optional [ str ] = None ,
2026-03-14 22:22:31 -07:00
model : Optional [ str ] = None ,
provider : Optional [ str ] = None ,
base_url : Optional [ str ] = None ,
2026-03-14 12:21:50 -07:00
task_id : str = None ,
) - > str :
return cronjob (
action = " create " ,
prompt = prompt ,
schedule = schedule ,
name = name ,
repeat = repeat ,
deliver = deliver ,
2026-03-14 22:22:31 -07:00
model = model ,
provider = provider ,
base_url = base_url ,
2026-03-14 12:21:50 -07:00
task_id = task_id ,
)
2026-02-02 08:26:42 -08:00
2026-03-14 12:21:50 -07:00
def list_cronjobs ( include_disabled : bool = False , task_id : str = None ) - > str :
return cronjob ( action = " list " , include_disabled = include_disabled , task_id = task_id )
2026-02-02 08:26:42 -08:00
def remove_cronjob ( job_id : str , task_id : str = None ) - > str :
2026-03-14 12:21:50 -07:00
return cronjob ( action = " remove " , job_id = job_id , task_id = task_id )
CRONJOB_SCHEMA = {
" name " : " cronjob " ,
" description " : """ Manage scheduled cron jobs with a single compressed tool.
2026-02-02 08:26:42 -08:00
2026-03-14 19:18:10 -07:00
Use action = ' create ' to schedule a new job from a prompt or one or more skills .
2026-03-14 12:21:50 -07:00
Use action = ' list ' to inspect jobs .
Use action = ' update ' , ' pause ' , ' resume ' , ' remove ' , or ' run ' to manage an existing job .
2026-02-02 08:26:42 -08:00
2026-03-14 12:21:50 -07:00
Jobs run in a fresh session with no current - chat context , so prompts must be self - contained .
2026-03-14 19:18:10 -07:00
If skill or skills are provided on create , the future cron run loads those skills in order , then follows the prompt as the task instruction .
On update , passing skills = [ ] clears attached skills .
2026-02-02 08:26:42 -08:00
2026-03-20 05:18:05 -07:00
NOTE : The agent ' s final response is auto-delivered to the target. Put the primary
user - facing content in the final response . Cron jobs run autonomously with no user
present — they cannot ask questions or request clarification .
2026-02-25 03:29:10 -08:00
2026-03-14 12:21:50 -07:00
Important safety rule : cron - run sessions should not recursively schedule more cron jobs . """ ,
2026-02-02 08:26:42 -08:00
" parameters " : {
" type " : " object " ,
" properties " : {
2026-03-14 12:21:50 -07:00
" action " : {
" type " : " string " ,
" description " : " One of: create, list, update, pause, resume, remove, run "
} ,
2026-02-02 08:26:42 -08:00
" job_id " : {
" type " : " string " ,
2026-03-14 12:21:50 -07:00
" description " : " Required for update/pause/resume/remove/run "
} ,
" prompt " : {
" type " : " string " ,
2026-03-14 19:18:10 -07:00
" description " : " For create: the full self-contained prompt. If skill or skills are also provided, this becomes the task instruction paired with those skills. "
2026-03-14 12:21:50 -07:00
} ,
" schedule " : {
" type " : " string " ,
" description " : " For create/update: ' 30m ' , ' every 2h ' , ' 0 9 * * * ' , or ISO timestamp "
} ,
" name " : {
" type " : " string " ,
" description " : " Optional human-friendly name "
} ,
" repeat " : {
" type " : " integer " ,
" description " : " Optional repeat count. Omit for defaults (once for one-shot, forever for recurring). "
} ,
" deliver " : {
" type " : " string " ,
feat(gateway): add Feishu/Lark platform support (#3817)
Adds Feishu (ByteDance's enterprise messaging platform) as a gateway
platform adapter with full feature parity: WebSocket + webhook transports,
message batching, dedup, rate limiting, rich post/card content parsing,
media handling (images/audio/files/video), group @mention gating,
reaction routing, and interactive card button support.
Cherry-picked from PR #1793 by penwyp with:
- Moved to current main (PR was 458 commits behind)
- Fixed _send_with_retry shadowing BasePlatformAdapter method (renamed to
_feishu_send_with_retry to avoid signature mismatch crash)
- Fixed import structure: aiohttp/websockets imported independently of
lark_oapi so they remain available when SDK is missing
- Fixed get_hermes_home import (hermes_constants, not hermes_cli.config)
- Added skip decorators for tests requiring lark_oapi SDK
- All 16 integration points added surgically to current main
New dependency: lark-oapi>=1.5.3,<2 (optional, pip install hermes-agent[feishu])
Fixes #1788
Co-authored-by: penwyp <penwyp@users.noreply.github.com>
2026-03-29 18:17:42 -07:00
" description " : " Delivery target: origin, local, telegram, discord, slack, whatsapp, signal, matrix, mattermost, homeassistant, dingtalk, feishu, email, sms, or platform:chat_id or platform:chat_id:thread_id for Telegram topics. Examples: ' origin ' , ' local ' , ' telegram ' , ' telegram:-1001234567890:17585 ' , ' discord:#engineering ' "
2026-03-14 12:21:50 -07:00
} ,
2026-03-14 22:22:31 -07:00
" model " : {
" type " : " string " ,
" description " : " Optional per-job model override used when the cron job runs "
} ,
" provider " : {
" type " : " string " ,
" description " : " Optional per-job provider override used when resolving runtime credentials "
} ,
" base_url " : {
" type " : " string " ,
" description " : " Optional per-job base URL override paired with provider/model routing "
} ,
2026-03-14 12:21:50 -07:00
" include_disabled " : {
" type " : " boolean " ,
" description " : " For list: include paused/completed jobs "
} ,
" skill " : {
" type " : " string " ,
2026-03-14 19:18:10 -07:00
" description " : " Optional single skill name to load before executing the cron prompt "
} ,
" skills " : {
" type " : " array " ,
" items " : { " type " : " string " } ,
" description " : " Optional ordered list of skills to load before executing the cron prompt. On update, pass an empty array to clear attached skills. "
2026-03-14 12:21:50 -07:00
} ,
" reason " : {
" type " : " string " ,
" description " : " Optional pause reason "
2026-02-02 08:26:42 -08:00
}
} ,
2026-03-14 12:21:50 -07:00
" required " : [ " action " ]
2026-02-02 08:26:42 -08:00
}
}
def check_cronjob_requirements ( ) - > bool :
"""
Check if cronjob tools can be used .
2026-03-14 12:21:50 -07:00
2026-02-21 12:46:18 -08:00
Available in interactive CLI mode and gateway / messaging platforms .
2026-03-17 01:40:02 -07:00
The cron system is internal ( JSON file - based scheduler ticked by the gateway ) ,
so no external crontab executable is required .
2026-02-02 08:26:42 -08:00
"""
2026-02-21 12:46:18 -08:00
return bool (
os . getenv ( " HERMES_INTERACTIVE " )
or os . getenv ( " HERMES_GATEWAY_SESSION " )
or os . getenv ( " HERMES_EXEC_ASK " )
)
2026-02-02 08:26:42 -08:00
def get_cronjob_tool_definitions ( ) :
""" Return tool definitions for cronjob management. """
2026-03-14 12:21:50 -07:00
return [ CRONJOB_SCHEMA ]
2026-02-21 20:22:33 -08:00
# --- Registry ---
from tools . registry import registry
registry . register (
2026-03-14 12:21:50 -07:00
name = " cronjob " ,
2026-02-21 20:22:33 -08:00
toolset = " cronjob " ,
2026-03-14 12:21:50 -07:00
schema = CRONJOB_SCHEMA ,
handler = lambda args , * * kw : cronjob (
action = args . get ( " action " , " " ) ,
job_id = args . get ( " job_id " ) ,
prompt = args . get ( " prompt " ) ,
schedule = args . get ( " schedule " ) ,
2026-02-21 20:22:33 -08:00
name = args . get ( " name " ) ,
repeat = args . get ( " repeat " ) ,
deliver = args . get ( " deliver " ) ,
include_disabled = args . get ( " include_disabled " , False ) ,
2026-03-14 12:21:50 -07:00
skill = args . get ( " skill " ) ,
2026-03-14 19:18:10 -07:00
skills = args . get ( " skills " ) ,
2026-03-14 22:22:31 -07:00
model = args . get ( " model " ) ,
provider = args . get ( " provider " ) ,
base_url = args . get ( " base_url " ) ,
2026-03-14 12:21:50 -07:00
reason = args . get ( " reason " ) ,
task_id = kw . get ( " task_id " ) ,
) ,
2026-02-21 20:22:33 -08:00
check_fn = check_cronjob_requirements ,
2026-03-15 20:21:21 -07:00
emoji = " ⏰ " ,
2026-02-21 20:22:33 -08:00
)