
    epjz              -          d Z ddlZddlZddlZddlZddlZddlZddlmZ ddl	m
Z
mZmZmZmZ ddlmZ  ej        e          ZdZdZej                            d e ee          j        j                             ddlmZmZmZmZm Z m!Z!m"Z"m#Z#m$Z$m%Z%m&Z&m'Z' dpd
Z(g dZ)g dZ*dZ+de+ dfde+ dfde+ dfde+ dfde+ ddfgZ,ddl-m.Z/ dZ0dZ1de2d	e3fdZ4dede2d	e3fd Z5d!ed	efd"Z6d!ed	efd#Z7d!ed	efd$Z8d!ed	e9ee:e         f         fd%Z;d!ed	efd&Z<d'ed	e9eef         fd(Z=d	eeeef                  fd)Z>d*eee
f         d+ee         d	ee         fd,Z?d*eee
f         d	efd-Z@dqd.ee         d/ee
         d	ee         fd0ZAd1d2d3ee
         d4e3d	ee         fd5ZBd3e
d	ee         fd6ZCd7ee
         d8ee
         d	ee         fd9ZDd:ee         d	ee         fd;ZEd*eee
f         d	eee
f         fd<ZFd*eee
f         d	eee
f         fd=ZG	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 drd>ed?ee         d!ee         d@ee         dAee         dBee2         dCee         dDe3d.ee         d/eee                  dEee         d7ee         d8ee         dFee         d:ee         dGeeeee         f                  dHeee                  dIee         dJee3         dKee3         dLed	ef,dMZHdNdOdPdQdRdSdQdTdSdQdUdSdQdVdSdQdWdSdXdYdSdQdZdSd[d\dQid]d^dQd_ e             d`dSdad1dbdcd[d\dQiddd^d[d\dQided^dQdfdSdadgdSdhd>gdidjZId	e3fdkZJddllKmLZLmMZM  eLjN        dNdNeIdm eJdno           dS )sz
Cron job management tools for Hermes Agent.

Expose a single compressed action-oriented tool to avoid schema/context bloat.
Compatibility wrappers remain for direct Python callers and legacy tests.
    N)Path)AnyDictListOptionalUnion)display_hermes_homeg      $@g     @)AmbiguousJobReferenceclaim_job_for_fire
create_jobget_job	list_jobsmark_job_runparse_schedule	pause_job
remove_jobresolve_job_ref
resume_job
update_jobreturnc                  J    	 ddl m}   |              dS # t          $ r Y dS w xY w)u   Tell the active cron scheduler provider the job set changed (no-op for
    the built-in). Best-effort — never lets a provider error break the tool.r   _notify_provider_jobs_changedN)cron.schedulerr   	Exceptionr   s    9/home/thesage/.hermes/hermes-agent/tools/cronjob_tools.py"_notify_provider_jobs_changed_safer   8   sP    @@@@@@%%'''''   s    
"")zJignore\s+(?:\w+\s+)*(?:previous|all|above|prior)\s+(?:\w+\s+)*instructionsprompt_injectionzdo\s+not\s+tell\s+the\s+userdeception_hidezsystem\s+prompt\s+overridesys_prompt_overridez<disregard\s+(your|all|any)\s+(instructions|rules|guidelines)disregard_rules)zKcat\s+[^\n]*(\.env|credentials|\.netrc|\.pgpass|id_rsa|id_ed25519|id_ecdsa)read_secrets)authorized_keysssh_backdoor)z/etc/sudoers|visudosudoers_mod)zrm\s+-rf\s+/destructive_root_rm)r   r    r"   r$   z:\$\{?\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)\w*\}?z curl\s+[^\n]*https?://[^\s"\'`]*exfil_curl_urlz wget\s+[^\n]*https?://[^\s"\'`]*exfil_wget_urlzIcurl\s+[^\n]*(?:--data(?:-raw|-binary|-urlencode)?|-d|--form|-F)\s+[^\n]*exfil_curl_dataz(wget\s+[^\n]*--post-(?:data|file)=[^\n]*exfil_wget_postzHcurl\s+[^\n]*(?:-H|--header)\s+["\']Authorization:\s*(?:Bearer|token)\s+z["\']exfil_curl_auth_header)INVISIBLE_CHARS))i  i )i &  i'  )i #  i#  )i i )   r1   i  cpc                 D     t           fdt          D                       S )Nc              3   >   K   | ]\  }}|cxk    o|k    nc V  d S )N ).0lohir2   s      r   	<genexpr>z_is_emoji_cp.<locals>.<genexpr>   s;      GG&"brR~~~~2~~~~GGGGGG    )any_EMOJI_NEIGHBOUR_CP_RANGES)r2   s   `r   _is_emoji_cpr=      s'    GGGG,FGGGGGGr:   textidxc                 8   |dz
  }|dk    rGt          | |                   t          k    r)|dz  }|dk    rt          | |                   t          k    )|dz   }|t          |           k     rTt          | |                   t          k    r6|dz  }|t          |           k     rt          | |                   t          k    6|dk    oV|t          |           k     oCt          t          | |                             o!t          t          | |                             S )zGReturn True when the ZWJ at text[idx] appears inside an emoji sequence.   r   )ord_VARIATION_SELECTOR_CPlenr=   )r>   r?   leftrights       r   _zwj_has_emoji_neighbourrG      s   7D
!))DJ+AAA	 !))DJ+AAA!GE
#d))

