Commit Graph

49650 Commits

Author SHA1 Message Date
Brooklyn Nicholson
cd94de7ea4 test(desktop): tool disclosure cache evicts on unmount
Regression test for #131121: after subscribe -> unsubscribe (a row's
mount -> unmount), a second call with the same id must return a fresh
atom — the entry was released instead of being retained for the
window's lifetime — while identity stays stable while subscribed.

Co-authored-by: Yuan Li <dskwelmcy@163.com>
2026-10-06 12:09:55 -05:00
Yuan Li
b8c844b99a fix(desktop): evict inactive tool disclosure atoms 2026-10-06 12:09:55 -05:00
Kuxin
0fa1fb600d fix(stt): normalize local command language hints 2026-10-06 12:08:04 -05:00
Kuxin
0d234e714c fix(stt): normalize local language hints 2026-10-06 12:08:04 -05:00
Austin Pickett
35f0b4b569 fix(dashboard): route native text paste through term.paste to stop last-char duplication
A native/DOM paste on the chat terminal (context-menu paste, middle-click,
or any Ctrl+V route that bypasses the keydown interception) fell through to
xterm's own paste listeners. xterm clears the hidden textarea during the
paste dispatch, but the browser's default action — inserting the pasted
text into that textarea — runs after dispatch, so a stale value survives
and the next typed character is duplicated (#52471: "when" → "whenn").

The host capture-phase paste handler now cancels the native paste for
plain text and delivers it via term.paste() exactly once — the same clean
route the Ctrl/Cmd+V keydown interception already uses. Image pastes keep
their upload path and are never routed as text.

Supersedes #52477 (credit: @liuhao1024 — same interception, extended with
the image route preserved and regression coverage).

Fixes #52471
2026-10-06 13:00:43 -04:00
Austin Pickett
7ed284fee0 fix(web): satisfy code-health ratchet and contributor attribution
- PtySession.close and _discard_active_session_file: waive the deliberate
  teardown catch-alls with reviewed '# health: allow' comments (BLE001+S110).
- pty_ws was over its main-pinned CC cap: extract the channel-marker prologue
  into _pty_channel_marker_state and the keep-alive writer loop into
  _pump_keepalive_input.
