From fdab380a1ada1e1fc127b3a51d5695f538cb774f Mon Sep 17 00:00:00 2001 From: Jonny Kovacs Date: Fri, 3 Jul 2026 06:49:03 -0500 Subject: [PATCH] fix(cron): run jobs under the profile secret scope Once profile isolation is active (multiple gateway profiles or room->profile multiplexing), get_secret() fails closed outside an installed scope. The cron ticker fires jobs from a thread with no per-turn scope, so run_job() died in resolve_runtime_provider() with UnscopedSecretError (e.g. for OPENROUTER_BASE_URL / CUSTOM_BASE_URL) before model selection - every cron job failed while interactive turns worked fine. Wrap run_job() in set_secret_scope(build_profile_secret_scope(...)) with a finally-reset, mirroring the proven per-turn pattern in gateway/run.py (_profile_runtime_scope). Single-profile installs are unaffected (the scope is just the profile's own .env). tests/cron: 611 passed, 1 pre-existing unrelated failure (TestRoutingIntents::test_all_token_case_insensitive fails identically on unmodified main in a full-suite run and passes in isolation). --- cron/scheduler.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/cron/scheduler.py b/cron/scheduler.py index e072fce7fd1..6c26efb8624 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -3114,7 +3114,26 @@ def run_one_job(job: dict, *, adapters=None, loop=None, verbose: bool = False) - ) return True # not an error — already handled/removed - success, output, final_response, error = run_job(job) + # Run the job under the profile's secret scope. get_secret() fails + # closed outside a scope once profile isolation is in play (multiple + # gateway profiles / room→profile multiplexing), and cron fires from + # the ticker thread where no per-turn scope is installed — so + # resolve_runtime_provider() raised UnscopedSecretError before model + # selection, breaking every cron job. Mirrors the per-turn pattern in + # gateway/run.py (_profile_runtime_scope). + from agent.secret_scope import ( + build_profile_secret_scope, + reset_secret_scope, + set_secret_scope, + ) + + _scope_token = set_secret_scope( + build_profile_secret_scope(_get_hermes_home()) + ) + try: + success, output, final_response, error = run_job(job) + finally: + reset_secret_scope(_scope_token) output_file = save_job_output(job["id"], output) if verbose: