Skip to content

feat(adopt): connect Grok Build, Qwen Code and Hermes Agent - #265

Merged
fylorn merged 1 commit into
devfrom
adopt-grok-qwen-hermes
Oct 1, 2026
Merged

fylorn merged 1 commit into
devfrom
adopt-grok-qwen-hermes

Conversation

@fylorn

@fylorn fylorn commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

What

Lite can now take over three more clients and include them in the MCP list and the security scan. All three are marked FieldsOnly: the field names were checked against each client's source, but none of the clients was installed or run on this machine.

Client File written (default location) Format When it takes effect
Grok Build (xAI) $GROK_HOME/config.toml (~/.grok) TOML Immediately, because Grok watches the file
Qwen Code $QWEN_HOME/settings.json (~/.qwen) JSON(C) After a restart
Hermes Agent (Nous Research) $HERMES_HOME/config.yaml (~/.hermes, default profile) YAML After a restart

What each client gets:

  • Takeover:
    • diff preview
    • full backup
    • field-level restore (byte-for-byte when the file was not touched in between; see the roundtrip tests)
    • manual-setup fields
  • Endpoint detection.
  • A model-list "stale" flag, for Grok and Qwen.
  • A read-only MCP listing (unverified_format).
  • Scanning of hooks, skills, commands, agents and instruction files.

Config location:

  • The config location can be moved, like the other directory-based clients.
  • When Lite reads its own home, it honours GROK_HOME, QWEN_HOME and HERMES_HOME, plus HERMES_DATA_DIR_SUFFIX. Hermes on Windows defaults to %LOCALAPPDATA%\hermes.

Grok Build

The takeover writes one table per gateway model. With the gateway offering claude-sonnet-5 and gpt-5.5, an empty config becomes this (comment block shortened):

# === ThinkWatch: begin ===
# no model.thinkwatch/claude-sonnet-5 originally
# ...
# === ThinkWatch: end ===
[model."thinkwatch/claude-sonnet-5"]
model = "claude-sonnet-5"
name = "claude-sonnet-5 (ThinkWatch)"
base_url = "http://127.0.0.1:8788/v1"
api_backend = "messages"
api_key = "<gateway key>"

[model."thinkwatch/gpt-5.5"]
model = "gpt-5.5"
name = "gpt-5.5 (ThinkWatch)"
base_url = "http://127.0.0.1:8788/v1"
api_backend = "responses"
api_key = "<gateway key>"

[models]
default = "thinkwatch/claude-sonnet-5"

[features]
campaigns = false

Every table carries its own api_key. Without one, Grok would send the user's xAI session token to the gateway. Two facts in the source make this so:

  • Grok picks credentials in this order: the model's api_key/env_key, then an auth-provider token, then the session token, then XAI_API_KEY (config.rs).
  • may_receive_session returns true for every URL (backend/grok.rs).

When the gateway has no key, each table gets api_key = "no-key" for the same reason.

Other Grok details:

  • api_backend:
    • messages for claude*
    • responses for gpt-*, the o-series, *codex* and grok*
    • chat_completions for everything else
    • Values are from 11-custom-models.md.
  • models.default:
    • Points at one of our tables.
    • If the current pick is still offered, it is kept.
    • If the current pick is a built-in model, it is mapped onto our table for the same model.
  • features.campaigns = false: remote campaign patches can rewrite models.default (26-config-reference.md). This is stated in the cost list.
  • Re-adopting:
    • Tables for models the gateway no longer lists are dropped (new clients::stale_container).
    • Restore still works if the user deletes our comment block.
  • No models on the gateway: nothing is written, and the new note adopt.plan.no_models_nothing_written says so. The same applies to Qwen and Hermes.
  • requirements.toml: counts as an overriding file only when it sets models (26-config-reference.md).

Qwen Code

{
  "modelProviders": {
    "thinkwatch": [
      { "id": "claude-sonnet-5", "name": "claude-sonnet-5 (ThinkWatch)", "baseUrl": "http://127.0.0.1:8788/v1", "envKey": "THINKWATCH_QWEN_API_KEY" },
      { "id": "gpt-5.5", "name": "gpt-5.5 (ThinkWatch)", "baseUrl": "http://127.0.0.1:8788/v1", "envKey": "THINKWATCH_QWEN_API_KEY" }
    ]
  },
  "providerProtocol": { "thinkwatch": "openai" },
  "env": { "THINKWATCH_QWEN_API_KEY": "<gateway key>" },
  "security": { "auth": { "selectedType": "openai" } },
  "model": { "name": "claude-sonnet-5", "baseUrl": "http://127.0.0.1:8788/v1" }
}