DK 0 04J J J
 #d))

DK 0 04J J J 		 	+ec$ii' 	+T$Z))	+T%[))**r:   promptc                     d| vr| S g }t          |           D ]1\  }}|dk    rt          | |          r|                    |           2d                    |          S )N   ‍ )	enumeraterG   appendjoin)rH   cleanedr?   chs       r   _strip_legitimate_emoji_zwjrQ      st    vGV$$  R>>6vsCC>r777r:   c                 X    t          j        dt           dd| t           j                  S )u  Strip the GitHub `Authorization: token $GITHUB_TOKEN` auth-header
    pattern so it doesn't trip the broader curl-auth-header exfil rule.

    Allows the bundled GitHub skill fallback without opening a blanket
    exemption for arbitrary Authorization-header exfiltration.

    Uses ``re.sub`` so EVERY occurrence is scrubbed, not just the first — a
    cron job that loads 2+ GitHub skills (e.g. github-issues +
    github-pr-workflow + github-code-review) contains several such blocks,
    and the old ``re.search`` + single ``str.replace`` left the rest to trip
    the exfil_curl_auth_header detector on every run. The trailing
    ``[^\s;&|$`]*`` consumes only the URL path — never whitespace, command
    separators, or subshell openers — so a payload smuggled onto the same
    line (``;``, ``&&``, ``|``, ``$(...)``, backticks) survives the strip
    and is still scanned. The host must be exactly ``api.github.com``
    followed by ``/``, whitespace, quote, or end: lookalike authorities
    (``api.github.com.evil.com``, ``api.github.com@evil.com``) are not the
    trusted construct and fall through to the exfil detectors, while
    legitimately quoted bare-host URLs stay exempt.
    zBcurl\s+[^\n;&|$`]*(?:-H|--header)\s+["\']Authorization:\s*token\s+zJ["\']\s+["\']?https://api\.github\.com(?::\d+)?(?:/|\s|$|["\'])[^\s;&|$`]*z curl https://api.github.com/user)flags)resub_CRON_SECRET_VAR_RE
IGNORECASE)rH   s    r   _strip_cron_safe_constructsrX      sC    * 6	QNa 	Q 	Q 	Q*m   r:   c                 l    t          |           }t          D ]}||v rdt          |          ddc S dS )zReturn an error string if the prompt contains invisible-unicode
    injection markers (ZWJ inside legitimate emoji sequences is allowed).
    z-Blocked: prompt contains invisible unicode U+04Xz (possible injection).rK   )rQ   _CRON_INVISIBLE_CHARSrB   )rH   prompt_for_invisible_scanchars      r   _check_invisible_unicoder^      sY     !<F C C% i i,,,h3t99hhhhhhh -2r:   c                    | s| g fS t                      }g }t          |           D ]v\  }}|t          v rS|dk    r&t          | |          r|                    |           :|                    dt          |          d           a|                    |           wd                    |          t          |          fS )u>  Strip invisible-unicode characters from *prompt*, preserving the ZWJ
    that lives inside legitimate emoji sequences.

    Returns ``(cleaned_prompt, removed_codepoints)`` where ``removed_codepoints``
    is the sorted list of ``U+XXXX`` labels that were stripped (empty when the
    prompt was already clean). Used by the skills-attached cron path, where the
    skill body is already vetted at install time by ``skills_guard.py`` — a
    stray zero-width space in a code example should be sanitized, not turned
    into a hard block that permanently kills the job.
    rJ   zU+rZ   rK   )	setrL   r[   rG   rM   addrB   rN   sorted)rH   removedrO   r?   rP   s        r   _strip_invisible_unicoderd      s      rz GGV$$  R&&&X~~":63"G"G~r"""KK*SWW***+++r777VG__,,r:   c                    t          |           }t          |          }|r|S t          D ]-\  }}t          j        ||t          j                  rd| dc S .t          D ]-\  }}t          j        ||t          j                  rd| dc S .dS )u}  Scan the USER-SUPPLIED cron prompt for critical threats.

    Strict pattern set — used at job create/update time and as a runtime
    defense-in-depth for prompts authored before the scanner existed.
    The user prompt is small and directive; bare `cat .env` or `rm -rf /`
    there is a smoking gun, not prose. Returns an error string when
    blocked, else empty string.
    (Blocked: prompt matches threat pattern 'D'. Cron prompts must not contain injection or exfiltration payloads.rK   )rX   r^   _CRON_THREAT_PATTERNSrT   searchrW   _CRON_EXFIL_COMMAND_PATTERNS)rH   prompt_to_scaninvisible_errpatternpids        r   _scan_cron_promptro     s     188N,^<<M - H H9Wnbm<< 	H Hc  H  H  H  H  H  H	H4 H H9Wnbm<< 	H Hc  H  H  H  H  H  H	H2r:   	assembledc                 6   t          |           \  }}|r<t                              dt          |          d                    |                     t          |          }t          D ]/\  }}t          j        ||t          j	                  r