- Map sinh3543@gmail.com -> sinh2024 (check-attribution, PR #133936).
2026-10-06 12:46:06 -04:00
Austin Pickett
3178ef3e21 test(pty): the channel marker dict is empty after every PTY lifecycle end
Regression coverage for #63553: _active_session_file_for_channel
registered channel→tempfile entries that no code path popped, so
pty_active_session_files grew without bound and churned a forgotten
tempfile+inode per reconnect.

Proves the invariant across every session end/forget path:
- keep-alive close (close_all, same path as TTL reap / child EOF)
- repeated same-channel reconnects keep one mapping
- legacy 1:1 pump (no attach token) pops its marker on disconnect
- ?fresh=1 drops the stale breadcrumb yet keeps the mapping so the
  next same-channel reconnect resumes the fresh child (the regression
  flagged in the #63563 review)

All four are red on 85db7c3a68.
2026-10-06 12:46:06 -04:00
sinh2024
56227dd6fd fix(web): clean up PTY active session markers 2026-10-06 12:46:06 -04:00
Austin Pickett
ee2ed10ef5 fix(sessions): validate imported values before persistence
Session import used shallow type casts at the persistence boundary:
float() accepted inf/NaN (a JSON 1e309), int() accepted integers
SQLite cannot bind (outside signed 64-bit range), model_config was
only shape-checked after its intentional decode, and a literal string
carrying the internal \\x00json: content marker was decoded as
structured content on read. Bad values reached sqlite3 binding or
persisted as inf, breaking session APIs with HTTP 500.

- reject non-finite floats in session float/timestamp columns, message
  timestamps, and scalar message content, with the field path in the error
- re-check every intentionally-decoded JSON-backed field (model_config,
  reasoning_details, codex_* items) for non-finite values
- bound int columns, token_count, and int scalar content to SQLite's
  signed 64-bit range; coerce-with-default at the bind site as a backstop
- escape literal \\x00json:-prefixed strings via the JSON wrapper instead
  of storing them raw; decoding already fails closed on malformed marker rows
- all rejection stays inside the existing atomic validate-then-write
  400 contract; valid exports round-trip unchanged

Fixes #63954
2026-10-06 12:40:04 -04:00
Austin Pickett
3225f357a5 fix(dashboard): show broken links as non-actionable entries in Files page
Backend listings now emit broken_link: true placeholder entries with
nullable mtime for dangling symlinks. Update ManagedFileEntry to match,
render broken links with an Unlink icon and a destructive badge, disable
download for them, and keep delete enabled so users can remove the dead
link from the UI.
2026-10-06 12:30:01 -04:00
Austin Pickett
1ca192bc1b fix(dashboard): don't 500 file listings on dangling symlinks or vanished entries
The dashboard file browser's listing route built entries via a bare
comprehension over os.scandir, calling _managed_file_entry() per entry.
That helper raises HTTP 500 ("Could not stat path") on any OSError from
resolved.stat(), so a single dangling symlink (or an entry removed
between readdir and stat) aborted the whole directory listing with a 500
— e.g. a broken ~/.claude link or Steam's recreated linux32/steam made
the entire Files page unusable (#47154).

Whole-class fix, consolidated from the open contributor PRs:

- _managed_file_entry() classifies a symlink whose target is definitively
  missing (FileNotFoundError) before the resolved-target boundary check
  and returns a `broken_link: true` placeholder entry instead of 500ing.
  The placeholder is only emitted for entries inside the managed root
  even when the missing target resolves outside it. Permission and other
  OSErrors still surface as 403/500 as before.
- Symlink stat results are captured in one snapshot (no second stat), so
  a target disappearing mid-listing cannot flip a result into a 500.
- New skip_missing flag lets the listing scan skip an entry that
  vanished between readdir and stat (returns None); the write-result
  caller stays strict.
- The listing loop appends placeholders and skips vanished entries, so
  one dead entry never loses its siblings. The sibling fs_list route
  already tolerated ENOENT; /api/files now agrees.

Reading/downloading a broken link directly still returns 404; 403
boundary and sensitive-path behavior are unchanged.

Fixes #47154

Co-authored-by: gangpangpang <2564186295@qq.com>
Co-authored-by: ous fifty <ousfifty@gmail.com>

Incorporates work by @Lqg97 (PR 47159; co-contributors qinggangli and Shiqian Ma, commit emails not linked to GitHub accounts).
2026-10-06 12:30:01 -04:00
Austin Pickett
ca38caeea0 fix(dashboard+auth): cap HTTP JSON response body reads
Dashboard-side urllib readers (gateway health probe, ElevenLabs voices)
and self-hosted OIDC httpx readers (discovery, token exchange, revocation)
buffered remote response bodies with no size bound — a hostile or
compromised upstream could exhaust memory. Both halves now read with a
1 MiB hard cap: a shared _read_dashboard_json_response for urllib JSON
reads and a streaming _request_limited_response for IdP endpoints that
prechecks Content-Length and caps the chunked body regardless.

Adapted from #54809 and #55122 (credit @ooiuuii) to current main, where
the token exchange lives in plugins/dashboard_auth/_shared.py and the
health probe in hermes_cli/web_server_gateway.py.

Fixes #54808, Fixes #55121
2026-10-06 12:28:55 -04:00
Austin Pickett
e93446126d fix(dashboard): surface the messaging platform's auth posture in the Test result
The Channels 'Test' button stopped at connectivity: a platform whose gateway
authorization posture denies every sender (no allowlists, no allow-all flag, no
pairing grants) still answered '<Platform> is connected.' — a green checkmark
on a bot that ignores everyone (#66030).

When the adapter reports connected, the test result now carries the platform's
access posture for the ACTIVE profile, mirroring the decision ladder of
AuthorizationMixin._principal_authorized:

- open: {PLATFORM}_ALLOW_ALL_USERS / GATEWAY_ALLOW_ALL_USERS
- allowlist: {PLATFORM}_ALLOWED_USERS, GATEWAY_ALLOWED_USERS, the Telegram
  group allowlist vars, or a config-enforced dm_policy: allowlist
- pairing: approved pairing grants in the profile's pairing store
- deny_all: nothing configured — with pending pairing requests counted in
  the message

The check is profile-aware: under a named profile it reads that profile's
.env/config/pairing store, never the dashboard process's os.environ (which
carries the ROOT install's credentials). ok stays True (the connection is up);
the new auth_posture field lets the UI render yellow instead of green.

Reuses the idea from PR #66055 (authored by @webtecnica) with the evaluator
made profile-aware and extended to cover GATEWAY_ALLOWED_USERS, pairing grants
and adapter/config-enforced allowlists, per its review.

Fixes #66030
2026-10-06 12:28:06 -04:00
Austin Pickett
82f2dcb286 fix(gateway): let GET /api/model/options opt out of unconfigured skeleton rows
include_unconfigured was hardcoded True in _handle_model_options, so external
API consumers had no way to request the explicit-only view. Forward the
include_unconfigured query param (default true, back-compatible) to
build_model_options_payload. With _append_unconfigured_rows now honoring
model_catalog.excluded_providers, this endpoint no longer resurrects excluded
providers on any view (#68816).

Also lifts model_options decoding (_request_reasoning_config /
_request_service_tier) into gateway/platforms/api_server_request_options.py to
offset api_server.py's frozen line cap (moved code keeps its cap; the facade
re-exports both names).
2026-10-06 12:28:00 -04:00
PRATHAMESH75
93ca74c3bb fix(cli): honor excluded_providers in the TUI /model picker's unconfigured rows
build_models_payload(include_unconfigured=True) — the path the in-session TUI
/model command uses — appended CANONICAL_PROVIDERS skeleton rows via
_append_unconfigured_rows() without consulting model_catalog.excluded_providers.
So every excluded provider reappeared in the TUI picker even though hermes model
(list_authenticated_providers) hid it correctly.

Filter excluded providers in the skeleton loop, normalized the same way
list_authenticated_providers does ({str(p).strip().lower()}), and exclude
unconditionally — including the configured-current provider — to match that
path, which drops an excluded provider even when it is current.

Adds include_unconfigured-path coverage: excluded provider removed (others
intact), case-insensitive match, current-excluded-provider dropped, and
empty/None excluded is a no-op.

Fixes #68816
2026-10-06 12:28:00 -04:00
Austin Pickett
da303d1729 fix(dashboard): preserve CLI-configured custom endpoints on same-endpoint model/set
POST /api/model/set re-registered a named custom_providers row for any
custom/local main assignment, even when the endpoint was already a fact
in config.yaml (the CLI gateway setup writes provider: custom with an
inline base_url + api_mode). The picker then rewrote the CLI's shape into
custom:<slug> with base_url: '' (#76324).

Snapshot the pre-save config and skip registration when the endpoint is
already known (inline model.base_url, a providers: entry, or a
custom_providers row); only a genuinely new endpoint gets a named row.

Fixes #76324
2026-10-06 12:27:41 -04:00
Austin Pickett
5515c4be55 fix(dashboard): backfill /api/sessions/search with title matches
The search endpoint composed only search_sessions_by_id (id/lineage LIKE)
with search_messages (FTS5 over message content), so a term that lived
solely in a manually-set sessions.title returned zero results — renamed
sessions were invisible to search (#66242).

The DB layer already knew how to LIKE-match titles across the whole
compression chain: list_sessions_rich(search_query=...) via
_chain_search_where (the same helper the sidebar listing uses). Reuse it
rather than adding a second title query path: after the id and FTS lanes,
backfill remaining result slots through list_sessions_rich, feeding rows
through the same add_lineage_result dedup path so lineage, limits, and
result shape stay one contract (content hits keep BM25 priority; the
title lane is best-effort on top).

Fixes #66242
2026-10-06 12:27:29 -04:00
Austin Pickett
fbf261a33f fix(web): preserve repeat count when creating cron jobs from dashboard
CronJobCreate did not declare a repeat field, so Pydantic dropped it during
request parsing, and _create_cron_job_sync never forwarded one to
cron.jobs.create_job — a finite repeat count submitted via POST /api/cron/jobs
was silently stored as an unlimited recurring job.

- Add optional repeat field to CronJobCreate (int or CLI string form)
- Forward it from _create_cron_job_sync; non-positive counts 400, string
  forms coerce through the shared normalize_repeat_value the CLI uses
- Add repeat?: number to the desktop CronJobCreatePayload type
- Route-level regression coverage: finite survives create+list, omission
  stays unlimited, invalid counts 4xx with no job stored
2026-10-06 12:27:22 -04:00
Austin Pickett
0620913eac fix(dashboard): gate the chat WebGL renderer and fall back to DOM
The chat terminal loaded @xterm/addon-webgl for every wide host with
nothing but a constructor try/catch. Three failure classes escaped it:

- Safari's WebGL atlas garbles Unicode box-drawing glyphs (#18773)
- software rasterizers (llvmpipe/SwiftShader) advertise WebGL but regl
  throws '(regl) webgl not supported' from inside the addon (#45520)
- the per-glyph atlas cannot shape complex scripts, so Bengali
  conjuncts render broken (#58685)

Gate the addon behind a pre-flight probe (real context + non-software
unmasked renderer, released immediately) and a Safari UA check, both in
a new pure module; when shaped-script text arrives over the PTY, drop
the live addon so xterm re-renders through the DOM renderer, whose text
shaping handles conjuncts. Context loss already disposed the addon.
2026-10-06 12:26:53 -04:00
Austin Pickett
e30c6aecef fix(desktop): a session-picker click re-selects the active session
The picker door (openSessionFromPicker — /resume overlay, ⌘K, artifacts)
only ran openSession, whose in-place path treats the already-active
session as a front-only no-op. The sidebar's own door always queues an
explicit requestSessionResume, which use-route-resume honours via its
explicitlyRequested branch to re-run resumeSession even when the session
is already active. Give the picker the same explicit reselect so clicking
the active session re-attaches the transcript and re-focuses the composer
(#62045) instead of doing nothing.
2026-10-06 12:24:07 -04:00
JoaoMarcos44
70b8d815e6 test(desktop): pin caret rekey semantics 2026-10-06 11:19:00 -05:00
JoaoMarcos44
5beac8a5ae fix(desktop): preserve caret on same-draft scope rekey 2026-10-06 11:19:00 -05:00
Brooklyn Nicholson
02a802a7ef fix(desktop): stream Windows update-marker probe over stdin
The marker probe (~8.3k chars encoded) was the second Windows SSH
command still over cmd.exe's 8191-char limit after the platform probe
moved to stdin: it died with 'The command line is too long' and the
relaunch gate could never observe CLEAR. Ride the same stdin transport
(powerShellStdinCommand) instead of splitting the probe, keeping the
C# no-follow marker reader and its fail-closed semantics intact, and
assert every emitted Windows probe command stays under the limit.

Co-authored-by: nikkoxgonzales <nikkoxgonzales@users.noreply.github.com>

Fixes #106716
Refs #106903
2026-10-06 11:15:52 -05:00
thegreatesthoneybee
25d7de6fad fix(desktop): preserve Windows SSH probe input encoding 2026-10-06 11:15:52 -05:00
thegreatesthoneybee
5dd73d4641 fix(desktop): stream Windows SSH probe over stdin
Avoid the Windows cmd.exe command-line limit by sending the PowerShell platform probe over SSH stdin instead of embedding a long encoded command. Keep probe statements newline-separated so PowerShell try/catch blocks parse correctly, and cover the real PowerShell path with regression tests.
2026-10-06 11:15:52 -05:00
Austin Pickett
aaf7f199d5 fix(state): chain-aware dashboard session deletes; stats exclude compression children
Dashboard sessions endpoints treated compression-continuation children as
independent rows in two places:

- /api/sessions/stats counted every physical row, so total/active_store/
  archived were inflated by the hidden chain links while the paired
  list_sessions_rich page collapses each chain to one row (#54298).
- The bulk and single delete endpoints removed only the selected physical
  row. A list row carries the chain tip's id, so the root survived and the
  next list reload re-projected it onto the previous link — deleted
  conversations reappeared (#57543).

delete_session/delete_sessions gain include_compression_chain (opt-in; the
dashboard endpoints pass True). Lineage expansion walks compression edges in
both directions with the same edge definition as get_compression_tip and the
list-projection CTE (branch/reset/delegate/tool children excluded), collects
stale sibling continuations, threads the write-guard pass through the
expanded lineage, sweeps transcript files of every chain member, and keeps
the returned count equal to the selected rows. The default stays chain-blind
for existing callers (CLI /exit --delete, ACP, gateway API).

Fixes #54298, Fixes #57543

Salvaged from #54300 (stats exclude_children; @liuhao1024) and #57996
(lineage expansion; @draix), adapted onto the web_routers split and the
guard-aware delete paths.

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: draix <draixagent@gmail.com>
2026-10-06 12:03:11 -04:00
Austin Pickett
535f2161dc test(skills): pin ambiguous display-name lookup — refuse to guess
Two skills sharing a frontmatter display name must not resolve by that
name in skill_manage (silently mutating the wrong skill); a unique
display name resolves and the directory names keep working. Mirrors
skill_view's same-tier collision refusal semantics in the manager.
2026-10-06 12:00:47 -04:00
Austin Pickett
cff9f5eb1d fix: resolve dashboard skills by display name
Originally authored by @deanjo in https://github.com/NousResearch/hermes-agent/pull/80883 (commit email not linked to a GitHub account).
2026-10-06 12:00:47 -04:00
liuhao1024
19b5b411ab fix(skills): resolve the frontmatter name _find_skill ignores
skills_list displays a skill's frontmatter name: when it diverges from
the directory name, every mutating skill_manage action (and the
/skills approve replay) called with that displayed name failed with a
misleading 'not found in active profile' error — nothing about profiles
was actually wrong (#99609).

_find_skill now falls back to matching the SKILL.md frontmatter name
after the directory-name/categorized scan finds nothing, with the same
MAX_NAME_LENGTH truncation skills_list applies when displaying it.
Directory-name matches keep priority (recorded only, returned after the
full scan), so one skill's frontmatter cannot shadow another skill's
directory. This also makes _create_skill's duplicate check catch a new
directory name colliding with an existing skill's displayed name — a
collision skills_list's seen-names dedup would silently hide.
2026-10-06 12:00:47 -04:00
Austin Pickett
aa30e02d86 fix(dashboard): badge follows the PTY runtime model, not config, during fallback (#54509) 2026-10-06 11:44:38 -04:00
Austin Pickett
dca25fe74d refactor(web): extract terminal appearance helpers; satisfy code-health ratchet
ChatPage.tsx crossed its FILE_LINES cap and the ChatPage function its
FUNC_LINES cap once the #36784 accessibility lines landed. Move the
module-level theme/font-tier helpers to web/src/lib/terminal-appearance.ts
(the ratchet's sanctioned remedy — moved code keeps its cap) and tighten
the new comments. No behaviour change: same helpers, same values.
2026-10-06 11:44:32 -04:00
Austin Pickett
fe6eb7f976 test(web): assert composer screen-reader mode behaviourally (#36784)
Replace the static source-text assertions with behavioural tests that
render ChatPage against a capturing FakeTerminal and observe what
assistive technology observes: the Terminal is constructed with
screenReaderMode: true, the host div is a labelled role=region, and the
single registered key handler lets Tab escape (WCAG 2.1.2) while other
keys still reach the terminal. Proven RED on base, GREEN with the fix.
2026-10-06 11:44:32 -04:00
David Metcalfe
a29eb0d8e0 fix(web): fold Tab escape into existing xterm key handler (#59091 review)
Teknium's review of the original PR flagged that the new
`term.attachCustomKeyEventHandler` at line 540 silently overwrote
the clipboard handler at line 464 — xterm.js stores a single
`_customKeyEventHandler` per Terminal
(node_modules/@xterm/xterm/src/browser/CoreBrowserTerminal.ts:914,
`this._customKeyEventHandler = customKeyEventHandler;`) and assigns
overwrites later registrations, so the Tab escape branch never
executed.

The rework folds the Tab branch into the existing handler at the
top (right after the type guard) so only one handler is registered.
The ARIA region and screenReaderMode additions remain untouched.

Adds web/src/pages/ChatPage.a11y.test.ts — regression test asserting
that `screenReaderMode: true` is set, the host div carries
`role="region"` + `aria-label=`, exactly one
`term.attachCustomKeyEventHandler(` call exists, and the single
handler contains the Tab-escape branch + keydown guard. Verified the
test fails on the original PR diff (catches the regression) and
passes on the rework.

Closes inline review thread at #59091.
2026-10-06 11:44:32 -04:00
David Metcalfe
cc26049c52 fix(web): enable screen-reader mode for chat composer (#36784)
The web UI chat composer uses xterm.js, which renders to a canvas
invisible to VoiceOver/NVDA. Add screenReaderMode: true to create
the offscreen textarea screen readers need, a Tab escape handler to
prevent keyboard traps (WCAG 2.1.2), and an ARIA region label on
the host div.
2026-10-06 11:44:32 -04:00
unsupportedpastels
0dbaf33f67 fix(gateway): measure fallback drift from the session's winning model on its primary route
The channel-override baseline from #131295 skipped the session /model tier and
normalized the expected model with the agent's current provider. Both disagree with
how the agent was built, which turned three cases main handled into wrong verdicts:

- a recorded Nous switch off a session /model choice was evicted (the baseline was
  the channel model, so the (alias, backing) record never matched);
- a session-primary fallback that landed on the channel's model was kept;
- an OpenRouter primary that fell back to native Anthropic was kept, because the
  channel slug normalized under the fallback provider equals the fallback model.

Resolve the baseline in the same order as _resolve_session_agent_runtime (session
/model, channel_overrides, global) and normalize it for the agent's primary provider,
the same route the welcome-host pin already uses. A session override whose provider
was unavailable this turn ran on the default route (the resolver drops it for that
turn and retries next turn), so it is the baseline only when its provider is the
agent's primary provider.

A model-name comparison cannot see a fallback that serves the configured model on
another endpoint; with a channel model that differs from the global one, #131295
turned that case from evicted into kept. A turn whose agent has
_provider_fallback_active (set only by _try_activate_fallback, cleared by
restore_primary_runtime) now evicts before any name comparison.

The baseline lives in GatewayAgentCacheMixin._fallback_baseline_model beside
_is_intentional_model_switch, which keeps gateway/run_turn.py under its code-health
line cap (the channel lookup from #131295 had grown it past main).

Session tier taken from #132238; both regressions diagnosed in review by @andrexibiza.

Co-authored-by: salch-cred <141555468+salch-cred@users.noreply.github.com>
Co-authored-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
2026-10-06 11:24:29 -04:00
teknium1
e72180bc7a test(gateway): channel_overrides turns keep one cached agent through the real turn path
Review fix-up for #131295. The committed cases call _run_agent_evict_on_fallback
directly with a SimpleNamespace and a mocked _evict_cached_agent, so they prove the
classifier's verdict but not the cache/pin lifecycle the incident is about. This drives
the real handler -> _run_agent -> TurnRunner.run_sync path (only the AIAgent class and
config reads are substituted) through human -> internal wake -> human and asserts one
agent instance, the override model in _agent_cache, and a surviving ephemeral pin.

Red/green across the branch's history:
  origin/main       -> named-channel-model fails (the #131294 eviction)
  7a3e9f5 (1st fix) -> welcome-host-pins-over-channel-model fails (the R1 regression)
  cdf5e12 (head)    -> both pass

Co-authored-by: Guerilla-ops <307102608+Guerilla-ops@users.noreply.github.com>
(cherry picked from commit 46e9f20e8c562dfcddf1c11b4d08874d5cd842a2)
2026-10-06 11:24:29 -04:00
EloquentBrush0x
b23877a5ec fix(gateway): the welcome host's pinned model is not a fallback either
The channel_overrides comparison in _run_agent_evict_on_fallback took the
channel's model as the expected one, but AIAgent.__init__ pins an agent on
the Nous welcome host to nous/welcome (pin_model_for_route) whatever the
chat configured. With a global nous/welcome and a channel override, that
looked like a fallback and evicted the cached agent after every turn
(reported in review by andrexibiza).

Apply the same pin to the expected model, on the agent's primary route
(_primary_runtime) so a fallback that lands on the welcome host still
evicts. The recorded alias->backing switch keeps matching
_is_intentional_model_switch.

(cherry picked from commit cdf5e12666a501eb02940ccab90ea05ce65d0b96)
2026-10-06 11:24:29 -04:00
EloquentBrush0x
1df1f13417 fix(gateway): a channel_overrides model no longer evicts the cached agent after every turn
_run_agent_evict_on_fallback compared the agent's model with the global config model, so a
chat, thread or parent with channel_overrides.<id>.model looked like a fallback after every
successful turn: the cached agent was evicted, rebuilt next turn, and its context pin cleared.
d6c53dcdcb fixed two other causes of the same per-turn eviction.

Compare with the channel's model, resolved through _resolve_model_for_channel with the
thread/parent lookup _resolve_session_agent_runtime uses. Without an override it is the
global model, as before. Tests cover a chat and a thread of an overridden parent, and a
fallback off the channel model still evicts.

(cherry picked from commit 7a3e9f5754bbb2597267ce499eac2658d7c9f17a)
2026-10-06 11:24:29 -04:00
Austin Pickett
ccff4df98c fix(skills): trust-scope inline-shell auto-exec to non-community hub skills
Inline `!`cmd`` expansion in SKILL.md bodies ran for every skill whenever the
global skills.inline_shell flag was on — a community hub skill and a builtin
were expanded identically. The hub's install gate (INSTALL_POLICY) blocks a
community install on the caution/dangerous verdicts the inline-shell DSL scans
as, but --force or a pre-scanner install puts that skill on disk anyway, and
the next view/slash/preload would auto-execute it on the host.

Gate expansion at the single preprocess_skill_content choke point (all three
surfaces route through it): community-trust hub lock entries never expand;
trusted/builtin hub entries and anything without a hub lock entry (bundled-
synced, user-created, project/external) keep the flag's contract. Lock read
failures skip the gate, like every other provenance consumer.

Fixes #63307
2026-10-06 11:20:43 -04:00
Austin Pickett
733d16973e fix: keep fs_read_text callable without a Request
The confinement guard reads only env/home state, so the Request the PR
threaded through fs_read_text/fs_download is unused. Make it optional
(request: Request = None keeps FastAPI injection while direct test
callers stay source-compatible) and drop the mandatory argument from
_hosted_fs_read_guard/_managed_files_policy.
2026-10-06 11:19:16 -04:00
Adolanium
b9d9aeeb58 fix(dashboard): confine fs preview and git diff reads to the managed root
The managed-files API (#57505) confines locked-dashboard reads through
_resolve_managed_path, but the file-preview and git-review routes resolve
through unconfined _fs_path / _git_path. On a locked deployment those
routes still returned auth.json / .env / mcp-tokens and any path outside
the managed root (including via git diff --no-index on an absolute file
argument).

Apply the same locked-root and sensitive-path checks to:
- /api/fs/list, /api/fs/read-text, /api/fs/read-data-url
- /api/git/review/diff and /api/git/file-diff (resolved file target)
- /api/git/review/commit-context (caller-supplied repo cwd; this route
  returns git diff --cached / git diff HEAD with no per-file arg, so the
  cwd itself is the read target)

Local dashboards with no locked root are unchanged. Regression tests cover
credential and outside-root blocks plus in-root allow paths, including a
locked-mode outside-root case for commit-context.

(cherry picked from commit b6ed59823f22fc537f123407a01915135dd6d5fe)
2026-10-06 11:19:16 -04:00
Austin Pickett
debc183d45 test(dashboard): password-form script test needs a password provider registered 2026-10-06 11:17:11 -04:00
Austin Pickett
082fae8058 fix(dashboard): cover whole prefix class — font URLs, native chooser, logout landing
Every root-absolute URL the server-rendered auth pages emit now honours
X-Forwarded-Prefix: @font-face url('/fonts/...') in both the login page
and the RFC 8252 native provider chooser, the chooser's data-prefix
injection, and the SPA logout navigation target. Empty-prefix output is
byte-identical to before.
2026-10-06 11:17:11 -04:00
liuhao1024
e33cf1b256 fix(dashboard): prefix post-login landing behind reverse proxy
When the dashboard is mounted at a sub-path via X-Forwarded-Prefix,
auth_callback and auth_password_login returned bare "/" as the
post-login landing URL instead of prepending the prefix. This caused
the browser to navigate outside the proxy mount after a successful
login.

Add _externalize_post_login_target() helper that prepends the prefix
to the validated landing path, and apply it in both auth_callback and
auth_password_login. Paths that already carry the prefix are returned
unchanged; empty prefix (direct deploy) is a no-op.

Reported-by: LeDominik
2026-10-06 11:17:11 -04:00
liuhao1024
53e5965b24 fix(dashboard): prepend X-Forwarded-Prefix in _safe_next_target() for post-login redirect
When the dashboard is behind a reverse proxy at a sub-path (e.g. /hermes),
the post-login redirect next= parameter must include the prefix so the
browser lands at /hermes/sessions instead of bare /sessions.

Auth-route and API-path rejection still operates on the original path
(before prefix) so detection works regardless of the mount point.

Addresses feedback from @grunjol and @LeDominik on #48625.
2026-10-06 11:17:11 -04:00
liuhao1024
aff64c886a fix(dashboard): include BASE path in logout redirect to /login
The logout handler used a hardcoded `/login` path for the post-logout
redirect. When the dashboard is served under a URL prefix (via
X-Forwarded-Prefix), this navigates to the wrong path, breaking the
logout flow for reverse-proxied deployments.

Use `${BASE}/login` to match the prefix-aware pattern already used
by the API calls in the same file.
2026-10-06 11:17:11 -04:00
liuhao1024
324fb9fdd0 fix(dashboard): respect X-Forwarded-Prefix in server-rendered login page
The login page emitted root-relative URLs (/auth/login, /auth/password-login)
for provider buttons and the password-login JS, breaking the OAuth and
password flows when the dashboard is served behind a reverse proxy at a
sub-path (e.g. /hermes/).

Now render_login_html() accepts a prefix parameter (resolved from the
X-Forwarded-Prefix header via the existing prefix_from_request() helper)
and prepends it to all auth URLs. The password-login JS reads the prefix
from a data-prefix attribute on the <main> element.

Fixes #48623
2026-10-06 11:17:11 -04:00
Austin Pickett
db26e55742 fix(dashboard): serve SPA assets with explicit MIME types
On Windows, mimetypes.init() reads the registry, where .js/.mjs can be
mapped to text/plain; starlette StaticFiles/FileResponse then serve the
dashboard bundle as text/plain, browsers reject the ESM module script
under strict MIME checking and the SPA stays blank (#28987).

Pin the served extensions (.js/.mjs/.cjs/.css/.json/.map/.svg/.wasm/
.webp/.webmanifest/.woff/.woff2/.ico) with explicit mimetypes.add_type
calls at web_server_dashboard import, so Content-Type is
platform-independent regardless of the host map. Idempotent.

Reuses the approach of closed PRs #28996 (xxxigm) and #29254
(Jiahui-Gu).
2026-10-06 11:15:27 -04:00
liuhao1024
727dbb17f2 fix(dashboard): move CORSMiddleware outermost so OPTIONS preflight bypasses auth
app.add_middleware(CORSMiddleware, ...) was registered before the
@app.middleware(http) auth decorators, making CORS the innermost
layer in Starlette's onion model.  A cross-origin OPTIONS preflight
hit auth first → 401, and the CORS headers were never sent.

Moving the add_middleware call after all @app.middleware registrations
makes CORS outermost, so preflights are answered before auth runs.

Fixes #59052
2026-10-06 11:15:20 -04:00
luyifan
d59c736fcb Cap dashboard password auth rate buckets 2026-10-06 11:14:42 -04:00