Requests always use the OpenAI wire with /v1, never the anthropic type. The anthropic type presents itself as claude-cli on any host that is not Anthropic's own (anthropicContentGenerator.ts, L476-L486).

This departs from "add them to the openai list". Instead, the takeover writes a custom provider id, thinkwatch, and maps it to the openai protocol through providerProtocol (model-providers.md). There are two reasons:

  1. The openai list would leak the user's OpenAI key to the gateway.
    • A running Qwen hot-reloads modelProviders (model-providers.md#L23), but it does not reload settings.env.
    • Our models would therefore appear before THINKWATCH_QWEN_API_KEY exists.
    • The key would then fall back to OPENAI_API_KEY or security.auth.apiKey (modelConfigResolver.ts), so the user's OpenAI key would go to the gateway.
    • providerProtocol is read only at startup (hot-reload.ts). A custom id therefore stays unusable until the restart that also loads settings.env.
  2. /auth rewrites the openai list. A separate id survives /auth, and the user's own openai entries stay untouched.

providerProtocol first shipped in v0.19.3 (0d20772), which is where the version note comes from. The key goes into settings.env, which has the lowest priority (auth.md). THINKWATCH_QWEN_API_KEY is listed among the environment variables that can override what was written.

Hermes Agent

model:
  provider: custom
  base_url: http://127.0.0.1:8788/v1
  api_key: <gateway key>
  api_mode: anthropic_messages
  default: claude-sonnet-5
  • api_mode: anthropic_messages for claude*, otherwise chat_completions. Plain custom ignores codex_responses on hosts that are not OpenAI (runtime_provider.py#L183-L193).
  • Base URL:
  • Profiles:
    • Only the default profile is changed.
    • Before the user confirms, Lite lists the other profiles. It also warns when active_profile makes a plain hermes start in another profile (main.py).
    • Diagnostics report that case as Blocking, with the fix hermes profile use default.
  • Legacy model: "<string>" form: the takeover is refused as a parse error, and nothing is written.

What the gateway answers on Hermes' probe paths (findings only; core is not changed)

For a local endpoint, Hermes runs a series of probes with Authorization: Bearer <key> and a 2-second timeout (model_metadata.py#L717-L772):

  1. GET /api/v1/models: any 200 means LM Studio.
  2. /api/tags: a 200 with models means Ollama.
  3. /v1/props, then /props: means llama.cpp.
  4. /version: means vLLM.

A negative result is cached for 5 minutes (L74-L80).

This was measured against twcore 0.57.0, using an isolated instance and a fake upstream that returns 404 for everything except /v1/models:

Request Without a key With the client's key
GET /api/v1/models, /api/tags, /v1/props, /props, /version 401 404 (the upstream's answer)
GET /v1/models, /v1/models/m1 401 200
  • How the probes travel:
    • With a key, these paths fall through to passthrough (server.rs).
    • They reach the upstream as POST with an empty body (hop.rs#L1046): /v1/api/v1/models, /v1/api/tags, /v1/props twice, and /v1/version.
    • The upstream's 404 comes back, so Hermes detects nothing, which is the correct outcome.
  • Side effects:
    • Each probe leaves one failed history row, with an empty model and status 404. The new cost note adopt.cost.hermes_agent.probes says so.
    • A 404 counts as ModelUnavailable (failure.rs#L71), which does not pause the upstream (health.rs#L173).
    • Before the last upstream's 404 is returned, the probe fails over across the other routed upstreams.
  • Risk, not addressed here:
    • An upstream that answers 200 to POST …/api/v1/models gets that 200 passed through, and Hermes would conclude the gateway is LM Studio. This was confirmed with the fake upstream.
    • A core change could answer these well-known probe paths itself, with a 404, no upstream hop and no history row.

Scanning

  • Grok Build:
    • config.toml ([mcp_servers], [hooks])
    • hooks/*.json, skills/, commands/*.md, agents/*.md, rules/*.md, AGENTS.md
    • the same under the project's .grok/ (07-mcp-servers.md, 10-hooks.md)
  • Qwen Code:
    • settings.json (mcpServers, including httpUrl; hooks)
    • skills/, commands/*.md|*.toml, agents/, rules/, QWEN.md, AGENTS.md
    • the project's .qwen/… and QWEN.md
  • Hermes Agent:
    • config.yaml (mcp_servers, hooks)
    • skills in its category layout, up to 3 levels deep
    • SOUL.md
    • the project's .hermes.md, HERMES.md and .hermes/skills
  • Cursor's hooks.json (user and project level) is listed under Cursor. Cursor runs it, and Grok also runs it by default (10-hooks.md). When Grok Build is installed, the hooks section of the MCP page adds one sentence saying this.
  • ~/.agents/skills stays under the shared folder from feat(adopt): connect Pi and oh-my-pi; list the shared skills folder #263. Grok and Qwen read it too (storage.ts).

Also in this PR

  • Restart diagnosis: when a client's process cannot be identified (Qwen runs as node, Hermes as python), diagnostics now say it cannot be told whether the client was restarted. Before, they reported "not running".
  • Diff masking now also covers credentials elsewhere in these files:
    • the api_key of other Grok tables
    • Qwen's env and security.auth.apiKey
    • credential-like keys in Hermes' config
    • the env vars and headers of YAML MCP servers
  • Logos:
    • Grok Build uses the xAI mark.
    • Qwen Code uses the Qwen mark.
    • Hermes Agent uses Lobe Icons hermesagent (MIT, the same 1.95.1 package as the other marks).
  • READMEs (en/zh): the three clients are added to the one-step list, and the MCP count changes to thirteen clients.
  • Message codes: 17 new codes, each with Chinese. Every one is a new sentence; no existing code's text was changed.

Not verified / caveats

  • None of the three clients was installed or run here. Everything above comes from reading source.
  • Grok Build:
    • In enterprise lockdown (disable_api_key_auth, force_login_team_uuid, GROK_DISABLE_API_KEY_AUTH), Grok treats loopback as an xAI URL and may swap our per-model key for the session token. Not tested.
    • Global [models] extra_headers and env_http_headers also apply to our tables.
    • MDM, the GROK_CONFIG overlay and allowed_models can override or block our tables.
    • Whether already-open sessions switch model after a reload is unverified.
  • Qwen Code:
    • Workspace and system settings can override what is written.
    • --bare ignores settings entirely.
    • Proxy users need the gateway address in NO_PROXY, which the cost list says.
  • Hermes Agent:
    • Managed /etc/hermes files override the user config.
    • Fallback providers and auxiliary models may bypass the gateway.
    • Gateway event hooks (HOOK.yaml/handler.py) and Python plugins are not scanned.
    • $VAR inside HERMES_HOME is not expanded by Lite.
    • The generic restart sentence ("once the terminal is reopened") does not fit Hermes' messaging gateway, which picks up the change with the next message (run_agent_cache.py). The Hermes cost note says so.
  • Hermes Agent logo: the mark is a dense portrait and may read poorly at 16 px. The letter tile is the fallback if it does.

Checks

  • cargo fmt --all -- --check: pass
  • cargo clippy --all-targets -- -D warnings: pass
  • cargo test (whole workspace, with twcore 0.57.0 in resources/): all pass
    • tw-adopt: 257 lib, 17 claude_desktop and 67 roundtrip tests
    • tw-scan: 31 lib and 35 scan tests
    • desktop: 338 lib, 8 control_plane, 5 msg_codes and 2 ts_bindings tests
  • UPDATE_MSG_CODES=1 / UPDATE_TS=1 regeneration: no diff
  • npx tsc --noEmit and npx tsc --noEmit -p scripts/shots: pass
  • npx vitest run: 611 passed

🤖 Generated with Claude Code

Takeover (diff preview, backup, restore) and MCP/skills/hooks scanning for
three more clients, all marked FieldsOnly.

- Grok Build: one [model."thinkwatch/<model>"] table per gateway model in
  ~/.grok/config.toml, each with its own api_key so Grok never falls back to
  sending the xAI session token; models.default and features.campaigns.
- Qwen Code: a custom "thinkwatch" provider (providerProtocol = openai,
  /v1) in ~/.qwen/settings.json with the key in settings.env, so requests
  keep Qwen Code's own User-Agent.
- Hermes Agent: model.provider = custom with base_url/api_key/api_mode/
  default in ~/.hermes/config.yaml (default profile only); other and active
  profiles and a CUSTOM_BASE_URL in .env are called out.
- Scan: their MCP servers and hooks, skills (Hermes' category layout),
  commands, agents and instruction files; Cursor's hooks.json, which Grok
  also runs.
- README: the one-step client list and the MCP client count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn merged commit 70524d2 into dev Oct 1, 2026
4 checks passed
@fylorn
fylorn deleted the adopt-grok-qwen-hermes branch October 1, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant