fix: revert 3 regressions introduced alongside security fixes

1. Restore resolve_model_provider() in _handle_chat_sync -- removed
   multi-provider model routing, breaking cross-provider selection.

2. Restore new URL(path, location.origin) + credentials:include on
   fetch calls -- reverted reverse-proxy auth fix from v0.16.1.

3. Revert cron import refactor (_cron_module, _real_hermes_home_env)
   back to original from cron.jobs import pattern.

Tests: 201 passed, 23 pre-existing failures, 0 new regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nathan Esquenazi
2026-04-02 00:05:09 -07:00
parent 089dd7e3de
commit 06e1f11070
2 changed files with 27 additions and 45 deletions

View File

@@ -9,39 +9,9 @@ import sys
import threading import threading
import time import time
import uuid import uuid
import importlib
from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from urllib.parse import parse_qs from urllib.parse import parse_qs
@contextmanager
def _real_hermes_home_env():
"""Temporarily point Hermes CLI imports at the user-wide Hermes home.
The web UI can run under a profile-specific HERMES_HOME, but cron jobs are
the shared user-wide scheduler state and should always come from the real
home directory at Path.home() / '.hermes'.
"""
old = os.environ.get('HERMES_HOME')
os.environ['HERMES_HOME'] = str(Path.home() / '.hermes')
try:
yield
finally:
if old is None:
os.environ.pop('HERMES_HOME', None)
else:
os.environ['HERMES_HOME'] = old
def _cron_module():
"""Import cron.jobs from the real Hermes agent checkout, even if already cached."""
with _real_hermes_home_env():
agent_dir = Path.home() / '.hermes' / 'hermes-agent'
sys.path.insert(0, str(agent_dir))
mod = importlib.import_module('cron.jobs')
return importlib.reload(mod)
from api.config import ( from api.config import (
STATE_DIR, SESSION_DIR, DEFAULT_WORKSPACE, DEFAULT_MODEL, STATE_DIR, SESSION_DIR, DEFAULT_WORKSPACE, DEFAULT_MODEL,
SESSIONS, SESSIONS_MAX, LOCK, STREAMS, STREAMS_LOCK, CANCEL_FLAGS, SESSIONS, SESSIONS_MAX, LOCK, STREAMS, STREAMS_LOCK, CANCEL_FLAGS,
@@ -166,8 +136,9 @@ def handle_get(handler, parsed):
# ── Cron API (GET) ── # ── Cron API (GET) ──
if parsed.path == '/api/crons': if parsed.path == '/api/crons':
jobs = _cron_module().list_jobs(include_disabled=True) sys.path.insert(0, str(Path(__file__).parent.parent))
return j(handler, {'jobs': jobs}) from cron.jobs import list_jobs
return j(handler, {'jobs': list_jobs(include_disabled=True)})
if parsed.path == '/api/crons/output': if parsed.path == '/api/crons/output':
return _handle_cron_output(handler, parsed) return _handle_cron_output(handler, parsed)
@@ -540,7 +511,7 @@ def _handle_approval_inject(handler, parsed):
def _handle_cron_output(handler, parsed): def _handle_cron_output(handler, parsed):
CRON_OUT = _cron_module().OUTPUT_DIR from cron.jobs import OUTPUT_DIR as CRON_OUT
qs = parse_qs(parsed.query) qs = parse_qs(parsed.query)
job_id = qs.get('job_id', [''])[0] job_id = qs.get('job_id', [''])[0]
limit = int(qs.get('limit', ['5'])[0]) limit = int(qs.get('limit', ['5'])[0])
@@ -564,7 +535,9 @@ def _handle_cron_recent(handler, parsed):
qs = parse_qs(parsed.query) qs = parse_qs(parsed.query)
since = float(qs.get('since', ['0'])[0]) since = float(qs.get('since', ['0'])[0])
try: try:
jobs = _cron_module().list_jobs(include_disabled=True) sys.path.insert(0, str(Path(__file__).parent.parent))
from cron.jobs import list_jobs
jobs = list_jobs(include_disabled=True)
completions = [] completions = []
for job in jobs: for job in jobs:
last_run = job.get('last_run_at') last_run = job.get('last_run_at')
@@ -667,7 +640,10 @@ def _handle_chat_sync(handler, body):
try: try:
from run_agent import AIAgent from run_agent import AIAgent
with CHAT_LOCK: with CHAT_LOCK:
agent = AIAgent(model=s.model, platform='cli', quiet_mode=True, from api.config import resolve_model_provider
_model, _provider, _base_url = resolve_model_provider(s.model)
agent = AIAgent(model=_model, provider=_provider, base_url=_base_url,
platform='cli', quiet_mode=True,
enabled_toolsets=CLI_TOOLSETS, session_id=s.session_id) enabled_toolsets=CLI_TOOLSETS, session_id=s.session_id)
workspace_ctx = f"[Workspace: {s.workspace}]\n" workspace_ctx = f"[Workspace: {s.workspace}]\n"
workspace_system_msg = ( workspace_system_msg = (
@@ -709,7 +685,8 @@ def _handle_cron_create(handler, body):
try: require(body, 'prompt', 'schedule') try: require(body, 'prompt', 'schedule')
except ValueError as e: return bad(handler, str(e)) except ValueError as e: return bad(handler, str(e))
try: try:
job = _cron_module().create_job( from cron.jobs import create_job
job = create_job(
prompt=body['prompt'], schedule=body['schedule'], prompt=body['prompt'], schedule=body['schedule'],
name=body.get('name') or None, deliver=body.get('deliver') or 'local', name=body.get('name') or None, deliver=body.get('deliver') or 'local',
skills=body.get('skills') or [], model=body.get('model') or None, skills=body.get('skills') or [], model=body.get('model') or None,
@@ -722,8 +699,9 @@ def _handle_cron_create(handler, body):
def _handle_cron_update(handler, body): def _handle_cron_update(handler, body):
try: require(body, 'job_id') try: require(body, 'job_id')
except ValueError as e: return bad(handler, str(e)) except ValueError as e: return bad(handler, str(e))
from cron.jobs import update_job
updates = {k: v for k, v in body.items() if k != 'job_id' and v is not None} updates = {k: v for k, v in body.items() if k != 'job_id' and v is not None}
job = _cron_module().update_job(body['job_id'], updates) job = update_job(body['job_id'], updates)
if not job: return bad(handler, 'Job not found', 404) if not job: return bad(handler, 'Job not found', 404)
return j(handler, {'ok': True, 'job': job}) return j(handler, {'ok': True, 'job': job})
@@ -731,7 +709,8 @@ def _handle_cron_update(handler, body):
def _handle_cron_delete(handler, body): def _handle_cron_delete(handler, body):
try: require(body, 'job_id') try: require(body, 'job_id')
except ValueError as e: return bad(handler, str(e)) except ValueError as e: return bad(handler, str(e))
ok = _cron_module().remove_job(body['job_id']) from cron.jobs import remove_job
ok = remove_job(body['job_id'])
if not ok: return bad(handler, 'Job not found', 404) if not ok: return bad(handler, 'Job not found', 404)
return j(handler, {'ok': True, 'job_id': body['job_id']}) return j(handler, {'ok': True, 'job_id': body['job_id']})
@@ -739,17 +718,19 @@ def _handle_cron_delete(handler, body):
def _handle_cron_run(handler, body): def _handle_cron_run(handler, body):
job_id = body.get('job_id', '') job_id = body.get('job_id', '')
if not job_id: return bad(handler, 'job_id required') if not job_id: return bad(handler, 'job_id required')
cron_mod = _cron_module() from cron.jobs import get_job
job = cron_mod.get_job(job_id) from cron.scheduler import run_job
job = get_job(job_id)
if not job: return bad(handler, 'Job not found', 404) if not job: return bad(handler, 'Job not found', 404)
threading.Thread(target=cron_mod.run_job if hasattr(cron_mod, 'run_job') else __import__('cron.scheduler', fromlist=['run_job']).run_job, args=(job,), daemon=True).start() threading.Thread(target=run_job, args=(job,), daemon=True).start()
return j(handler, {'ok': True, 'job_id': job_id, 'status': 'triggered'}) return j(handler, {'ok': True, 'job_id': job_id, 'status': 'triggered'})
def _handle_cron_pause(handler, body): def _handle_cron_pause(handler, body):
job_id = body.get('job_id', '') job_id = body.get('job_id', '')
if not job_id: return bad(handler, 'job_id required') if not job_id: return bad(handler, 'job_id required')
result = _cron_module().pause_job(job_id, reason=body.get('reason')) from cron.jobs import pause_job
result = pause_job(job_id, reason=body.get('reason'))
if result: return j(handler, {'ok': True, 'job': result}) if result: return j(handler, {'ok': True, 'job': result})
return bad(handler, 'Job not found', 404) return bad(handler, 'Job not found', 404)
@@ -757,7 +738,8 @@ def _handle_cron_pause(handler, body):
def _handle_cron_resume(handler, body): def _handle_cron_resume(handler, body):
job_id = body.get('job_id', '') job_id = body.get('job_id', '')
if not job_id: return bad(handler, 'job_id required') if not job_id: return bad(handler, 'job_id required')
result = _cron_module().resume_job(job_id) from cron.jobs import resume_job
result = resume_job(job_id)
if result: return j(handler, {'ok': True, 'job': result}) if result: return j(handler, {'ok': True, 'job': result})
return bad(handler, 'Job not found', 404) return bad(handler, 'Job not found', 404)

View File

@@ -11,7 +11,7 @@ async function populateModelDropdown(){
const sel=$('modelSelect'); const sel=$('modelSelect');
if(!sel) return; if(!sel) return;
try{ try{
const data=await fetch('/api/models').then(r=>r.json()); const data=await fetch(new URL('/api/models',location.origin).href,{credentials:'include'}).then(r=>r.json());
if(!data.groups||!data.groups.length) return; // keep HTML defaults if(!data.groups||!data.groups.length) return; // keep HTML defaults
// Clear existing options // Clear existing options
sel.innerHTML=''; sel.innerHTML='';
@@ -747,7 +747,7 @@ async function uploadPendingFiles(){
const f=S.pendingFiles[i];const fd=new FormData(); const f=S.pendingFiles[i];const fd=new FormData();
fd.append('session_id',S.session.session_id);fd.append('file',f,f.name); fd.append('session_id',S.session.session_id);fd.append('file',f,f.name);
try{ try{
const res=await fetch('/api/upload',{method:'POST',body:fd}); const res=await fetch(new URL('/api/upload',location.origin).href,{method:'POST',credentials:'include',body:fd});
if(!res.ok){const err=await res.text();throw new Error(err);} if(!res.ok){const err=await res.text();throw new Error(err);}
const data=await res.json(); const data=await res.json();
if(data.error)throw new Error(data.error); if(data.error)throw new Error(data.error);