# NousResearch/hermes-agent · Model picker filtering, visibility, and Copilot auth fixes

## Open now

- 13 issues, 10 PRs open on GitHub.
- #13796: /model picker: add option to show only user-configured models
- #15267: \[Feature Request\] Filter unconfigured providers from model selection UI
- #16608: \[Feature\]: Model allowlist — let users control which providers appear in /model list
- #23117: feat(tui/cli): allow users to bookmark/favorite models for quick switching
- #25246: \[Bug\]: copilot_auth.py fallback to gh auth token causes false positive for Copilot availability
- #35818: feat(model): picker_providers config key to filter /model picker list
- #42085: feat(model_switch): respect picker_providers whitelist + custom provider improvements
- #46304: Option to hide unconfigured providers from model switcher
- #49114: Desktop model visibility preferences revert because they are stored only in localStorage
- #51652: \[Bug\]: copilot credential auto-seeded from gh CLI with no consent gate — contaminates credential pool & breaks subagent/delegation routing for unrelated providers (model_not_supported)
- #53113: \[Feature\]: Add model_catalog.visible_models config to filter the model picker
- #60379: 严重安全违规：Hermes 在用户不知情、未授权的情况下窃取 GitHub Token (gh auth token)
- #60382: CRITICAL SECURITY VIOLATION: Hermes silently steals your GitHub token via \[redacted:github-token\] without consent
- PR #14571: feat(/model): honor picker_providers across pickers
- PR #18023: feat(cli): add picker_configured_only flag + /model configured (#13796)
- PR #25250: fix(copilot): stop using gh auth as token fallback
- PR #26818: fix(copilot): remove gh auth token fallback
- PR #27025: feat(models): add model_allowlist filter for dynamic-discovery providers
- PR #51655: fix(credential-pool): gate copilot auto-seed behind explicit provider configuration (#51652)
- PR #52094: fix(desktop): persist model visibility preferences to profile-scoped backend store (#49114)
- PR #52440: feat(gateway): optional model.allowlist to restrict /model targets
- PR #60404: fix(security): prevent unauthorized GitHub token access from gh CLI
- PR #131881: fix(copilot): stop using gh auth as token fallback \[risk 0.85\]