|d| dfc S 0|dfS )u  Scan an ASSEMBLED cron prompt that includes loaded skill content.

    Looser pattern set — only catches unambiguous prompt-injection
    directives. Drops command-shape patterns (cat .env, rm -rf /,
    authorized_keys, /etc/sudoers) because they false-positive on
    legitimate skill markdown that *describes* attack commands in
    security postmortems and runbooks.

    Invisible unicode is SANITIZED, not blocked. Skill bodies are
    user-curated and already scanned at install time by
    ``skills_guard.py``; a stray zero-width space in a code example
    (common in copy-pasted unicode docs) should not permanently kill the
    job. The offending codepoints are stripped and logged, the cleaned
    prompt is returned. The hard block remains for raw user prompts via
    ``_scan_cron_prompt`` — that path is the actual injection surface.

    Returns ``(cleaned_prompt, error)``; ``error`` is empty when the
    prompt passed (after sanitization).
    zaCron skill-assembled prompt: stripped %d invisible-unicode char(s) (%s) from vetted skill contentz, rf   rg   rK   )
rd   loggerwarningrD   rN   rX   _CRON_SKILL_ASSEMBLED_PATTERNSrT   ri   rW   )rp   rO   rc   rk   rm   rn   s         r   _scan_cron_skill_assembledru     s    ( 0	::GW 
5LL$))G,,	
 	
 	

 199N6 Q Q9Wnbm<< 	Q  Qs  Q  Q  Q  Q  Q  Q  Q	QB;r:   c                      ddl m}   | d          } | d          }|rL|rJ | d          pd }|rt                              d|||           || | d          pd | | d          pd d	S d S )
Nr   )get_session_envHERMES_SESSION_PLATFORMHERMES_SESSION_CHAT_IDHERMES_SESSION_THREAD_IDz+Cron origin captured thread_id=%s for %s:%sHERMES_SESSION_CHAT_NAMEHERMES_SESSION_USER_ID)platformchat_id	chat_name	thread_iduser_id)gateway.session_contextrw   rr   debug)rw   origin_platformorigin_chat_idr   s       r   _origin_from_envr   ;  s    777777%o&?@@O$_%=>>N 
> 
#O$>??G4	 	LL=?N  
 (%()CDDL" ''?@@HD
 
 	
 4r:   jobuser_deliverc                     |pd                                                                 dk    rdS 	 ddlm}  ||           rdS n(# t          $ r |                     d          rY dS Y nw xY w	 dS )uU  Return an informational notice when a created job won't deliver anywhere.

    TUI/CLI sessions cannot be captured as a cron ``origin`` (no
    ``HERMES_SESSION_PLATFORM``/``CHAT_ID`` is set for them), so a
    ``deliver="origin"`` request — or an omitted ``deliver`` that defaults to
    origin-or-local — produces a job that runs and saves output to
    ``last_output`` but is never delivered back into the session. This is by
    design (there is no live-delivery channel for local sessions), but silently
    dropping the user's "tell me when it runs" intent is the trap reported in
    #51568. Surface it at create time so the agent can relay it instead of
    promising a delivery that never happens.

    Returns ``None`` when the user explicitly asked for ``local`` (no surprise),
    or when the job resolves to a real delivery target.
    rK   localNr   )_resolve_delivery_targetsoriginuQ  This is a local-only cron job: its output is saved (view it with cronjob(action='list')) but will NOT be delivered back into this session — CLI/TUI sessions have no live-delivery channel. To be notified when it runs, recreate or update the job with deliver set to a gateway-connected platform, e.g. deliver='telegram' or deliver='all'.)striplowerr   r   r   get)r   r   r   s      r   _local_delivery_noticer   U  s    " 	!!##))++w66t<<<<<<$$S)) 	4	   778 	44	 	
	R s   A A)(A)c                     |                      d          pi                      d          }|                      d          pi                      dd          }|dS |dk    r
|dk    rdndS |r| d	| n| d
S )Nrepeattimes	completedr   foreverrA   oncez1/1/z timesr   )r   r   r   s      r   _repeat_displayr   z  s    WWX$"))'22E""(b--k1==I}yzz"avvU2%.Di!!%!!!u4D4D4DDr:   skillskillsc                     || r| gng }n(t          |t                    r|g}nt          |          }g }|D ]@}t          |pd                                          }|r||vr|                    |           A|S )NrK   )
isinstancestrlistr   rM   )r   r   	raw_items
normalizeditemr>   s         r   _canonical_skillsr     s    ~$,UGG"			FC	 	  !H		LL	J $ $4:2$$&& 	$D
**d###r:   Fstrip_trailing_slashvaluer   c                    | d S t          |                                           }|r|                    d          }|pd S )Nr   )r   r   rstrip)r   r   r>   s      r   _normalize_optional_job_valuer     sF    }tu::D  {{3<4r:   c                     | dS t          | t          t          f          r%d | D             }|rd                    |          ndS t	          |                                           }|pdS )u  Normalize a user-supplied ``deliver`` value to the canonical string form.

    The cron schema documents ``deliver`` as a string (``"local"``, ``"origin"``,
    ``"telegram"``, ``"telegram:chat_id[:thread_id]"``, or comma-separated combos).
    Some callers — MCP clients passing arrays, scripts building the payload as a
    list — supply ``["telegram"]``.  ``create_job``/``update_job`` store it as-is,
    and the scheduler's ``str(deliver).split(",")`` then serializes the list to
    the literal ``"['telegram']"`` which is not a known platform.  Flatten lists
    / tuples at the API boundary so storage is always a string.  Returns ``None``
    for ``None``/empty so callers can treat it as "not supplied".
    Nc                     g | ]D}t          |                                          #t          |                                          ES r5   r   r   )r6   ps     r   
