Skip to content

feat(console): Add to ThinkWatch Lite; gateway address for every user; model lists follow the key - #100

Merged
fylorn merged 3 commits into
devfrom
feat/add-to-lite
Oct 11, 2026
Merged

fylorn merged 3 commits into
devfrom
feat/add-to-lite

Conversation

@fylorn

@fylorn fylorn commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

What

  • Add to ThinkWatch Lite. The new-key and rotated-key dialogs and the end of first-run setup get an "Add to ThinkWatch Lite" button beside Copy. It opens thinkwatch://import?service=thinkwatch&name=thinkwatch&url=<gateway base URL>&key=<key> (each value encodeURIComponent-encoded), which ThinkWatch Lite 2026.10.16 and later turns into a new upstream for this gateway. It is disabled, with the reason beside it, for a key without the ai_gateway surface and for a gateway whose public address Lite would refuse (plain HTTP outside this machine and private networks, or anything but a bare scheme://host[:port]). Once clicked, a line under it reads "Nothing happened? Install ThinkWatch Lite (2026.10.16 or later) first" (zh: 「没有反应?先安装 ThinkWatch Lite(2026.10.16 或更新版本)」) and links to thinkwat.ch/lite or thinkwat.ch/zh-CN/lite, following the console language. Below the md breakpoint only Copy shows.
  • Configuration Guide → ThinkWatch Lite tab, first among the AI gateway clients: two sentences on what Lite does, a "Create a key and add it to Lite" button that opens the create-key dialog prefilled (name "ThinkWatch Lite", surface ai_gateway) for users with api_keys:create, and a download link. Users without that permission are told how to get a key and add it by hand.
  • GET /api/gateway/info (console port, any signed-in user): {"base_url": "https://gateway.example.com"}, computed server-side from general.public_protocol/host/port. Unset parts come from the address the console was opened at (X-Forwarded-Proto, else http; X-Forwarded-Host, else Host) and the gateway's listening port; a default port is left out; a configured host that is more than a host (a port, a path, ${…}) is passed over.

Fixes

  • The guide showed a wrong gateway address to non-admins. It read /api/admin/settings/system, which needs settings:read; everyone else got a 403 and browser host + :3000, whatever general.public_* said. It now reads /api/gateway/info.
  • /v1/models and /v1beta/models listed every model whatever the key. They now list only the models a request with that key is let through for: the request-time check and the listings share GatewayRequestIdentity::may_call (the key's allowed_models intersected with the owner's role grants, exact or prefix match, empty = nothing), applied after the same alias mapping a request goes through. CHANGELOG has this under "Read before upgrading" because clients with narrowed keys will see shorter lists.

Credential handling

  • The import link is built in the browser from the plaintext key already on screen and put in the href of an <a> in the same dialog. No request carries it; the download link carries nothing but the page URL (rel="noopener noreferrer"). The key is never stored or logged anywhere new.
  • /api/gateway/info returns only the public address. The headers it reads fill only what an administrator left unset, and the answer goes back to the same caller, so a forged header misleads nobody else. The console query for it passes no401Redirect, so a 401 can never navigate away from a screen that shows a key once.
  • The model listings can only narrow what was listed before; the request-time check is unchanged in behaviour (the matching moved into may_call, which is_access_allowed now calls).

Plain HTTP on private networks

The button follows ThinkWatch Lite's import-link contract v2.2: https:// always; http:// for loopback (localhost, 127.0.0.1, [::1]), private IPv4 (10/8, 172.16/12, 192.168/16, 100.64/10, 169.254/16), private IPv6 (fc00::/7, fe80::/10), and names with no dot or ending in .local, .lan, .internal, .intranet, .corp, .home.arpa, .localhost. Any other http:// host keeps a disabled button with the reason. The edges are unit-tested in lite-link.test.ts (172.15/172.16, 172.31/172.32, 100.63/100.64, 100.127/100.128, corp, example.corp, example.com, fc00/fe80/febf/fec0, …).

Verification

  • cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, cargo clippy --workspace --lib --bins -- -D warnings: clean.
  • cargo nextest run --workspace --lib --bins --tests: 806 passed.
  • Integration suite (--run-ignored only, own Postgres 18 / Redis 8 / ClickHouse 26.3 containers): 444 passed, 1 skipped. New: gateway_authz::model_listings_follow_the_key_allow_list, gateway_authz::model_listings_follow_the_owners_roles, console_admin::gateway_info_gives_any_signed_in_user_the_public_gateway_url; openapi_contract covers the new path.
  • web/: pnpm lint, pnpm test (159 passed; new lite-link.test.ts, Lite cases in api-keys.test.tsx and setup.test.tsx), pnpm build (check-i18n parity: 1848 keys), rerun after the private-network change. The Rust side did not change in that commit.
  • Walked through in headless Chromium against a local backend and the Vite dev server, en and zh: setup → Add to ThinkWatch Lite (link checked), guide → Lite tab → prefilled dialog → create → Add → hint line; at 375 px the Lite buttons are hidden and Copy spans the row.

🤖 Generated with Claude Code

fylorn and others added 3 commits October 11, 2026 20:34
GET /v1/models and GET /v1beta/models listed every routed model whatever
the key, while a request for a model outside the key's allow-list, or
outside what its owner's roles grant, is refused. A client that picks a
model from the list could pick one it is not allowed to call.

The request-time check and the listings now share one decision,
GatewayRequestIdentity::may_call (exact or prefix match against the
key's allowed_models intersected with the role grants; an empty list
allows nothing). The listings put each routed name through the same
alias mapping a request would, so a listed model is one a request for it
is let through for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ery user

The screens that show a key once (a new key, a rotated key, the end of
first-run setup) get an "Add to ThinkWatch Lite" button beside Copy. It
opens thinkwatch://import?service=thinkwatch&name=thinkwatch&url=...&key=...,
the link ThinkWatch Lite 2026.10.16 and later turns into a new upstream
for this gateway. The link is built in the browser from the key already
on screen; nothing new is sent anywhere. Values Lite would refuse are
caught first and the button says why instead: a key without the
ai_gateway surface, or a plain-HTTP gateway address other than this
machine's. Once clicked, a line links to the download page in case Lite
is not installed. Lite is a desktop app, so below the md breakpoint only
Copy shows.

The button needs the gateway's public address, and so does the
Configuration Guide, which read it from /api/admin/settings/system: that
needs settings:read, so every other user got a 403 and an address made
of the browser's host and port 3000, whatever general.public_* said.
GET /api/gateway/info now answers any signed-in user with the base URL,
computed server-side from general.public_*; unset parts come from the
address the console was opened at (X-Forwarded-Proto, X-Forwarded-Host
or Host) and the gateway's listening port, which is what "auto-detect
from the browser" meant. Those headers only fill gaps an administrator
left, and the answer goes back to the caller who sent them. The query
does not sign the user out on a 401, so it can never take a one-time key
screen away.

The guide gets a ThinkWatch Lite tab, first among the AI gateway
clients, whose button opens the create-key dialog prefilled for Lite
(name "ThinkWatch Lite", AI gateway only). The dialog's catalogs move
into useKeyDialogCatalogs so the guide can open the same dialog,
loading them only while it is open. Users without api_keys:create are
told how to get a key and add it by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ThinkWatch Lite's import-link contract (v2.2) now takes http:// for
loopback, private IPv4 ranges (10/8, 172.16/12, 192.168/16, 100.64/10,
169.254/16), private IPv6 ranges (fc00::/7, fe80::/10) and intranet-style
names (no dot, or ending in .local, .lan, .internal, .intranet, .corp,
.home.arpa, .localhost). Company gateways are often served over plain
HTTP inside the network, and clients set up by hand already send the key
in plain text there, so refusing the link added no safety. The button's
check follows that list exactly; any other http:// host keeps the
disabled button and its reason, reworded to say what Lite accepts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn merged commit 4f87839 into dev Oct 11, 2026
6 checks passed
@fylorn
fylorn deleted the feat/add-to-lite branch October 11, 2026 13:13
@fylorn fylorn mentioned this pull request Oct 11, 2026
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