Files
hermes-agent/cron/execution_identity.py
Teknium 61f8365da7 feat(plugins): ctx.current_cron_execution() tells a plugin which cron run it is in
Plugins could only see a cron run as the task_id string `cron:<job>:<execution>`,
with no way to tell a scheduled fire from a manual `hermes cron run`. A
security plugin that may only act inside a real scheduled occurrence had
nothing to check.

The scheduler now binds a frozen CronExecution (job id/name, execution id,
source, scheduled_instant, started_at, profile) in a ContextVar once the run
wins its claimed->running CAS (or a restart-safe worker adopted the row), and
resets it when the fire ends. ContextVar scoping keeps concurrent jobs and
profiles apart and follows the agent onto the threads the scheduler already
copies context into. Delegated children read None.

Also trims duplicated comments in _run_one_job_body to keep the facade within
its code-health line cap.

Closes #130722
2026-10-05 00:55:03 -07:00

55 lines
2.1 KiB
Python

"""Which cron execution the current code runs inside, for plugins (``ctx.current_cron_execution()``).
The scheduler binds it once it has won the execution's ``claimed`` → ``running`` transition (or a
restart-safe worker adopted it) and resets it when the fire ends. It is a ContextVar, so it follows
the agent onto the threads the scheduler already runs it on via ``contextvars.copy_context()`` and
never leaks to another concurrently firing job or profile. Model output cannot reach it: tool
subprocesses do not share the interpreter. Delegated children read ``None`` — they are not the
scheduled occurrence.
"""
from __future__ import annotations
from contextvars import ContextVar, Token
from dataclasses import dataclass
from typing import Any, Mapping, Optional
@dataclass(frozen=True)
class CronExecution:
job_id: str
job_name: str
execution_id: str
source: str # "builtin" (ticker), "direct" (manual run / dashboard Run now), provider name...
scheduled_instant: Optional[str] # the occurrence fired; None for an off-schedule run
started_at: Optional[str]
profile: str
_CURRENT: ContextVar[Optional[CronExecution]] = ContextVar("hermes_cron_execution", default=None)
def enter_cron_execution(job: Mapping[str, Any], execution_id: str, record: Mapping[str, Any]) -> Token:
"""Bind this fire's identity from *record*, the ledger row of the running transition."""
from hermes_cli.profiles import current_profile_name
return _CURRENT.set(CronExecution(
job_id=str(job["id"]),
job_name=str(job.get("name") or job["id"]),
execution_id=str(execution_id),
source=str(record.get("source") or ""),
scheduled_instant=record.get("scheduled_instant"),
started_at=record.get("started_at"),
profile=current_profile_name("default") or "default",
))
def exit_cron_execution(token: Optional[Token]) -> None:
if token is not None:
_CURRENT.reset(token)
def current_cron_execution() -> Optional[CronExecution]:
from agent.delegation_context import is_delegated_child_context
return None if is_delegated_child_context() else _CURRENT.get()