<listcomp>z,_normalize_deliver_param.<locals>.<listcomp>  s9    AAAA#a&&,,..AQAAAr:   ,)r   r   tuplerN   r   r   )r   partsr>   s      r   _normalize_deliver_paramr     sq     }t%$'' 2AAAAA"'1sxxT1u::D<4r:   providerbase_urlc                    t          |d          }|sdS t          |           }|s	 dS 	 ddlm}m}m} ddlm} ddlm}m	}	 n# t          $ r	 d	|d
cY S w xY w|                                dk    rdS  ||          rb	  ||          }
n# t          $ r d}
Y nw xY w|
r! |	|
pi                     dd                    nd}|r |||          rdS d|d|d|pd dS 	  ||          }n# t          $ r |}Y nw xY wt          |t                    r|                    |          nd} |	|rt          |dd          nd          }|r |||          rdS d|d|dS )a  Reject pairing a named provider's stored credential with an off-host base_url.

    The cron tool is model-callable, so a prompt-injected job could set a real
    provider plus an attacker ``base_url``; on fire the scheduler resolves that
    provider's stored API key and sends it to the URL, exfiltrating the
    credential (CWE-200/CWE-522). Allow a ``base_url`` override only when it
    cannot leak a stored secret: no override at all, a configured custom/byok
    provider that carries its own endpoint+key, or an override whose host
    matches the named provider's own endpoint.

    Returns an error string if blocked, else None (valid).
    Tr   Nzwbase_url override requires an explicit provider. Set provider to a configured custom provider to use a custom endpoint.r   )has_named_custom_providerresolve_requested_provider_get_named_custom_provider)PROVIDER_REGISTRY)base_url_host_matchesbase_url_hostnamez2Unable to validate base_url override for provider z
; refused.customr   rK   z	base_url z is not allowed for provider z_. A named custom provider's stored credential may only be sent to its own configured endpoint (unknownz).inference_base_urlz. A named provider's stored credential may only be sent to its own endpoint; use a configured custom provider (provider="custom") for a custom base_url.)r   hermes_cli.runtime_providerr   r   r   hermes_cli.authr   utilsr   r   r   r   r   r   r   getattr)r   r   buprovr   r   r   r   r   r   r2   cfg_hostresolvedpconfig
known_hosts                  r   _validate_cron_base_urlr     s    
'xd	K	K	KB t(22D 

C	
 	

W	
 	
 	
 	
 	
 	
 	
 	
 	
 	

 	655555BBBBBBBBB W W WVDVVVVVVW zz||x t  && 

	++D11BB 	 	 	BBB	HJR$$bhB^^J%C%CDDDPR 	--b(;; 	4> > >4 > >$,$9	> > >	

--d33   1;Hc1J1JT##H---PTG""QX#`774H"#M#M#M^`aaJ ++B
;; t	WB 	W 	Wt 	W 	W 	Ws5   A AA B BB"C. .C=<C=scriptc                 ^   | r|                                  sdS ddlm} |                                  }|                    d          st	          |          dk    r|d         dk    rd|d	S dd
lm}  |            dz  }|                    dd            |||z  |          }|rd|S dS )a3  Validate a cron job script path at the API boundary.

    Scripts must be relative paths that resolve within HERMES_HOME/scripts/.
    Absolute paths and ~ expansion are rejected to prevent arbitrary script
    execution via prompt injection.

    Returns an error string if blocked, else None (valid).
    Nr   )get_hermes_home)r   ~   rA   :zXScript path must be relative to ~/.hermes/scripts/. Got absolute or home-relative path: z@. Place scripts in ~/.hermes/scripts/ and use just the filename.)validate_within_dirscriptsT)parentsexist_okz9Script path escapes the scripts directory via traversal: )r   hermes_constantsr   
startswithrD   tools.path_securityr   mkdir)r   r   rawr   scripts_dircontainment_errors         r   _validate_cron_script_pathr     s      t000000
,,..C ~~j!! 
c#hh!mmA#N36N N N	
 877777!/##i/KdT222++K#,={KK 
OOO	
 4r:   c           	      x   t          |                     d          pd          }t          |                     d          |                     d                    }t          |                     d          pd          }t          |                     d          p|d d         p|r|d	         ndp|pd
          }i d|d|d|r|d	         nd d|dt          |          dk    r|d d         dz   n|d|                     d          d|                     d          d|                     d          d|                     d          pddt	          |           d|                     dd          d|                     d          d|                     d          d|                     d          d|                     d          d|                     dd          d|                     d|                     dd          rdnd           |                     d!          |                     d"          d#}|                     d$          r| d$         |d$<   |                     d%          rd|d%<   |                     d&          r| d&         |d&<   |                     d'          r| d'         |d'<   |S )(NrH   rK   r   r   idr   name2   r   zcron jobjob_idprompt_previewd   z...modelr   r   scheduleschedule_display?r   deliverr   next_run_atlast_run_atlast_statuslast_delivery_errorenabledTstate	scheduledpaused	paused_atpaused_reason)r   r   r   no_agentenabled_toolsetsworkdir)r   r   r   rD   r   )r   rH   r   r   r   results         r   _format_jobr   +  s   ""(b))Fswww//1B1BCCF+),,Fswwvf&"+fv2M&))2fRXf\fggD& 	f.$ 	&	
 	#f++2C2C&#,.. 	!! 	CGGJ'' 	CGGJ'' 	CGG.//63 	/#&& 	3779g.. 	sww}-- 	sww}-- 	sww}-- 	sww'<==  	3779d++!" 	D1I1I"W++xXX#$ WW[))11'  F* wwx )x=x
wwz "!z
ww!"" =%();%<!"
wwy +	NyMr:   c                    | d         }	 ddl m} t          |          sNt          |          }|d}n4|                    dd          r|                    d          d	k    rd
}nd}dd|dS 	 ddlm}  |            n# t          $ r dY nw xY wt          j	                    d}Wt          |                     d          p|          dfd}t          j        |dd          }|                                 	  ||           }                                 ||                    t          dz              n:#                                  ||                    t          dz              w w xY wt          |          pi }|                    d          dk    }	dt!          |o|	          |                    d          dS # t          $ ri}
t"                              d||
           	 t'          |dt          |
                     n# t          $ r Y nw xY wddt          |
          dcY d}
~
S d}
~
ww xY w)u  Execute a cron job immediately, outside the scheduler tick.

    Atomically claims the job first via ``claim_job_for_fire`` — the same
    at-most-once CAS the scheduler/external-provider fire path uses — so a
    concurrently-running gateway ticker cannot also fire it (the claim both
    blocks a duplicate fire and advances ``next_run_at`` for recurring jobs).
    If the claim is lost (another fire is in flight), this is a no-op.

    The actual firing is delegated to ``run_one_job`` — the single shared
    execute→save→deliver→mark body the ticker and external providers use — so
    failure delivery, ``[SILENT]`` handling, and live-adapter delivery stay
    identical across paths and can't drift.

    Returns {"claimed": bool, "success": bool, "error": str|None}.
    r   r   )run_one_jobNz%Job no longer exists; nothing to run.r   Tr   r   z1Job is paused/disabled; resume it before running.z;Job is already being fired by the scheduler; not run again.F)claimedsuccesserror)get_activity_callbackr   r   c                  z   t          j                    }                     t                    st          j                    | z
  }|t          k    rt
                              d|           d S 	  d dt          |           d           n# t          $ r Y w xY w                    t                    d S d S )Nuu   cronjob run heartbeat ceiling reached for job '%s' (%.0fs) — stopping heartbeat; gateway watchdog regains authorityzcronjob: running job 'z' (z