These items cover two adjacent themes: controlling which models and providers appear in model pickers (allowlists, hiding unused providers, favorites/LRU, explicit-only filtering, persistence of preferences), and a recurring issue with the Copilot auth layer incorrectly falling back to \`gh auth token\`, causing false-positive availability signals and raising security concerns.

## TL;DR

- 42 member(s): 13 open issue(s), 11 closed issue(s), 10 open PR(s), 2 merged PR(s), 6 PR(s) closed without merging.
- A fix was merged to main in PR #60514. Verify it addresses your case.
- **Recommended:** read PR #60514 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-60514`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.04`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #13796: /model picker: add option to show only user-configured models
- #15267: \[Feature Request\] Filter unconfigured providers from model selection UI
- #16608: \[Feature\]: Model allowlist — let users control which providers appear in /model list
- #23117: feat(tui/cli): allow users to bookmark/favorite models for quick switching
- #25246: \[Bug\]: copilot_auth.py fallback to gh auth token causes false positive for Copilot availability
- #35818: feat(model): picker_providers config key to filter /model picker list
- #42085: feat(model_switch): respect picker_providers whitelist + custom provider improvements
- #46304: Option to hide unconfigured providers from model switcher
- #49114: Desktop model visibility preferences revert because they are stored only in localStorage
- #51652: \[Bug\]: copilot credential auto-seeded from gh CLI with no consent gate — contaminates credential pool & breaks subagent/delegation routing for unrelated providers (model_not_supported)
- #53113: \[Feature\]: Add model_catalog.visible_models config to filter the model picker
- #60379: 严重安全违规：Hermes 在用户不知情、未授权的情况下窃取 GitHub Token (gh auth token)
- #60382: CRITICAL SECURITY VIOLATION: Hermes silently steals your GitHub token via \[redacted:github-token\] without consent

## Closed / duplicate issues

- #12655: feat(model-picker): add \`picker_providers\` config to filter which providers appear in /model
- #25245: copilot_auth.py fallback to gh auth token causes false positive for Copilot availability
- #25363: 模型选择器 /model 支持置顶常用模型（favorites / LRU）
- #26386: \[Feature\]: Config option to hide/disable built-in providers from the picker
- #27554: feat: add  config key to hide unused providers from model picker
- #33195: /model picker auto-discovers GitHub Copilot from gh auth token without explicit configuration
- #49002: gh auth token auto-promoted to Copilot provider with no scope validation; pollutes model picker with dead entries
- #56974: Desktop chat model picker shows implicitly discovered providers as if they were configured
- #57886: feat: allow disabling/hiding providers in model picker
- #59483: \[Feature\]: Filter model selector dropdown to only show providers with configured API keys
- #59733: feat: Only show configured providers in model picker, or add hide/disable option

## Prior & competing PRs

- **#25254: fix(copilot): remove gh auth token fallback from Copilot auth resolution**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#25250`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#60514: fix: limit desktop model pickers to explicit providers (#56974)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `apps/desktop/src/app/chat/index.tsx`, `apps/desktop/src/app/settings/model-settings.test.tsx`, `apps/desktop/src/components/model-visibility-dialog.tsx`, `apps/desktop/src/components/onboarding/flow.tsx`, `apps/desktop/src/components/onboarding/index.tsx`, `apps/desktop/src/hermes.test.ts`, `apps/desktop/src/hermes.ts`, `apps/desktop/src/lib/model-options.ts`, `apps/desktop/src/store/onboarding.ts`, `hermes_cli/auth.py`, `hermes_cli/inventory.py`, `hermes_cli/web_server.py`, `scripts/release.py`, `tests/hermes_cli/test_auth_provider_gate.py`, `tests/hermes_cli/test_inventory.py`, `tests/hermes_cli/test_web_server_profile_unification.py`, `tests/test_tui_gateway_server.py`, `tui_gateway/server.py`, `ui-tui/src/components/modelPicker.tsx`, `web/src/components/ModelPickerDialog.tsx` (+2 more)
  - fix hunks:
  - `apps/desktop/src/app/chat/index.tsx`

    ```diff
    @@ -358,7 +358,10 @@ export function ChatView({
             throw new Error('Hermes gateway unavailable')
           }
     
    -      return gateway.request<ModelOptionsResponse>('model.options', { session_id: activeSessionId })
    +      return gateway.request<ModelOptionsResponse>('model.options', {
    +        session_id: activeSessionId,
    +        explicit_only: true
    +      })
         },
         enabled: gatewayOpen
       })
    ```
  - `apps/desktop/src/app/settings/model-settings.test.tsx`

    ```diff
    @@ -13,8 +13,10 @@ beforeAll(() => {
     const getGlobalModelInfo = vi.fn()
     const getGlobalModelOptions = vi.fn()
     const getAuxiliaryModels = vi.fn()
    +const getMoaModels = vi.fn()
     const setModelAssignment = vi.fn()
     const getRecommendedDefaultModel = vi.fn()
    +const saveMoaModels = vi.fn()
     const setEnvVar = vi.fn()
     const getHermesConfigRecord = vi.fn()
     const saveHermesConfig = vi.fn()
    ```

    ```diff
    @@ -24,8 +26,10 @@ vi.mock('@/hermes', () => ({
       getGlobalModelInfo: () => getGlobalModelInfo(),
       getGlobalModelOptions: () => getGlobalModelOptions(),
       getAuxiliaryModels: () => getAuxiliaryModels(),
    +  getMoaModels: () => getMoaModels(),
       setModelAssignment: (body: unknown) => setModelAssignment(body),
       getRecommendedDefaultModel: (slug: string) => getRecommendedDefaultModel(slug),
    +  saveMoaModels: (body: unknown) => saveMoaModels(body),
       setEnvVar: (key: string, value: string) => setEnvVar(key, value),
       getHermesConfigRecord: () => getHermesConfigRecord(),
       saveHermesConfig: (config: unknown) => saveHermesConfig(config)
    ```

    ```diff
    @@ -45,15 +49,6 @@ beforeEach(() => {
             models: ['hermes-4', 'hermes-4-mini'],
             authenticated: true,
             capabilities: { 'hermes-4': { reasoning: true, fast: true } }
    -      },
    -      // An unconfigured api_key provider — surfaced by the full-universe payload.
    -      {
    -        name: 'DeepSeek',
    -        slug: 'deepseek',
    -        models: [],
    -        authenticated: false,
    -        auth_type: 'api_key',
    -        key_env: 'DEEPSEEK_API_KEY'
           }
         ]
       })
    ```

    ```diff
    @@ -61,8 +56,9 @@ beforeEach(() => {
         main: { provider: 'nous', model: 'hermes-4' },
         tasks: [{ task: 'vision', provider: 'auto', model: '', base_url: '' }]
       })
    +  getMoaModels.mockResolvedValue(null)
       setModelAssignment.mockResolvedValue({ provider: 'nous', model: 'hermes-4', gateway_tools: [] })
    -  getRecommendedDefaultModel.mockResolvedValue({ provider: 'deepseek', model: 'deepseek-chat', free_tier: null })
    +  getRecommendedDefaultModel.mockResolvedValue({ provider: 'nous', model: 'hermes-4', free_tier: null })
       setEnvVar.mockResolvedValue({ ok: true })
       getHermesConfigRecord.mockResolvedValue({ agent: { reasoning_effort: 'medium', service_tier: 'normal' } })
       saveHermesConfig.mockResolvedValue({ ok: true })
    ```

    ```diff
    @@ -80,43 +76,19 @@ async function renderModelSettings() {
     }
     
     describe('ModelSettings', () => {
    -  it('loads the current main model and lists the full provider universe', async () => {
    +  it('loads the current main model and lists configured providers only', async () => {
         await renderModelSettings()
     
         await waitFor(() => expect(getGlobalModelInfo).toHaveBeenCalled())
         await waitFor(() => expect(getGlobalModelOptions).toHaveBeenCalled())
     
    -    // Open the provider Select — every provider from the full payload should be
    -    // listed, including the unconfigured one with its "set up" hint.
    +    // Open the provider Select — only configured providers should be listed.
         const triggers = await screen.findAllByRole('combobox')
         fireEvent.click(triggers[0])
     
    -    // "Nous" shows in both the trigger and the open list; the unconfigured
    -    // provider + its setup hint are the unique signal of the full universe.
    +    // "Nous" shows in both the trigger and the open list.
         expect((await screen.findAllByText('Nous')).length).toBeGreaterThan(0)
    -    expect(await screen.findByText(/DeepSeek/)).toBeTruthy()
    -    expect(await screen.findByText(/set up/)).toBeTruthy()
    -  })
    -
    -  it('activates an unconfigured api_key provider inline by saving its key', async () => {
    -    await renderModelSettings()
    -
    -    await waitFor(() => expect(getGlobalModelOptions).toHaveBeenCalled())
    -
    -    // Open the provider Select and pick the unconfigured provider.
    -    const triggers = screen.getAllByRole('combobox')
    -    fireEvent.click(triggers[0])
    -    const deepseekOption = await screen.findByText(/DeepSeek/)
    -    fireEvent.click(deepseekOption)
    -
    -    // The inline key input appears for an api_key provider that needs setup.
    -    const keyInput = await screen.findByPlaceholderText(/Paste DEEPSEEK_API_KEY/)
    -    fireEvent.change(keyInput, { target: { value: 'sk-test-123' } })
    -
    -    const activate = await screen.findByRole('button', { name: /Activate/ })
    -    fireEvent.click(activate)
    -
    -    await waitFor(() => expect(setEnvVar).toHaveBeenCalledWith('DEEPSEEK_API_KEY', 'sk-test-123'))
    +    expect(screen.queryByText(/DeepSeek/)).toBeNull()
       })
     
       it('writes the profile default speed (service_tier) when the fast switch is toggled', async () => {
    ```
  - `apps/desktop/src/components/model-visibility-dialog.tsx`

    ```diff
    @@ -45,7 +45,10 @@ export function ModelVisibilityDialog({
         queryKey: ['model-options', sessionId || 'global'],
         queryFn: (): Promise<ModelOptionsResponse> => {
           if (gw && sessionId) {
    -        return gw.request<ModelOptionsResponse>('model.options', { session_id: sessionId })
    +        return gw.request<ModelOptionsResponse>('model.options', {
    +          session_id: sessionId,
    +          explicit_only: true
    +        })
           }
     
           return getGlobalModelOptions()
    ```
  - `apps/desktop/src/components/onboarding/flow.tsx`

    ```diff
    @@ -237,7 +237,7 @@ function ConfirmingModelPanel({
       // shows the same $/Mtok + Free/Pro info the picker and CLI do.
       const options = useQuery({
         queryKey: ['onboarding-model-options', flow.providerSlug],
    -    queryFn: () => getGlobalModelOptions()
    +    queryFn: () => getGlobalModelOptions({ includeUnconfigured: true, explicitOnly: false })
       })
     
       const providerRow = options.data?.providers?.find(
    ```
  - `apps/desktop/src/components/onboarding/index.tsx`

    ```diff
    @@ -99,7 +99,7 @@ function useApiKeyCatalog(): ApiKeyOption[] {
         // Promise.resolve().then so a synchronous throw (e.g. no desktop bridge in
         // tests) is funneled into the same .catch instead of escaping.
         void Promise.resolve()
    -      .then(() => getGlobalModelOptions())
    +      .then(() => getGlobalModelOptions({ includeUnconfigured: true, explicitOnly: false }))
           .then(res => {
             if (!cancelled) {
               setRows(res.providers ?? [])
    ```
  - `apps/desktop/src/hermes.test.ts`

    ```diff
    @@ -100,7 +100,7 @@ describe('Hermes REST session helpers', () => {
           [getHermesConfig, '/api/config'],
           [getHermesConfigDefaults, '/api/config/defaults'],
           [getGlobalModelInfo, '/api/model/info'],
    -      [() => getGlobalModelOptions(), '/api/model/options'],
    +      [() => getGlobalModelOptions(), '/api/model/options?explicit_only=1'],
           [getCronJobs, '/api/cron/jobs']
         ]
     
    ```

    ```diff
    @@ -134,4 +134,24 @@ describe('Hermes REST session helpers', () => {
           profile: 'xiaoxuxu'
         })
       })
    +
    +  it('defaults model options to configured providers only', async () => {
    +    await getGlobalModelOptions()
    +
    +    expect(api).toHaveBeenCalledWith(
    +      expect.objectContaining({
    +        path: '/api/model/options?explicit_only=1'
    +      })
    +    )
    +  })
    +
    +  it('can opt into unconfigured providers for onboarding flows', async () => {
    +    await getGlobalModelOptions({ includeUnconfigured: true, refresh: true, explicitOnly: false })
    +
    +    expect(api).toHaveBeenCalledWith(
    +      expect.objectContaining({
    +        path: '/api/model/options?refresh=1&include_unconfigured=1'
    +      })
    +    )
    +  })
     })
    ```
  - `apps/desktop/src/hermes.ts`

    ```diff
    @@ -869,10 +869,28 @@ export function getUsageAnalytics(days = 30): Promise<AnalyticsResponse> {
       })
     }
     
    -export function getGlobalModelOptions(opts?: { refresh?: boolean }): Promise<ModelOptionsResponse> {
    +export function getGlobalModelOptions(opts?: {
    +  refresh?: boolean
    +  includeUnconfigured?: boolean
    +  explicitOnly?: boolean
    +}): Promise<ModelOptionsResponse> {
    +  const params = new URLSearchParams()
    +
    +  if (opts?.refresh) {
    +    params.set('refresh', '1')
    +  }
    +
    +  if (opts?.includeUnconfigured) {
    +    params.set('include_unconfigured', '1')
    +  }
    +
    +  if (opts?.explicitOnly !== false) {
    +    params.set('explicit_only', '1')
    +  }
    +
       return window.hermesDesktop.api<ModelOptionsResponse>({
         ...profileScoped(),
    -    path: opts?.refresh ? '/api/model/options?refresh=1' : '/api/model/options',
    +    path: params.size > 0 ? `/api/model/options?${params.toString()}` : '/api/model/options',
         timeoutMs: STARTUP_REQUEST_TIMEOUT_MS
       })
     }
    ```
  - `apps/desktop/src/lib/model-options.ts`

    ```diff
    @@ -1,12 +1,17 @@
     import { getGlobalModelOptions, type HermesGateway, type ModelOptionsResponse } from '@/hermes'
     
     interface ModelOptionsRequest {
    +  /** When false, include ambient/unconfigured providers (onboarding/setup
    +   *  surfaces). Chat pickers default to true so only explicitly configured
    +   *  providers are listed (#56974). */
    +  explicitOnly?: boolean
       gateway?: HermesGateway
       refresh?: boolean
       sessionId?: null | string
     }
     
     export function requestModelOptions({
    +  explicitOnly = true,
       gateway,
       refresh = false,
       sessionId
    ```

    ```diff
    @@ -22,8 +27,12 @@ export function requestModelOptions({
           params.refresh = true
         }
     
    +    if (explicitOnly) {
    +      params.explicit_only = true
    +    }
    +
         return gateway.request<ModelOptionsResponse>('model.options', params)
       }
     
    -  return getGlobalModelOptions(refresh ? { refresh: true } : undefined)
    +  return getGlobalModelOptions({ explicitOnly, ...(refresh ? { refresh: true } : {}) })
     }
    ```
  - `apps/desktop/src/store/onboarding.ts`

    ```diff
    @@ -239,7 +239,7 @@ async function fetchProviderDefaultModel(
       let options
     
       try {
    -    options = await getGlobalModelOptions()
    +    options = await getGlobalModelOptions({ includeUnconfigured: true, explicitOnly: false })
       } catch {
         return null
       }
    ```
  - `hermes_cli/auth.py`

    ```diff
    @@ -1495,6 +1495,33 @@ def is_provider_explicitly_configured(provider_id: str) -> bool:
                 if has_usable_secret(os.getenv(env_var, "")):
                     return True
     
    +    # 4. Check persisted credential-pool entries that came from EXPLICIT flows
    +    # the user initiated inside Hermes (manual add / device-code / PKCE), plus
    +    # env-backed pool entries. This intentionally excludes ambient borrowed
    +    # sources like gh_cli / claude_code / qwen-cli.
    +    try:
    +        for entry in read_credential_pool(normalized):
    +            if not isinstance(entry, dict):
    +                continue
    +            source = str(entry.get("source") or "").strip().lower()
    +            if not source:
    +                continue
    +            if source.startswith("env:"):
    +                # A stale env-seeded pool entry survives in auth.json after
    +                # the user deletes the env var (#55790) — only count it when
    +                # the referenced var still resolves to a usable secret NOW.
    +                env_var = entry.get("source", "").split(":", 1)[1].strip()
    +                if env_var and has_usable_secret(os.getenv(env_var, "")):
    +                    return True
    +                continue
    +            if (
    +                source in {"device_code", "loopback_pkce", "hermes_pkce", "manual"}
    +                or source.startswith("manual:")
    +            ):
    +                return True
    +    except Exception:
    +        pass
    +
         return False
     
     
    ```
  - `hermes_cli/inventory.py`

    ```diff
    @@ -111,6 +111,7 @@ def load_picker_context() -> ConfigContext:
     def build_models_payload(
         ctx: ConfigContext,
         *,
    +    explicit_only: bool = False,
         include_unconfigured: bool = False,
         picker_hints: bool = False,
         canonical_order: bool = False,
    ```

    ```diff
    @@ -126,6 +127,10 @@ def build_models_payload(
         needs from a single substrate call.
     
         Flags:
    +    - ``explicit_only``: keep only providers the user explicitly configured
    +      (current provider, providers from config, or providers backed by
    +      provider-specific env vars). This hides ambient / auto-seeded
    +      credentials from desktop chat pickers.
         - ``include_unconfigured``: append ``CANONICAL_PROVIDERS`` rows that
           ``list_authenticated_providers`` didn't emit (TUI uses this to show
           the full provider universe in the picker).
    ```

    ```diff
    @@ -180,6 +185,9 @@ def build_models_payload(
         if moa_row is not None:
             rows = [moa_row] + [r for r in rows if str(r.get("slug", "")).lower() != "moa"]
     
    +    if explicit_only:
    +        rows = _filter_explicit_provider_rows(rows, ctx)
    +
         # --- Deduplicate: remove models from aggregators that overlap with
         # user-defined providers.  When a local proxy (e.g. litellm-proxy)
         # serves a model whose name also appears in an aggregator's curated
    ```

    ```diff
    @@ -308,6 +316,37 @@ def _append_unconfigured_rows(rows: list[dict], ctx: ConfigContext) -> list[dict
         return extras
     
     
    +def _filter_explicit_provider_rows(rows: list[dict], ctx: ConfigContext) -> list[dict]:
    +    """Keep only rows backed by explicit user configuration.
    +
    +    ``list_authenticated_providers`` intentionally discovers ambient / auto-
    +    seeded credentials (for example GitHub CLI -> Copilot). Desktop chat model
    +    pickers want the narrower subset the user explicitly configured for Hermes.
    +    """
    +    from hermes_cli.auth import is_provider_explicitly_configured
    +
    +    current_slug = str(ctx.current_provider or "").strip().lower()
    +    kept: list[dict] = []
    +    for row in rows:
    +        slug = str(row.get("slug", "")).strip().lower()
    +        if not slug:
    +            continue
    +        if row.get("is_user_defined"):
    +            kept.append(row)
    +            continue
    +        if current_slug and slug == current_slug:
    +            kept.append(row)
    +            continue
    +        if slug == "moa":
    +            # MoA is a virtual routing mode, not an independently configured
    +            # provider. Hide it from explicit-only pickers unless it is the
    +            # current provider (handled above).
    +            continue
    +        if is_provider_explicitly_configured(slug):
    +            kept.append(row)
    +    return kept
    +
    +
     def _apply_picker_hints(rows: list[dict]) -> None:
         """Add ``authenticated``/``auth_type``/``key_env``/``warning`` per row.
     
    ```
  - `hermes_cli/web_server.py`

    ```diff
    @@ -4284,7 +4284,12 @@ def get_model_info(profile: Optional[str] = None):
     
     
     @app.get("/api/model/options")
    -def get_model_options(profile: Optional[str] = None, refresh: bool = False):
    +def get_model_options(
    +    profile: Optional[str] = None,
    +    refresh: bool = False,
    +    include_unconfigured: bool = False,
    +    explicit_only: bool = False,
    +):
         """Return authenticated providers + their curated model lists.
     
         REST equivalent of the ``model.options`` JSON-RPC on tui_gateway, so the
    ```

    ```diff
    @@ -4303,18 +4308,15 @@ def get_model_options(profile: Optional[str] = None, refresh: bool = False):
         try:
             from hermes_cli.inventory import build_models_payload, load_picker_context
     
    -        # include_unconfigured + picker_hints + canonical_order mirror the
    -        # tui_gateway `model.options` JSON-RPC handler exactly, so every GUI
    -        # surface fed by this endpoint (Settings → Model, the first-run
    -        # onboarding picker) sees the SAME full provider universe `hermes model`
    -        # exposes — not just the authenticated subset. Unconfigured providers
    -        # come back as skeleton rows carrying `authenticated=False` +
    -        # `auth_type`/`key_env`/`warning` so the GUI can render a setup
    -        # affordance instead of hiding the provider entirely.
    +        # Most desktop surfaces should only list providers the user has already
    +        # configured. Onboarding opts into the full provider universe via
    +        # include_unconfigured=1 so it can still render setup affordances for
    +        # providers that are not yet authenticated.
             with _profile_scope(profile):
                 return build_models_payload(
                     load_picker_context(),
    -                include_unconfigured=True,
    +                explicit_only=bool(explicit_only),
    +                include_unconfigured=bool(include_unconfigured),
                     picker_hints=True,
                     canonical_order=True,
                     pricing=True,
    ```
  - `scripts/release.py`

    ```diff
    @@ -90,6 +90,7 @@
         "[redacted:email]": "VolodymyrBg",  # PR #2861 salvage (webhook: drop the unused full request payload from retained _delivery_info entries — up to ~1MB dead weight per delivery for the 1h idempotency TTL)
         "[redacted:email]": "CharmingGroot",  # PR #2794 salvage (email: guard msg_data[0][1] against malformed IMAP fetch structures so one bad response can't abort the batch and permanently lose seen-marked messages; Message-ID domain falls back to localhost when EMAIL_ADDRESS lacks '@')
         "[redacted:email]": "sahibzada-allahyar",  # PR #39227 salvage (desktop: configured terminal.cwd overrides a stale remembered workspace-cwd localStorage value when no session is active; #38855)
    +    "[redacted:email]": "rarf",  # PR #56966 salvage (desktop chat model picker: hide implicitly discovered providers unless explicitly configured)
         "[redacted:email]": "plcunha",  # PR #55300 salvage (gateway: record child gateway peer metadata after a compression session-id rotation and repoint stale sessions.json compression-parent entries to the recovered live child; consolidated in the compression-routing-integrity salvage)
         "[redacted:email]": "jakepresent",  # PR #55721 salvage (gateway: identity-guard stale in-flight compression splits — a late run may publish its compressed child only if its run generation is still current and the session key still points at the run's original parent, so an old run can't overwrite a newer /new or moved binding)
         "[redacted:email]": "gumclaw",  # PR #57322 salvage (gateway: close per-delivery webhook sessions on completion so prune_sessions can reap them — fixes unbounded state.db growth from unprunable ended_at=NULL webhook rows)
    ```
  - `tests/hermes_cli/test_auth_provider_gate.py`

    ```diff
    @@ -82,3 +82,92 @@ def test_claude_code_oauth_token_does_not_count_as_explicit(tmp_path, monkeypatc
     
         from hermes_cli.auth import is_provider_explicitly_configured
         assert is_provider_explicitly_configured("anthropic") is False
    +
    +
    +def test_ambient_pool_source_does_not_count_as_explicit(tmp_path, monkeypatch):
    +    """gh_cli-seeded Copilot pool entries are ambient, not explicit config (#56974)."""
    +    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
    +    monkeypatch.delenv("COPILOT_GITHUB_TOKEN", raising=False)
    +    monkeypatch.delenv("GH_TOKEN", raising=False)
    +    monkeypatch.delenv("GITHUB_TOKEN", raising=False)
    +    _write_auth_store(tmp_path, {
    +        "version": 1,
    +        "providers": {},
    +        "active_provider": None,
    +        "credential_pool": {
    +            "copilot": [{
    +                "id": "abc123",
    +                "source": "gh_cli",
    +                "auth_type": "api_key",
    +                "access_token": "ghu_sometoken",
    +            }],
    +        },
    +    })
    +
    +    from hermes_cli.auth import is_provider_explicitly_configured
    +    assert is_provider_explicitly_configured("copilot") is False
    +
    +
    +def test_explicit_pool_source_counts_as_explicit(tmp_path, monkeypatch):
    +    """manual / device_code / PKCE pool entries reflect explicit Hermes flows."""
    +    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
    +    _write_auth_store(tmp_path, {
    +        "version": 1,
    +        "providers": {},
    +        "active_provider": None,
    +        "credential_pool": {
    +            "anthropic": [{
    +                "id": "def456",
    +                "source": "manual:key-1",
    +                "auth_type": "api_key",
    +                "access_token": "sk-ant-api03-key",
    +            }],
    +        },
    +    })
    +
    +    from hermes_cli.auth import is_provider_explicitly_configured
    +    assert is_provider_explicitly_configured("anthropic") is True
    +
    +
    +def test_stale_env_pool_entry_does_not_count_when_var_unset(tmp_path, monkeypatch):
    +    """An env-seeded pool entry left in auth.json after the env var was removed
    +    must not mark the provider configured (#55790): the picker showed removed
    +    providers forever because the record existed even though no secret resolves."""
    +    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
    +    monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
    +    _write_auth_store(tmp_path, {
    +        "version": 1,
    +        "providers": {},
    +        "active_provider": None,
    +        "credential_pool": {
    +            "deepseek": [{
    +                "id": "aaa111",
    +                "source": "env:DEEPSEEK_API_KEY",
    +                "auth_type": "api_key",
    +            }],
    +        },
    +    })
    +
    +    from hermes_cli.auth import is_provider_explicitly_configured
    +    assert is_provider_explicitly_configured("deepseek") is False
    +
    +
    +def test_env_pool_entry_counts_when_var_still_resolves(tmp_path, monkeypatch):
    +    """The same env-seeded pool entry IS explicit while the var still resolves."""
    +    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
    +    monkeypatch.setenv("DEEPSEEK_API_KEY", "[redacted:openai-key]")
    +    _write_auth_store(tmp_path, {
    +        "version": 1,
    +        "providers": {},
    +        "active_provider": None,
    +        "credential_pool": {
    +            "deepseek": [{
    +                "id": "aaa111",
    +                "source": "env:DEEPSEEK_API_KEY",
    +                "auth_type": "api_key",
    +            }],
    +        },
    +    })
    +
    +    from hermes_cli.auth import is_provider_explicitly_configured
    +    assert is_provider_explicitly_configured("deepseek") is True
    ```
  - `tests/hermes_cli/test_inventory.py`

    ```diff
    @@ -378,6 +378,44 @@ def test_include_unconfigured_skips_already_present_slugs():
         assert or_rows[0]["models"] == ["m1"]  # the authenticated row, not skeleton
     
     
    +def test_explicit_only_filters_ambient_credentials_but_keeps_current_and_custom_rows():
    +    rows = [
    +        {"slug": "openai-codex", "name": "OpenAI Codex", "models": ["gpt-5.4"],
    +         "total_models": 1, "is_current": True, "is_user_defined": False,
    +         "source": "hermes"},
    +        {"slug": "gemini", "name": "Gemini", "models": ["gemini-2.5-pro"],
    +         "total_models": 1, "is_current": False, "is_user_defined": False,
    +         "source": "built-in"},
    +        {"slug": "copilot", "name": "Copilot", "models": ["gpt-5.4"],
    +         "total_models": 1, "is_current": False, "is_user_defined": False,
    +         "source": "hermes"},
    +        {"slug": "nous", "name": "Nous", "models": ["anthropic/claude-sonnet-5"],
    +         "total_models": 1, "is_current": False, "is_user_defined": False,
    +         "source": "hermes"},
    +        {"slug": "custom:lab", "name": "Lab", "models": ["lab-1"],
    +         "total_models": 1, "is_current": False, "is_user_defined": True,
    +         "source": "user-config"},
    +        {"slug": "moa", "name": "MoA", "models": ["default"],
    +         "total_models": 1, "is_current": False, "is_user_defined": False,
    +         "source": "virtual"},
    +    ]
    +    ctx = _empty_ctx(provider="openai-codex", model="gpt-5.4")
    +    with (
    +        _list_auth_returning(rows),
    +        patch(
    +            "hermes_cli.auth.is_provider_explicitly_configured",
    +            side_effect=lambda slug: slug == "gemini",
    +        ),
    +    ):
    +        payload = build_models_payload(ctx, explicit_only=True)
    +
    +    assert [row["slug"] for row in payload["providers"]] == [
    +        "openai-codex",
    +        "gemini",
    +        "custom:lab",
    +    ]
    +
    +
     # ─── picker_hints ──────────────────────────────────────────────────────
     
     
    ```
  - `tests/hermes_cli/test_web_server_profile_unification.py`

    ```diff
    @@ -316,6 +316,36 @@ def test_model_options_unknown_profile_404(self, client, isolated_profiles):
             resp = client.get("/api/model/options", params={"profile": "ghost"})
             assert resp.status_code == 404
     
    +    def test_model_options_hides_unconfigured_providers_by_default(self, client, monkeypatch):
    +        calls = []
    +
    +        monkeypatch.setattr(
    +            "hermes_cli.inventory.load_picker_context",
    +            lambda: object(),
    +        )
    +
    +        def _fake_build_models_payload(_ctx, **kwargs):
    +            calls.append(kwargs)
    +            return {"providers": [], "model": "", "provider": ""}
    +
    +        monkeypatch.setattr(
    +            "hermes_cli.inventory.build_models_payload",
    +            _fake_build_models_payload,
    +        )
    +
    +        resp = client.get("/api/model/options")
    +        assert resp.status_code == 200
    +        assert calls[-1]["explicit_only"] is False
    +        assert calls[-1]["include_unconfigured"] is False
    +
    +        resp = client.get("/api/model/options", params={"explicit_only": "1"})
    +        assert resp.status_code == 200
    +        assert calls[-1]["explicit_only"] is True
    +
    +        resp = client.get("/api/model/options", params={"include_unconfigured": "1"})
    +        assert resp.status_code == 200
    +        assert calls[-1]["include_unconfigured"] is True
    +
         def test_model_info_unknown_profile_404(self, client, isolated_profiles):
             """Regression: the broad except used to convert the 404 into a 200
             with empty model info ("no model set" — silently wrong)."""
    ```
  - `tests/test_tui_gateway_server.py`

    ```diff
    @@ -5984,6 +5984,52 @@ def test_model_options_propagates_list_exception(monkeypatch):
         assert "catalog blew up" in resp["error"]["message"]
     
     
    +def test_model_options_hides_unconfigured_providers_by_default(monkeypatch):
    +    from hermes_cli.inventory import ConfigContext
    +
    +    calls = []
    +
    +    monkeypatch.setattr(server, "_resolve_model", lambda: "")
    +    monkeypatch.setattr(
    +        "hermes_cli.inventory.load_picker_context",
    +        lambda: ConfigContext(
    +            current_provider="",
    +            current_model="",
    +            current_base_url="",
    +            user_providers={},
    +            custom_providers=[],
    +        ),
    +    )
    +
    +    def _fake_build_models_payload(_ctx, **kwargs):
    +        calls.append(kwargs)
    +        return {"providers": [], "model": "", "provider": ""}
    +
    +    monkeypatch.setattr(
    +        "hermes_cli.inventory.build_models_payload",
    +        _fake_build_models_payload,
    +    )
    +
    +    resp = server._methods["model.options"](99, {"session_id": ""})
    +    assert "result" in resp, resp
    +    assert calls[-1]["explicit_only"] is False
    +    assert calls[-1]["include_unconfigured"] is False
    +
    +    resp = server._methods["model.options"](
    +        100,
    +        {"session_id": "", "explicit_only": True},
    +    )
    +    assert "result" in resp, resp
    +    assert calls[-1]["explicit_only"] is True
    +
    +    resp = server._methods["model.options"](
    +        101,
    +        {"session_id": "", "include_unconfigured": True},
    +    )
    +    assert "result" in resp, resp
    +    assert calls[-1]["include_unconfigured"] is True
    +
    +
     # ---------------------------------------------------------------------------
     # prompt.submit — auto-title
     # ---------------------------------------------------------------------------
    ```
  - `tui_gateway/server.py`

    ```diff
    @@ -12398,17 +12398,18 @@ def _(rid, params: dict) -> dict:
                 ),
                 current_base_url=getattr(agent, "base_url", "") if agent else "",
             )
    -        # picker_hints + canonical_order produce the TUI's required shape:
    +        # picker_hints + canonical_order produce the TUI/desktop picker shape:
             # `authenticated`/`auth_type`/`key_env`/`warning` per row, in
    -        # CANONICAL_PROVIDERS declaration order. include_unconfigured=True
    -        # so the picker can show the full provider universe (with the
    -        # setup-hint warning attached) instead of only authed rows.
    +        # CANONICAL_PROVIDERS declaration order. Desktop pickers default to the
    +        # configured subset; callers that need setup affordances can pass
    +        # include_unconfigured=true explicitly.
             # Curated model lists are preserved — list_authenticated_providers
             # populates `models` from the curated catalog, not provider_model_ids
             # (which would pull non-agentic models like TTS/embeddings/etc.).
             payload = build_models_payload(
                 ctx,
    -            include_unconfigured=True,
    +            explicit_only=bool(params.get("explicit_only")),
    +            include_unconfigured=bool(params.get("include_unconfigured")),
                 picker_hints=True,
                 canonical_order=True,
                 pricing=True,
    ```
  - `ui-tui/src/components/modelPicker.tsx`

    ```diff
    @@ -60,7 +60,12 @@ export function ModelPicker({
       useEffect(() => {
         gw.request<ModelOptionsResponse>('model.options', {
           ...(sessionId ? { session_id: sessionId } : {}),
    -      ...(initialRefresh ? { refresh: true } : {})
    +      ...(initialRefresh ? { refresh: true } : {}),
    +      // The TUI picker shows the full provider universe with setup
    +      // affordances ("paste KEY to activate"), so opt into unconfigured
    +      // rows — the backend now defaults to the configured subset for
    +      // desktop chat pickers (#56974).
    +      include_unconfigured: true
         })
           .then(raw => {
             const r = asRpcResult<ModelOptionsResponse>(raw)
    ```
  - `web/src/components/ModelPickerDialog.tsx`

    ```diff
    @@ -138,6 +138,10 @@ export function ModelPickerDialog(props: Props) {
               {
                 ...(sessionId ? { session_id: sessionId } : {}),
                 ...(refresh ? { refresh: true } : {}),
    +            // Dashboard picker mirrors the TUI: full provider universe with
    +            // setup warnings. The backend now defaults to the configured
    +            // subset (#56974), so opt into unconfigured rows explicitly.
    +            include_unconfigured: true,
               },
             );
     
    ```
  - `web/src/lib/api.test.ts`

    ```diff
    @@ -22,7 +22,7 @@ describe("api.getModelOptions", () => {
         await api.getModelOptions({ refresh: true });
     
         expect(fetchMock).toHaveBeenCalledWith(
    -      "/api/model/options?refresh=1",
    +      "/api/model/options?refresh=1&include_unconfigured=1",
           expect.objectContaining({ credentials: "[redacted:secret-assignment]" }),
         );
       });
    ```

    ```diff
    @@ -41,7 +41,7 @@ describe("api.getModelOptions", () => {
         await api.getModelOptions({ profile: "default", refresh: true });
     
         expect(fetchMock).toHaveBeenCalledWith(
    -      "/api/model/options?profile=default&refresh=1",
    +      "/api/model/options?profile=default&refresh=1&include_unconfigured=1",
           expect.objectContaining({ credentials: "[redacted:secret-assignment]" }),
         );
       });
    ```
  - `web/src/lib/api.ts`

    ```diff
    @@ -487,6 +487,11 @@ export const api = {
         const qs = new URLSearchParams();
         if (profile) qs.set("profile", profile);
         if (refresh) qs.set("refresh", "1");
    +    // Dashboard surfaces (Models page, profile builder, cron) are
    +    // management/setup UIs: keep the full provider universe with setup
    +    // affordances. The endpoint now defaults to the configured subset for
    +    // desktop chat pickers (#56974), so opt in explicitly here.
    +    qs.set("include_unconfigured", "1");
         const suffix = qs.toString() ? `?${qs.toString()}` : "";
         return fetchJSON<ModelOptionsResponse>(`/api/model/options${suffix}`);
       },
    ```
- **#60757: fix(desktop): keep configured MoA presets in model picker**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/inventory.py`, `tests/hermes_cli/test_inventory.py`
  - fix hunks:
  - `hermes_cli/inventory.py`

    ```diff
    @@ -340,13 +340,61 @@ def _filter_explicit_provider_rows(rows: list[dict], ctx: ConfigContext) -> list
             if slug == "moa":
                 # MoA is a virtual routing mode, not an independently configured
                 # provider. Hide it from explicit-only pickers unless it is the
    -            # current provider (handled above).
    +            # current provider (handled above) or the user explicitly wrote an
    +            # enabled MoA preset into config.yaml. Use raw config so the
    +            # DEFAULT_CONFIG preset does not make every desktop picker show MoA.
    +            if _raw_config_has_enabled_moa_preset():
    +                kept.append(row)
                 continue
             if is_provider_explicitly_configured(slug):
                 kept.append(row)
         return kept
     
     
    +def _raw_config_has_enabled_moa_preset() -> bool:
    +    """Return True when the user's raw config explicitly enables MoA.
    +
    +    ``load_config()`` includes ``DEFAULT_CONFIG["moa"].presets.default`` for
    +    everyone. Explicit-only model pickers must not treat that default as a user
    +    choice, but they should keep MoA visible once the user has saved at least
    +    one enabled preset (or an older flat MoA config) in their own config.yaml.
    +    """
    +    try:
    +        from hermes_cli.config import read_raw_config
    +
    +        raw = read_raw_config()
    +    except Exception:
    +        return False
    +
    +    if not isinstance(raw, dict):
    +        return False
    +    moa = raw.get("moa")
    +    if not isinstance(moa, dict):
    +        return False
    +
    +    presets = moa.get("presets")
    +    if isinstance(presets, dict):
    +        for name, preset in presets.items():
    +            if not str(name or "").strip():
    +                continue
    +            if not isinstance(preset, dict):
    +                return True
    +            if preset.get("enabled", True):
    +                return True
    +        return False
    +
    +    legacy_keys = {
    +        "reference_models",
    +        "aggregator",
    +        "reference_temperature",
    +        "aggregator_temperature",
    +        "max_tokens",
    +        "reference_max_tokens",
    +        "fanout",
    +    }
    +    return any(key in moa for key in legacy_keys) and bool(moa.get("enabled", True))
    +
    +
     def _apply_picker_hints(rows: list[dict]) -> None:
         """Add ``authenticated``/``auth_type``/``key_env``/``warning`` per row.
     
    ```
  - `tests/hermes_cli/test_inventory.py`

    ```diff
    @@ -402,6 +402,7 @@ def test_explicit_only_filters_ambient_credentials_but_keeps_current_and_custom_
         ctx = _empty_ctx(provider="openai-codex", model="gpt-5.4")
         with (
             _list_auth_returning(rows),
    +        patch("hermes_cli.config.read_raw_config", return_value={}),
             patch(
                 "hermes_cli.auth.is_provider_explicitly_configured",
                 side_effect=lambda slug: slug == "gemini",
    ```

    ```diff
    @@ -416,6 +417,43 @@ def test_explicit_only_filters_ambient_credentials_but_keeps_current_and_custom_
         ]
     
     
    +def test_explicit_only_keeps_moa_when_raw_config_has_enabled_preset():
    +    rows = [
    +        {"slug": "moa", "name": "MoA", "models": ["review"],
    +         "total_models": 1, "is_current": False, "is_user_defined": False,
    +         "source": "virtual"},
    +    ]
    +    ctx = _empty_ctx(provider="openrouter", model="anthropic/claude-opus-4.8")
    +    raw_config = {
    +        "moa": {
    +            "active_preset": "review",
    +            "presets": {
    +                "review": {
    +                    "enabled": True,
    +                    "reference_models": [
    +                        {"provider": "openai-codex", "model": "gpt-5.5"},
    +                    ],
    +                    "aggregator": {
    +                        "provider": "openrouter",
    +                        "model": "anthropic/claude-opus-4.8",
    +                    },
    +                },
    +            },
    +        },
    +    }
    +
    +    with (
    +        _list_auth_returning(rows),
    +        patch("hermes_cli.config.load_config", return_value=raw_config),
    +        patch("hermes_cli.config.read_raw_config", return_value=raw_config),
    +        patch("hermes_cli.auth.is_provider_explicitly_configured", return_value=False),
    +    ):
    +        payload = build_models_payload(ctx, explicit_only=True)
    +
    +    assert [row["slug"] for row in payload["providers"]] == ["moa"]
    +    assert payload["providers"][0]["models"] == ["review"]
    +
    +
     # ─── picker_hints ──────────────────────────────────────────────────────
     
     
    ```
- **#14571: feat(/model): honor picker_providers across pickers**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `619bd782738adfa87544d0fda7a1114defe86e4c`
  - files: `gateway/run.py`, `hermes_cli/inventory.py`, `hermes_cli/model_switch.py`, `tests/gateway/test_model_command_custom_providers.py`, `tests/hermes_cli/test_inventory.py`, `tests/hermes_cli/test_list_picker_providers.py`, `tests/hermes_cli/test_model_picker_provider_filter.py`, `tests/test_tui_gateway_server.py`
- **#18023: feat(cli): add picker_configured_only flag + /model configured (#13796)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `37a4cf9000`
  - files: `cli-config.yaml.example`, `cli.py`, `gateway/run.py`, `hermes_cli/model_switch.py`, `hermes_cli/web_server.py`, `tests/cli/test_cli_provider_resolution.py`, `tests/gateway/test_model_command_custom_providers.py`, `tests/hermes_cli/test_picker_configured_only.py`, `tui_gateway/server.py`, `website/docs/user-guide/configuring-models.md`
- **#25250: fix(copilot): stop using gh auth as token fallback**. open
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`
  - files: `agent/auxiliary_client.py`, `agent/credential_pool.py`, `agent/credential_sources.py`, `hermes_cli/copilot_auth.py`, `hermes_cli/model_setup_flows.py`, `hermes_cli/models.py`, `run_agent.py`, `tests/agent/test_credential_pool.py`, `tests/hermes_cli/test_api_key_providers.py`, `tests/hermes_cli/test_copilot_auth.py`, `tests/hermes_cli/test_copilot_catalog_oauth_fallback.py`, `tests/run_agent/test_run_agent_codex_responses.py`, `tests/test_windows_subprocess_no_window_flags.py`, `website/docs/integrations/providers.md`, `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/integrations/providers.md`
- **#25252: fix(copilot): remove gh auth token fallback to prevent false positive availability**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#25250`, `#25246`
- **#25372: feat: model picker favorites/LRU + reasoning sub-picker**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`
- **#26818: fix(copilot): remove gh auth token fallback**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `hermes_cli/copilot_auth.py`, `tests/hermes_cli/test_api_key_providers.py`, `tests/hermes_cli/test_copilot_auth.py`
- **#27025: feat(models): add model_allowlist filter for dynamic-discovery providers**. open
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#20298`, `#14571`, `#18023`, `#19526`
  - files: `hermes_cli/config.py`, `hermes_cli/model_catalog.py`, `hermes_cli/models.py`, `tests/gateway/test_discord_document_handling.py`
- **#49102: fix(copilot): validate token scope at seed time, skip dead credential pool entries**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#51655: fix(credential-pool): gate copilot auto-seed behind explicit provider configuration (#51652)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `4a4a0c2fc`
  - files: `agent/credential_pool.py`, `tests/agent/test_credential_pool.py`, `tests/hermes_cli/test_auth_commands.py`
- **#51664: fix: add consent gate to copilot provider in _seed_from_singletons**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#51655`, `#51652`, `#51655`, `#51655`
- **#52094: fix(desktop): persist model visibility preferences to profile-scoped backend store (#49114)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts`, `apps/desktop/src/store/model-visibility.ts`, `tests/test_tui_gateway_server.py`, `tui_gateway/server.py`
- **#52440: feat(gateway): optional model.allowlist to restrict /model targets**. open
  - maintainer direction: category `partial_fix`; implication `needs_llm`; refs `#48611`, `#108771`
  - files: `cli-config.yaml.example`, `gateway/slash_commands.py`, `hermes_cli/config.py`, `hermes_cli/web_server.py`, `locales/af.yaml`, `locales/de.yaml`, `locales/en.yaml`, `locales/es.yaml`, `locales/fr.yaml`, `locales/ga.yaml`, `locales/hu.yaml`, `locales/it.yaml`, `locales/ja.yaml`, `locales/ko.yaml`, `locales/pt.yaml`, `locales/ru.yaml`, `locales/tr.yaml`, `locales/uk.yaml`, `locales/zh-hant.yaml`, `locales/zh.yaml` (+2 more)
- **#60404: fix(security): prevent unauthorized GitHub token access from gh CLI**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `agent/credential_pool.py`, `tests/agent/test_credential_pool.py`, `tests/hermes_cli/test_auth_commands.py`
- **#61887: fix(moa): keep MoA presets in explicit-only desktop pickers (#61889)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `37a4cf900`, `#60757`, `#60722`, `#37`, `#61889`, `#60722`
- **#131881: fix(copilot): stop using gh auth as token fallback \[risk 0.85\]**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `5670d82bee891df1b13bed130d0349c66be87e68`
  - files: `agent/auxiliary_client.py`, `agent/credential_pool.py`, `agent/credential_sources.py`, `hermes_cli/copilot_auth.py`, `hermes_cli/model_setup_flows.py`, `hermes_cli/models.py`, `hermes_cli/models_catalog_static.py`, `tests/agent/test_credential_pool.py`, `tests/e2e/core/parity/test_entrypoint_parity.py`, `tests/hermes_cli/test_api_key_providers.py`, `tests/hermes_cli/test_copilot_auth.py`, `tests/hermes_cli/test_runtime_provider_resolution.py`, `tests/tui_gateway/test_cold_start_gil_stall.py`, `website/docs/integrations/providers.md`, `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/integrations/providers.md`

## Fix status

- Merged to main in PR #60514 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-10`.
- Subsystem: `agent`
