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:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user