s elapsed))	time	monotonicwait_CRON_RUN_HEARTBEAT_INTERVAL_CRON_RUN_HEARTBEAT_CEILINGrr   rs   intr   )startedelapsed_heartbeat_stopactivity_cbjob_names     r   _heartbeat_loopz)_execute_job_now.<locals>._heartbeat_loop  s    .**)../KLL !"n..8G!<<< 9 %g	   !#ZXZZ#g,,ZZZ    % ! ! ! !	!! *../KLL ! ! ! ! !s   /B 
BBzcronjob-run-heartbeat)targetdaemonr   rA   )timeoutr   ok
last_errorz-Failed to execute cron job %s immediately: %sr   N)r   r   r   r   r   tools.environments.baser   r   	threadingEventr   Threadstartr`   rN   r  boolrr   r   r   )r   r   r   	refreshedreasonr   _heartbeat_threadr	  	processedr  er  r  r  s              @@@r   _execute_job_nowr  P  s	     YFbD...... "&)) 	I
  I @]]9d33 Wy}}W7M7MQY7Y7YLV$HHH	EEEEEE
 0/11KK 	 	 	KKK	 $/++ "3776??4f55H! ! ! ! ! ! ! !0 !* 0&,! ! !
 ##%%%	Q#C((I!!! ,!&&/Ka/O&PPP !!! ,!&&/Ka/O&PPPP -FOO)r	]]=))T1I,"--]]<00
 
 	
  D D DDfaPPP	A//// 	 	 	D	ECFFCCCCCCCCDs   A"G 1B G BG BA1G E 5G 7E::AG 
IH=6HH=
H"H=!H""H=7I=Iactionr   r   r   r   r   include_disabledr   r  context_fromr   r   r   attach_to_sessiontask_idc                 F   ~	 | pd                                                                 }|dk    rz|st          dd          S t          ||	          }t	          |          }|r|st          dd          S n|s|st          dd          S |r"t          |          }|rt          |d          S |r"t          |          }|rt          |d          S t          ||          }|rt          |d          S |rGdd	lm	} t          |t                    r|gn|}|D ]$} ||          st          d
| dd          c S %t          dRi d|pdd|d|d|dt          |          dt                      d|dt          |
          dt          |          dt          |d          dt          |          d|d|pddt          |          d|d|}t!                       d|d          d }t#          |t          |                    } | r| d!|  }t%          j        d|d"         |d         |                    d#          |                    dg           |d$         t+          |          |                    dd%          |d&         t-          |          |d'd()          S |d*k    rAd+ t/          |,          D             }!t%          j        dt1          |!          |!d-d()          S |st          d.| d/d          S 	 t3          |          }nM# t4          $ r@}"t%          j        dt          |"          d0 |"j        D             d1d()          cY d}"~"S d}"~"ww xY w|st%          j        dd2| d3d4d()          S |d"         }|d5k    rut9          |          }#|#st          d6| d/d          S t!                       t%          j        dd|d          d7||d         |                    d$          d8d9d()          S |d:k    rEt;          ||;          }$t!                       t%          j        dt-          |$          d<d()          S |d=k    rCt=          |          }$t!                       t%          j        dt-          |$          d<d()          S |d>v rt?          |          }%|%                    d?d          rt!                       t-          t          |          pd"|i          }&|%                    d?d          |&d@<   |%                    dAd          |&dB<   |%                    d?d          s|%                    dC          pdD|&dE<   n |%                    dC          r|%dC         |&dF<   t%          j        d|&d<d()          S |dGk    rAi }'|'t          |          }|rt          |d          S ||'d<   |||'d<   |t          |          |'d<   |	|$t          ||	          }||'d<   |r|d         nd|'d#<   |
t          |
          |'d<   |t          |          |'d<   |t          |d          |'d<   d|'v r|'d         n|                    d          }(d|'v r|'d         n|                    d          })t          |(|)          }|rt          |d          S |:|r"t          |          }|rt          |d          S |rt          |          nd|'d<   |t          |t                    r,|                                 r|                                 gng }ndH |D             }|r-dd	lm	} |D ]$} ||          st          d
| dd          c S %|pd|'d<   ||pd|'d<   |t	          |          |'d<   |t          |          pd|'d<   |Wt	          |          }*|*rAd|'v r|'                    d          n|                    d          }+|+st          dId          S |*|'d<   |8|dk    rdn|},tA          |                    d          pi           }-|,|-dJ<   |-|'d<   |PtC          |          }.|.|'d<   |.                    dK|          |'d$<   |                    dL          dMk    r
dN|'dL<   d|'dO<   |'st          dPd          S tE          ||'          }$t!                       t%          j        dt-          |$          d<d()          S t          dQ|  d/d          S # tF          $ r(}/t          t          |/          d          cY d}/~/S d}/~/ww xY w)Sz!Unified cron job management tool.rK   createzschedule is required for createF)r   uF   create with no_agent=True requires a script — the script is the job.z3create requires either prompt or at least one skillr   )r   zcontext_from job 'z>' not found. Use cronjob(action='list') to see available jobs.rH   r   r   r   r   r   r   r   r   r   Tr   r   r  r   Nr   r   r  z
Cron job 'z
' created. r   r   r   r   r   )r   r   r   r   r   r   r   r   r   r   messager   )indentr   c                 ,    g | ]}t          |          S r5   )r   )r6   r   s     r   r   zcronjob.<locals>.<listcomp><  s     ]]]K$$]]]r:   )r  )r   countjobszjob_id is required for action ''c                     g | ]H}|d          |                     d          |                     d          |                     d          dIS )r   r   r   r   )r   r   r   r   r   )r6   ms     r   r   zcronjob.<locals>.<listcomp>I  sd            #$D'$%EE&MM().@(A(A+,55+?+?	      r:   )r   r   matcheszJob with ID or name 'z8' not found. Use cronjob(action='list') to inspect jobs.)r   r   removezFailed to remove job 'z
' removed.)r   r   r   )r   r$  removed_jobpause)r  )r   r   resume>   runrun_nowtriggerr   executedr   execution_successr   z4Already being fired by the scheduler; not run again.execution_skippedexecution_errorupdatec                     g | ]D}t          |                                          #t          |                                          ES r5   r   )r6   js     r   r   zcronjob.<locals>.<listcomp>  s9    SSSqCFFLLNNSCFFLLNNSSSr:   ziCannot set no_agent=True on a job without a script. Set `script` in the same update, or on the job first.r   displayr   r   r   r   zNo updates provided.zUnknown cron action 'r5   )$r   r   
tool_errorr   r  ro   r   r   	cron.jobsr   r   r   r   r   r   r   r   r   jsondumpsr   r   r   r   rD   r   r
   r,  r   r   r   r  dictr   r   r   )0r  r   rH   r   r   r   r   r  r   r   r   r   r   r  r   r  r   r   r   r  r   r   canonical_skills	_no_agent
scan_errorscript_errorbase_url_error_get_jobrefsref_idr   _create_message_local_noticer(  excrc   updatedexec_resultr   updateseff_providereff_base_urltarget_no_agenteffective_scriptnormalized_repeatrepeat_stateparsed_scheduler  s0                                                   r   cronjobrV    s   0 	_1l))++1133
!! T!"CUSSSS0??XI  h %1 %     h$4 h!"Wafgggg A.v66
 A%j%@@@@  C9&AA C%lEBBBB 5XxHHN A!.%@@@@  	999999)3L#)F)FX~~L"  F#8F++ )P P P P$)            |! T v	
 1999 ())) (' 4E::: 7x@@@ 7xVZ[[[[ 5V<<< *\ "2!9T 6g>>> #  #4"3!C$ /000B3v;BBBO238PQX8Y8YZZM G%4"F"F}"F"F:#!$iK WWW--!ggh33 #$6 7-c22"wwy'::#&}#5&s++.     " ]]	K[0\0\0\]]]D:$TDQQZ[\\\\ 	^M
MMMW\]]]]	!&))CC$ 	 	 	:$ XX    "%               	"  	:!  -EF  -E  -E  -E  F  F   
 T!! ((G U!"D6"D"D"DeTTTT.000:#CCKCCC$ #F$'GG,>$?$?$ $        v666G.000:${77K7KLLUVWWWW!! ((G.000:${77K7KLLUVWWWW666 +3//K
 y%00 52444 !BT6NCCF!,E!B!BF:*5//)U*K*KF&'??9e44 A.9oog.F.F /J *++ )) A,7,@():$v>>qIIII!!&(G!.v66
 A%j%@@@@$*!"&"%=g%F%F	"!U%6#4UF#C#C $4!:J#T#3A#6#6PT  #@#G#G #&CH&M&M
##&CHcg&h&h&h
# (2W'<'<
###''*BUBU  (2W'<'<
###''*BUBU  5\<PPN A!.%@@@@! G#=f#E#EL# G),FFFFMS$]$A&$I$I$IY]!' lC00 T5A5G5G5I5IQL..0011rDDSSLSSSD ======"&  'x// #-!TV !T !T !T(-$ $ $    +/,$'+.>.F$*+ ,/34E/F/F+," &C7%K%K%St	"# #'x.." @HG@S@Sw{{8'<'<'<Y\Y`Y`aiYjYj$+ )T$)      
 '6
#!,2aKKDDV!#CGGH$5$5$;<<(9W%$0!#"0":":&5
#.=.A.A)X.V.V*+777##x//'2GG$)-GI& I!"8%HHHH 11G.000:${77K7KLLUVWWWW;&;;;UKKKK 1 1 1#a&&%0000000001s   Ac. 3c. 9c. #c. 3#c. "c. :Ac. E)c. ,Ac. 3c. L c. 
M%%5M M%c.  M%%!c. 3c. ;Ac. 
A
c. Ac. C2c. ,c. >C*c. )%c. Bc. "Bc. 1B#c. Ac. c. .
d 8dd d rV  u  Manage scheduled cron jobs with a single compressed tool.

Use action='create' to schedule a new job from a prompt or one or more skills.
Use action='list' to inspect jobs.
Use action='update', 'pause', 'resume', 'remove', or 'run' to manage an existing job.

To stop a job the user no longer wants: first action='list' to find the job_id, then action='remove' with that job_id. Never guess job IDs — always list first.

Jobs run in a fresh session with no current-chat context, so prompts must be self-contained.
If 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.

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.

Important safety rule: cron-run sessions should not recursively schedule more cron jobs.objectstringz~One of: create, list, update, pause, resume, remove, run. When action=create, the 'schedule' and 'prompt' fields are REQUIRED.)typedescriptionz+Required for update/pause/resume/remove/runzFor create: the full self-contained prompt. If skills are also provided, this becomes the task instruction paired with those skills.a  REQUIRED for action=create. For create/update: '30m', 'every 2h', '0 9 * * *', or ISO timestamp. Examples: '30m' (every 30 minutes), 'every 2h' (every 2 hours), '0 9 * * *' (daily at 9am), '2026-06-01T09:00:00' (one-shot). You MUST include this field when action=create.zOptional human-friendly nameintegerzTOptional repeat count. Omit for defaults (once for one-shot, forever for recurring).a  Omit this parameter to auto-deliver back to the current chat and topic (recommended). Auto-detection preserves thread/topic context. Only set explicitly when the user asks to deliver somewhere OTHER than the current conversation. Values: 'origin' (same as omitting), 'local' (no delivery, save only), 'all' (fan out to every connected home channel), or platform:chat_id:thread_id for a specific destination. Combine with comma: 'origin,all' delivers to the origin plus every other connected channel. Examples: 'telegram:-1001234567890:17585', 'discord:#engineering', 'sms:+15551234567', 'all'. WARNING: 'platform:chat_id' without :thread_id loses topic targeting. 'all' resolves at fire time, so a job created before a channel was wired up will pick it up automatically once connected.arrayrY  zOptional ordered list of skill names to load before executing the cron prompt. On update, pass an empty array to clear attached skills.)rY  itemsrZ  a3  Optional path to a script that runs each tick. In the default mode its stdout is injected into the agent's prompt as context (data-collection / change-detection pattern). With no_agent=True, the script IS the job and its stdout is delivered verbatim (classic watchdog pattern). Relative paths resolve under zx/scripts/. ``.sh``/``.bash`` extensions run via bash, everything else via Python. On update, pass empty string to clear.booleanu_  Default: False (LLM-driven job — the agent runs the prompt each tick). Set True to skip the LLM entirely: the scheduler just runs ``script`` on schedule and delivers its stdout verbatim. No tokens, no agent loop, no model override honoured. 

REQUIREMENTS when True: ``script`` MUST be set (``prompt`` and ``skills`` are ignored). 

DELIVERY SEMANTICS when True: (a) non-empty stdout is sent verbatim as the message; (b) EMPTY stdout means SILENT — nothing is sent to the user and they won't see anything happened, so design your script to stay quiet when there's nothing to report (the watchdog pattern); (c) non-zero exit / timeout sends an error alert so a broken watchdog can't fail silently. 

WHEN TO USE True: recurring script-only pings where the script itself produces the exact message text (memory/disk/GPU watchdogs, threshold alerts, heartbeats, CI notifications, API pollers with a fixed output shape). WHEN TO USE False (default): anything that needs reasoning — summarize a feed, draft a daily briefing, pick interesting items, rephrase data for a human, follow conditional logic based on content.)rY  defaultrZ  u  Optional job ID or list of job IDs whose most recent completed output is injected into the prompt as context before each run. Use this to chain cron jobs: job A collects data, job B processes it. Each entry must be a valid job ID (from cronjob action='list'). Note: injects the most recent completed output — does not wait for upstream jobs running in the same tick. On update, pass an empty array to clear.u  Optional list of toolset names to restrict the job's agent to (e.g. ["web", "terminal", "file", "delegation"]). When set, only tools from these toolsets are loaded, significantly reducing input token overhead. When omitted, all default tools are loaded. Infer from the job's prompt — e.g. use "web" if it calls web_search, "terminal" if it runs scripts, "file" if it reads files, "delegation" if it calls delegate_task. On update, pass an empty array to clear.u1  Optional absolute path to run the job from. When set, AGENTS.md / CLAUDE.md / .cursorrules from that directory are injected into the system prompt, and the terminal/file/code_exec tools use it as their working directory — useful for running a job inside a specific project repo. Must be an absolute path that exists. When unset (default), preserves the original behaviour: no project context files, tools use the scheduler's cwd. On update, pass an empty string to clear. Jobs with workdir run sequentially (not parallel) to keep per-job directories isolated.u  When True, this job becomes CONTINUABLE: the user can reply to its delivery and the agent has the brief in context instead of asking 'what is that?'. On thread-capable platforms (Telegram topics, Discord/Slack threads) a dedicated thread is opened for the job and its replies; on DM-only platforms (WhatsApp/Signal) the brief is mirrored into the origin DM session. Use this for conversational recurring jobs the user will reply to — daily briefings, reminders that kick off follow-up work. Leave unset for fire-and-forget alerts/watchdogs. Overrides the global cron.mirror_delivery config for this one job. Only the origin chat is touched (never fan-out targets); no effect when deliver='local'.)r  r   rH   r   r   r   r   r   r   r   r  r   r   r  )rY  
propertiesrequired)r   rZ  
parametersc                  P    ddl m}   | d          p | d          p
 | d          S )u  
    Check if cronjob tools can be used.

    Available in interactive CLI mode and gateway/messaging platforms.
    The cron system is internal (JSON file-based scheduler ticked by the gateway),
    so no external crontab executable is required.

    Session env vars must hold an explicit truthy string (``1``, ``true``,
    ``yes``, ``on``) — false-like values (``0``, ``false``, ``no``, ``off``)
    leave the tool disabled. Uses the shared ``env_var_enabled`` helper so
    every consumer of these flags agrees on the truthy set.
    r   env_var_enabledHERMES_INTERACTIVEHERMES_GATEWAY_SESSIONHERMES_EXEC_ASK)r   re  rd  s    r   check_cronjob_requirementsri  p  sQ     &%%%%% 	,-- 	.?344	.?,--r:   )registryr<  c           	         t          di d|                     dd          d|                     d          d|                     d          d|                     d          d|                     d          d|                     d          d|                     d          d	|                     d	d
          d|                     d          d|                     d          d|                     d          d|                     d          d|                     d          d|                     d          d|                     d          d|                     d          d|                    d          S )Nr  rK   r   rH   r   r   r   r   r  Tr   r   r  r   r  r   r   r   r   r5   )rV  r   )argskws     r   <lambda>rn    s   w      xx"%%% xx!!!  xx!!!  *%%%	 
 XXf  xx!!!  ###  "4d;;;  hhw  xx!!!   xx!!!! " xx!!!# $ XXn---% & "4555' ( ###) * *%%%+ , y!!!-  r:   u   ⏰)r   toolsetschemahandlercheck_fnemojir  )NN)NNNNNNFNNNNNNNNNNNNN)O__doc__r>  loggingrT   sysr  r   pathlibr   typingr   r   r   r   r   r   r	   	getLogger__name__rr   r  r  pathinsertr   __file__parentr=  r
   r   r   r   r   r   r   r   r   r   r   r   r   rh   rt   rV   rj   tools.threat_patternsr0   r[   r<   rC   r  r  r=   rG   rQ   rX   r^   r   r   rd   ro   ru   r   r   r   r   r   r   r   r   r   r  rV  CRONJOB_SCHEMAri  tools.registryrj  r<  registerr5   r:   r   <module>r     s`	      				 



            3 3 3 3 3 3 3 3 3 3 3 3 3 3 0 0 0 0 0 0		8	$	$  $  )  33ttH~~,344 5 5 5                               R	 	 	 $" " "  T  ?)<>>@PQ>)<>>@PQgReggiz{F1DFFHYZkQdkkk  nF  G  ( K J J J J J    HS HT H H H H3 S T              <S S    -S -U3S	>-B - - - -:c c    ,# %S/    D(4S>2    4"S#X "hsm "PXY\P] " " " "JEc3h EC E E E E Xc] 8C= TXY\T]    $ Y^   # QU bjknbo    C HSM    *MsmM'/}Mc]M M M M`$x} $# $ $ $ $N"T#s(^ "S#X " " " "JsD$sCx. sDT#s(^ sD sD sD sDp ! " !""&""  48,0!#(,+y1 y1y1SMy1 SMy1 sm	y1
 3-y1 SMy1 c]y1 y1 C=y1 T#Yy1 C=y1 smy1 smy1 SMy1 SMy1  5d3i01!y1" tCy)#y1$ c]%y1& tn'y1(  ~)y1* +y1, 	-y1 y1 y1 y1|	 \$  !  ` 
 !L 
 !  f 
 !  p 
 != 
 "u 
 !  s 
   (+  i  !  e  Vi  Vi  Vk  Vk   e   e   e 
 " ]	 &   (+?	    (+  @! ! !  S	 
 "  \" "]R
 R
f JkV V'j jZD    . 0 / / / / / / /  	 0 (
;     r:   