hermes-agent/agent
kshitij 80f37e36ed fix(cron): don't let a cron job inherit a kanban worker's dispatcher identity
A kanban worker that fires a cron job in-process no longer leaks its task
identity into the cron agent.

The worker is a normal `hermes chat -q` CLI agent whose default toolset
includes `cronjob`, running with HERMES_KANBAN_TASK legitimately set in its
own environment. `cronjob(action="run")` calls run_one_job() -> run_job()
in that same process, so the cron AIAgent was misidentified as that worker:
kanban toolset force-added, kanban-worker protocol injected into its system
prompt, and kanban_complete defaulting task_id to $HERMES_KANBAN_TASK --
letting an unrelated cron job close the worker's task and overwrite real
results.

Fixed with a ContextVar (`non_dispatcher_owned_context`), not by clearing
os.environ. The env is process-global and shared with three concurrent
readers that all need the real values:

  * the worker's own claim heartbeat -- run_agent._touch_activity ->
    heartbeat_current_worker_from_env reads TASK/CLAIM_LOCK/RUN_ID, and the
    cron-run heartbeat thread drives it every 10s. Clearing them silently
    no-ops the heartbeat, so after DEFAULT_CLAIM_TTL_SECONDS (15 min) the
    dispatcher reclaims a task whose worker is still alive and re-dispatches
    it -- the same duplicate-work failure from the other direction.
  * the gateway's kanban watchers, which do their own HERMES_KANBAN_BOARD
    save/restore around a slow decompose_task() LLM call.
  * concurrent cron jobs, which take a *shared* read lock
    (_terminal_cwd_lock.acquire_read) and so interleave: job A clears, job B
    snapshots empty, A restores, B clears and its restore no-ops -- the
    worker's identity is destroyed permanently.

`is_dispatcher_owned_worker_context()` is now the single predicate every
HERMES_KANBAN_* identity gate consults before trusting those vars. It also
closes a pre-existing gap in agent/skill_utils.py, which read the vars
without consulting the delegate_task ContextVar at all; the `kanban` verdict
additionally bypasses _ENV_DETECT_CACHE, since a context-dependent answer
must not be memoized process-wide.

HERMES_KANBAN_BOARD/DB/WORKSPACES_ROOT are left untouched, so the #20074
board pin and the dispatcher's path overrides keep working.

Tests: 18 new, including thread-isolation, concurrent-cron-jobs, and an AST
invariant over _default_spawn that fails if the dispatcher gains a var that
is neither identity-gated nor explicitly classified behaviour-only. All six
mutations are caught, including one that reintroduces the os.environ clear.
tests/cron/ + kanban suites 440 passed; model_tools/skill_utils/boards 63
passed; ruff clean.

Reported and diagnosed by Geoff Friesen (#78961), who identified the symptom
and the exact gating mechanism.

Co-authored-by: Geoff Friesen <gfriesen1@users.noreply.github.com>
2026-08-06 03:16:26 +05:30
..
lsp
…
monitoring
…
pet
…
proxy_sources
…
secret_sources
…
transports feat(providers): env-var metadata, config-driven local no-auth, reasoning-effort clamp for Actual 2026-08-05 14:08:32 -07:00
__init__.py
…
account_usage.py
…
agent_init.py Hermes can read the in-app browser (#79482) 2026-08-05 16:35:00 +00:00
agent_runtime_helpers.py Hermes can read the in-app browser (#79482) 2026-08-05 16:35:00 +00:00
anthropic_adapter.py
…
async_utils.py
…
aux_accounting.py
…
auxiliary_client.py feat(providers): add Actual Computer inference provider 2026-08-05 14:08:32 -07:00
azure_identity_adapter.py
…
backend_identity.py
…
background_review.py
…
battery.py
…
bedrock_adapter.py
…
billing_links.py
…
billing_usage.py
…
billing_view.py
…
bounded_response.py
…
browser_provider.py
…
browser_registry.py
…
chat_completion_helpers.py feat(providers): env-var metadata, config-driven local no-auth, reasoning-effort clamp for Actual 2026-08-05 14:08:32 -07:00
codex_responses_adapter.py
…
codex_runtime.py
…
coding_context.py
…
context_breakdown.py
…
context_compressor.py fix(compression): durable-sync the prune runway on model switch + fast no-op for incapable stores 2026-08-06 02:22:08 +05:30
context_engine.py
…
context_references.py
…
conversation_compression.py fix(compression): durable-sync the prune runway on model switch + fast no-op for incapable stores 2026-08-06 02:22:08 +05:30
conversation_loop.py fix(cache): make proactive pruning durable and cache-aware 2026-08-06 02:22:08 +05:30
copilot_acp_client.py
…
credential_persistence.py
…
credential_pool.py
…
credential_sources.py
…
credits_tracker.py
…
curator.py
…
curator_backup.py
…
delegation_context.py fix(cron): don't let a cron job inherit a kanban worker's dispatcher identity 2026-08-06 03:16:26 +05:30
display.py
…
error_classifier.py
…
errors.py
…
file_safety.py
…
gemini_native_adapter.py
…
gemini_schema.py
…
i18n.py
…
image_gen_provider.py
…
image_gen_registry.py
…
image_routing.py
…
insights.py
…
interrupt_compat.py
…
iteration_budget.py
…
jiter_preload.py
…
kanban_stop.py
…
learn_prompt.py
…
learning_graph.py
…
learning_graph_render.py
…
learning_mutations.py
…
lmstudio_reasoning.py
…
manual_compression_feedback.py
…
markdown_tables.py
…
memory_manager.py
…
memory_provider.py
…
message_content.py
…
message_sanitization.py
…
moa_loop.py
…
moa_trace.py
…
model_metadata.py
…
models_dev.py
…
moonshot_schema.py
…
nous_rate_guard.py
…
onboarding.py
…
oneshot.py
…
outbound_webhooks.py
…
plugin_llm.py
…
portal_tags.py
…
process_bootstrap.py
…
prompt_builder.py
…
prompt_caching.py
…
rate_limit_tracker.py
…
reactions.py
…
reasoning_timeouts.py
…
redact.py
…
relay_llm.py
…
relay_runtime.py
…
relay_tools.py
…
replay_cleanup.py
…
retry_utils.py
…
runtime_cwd.py
…
secret_scope.py
…
session_activity.py
…
shell_hooks.py
…
skill_bundles.py
…
skill_commands.py
…
skill_preprocessing.py
…
skill_utils.py fix(cron): don't let a cron job inherit a kanban worker's dispatcher identity 2026-08-06 03:16:26 +05:30
ssl_guard.py
…
ssl_verify.py
…
stream_diag.py
…
stream_single_writer.py
…
subagent_lifecycle.py
…
subdirectory_hints.py
…
subscription_view.py
…
system_prompt.py
…
think_scrubber.py
…
thinking_timeout_guidance.py
…
thread_scoped_output.py
…
title_generator.py
…
tool_dispatch_helpers.py
…
tool_executor.py Hermes can read the in-app browser (#79482) 2026-08-05 16:35:00 +00:00
tool_guardrails.py
…
tool_result_classification.py
…
trace_upload.py
…
trajectory.py
…
transcription_provider.py
…
transcription_registry.py
…
tts_provider.py
…
tts_registry.py
…
turn_context.py
…
turn_finalizer.py
…
turn_retry_state.py
…
turn_summary.py
…
usage_pricing.py
…
verification_evidence.py
…
verification_stop.py
…
verify_hooks.py
…
vertex_adapter.py
…
video_gen_provider.py
…
video_gen_registry.py
…
web_search_provider.py
…
web_search_registry.py
…