v0.50.257 — Codex OAuth, cron history, per-session toolsets, custom-provider routing fix
Three new features and a routing bug fix, plus five Opus pre-release advisor follow-ups including one CRITICAL finding that would have shipped a non-functional feature.
Added
-
Cron run history + full-output viewer (#468) — new
GET /api/crons/history?job_id=X&offset=N&limit=Mendpoint lists output files with metadata (filename + size + mtime) without loading content. NewGET /api/crons/run?job_id=X&filename=Yreturns full content + a snippet extracted from the## Responsesection. Tasks panel renders a per-job run history with click-to-expand. (#1402, @bergeouss) -
Per-session toolset overrides (#493) — new
Session.enabled_toolsets: list[str] | Nonefield threaded through_run_agent_streaming. NewPOST /api/session/toolsetsendpoint validates input shape (non-empty list of non-empty strings, or null to clear). Settings panel adds a per-session toolset chip with global/custom modes. (#1402, @bergeouss) -
Codex OAuth in-app device-code flow — new
api/oauth.py(stdlib only — no external HTTP libs). Two endpoints:GET /api/oauth/codex/start(initiates Codex device-code flow, returnsuser_code+verification_uri) andGET /api/oauth/codex/poll?device_code=X(SSE for polling token endpoint). Successful poll writes credentials to~/.hermes/auth.jsonundercredential_pool.openai-codex. Onboarding wizard adds a "Sign in with ChatGPT" path. Idempotent: existing OAuth credential entries are updated in place; new ones useuuid.uuid4().hex[:8]with retry-on-collision. (#1402, @bergeouss)
Fixed
- Named custom provider routing in model picker —
@custom:NAME:modelform preserved (follow-up to #1390) — when the model picker iteratedcustom_providersentries with anamefield (e.g.[{name: "sub2api", base_url, models: [...]}]), the option IDs were stored as bare model strings. On chat start, the backend resolved those bare strings through the active/default provider, silently routing the request to the wrong endpoint (e.g. DeepSeek instead of the user's selectedsub2apiproxy). Now the picker prefixes IDs with@<slug>:<model>whenever the active provider differs from the named slug, so_resolve_compatible_session_model_state(added by #1390) routes through the correct named provider. The frontend_findModelInDropdownalready strips@provider:prefixes during normalization, so legacylocalStorage["hermes-webui-model"]values with bare IDs continue to resolve. (#1415, @Thanatos-Z)
Pre-release hardening (Opus advisor)
-
CRITICAL: per-session toolset override (#493) was non-functional —
_run_agent_streamingcalled_session_meta.get('enabled_toolsets')on the result ofSession.load_metadata_only(), which returns a Session instance (not a dict). TheAttributeErrorwas swallowed by the surroundingexcept Exception:block, so the user's toolset chip silently no-op'd every time and the agent always ran with global toolsets. CI was green, all five of #1402's targeted tests passed — would have shipped non-functional. Fix usesgetattr(_session_meta, 'enabled_toolsets', None). Source-level negative-pattern test prevents the dict-access shape from returning. -
api/oauth.py::_write_auth_jsonchmod 0600 BEFORE rename —tmp.replace()preserves the temp file's umask-derived mode (commonly 0644 or 0664).auth.jsoncontains OAuth access/refresh tokens; on shared systems those tokens landed world-readable through the temp-file→rename window. Fix setstmp.chmod(0o600)before the atomic rename, with atry/except OSErrorthat logs but doesn't abort if chmod fails on filesystems that don't support POSIX modes. -
_handle_cron_historyand_handle_cron_run_detailregex-validatejob_id— the_checkpoint_root() / ws_hash / checkpointpath-traversal vector caught in v0.50.255 (#1405) had a sibling here:CRON_OUT / job_id / *.md.Path() / "../escape"does NOT normalize. New regex^[A-Za-z0-9_-][A-Za-z0-9_.-]{0,63}$with explicit./..rejection at the parameter boundary. Mirrors the rollback fix shape. -
_handle_cron_historyclampsoffsetandlimit— rawint(qs.get("offset", ["0"])[0])raisedValueErroron?offset=fooand surfaced as a generic 500. No upper bound onlimiteither. Now wrapped intry/except (ValueError, TypeError)returning a 400 on bad input, andlimitclamped to[1, 500].
Tests
3604 passed, 2 skipped, 3 xpassed. Browser tests + Phase 2 API sanity all green. Opus advisor reviewed the combined stage diff and caught the CRITICAL toolset finding empirically (CI alone would have shipped the broken feature). Independent end-to-end review by @nesquena.
Contributors
@bergeouss · @Thanatos-Z · @nesquena (review)
Full Changelog: v0.50.256...v0.50.257