mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-01 04:02:02 +08:00
134bdc0e656d2717af772c09989e7395aefa63d2
278 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
134bdc0e65 |
docs(sessions): record the renderer trust boundary for terminal launch
`launch_session_terminal` takes an arbitrary string from the renderer and hands it to a shell. External audits report this as arbitrary command execution over IPC. Document it as a known, accepted risk instead of leaving it to be re-reported every audit cycle. The precondition for exploiting it is control over the renderer, which already implies local code execution as the user -- at which point going through this command grants nothing extra. The renderer is treated as a trusted boundary, supported by four facts each verified against the tree: - the only `dangerouslySetInnerHTML` (ProviderIcon) takes an icon *name*, gated by `hasIcon()`, and reads the SVG from a build-time registry; neither users nor deep links can supply markup - no `eval` / `new Function` anywhere in the frontend - `frontendDist` points at the bundled output, the webview loads no remote origin, and there are no `<iframe>` / `<webview>` elements - CSP is `script-src 'self'` -- no inline and no external scripts The note lists what invalidates the conclusion, so the exemption is falsifiable rather than a standing opinion: rendering network- or config-sourced rich text, embedding a webview or navigating to a remote origin, relaxing `script-src`, or introducing any way to execute external code in the renderer. Any of those and this command must be changed to accept a session identifier and rebuild the command in the backend. It also states explicitly that `cwd` is *not* covered. That value comes from disk scanning and can legitimately contain `$(...)` regardless of renderer trust, which is why it is escaped rather than exempted. Without that sentence "the renderer is trusted" invites being read as "nothing on this path needs handling". |
||
|
|
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 |
||
|
|
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>
|
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
8dcedbc062 |
feat(tools): prefer xAI native Grok installer with npm fallback
Treat Grok like Claude/OpenCode: install via the official xAI installer (POSIX shell / Windows PowerShell), keep npm as fallback, discover ~/.grok/bin, and anchor updates to `grok update` only for native installs. |
||
|
|
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. |
||
|
|
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. |
||
|
|
01fca69641 |
test(updater): align grok npm anchor expectation with PATH prefix
bf047990 prefixed anchored npm invocations with their sibling bin directory but missed updating the grok test expectation. |
||
|
|
a35209a6e7 |
feat(xai): add Grok OAuth device-flow backend and proxy routing
Add an xAI OAuth manager using the OAuth 2.0 Device Authorization Grant with endpoints resolved from xAI's OIDC discovery document. All HTTP goes through the app-managed proxy client. - Managed provider kind xai_oauth: forced openai_responses wire format, pinned api.x.ai base URL, bearer injection gated to the xAI origin, tokens registered for log redaction, single-auth-key takeover policy. - Token cache cannot bypass account state: cache hits re-validate account usability, refresh commits run under the mutation lock with a refresh-token CAS check, and pending logins are re-checked before an account is persisted. - Refresh classification: 401/403 with any body and 400 with a non-JSON body mark the account for re-auth; 429/5xx stay transient. - Shared auth_* commands dispatch to xAI with guard types mirroring the Copilot/Codex branches. |
||
|
|
17b053ed94 |
fix(updater): resolve Node for anchored npm commands
Prefix anchored npm invocations with their sibling bin directory so env-based Node shebangs work under a minimal GUI PATH. Cover Codex repair commands and add a regression test. |
||
|
|
22d2872c33 |
feat(logging): persist diagnostics across restarts and redact secrets
Backend half of the logging overhaul. Retention: - Keep the last 4 rotated files at 20MB each and stop deleting logs on startup, so a crash's prior-run logs survive a restart. - Apply the persisted log level right after plugin registration instead of at the end of setup. - Bound crash.log with size-based rotation (5MB x 2). Secret redaction on every log path: - Proxy: strip userinfo/query from upstream URLs (keep path for diagnostics), exact-match redact known auth.api_key/access_token, classify request/response bodies instead of logging them, header allowlist. - Redact the Gemini `?key=` in cache-trace endpoints. - Omit MCP custom-field values (headers may carry tokens). - Redact deeplink and model-fetch URLs before logging. Docs: - FAQ points users to the persistent crash.log for support workflows. |
||
|
|
3bc828aecc |
fix(windows): eliminate console flash and UI freeze on provider switch
Switching providers or toggling proxy takeover on Windows flashed a transient cmd window and froze the UI for up to a couple of seconds. Three fixes: - Spawn `codex debug models --bundled` with CREATE_NO_WINDOW so the console child of the GUI-subsystem app (npm's codex.cmd runs via cmd.exe) no longer opens a visible window. - Cache the ProxyChat model catalog template in a process-wide OnceCell: only successful loads are cached, failures stay retryable, so the Codex CLI starts at most once per app run instead of on every switch. - Run switch_provider via spawn_blocking: sync Tauri commands execute on the main thread, so the blocking catalog generation (and the block_on'd per-app switch lock) froze the UI. Concurrency is already serialized by ProxyService::lock_switch_for_app. |
||
|
|
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 |
||
|
|
f6e37ed994 |
fix(ci): run backend checks on Windows/macOS and repair platform-gated tests (#5138)
* fix(ci): run backend checks on Windows/macOS and repair platform-gated tests
The backend CI job ran only on ubuntu-22.04, so every #[cfg(target_os =
"windows")] / macOS path — tests and clippy lints alike — was never
compiled or run. As a result main shipped 8 failing Windows unit tests and
a hard `cargo clippy -- -D warnings` failure on Windows, all invisible to
CI because it only builds and tests Linux.
Changes:
- ci: run the backend job on a {ubuntu-22.04, windows-latest, macos-latest}
matrix (fail-fast: false); gate the apt install step on runner.os ==
'Linux' and use `shell: bash` for the dist placeholder so it works on all
three runners.
- misc.rs: fix 6 stale `anchored_upgrade_windows` tests. Commit
|
||
|
|
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). |
||
|
|
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. |
||
|
|
f2c6d48e19 |
fix(providers): skip reachability probes for official OAuth entries
Do not derive unauthenticated health-check targets from runtime adapter defaults for official providers. Batch checks now skip official entries, and direct base-URL resolution fails explicitly instead of probing first-party endpoints without credentials. Cover the Codex official provider in the stream-check regression tests so future adapter changes cannot silently reintroduce these probes. |
||
|
|
51d6c458ff |
feat(codex): route native ChatGPT sessions through proxy takeover
Allow the built-in Codex official provider to participate in takeover mode while preserving Codex's native OAuth or API-key credentials instead of persisting them into provider records. Project official routing into a dedicated TOML provider, normalize inline tables, clean stale managed placeholders, and fail closed when the live configuration cannot be transformed safely. Validate forwarded authorization, make official 401/403 responses non-retryable, avoid circuit-breaker pollution, and share the first-party ChatGPT endpoint across the Codex and Claude adapters. |
||
|
|
95c917b337 |
feat(provider): add Zhipu team plan quota query support (#5128)
智谱团队套餐(Team Plan)的额度查询与个人版不同:同一 quota 端点加 `?type=2`,需额外 `bigmodel-organization` / `bigmodel-project` 请求头 (api_key + 组织 ID + 项目 ID 三者缺一不可),且仅存在于国内站 open.bigmodel.cn。参考 token-monitor/src/shared/zaiTeamLimits.js 实现。 - Backend (services/coding_plan.rs): 新增 query_zhipu_team(固定 CN 站、 ?type=2、org/project 头);抽出 zhipu_quota_from_body 与个人版共用解析; 入口 get_coding_plan_quota 靠显式 coding_plan_provider == "zhipu_team" 路由(base_url 与个人版智谱相同,detect_provider 无法区分)。新字段经 UsageScript、IPC 命令、后台查询路径(query_provider_usage_inner)透传。 - Frontend: UsageScriptModal 新增「Zhipu GLM Team」选项 + 组织/项目 ID 输入;模板切换时保留 team 字段;测试与保存逻辑按 team 传参。 - i18n: en/zh/zh-TW/ja 四个 locale 更新。 - Tests: 凭据校验/路由(缺任一凭据 → NotFound,标识大小写不敏感)+ 本地 server 验证 ?type=2 与 org/project 请求头形状。 Co-authored-by: XuZhanXin <1239576606@qq.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
88d5ffba44 |
fix(codex): move common-config TOML merge off smol-toml to backend toml_edit
updateTomlCommonConfigSnippet re-serialized the whole document through
smol-toml (parse -> deepMerge -> stringify): comments dropped, keys
reordered, and empty parent table headers synthesized -- the
long-standing "config.toml keeps getting reordered" symptom (audit C5,
introduced in
|
||
|
|
94fc1cc064 |
fix(mcp): surface per-app failures when importing MCP servers from apps
import_mcp_from_apps swallowed every importer error with unwrap_or(0), so a corrupt config.toml surfaced as "imported 0 servers" with no hint that anything went wrong. Move the aggregation into McpService::import_from_all_apps: each app imports best-effort (one bad file doesn't block the rest), and failures are collected into a single error naming the failing apps alongside the count that did import. The frontend now refreshes the server list on settle rather than success, since a partial failure still means new servers were persisted. |
||
|
|
2df2212ceb |
fix(usage): reject transient transport failures so retry and keep-last-good work
Usage/quota queries frequently showed spurious "query failed" states that manual refresh could not clear (#3820). Root cause: all transport-level failures were folded into Ok(success:false), so react-query's retry never fired and the failure body poisoned the cache as regular data. Backend: - balance/coding_plan/subscription services now return Err for send failures and body-read failures (read body via bytes() before serde_json::from_slice; reqwest's json() wraps read errors as Decode, making error-kind checks on it dead code). Auth/4xx/parse errors stay Ok(success:false) and surface immediately. - Script path maps transient AppError keys (request_failed / read_response_failed) to Err; Volcengine adds a Transient call variant. - Command layer skips snapshot persistence, usage-cache-updated emit and tray refresh on Err so the cache bridge cannot overwrite retained data. - Expired-credential retry propagates transient errors instead of rewriting them as "OAuth token has expired". Frontend: - resolveDisplayUsage generalized to subscription quotas; 5th param is now an options object with a `rejected` flag: stale success data retained by react-query across rejections is re-anchored to dataUpdatedAt and expires through the same 10-minute keep-last-good window instead of being shown indefinitely (a total outage used to mask longer than a single 5xx). - useSubscriptionQuota / useCodexOauthQuota gain keep-last-good with per-scope snapshot reset; rejected queries with no displayable value synthesize a failure placeholder carrying the real error message so footers keep rendering a retry entry point. - HTTP 429 is classified transient (retry-later) alongside 5xx. - UsageScriptModal surfaces string rejections via extractErrorMessage. Tests: 6 backend behavior tests drive real HTTP against a local listener to pin the Err/Ok channel semantics; frontend suite extends keepLastGoodUsage coverage (405 passing). |
||
|
|
9f7642e29c |
refactor(profiles): drop manual snapshot update now that switching autosaves
Since switching projects automatically saves the current configuration back to the project being left, the per-project "update from current" button is redundant and even harmful: it allowed overwriting another project's snapshot with the current state, breaking the invariant that a project holds the state you last left it in. - Remove the resnapshot button and confirm dialog from the manage dialog; drop the now-unused scope prop and PROFILE_SCOPE_LABELS - Add a footer Close button to the manage dialog (overlay click is disabled app-wide, so it previously could only be closed via Esc) - Update manageDescription in all four locales to describe the autosave behavior; remove the two dead i18n keys - Fix a shadowed `warnings` vec in ProfileService::apply that silently dropped autosave-failure warnings before they reached the frontend - Mention auto-capture on next switch in the "no snapshot for this scope yet" warning and doc comments The backend update command keeps its resnapshot parameter; it is now only exercised by the pre-switch autosave path. |
||
|
|
754af2cc31 | feat(profiles): split Claude Desktop into independent profile scope | ||
|
|
3ec83578f6 | fix(profiles): stop proxy server when profile switch leaves no takeovers active | ||
|
|
dbb5999d1e |
refactor(profiles): shared project entity with per-scope switching
Projects are now global shared entities; Claude and Codex groups switch independently via scoped current pointers and scoped payload slots. - Remove scope column from profiles; keep current_profile_id_<scope> - Use Option<Vec<String>> for mcp/skills to distinguish 'never captured' from 'captured empty', preventing cross-side accidental disable - Update/apply operations scoped to the active group via merge_scope_from - Tray menu nests same shared list under Claude Code/Codex groups - Add i18n for per-scope tooltips and 'not saved for this side' hint Refs: profile P1 shared-entity redesign |
||
|
|
8f018a2d45 |
feat: add project profiles for snapshot-based config switching
Add a profile feature that captures the current provider, MCP, skills and prompt state for Claude Code and Codex as a named snapshot, and re-applies it in one click from the header switcher or the tray Projects submenu. - New profiles table (schema v12) with current marker in settings - ProfileService orchestrates the four existing switch primitives (provider first, then MCP diff, skills diff, prompt enable) - Best-effort apply: dangling references become warnings, no rollback - Header combobox switcher + snapshot-style manage dialog - Tray Projects submenu shared with the UI apply/event pipeline - i18n for zh/en/ja/zh-TW under the new profiles domain - Integration tests covering roundtrip, dangling refs and clear |
||
|
|
cd9e025b5a |
feat(presets): switch Claude Sonnet tier default to Sonnet 5
Bump every claude-sonnet-4-6/claude-sonnet-4.6 default pin to claude-sonnet-5 across all provider presets (claude, claudeDesktop, hermes, openclaw, opencode, universal NEWAPI_DEFAULT_MODELS) and the Claude Desktop DEFAULT_PROXY_ROUTES sonnet route id. Update route-derivation test expectations accordingly. Non-Anthropic pins (gpt/gemini/glm/sonnet-4-5) left untouched. |
||
|
|
8f484c54ce | fix(detect): dedupe Windows Codex npm shims (#4782) | ||
|
|
edeee25fae |
feat(db): in-app recovery screen with upgrade button when DB version is too new (#4575)
* feat(db): in-app recovery screen with upgrade button when DB version is too new
When the SQLite user_version is newer than the app supports (SCHEMA_VERSION),
Database::init() fails and previously dead-ended in a native Retry/Exit dialog
(Retry just fails again). The app now boots a dedicated recovery screen instead.
- Detect the recoverable "version too new" case and surface it via init_status
(kind="db_version_too_new" + db_version/supported_version); force-show the main
window and skip normal AppState boot (recovery commands need only AppHandle).
- The recovery screen first checks for an available update:
- update available -> "Upgrade app" runs the updater (download + install +
restart) with a download progress bar.
- no update (already latest) -> warns that the DB is too new even for the latest
build (likely a third-party client), so upgrading cannot help.
- install_update_and_restart now emits `update-download-progress` events; new
`check_app_update_available` command.
- i18n: en / ja / zh / zh-TW.
Verified: pnpm typecheck + format:check + test:unit (351) green; cross-model
review of Rust correctness and recovery-mode safety (no AppState panic).
* fix(db): pre-check too-new DB before schema writes; exit on close in recovery mode
Addresses review feedback on the too-new-DB recovery flow.
- P1: stored_user_version_exceeds_supported() is now checked BEFORE
Database::init(), so create_tables()'s DDL (incl. the unconditional
DROP INDEX/DROP TABLE IF EXISTS failover_queue) never runs against a
database whose user_version we cannot understand. The earlier
post-init-failure recovery branch is removed; the pre-check is the
single authoritative guard, so "don't write to a DB we can't read"
now holds from the very first DB access.
- P2: the window CloseRequested handler now detects recovery mode
(init_error kind = db_version_too_new) and exits the app instead of
honoring minimize_to_tray_on_close. Recovery mode returns before the
tray is created, so hiding the window would leave the app running with
no tray to bring it back; native-close now quits cleanly.
Verified: cargo fmt --check clean; cross-model Rust review of both fixes
(compile-correctness + no normal-startup/close regression).
|
||
|
|
c4630b5c26 |
feat(providers): add Volcengine Ark usage query with AK/SK signing
Query coding-plan / agent-plan usage for Volcengine Ark via the control-plane OpenAPI (open.volcengineapi.com), which requires account-level AccessKey signing rather than the inference API key. - Implement Volcengine Signature V4 (an AWS SigV4 variant: fixed canonical header order, "HMAC-SHA256" algorithm without the AWS4 prefix, scope ending in "request", service "ark") in services/coding_plan.rs - Auto-detect the plan by probing GetAFPUsage (Agent Plan, AFP five-hour/weekly/monthly quotas) first, falling back to GetCodingPlanUsage (Coding Plan, session/weekly/monthly percentages); a single credential covers whichever plan the account holds - Parse the real response shape: the window label lives in the `Level` field; guard ResetTimestamp <= 0 (session returns -1) - Store account-level credentials on UsageScript (accessKeyId / secretAccessKey), threaded through both the test command and the TOKEN_PLAN auto-refresh path - Add an independent AK/SK input block with a detailed hint pointing to the Volcengine console -> API Access Keys - Add the `monthly` tier label (frontend TIER_I18N_KEYS + tray TIER_LABEL_GROUPS + subscription service) - Sync zh / en / ja / zh-TW locales and the usage-query docs |
||
|
|
1042fb2a32 |
fix(terminal): respect user shell for provider terminals (#4140)
* fix(terminal): detect user default shell instead of hardcoded bash All macOS/Linux terminal launch functions in misc.rs hardcoded `bash` as the execution shell, causing zsh users' .zshrc to not load. Now detect the user's default shell via $SHELL (macOS fallback /bin/zsh, Linux fallback /bin/bash) and use it consistently across shebang, AppleScript, spawn args, and interactive shell flags. - Add get_user_shell(), get_shell_name(), interactive_flags_for_shell() - Replace 10 hardcoded bash locations with dynamic shell detection - zsh gets --norcs --no-globalrcs, bash keeps --norc --noprofile - run_tool_lifecycle_silently unchanged (bash-specific syntax) - Fixes #1546, Related #2385 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(terminal): use POSIX sh for script execution, user shell for interactive session Address code review issues: 1. Replace shell-specific `read -n 1 -s`/`read -k 1 -s` with POSIX `read -r _` (press Enter to close) — ensures portability across bash/zsh/fish/dash 2. Unify AppleScript to use `sh` consistently (not mixing shell/shell_name) 3. Protect fish users: scripts always interpreted by POSIX sh, not fish (fish doesn't support trap/exec). exec line switches to user's shell. 4. Use build_exec_line to avoid trailing space when interactive_flags is empty All terminal launchers now execute scripts via `sh`. Script shebangs are `#!/bin/sh`. The exec line at script end switches to the user's detected shell with appropriate clean-start flags. Terminal launcher functions no longer need shell/shell_name parameters — simplified signatures. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: validate user shell for terminal launch * fix: load user shell config after terminal command * test: align terminal shell exec expectations * refactor(terminal): remove dead code, quote Warp path, unify shebangs - Remove `interactive_flags_for_shell` (always returned "") and `get_shell_name` (no longer needed after removing flags); simplify `build_exec_line` to single-arg form - Quote the Warp launcher script path with `shell_single_quote` for consistency with the main launcher scripts - Unify all script shebangs to `#!/usr/bin/env sh` for better portability (e.g. NixOS where /bin/sh may not exist) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(terminal): fix linux shell exec helper call * fix(terminal): run provider command through user shell * fix(terminal): validate user shell executable * fix(terminal): avoid interactive provider shell job * fix(terminal): harden user shell launcher Load zsh provider commands through the user's login and interactive environment, preserve launch cwd handling, and replace Terminal/iTerm launcher shells so the final user shell is restored cleanly. --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5092fe51ce |
fix(updater): prevent codex self-update from breaking npm platform-dispatch installs
Codex ships as an npm platform-dispatch package (JS launcher @openai/codex + per-platform binary optional deps like @openai/codex-darwin-arm64). The upgrade chain ran `<bin>/codex update || <bin>/npm i -g @openai/codex@latest`, which can leave codex throwing "Missing optional dependency @openai/codex-darwin-arm64": - `codex update` on an npm install is a bare `npm install -g @openai/codex` that prints success and exits 0 even when the platform binary fails to land, short-circuiting the `||` npm fallback. - The npm fallback is a no-op when version==latest and only targets the main package, so it can never re-land the missing platform binary. Fixes: (1) remove codex from prefers_official_update (Posix+Windows) so npm-managed codex no longer runs the false-success `codex update`; (2) add a runnable=false gate in installs_anchored_command emitting an uninstall+install self-heal — the only repair that re-lands the platform binary; (3) narrow by source/real to npm-managed sources (nvm/fnm/mise/homebrew, non-brew-formula) so broken brew-formula/volta/bun installs fall back to their own anchored commands instead of being mis-repaired with npm. Reuses the existing enumerate runnable signal; no new FS probing. |
||
|
|
a5903d8600 |
feat(health-check): replace real-LLM probe with HTTP reachability check
The provider panel health check sent a real streaming model request, which many third-party providers block (401/403/WAF), causing false negatives while only stable official endpoints passed. Replace it with a lightweight reachability probe: GET the provider base_url and treat any HTTP response (200/4xx/5xx) as reachable; only DNS/connect/TLS/timeout count as failure. Latency is the probe's TTFB. Backend (services/stream_check.rs): rewrite ~2200 -> ~350 lines, dropping real-request building, format conversion, auth and API-path resolution while keeping per-app base_url extraction. Defaults: 8s timeout, 1 retry, 1500ms degraded threshold. Failover invariant: the reachability check must never reset the circuit breaker (reachable != usable; a 403 host is reachable but broken for real traffic). Remove the resetCircuitBreaker call from useStreamCheck; failover failure detection stays driven solely by real proxy traffic (forwarder/circuit_breaker untouched). useResetCircuitBreaker is kept dormant for a future manual-recovery entry. Open the check to all providers: drop the official/copilot/codex-oauth/third-party gating and the 'sends a real request' confirm dialog. For official providers whose base_url is intentionally empty, fall back to the endpoint the client actually uses (Claude -> api.anthropic.com, Codex -> chatgpt.com/backend-api/codex, Gemini -> generativelanguage). Non-official providers with a missing base_url still error to avoid a false green light. Claude Desktop Official is native 1P mode (talks to claude.ai, cc-switch not in the request path, no reliable endpoint) so its button stays hidden. Slim StreamCheckConfig and per-provider testConfig to timeout/threshold/retries (drop test model + prompt); sync zh/en/ja/zh-TW. Retain the now-unused anthropic_to_openai/anthropic_to_gemini transform utilities and their test suites. |
||
|
|
0c46efe1be |
fix(macos): prevent duplicate provider terminal sessions (#4156)
* fix(macos): prevent duplicate provider terminals * fix(macos): keep Ghostty fallback on AppleScript failure --------- Co-authored-by: thisTom <19346741+thisTom@users.noreply.github.com> |
||
|
|
eab6bfd20c |
feat(codex): add opt-in migration and ledger-based restore for unified session history
- Enable dialog gains a checkbox (default off) to migrate existing official sessions from the built-in "openai" bucket into the shared "custom" bucket, with per-generation backups; failed migrations retry at startup - Disable dialog offers a precise restore driven by the backup ledger: only sessions recorded as "openai" in backups are flipped back, and sessions created while the toggle was on are never touched - Completion marker and backup generations are bound to the canonical Codex config dir; migrate/restore serialize on an op lock and the marker is written conditionally inside the settings write lock - save_settings rolls back the toggle and fails the save when the live rewrite fails; migration additionally requires the live config to actually route to the shared bucket (skips with live_not_unified so refused injection or proxy takeover can't split history) - Restore refuses to run while the toggle is (re-)enabled and reports nothing_to_restore instead of a zero-count success; local migration markers are now backend-owned in merge_settings_for_save so stale frontend payloads can't resurrect them - Settings autosave reverts optimistic form state on failure so a failed toggle change can't be replayed by an unrelated save - ConfirmDialog supports an optional checkbox; all four locales updated |
||
|
|
948d762792 |
feat(codex): add unified session history toggle for official providers
Codex buckets resume history by the model_provider id recorded in each session: official runs (no key, built-in "openai") and cc-switch third-party runs (shared "custom") are mutually invisible in the resume picker. Add an opt-in setting that runs official providers under the shared "custom" id so future official sessions land in the same history bucket as third-party ones. Forward-only by design: existing sessions are not migrated. When enabled, official live config.toml gets model_provider = "custom" plus a [model_providers.custom] entry that mirrors the built-in openai provider (requires_openai_auth routes auth to the ChatGPT login in auth.json, name "OpenAI" keeps is_openai() feature gates, explicit supports_websockets/wire_api restore built-in defaults). auth.json is untouched. Key invariants: - Injection lives only in the live config: switch-away backfill strips the exact injected shape, so stored provider configs stay clean and turning the toggle off fully reverts on the next write. - Toggle changes apply immediately via a takeover-aware reapply: when the proxy owns the live config (backup/placeholder present), only the live backup is updated, mirroring the provider-switch path. - The takeover backup path runs the same injection so a takeover release restores a config that still carries the unified routing. - Injection refuses to activate a foreign [model_providers.custom] table (e.g. stale entry with a third-party base_url) to avoid routing ChatGPT OAuth traffic to an unknown backend. The toggle lives under Settings → Codex App Enhancements; the description warns that resuming old sessions across providers may fail because encrypted_content reasoning only decrypts on the backend that created it (upstream treats cross-provider resume as unsupported). |
||
|
|
a75f479576 |
feat(usage): lift provider/model filters to dashboard-wide scope
The provider/model filters only lived inside the request-log table, so there was no way to see "how much did app X spend on source Y" across the whole dashboard. Promote them to the top bar next to the app filter, applying globally to the hero summary, trend chart, request logs, and both stats tabs. Backend: the five stats queries (summary, summary-by-app, trends, provider stats, model stats) accept optional provider_name/model filters, applied to both the detail and daily-rollup branches (the rollup PK already carries provider_id/model/pricing_model). Sources match by exact display name via provider_name_coalesce, so session placeholder rows like "Claude (Session)" are selectable; models match by effective pricing model (pricing_model falling back to model), the same grouping key the model-stats tab uses. Request-log filtering switches from LIKE to these exact semantics. Frontend: two truncating dropdowns list only sources/models that have data in the current range, with the model list cascading from the selected source. Dynamic option values are prefix-encoded so a source literally named "all" cannot collide with the sentinel, query keys fall back to null instead of "all", and the option queries follow the dashboard refresh interval (otherwise their default 30s polling drags same-key stats queries along even with refresh disabled). The request-log filter bar keeps only the log-specific status-code select. Labels read "sources" rather than "providers" because direct-connect session buckets sit alongside real providers. i18n updated across zh/en/ja/zh-TW. |
||
|
|
7bb59fa5a6 |
fix(proxy): harden takeover-residue recovery across config-dir switches (#4076)
Changing app_config_dir relocates the SQLite database, so a restart triggered while proxy takeover is active used to strand the live configs: the new instance reads a fresh DB with no live backups, the first-run import then persisted the PROXY_MANAGED placeholder as the `default` provider, and the no-backup recovery path wrote that placeholder right back to the live files — leaving Claude/Codex/Gemini pointed at a dead local proxy with no automatic way out. Three orthogonal fixes, defense in depth: - restart_app now awaits cleanup_before_exit() before app.restart(). Since #4069 the ExitRequested handler intentionally defers restart requests to Tauri's default re-exec without custom cleanup, which is correct for same-DB restarts but not for this command's dir-change use case: only the old instance holds the backups needed to restore the taken-over live files, so it must restore them while its event loop is still alive. - import_default_config refuses to import a live config that is under proxy takeover (placeholder detected), instead of persisting it as the current provider. - restore_live_from_ssot_for_app validates that the current provider's settings_config does not itself contain takeover placeholders before writing it back; polluted SSOT now falls through to the placeholder cleanup fallback. Regression tests cover the import guard and the no-backup recovery path (the latter fails before this change by writing PROXY_MANAGED back to live). |
||
|
|
4f3e85fcd1 |
fix(updater): drive download/install/restart from backend to avoid hang (#4074)
* fix(updater): drive download/install/restart from backend to avoid hang The 3.16/3.16.1 update flow was frontend-driven: downloadAndInstall() then relaunchApp(). relaunch() routed through AppHandle::restart(), and the old WebView had to keep running JS after the .app bundle was already swapped — an unstable window that could hang the update or leave the old version running until a manual restart. Move the whole download -> install -> cleanup -> restart chain into a new backend command install_update_and_restart, so it no longer depends on the old WebView running JS after the bundle is swapped. Platform-aware install ordering (install() behaves differently per OS): - Windows: install() launches the external installer and exits the process internally, so cleanup + single-instance destroy must run before install. Surface a recovery hint on failure since the proxy may already be stopped. - macOS/Linux: install() returns, so install first then cleanup — an install failure no longer wrongly stops the proxy / reverts takeover. Eliminate the restart vs single-instance race: restart_process() destroys the single-instance lock (remove socket on macOS, ReleaseMutex on Windows) before tauri::process::restart(), so the freshly spawned process can't connect to the old listener and exit itself. Also remove the now-dead frontend update plumbing (relaunchApp, UpdateHandle, mapUpdateHandle) and surface backend errors in the toast. Adapted from the original af4271f4 while rebasing onto #4069: the ExitRequested handler changes were dropped entirely — the classifier from #4069 already routes RESTART_EXIT_CODE to Tauri's default restart flow, and the original should_restart branch (prevent_exit + async cleanup) would have reintroduced the window-state deadlock that #4069 fixed. install_update_and_restart bypasses ExitRequested entirely, so the two fixes compose cleanly. * fix(updater): clear tray icon on the direct-restart update path restart_process re-execs via tauri::process::restart (spawn + exit(0)), which skips Tauri's internal cleanup_before_exit and all RunEvent::Exit plugin hooks. Window state, proxy/live restore and the single-instance lock were already compensated explicitly; the tray icon was not. On macOS/Linux the OS drops the status item when the process dies, so the gap there was cosmetic at most. The real residue risk is the Windows branch, which never reaches restart_process at all: update.install() exits the process inside the updater plugin (std::process::exit(0)), bypassing TrayIcon::drop — no NIM_DELETE is sent and a stale icon lingers in the shell until hovered, the same failure remove_tray_icon_before_exit was originally added for on the quit path. Call remove_tray_icon_before_exit (set_visible(false), proxied to the main thread via run_item_main_thread) in restart_process and before the Windows install. Deliberately not AppHandle::cleanup_before_exit(): it drops tray icons on the calling thread, which is not safe off the main thread on macOS (NSStatusItem). |
||
|
|
8b925c2f2f |
feat(proxy): honor custom User-Agent across stream check and model fetch
Extract a shared `parse_custom_user_agent` helper in provider.rs returning `Result<Option<HeaderValue>>`, and reuse it in the forwarder, stream check, and model fetch paths so detection, forwarding, and model listing all apply the same provider-level User-Agent. Previously only the forwarder honored it, so stream check could fail (or model listing 403) on UA-gated upstreams that the proxy itself handled fine. - stream_check injects the provider's custom UA on the claude/codex paths and still skips the GitHub Copilot fingerprint UA. - model_fetch service + command and the model-fetch.ts wrapper thread an optional UA through to GET /v1/models. - runtime callers silently ignore invalid values via `.ok().flatten()` (no save-time block, so deeplink imports stay lenient). |
||
|
|
473f21971d |
feat(usage): add official subscription quota template with unified tier rendering
Changes: - Add official_subscription template type for Claude/Codex/Gemini - Replace implicit 'category=official auto-query' with explicit opt-in template - Default disabled; users enable via usage script modal with configurable interval - Unify tier→label mapping across subscription and script paths via labeled_tier_parts() - Fix tray rendering: week aliases (seven_day/opus/sonnet) now use highest utilization - Add depth guard: official_subscription checks enabled flag in query_provider_usage_inner - Add cache invalidation symmetry: invalidate_subscription() for disabled providers - i18n: add templateOfficialSubscription + hint in zh/en/ja/zh-TW Backend (Rust): - provider.rs: add TEMPLATE_TYPE_OFFICIAL_SUBSCRIPTION branch, flatten SubscriptionQuota→UsageData - tray.rs: extract labeled_tier_parts() shared by both summary functions, use max_by for multi-alias groups - usage_cache.rs: add invalidate_subscription() method - Test coverage: add week-alias highest-utilization tests for both paths Frontend (TypeScript): - UsageScriptModal: add official_subscription to templates, auto-detect for official providers - ProviderCard: gate useUsageQuery with !isOfficialSubscriptionUsage, pass autoQueryInterval to footer - SubscriptionQuotaFooter: accept autoQueryInterval prop, default 0 (disabled) - constants.ts: add TEMPLATE_TYPES.OFFICIAL_SUBSCRIPTION Fixes tier rendering regression where: - Claude/Codex: seven_day was missed (only weekly_limit matched) → lost 7-day window in tray - Gemini: gemini_pro/flash/flash_lite fell through to fallback → leaked machine names - Multi-window (opus+sonnet): find() took first, not worst → underestimated utilization and emoji color All tests pass (cargo test + cargo clippy clean). |
||
|
|
0527002cca |
feat(usage): add OpenCode session usage sync (#3215)
* feat(usage): add OpenCode session usage sync Add OpenCode as a fourth app type in the usage statistics system. Reads per-message token data from opencode's local SQLite database (~/.local/share/opencode/opencode.db) and imports into proxy_request_logs. - New session_usage_opencode.rs module following Codex/Gemini pattern - Parses assistant message.data JSON for tokens, cost, model - Adds "opencode" to AppType union and filter tabs - Updates dedup filters to include opencode_session data_source - Adds i18n keys for all 4 locales * fix(usage): add opencode to UsageHero title themes * fix: respect XDG_DATA_HOME and platform defaults for OpenCode DB path - Support OPENCODE_DB env var override (absolute and relative paths) - Use ~/Library/Application Support/opencode/ on macOS - Use XDG_DATA_HOME/opencode/ when set - Fall back to ~/.local/share/opencode/ on Linux - Rename misleading test to test_parse_message_data_ignores_role * fix(usage): use ~/.local/share/opencode on all platforms OpenCode relies on xdg-basedir, which ignores macOS/Windows conventions, so its DB always lives at ~/.local/share/opencode. The previous macOS default pointed at ~/Library/Application Support/opencode, which does not exist, making the sync a silent no-op for macOS users without XDG_DATA_HOME set. * fix(usage): include opencode -wal mtime in freshness check OpenCode runs its SQLite DB in WAL mode; new commits land in the -wal file and the main DB file's mtime only advances on checkpoint. Keying the freshness gate solely on opencode.db could skip newly written sessions until a checkpoint occurred. Take the max of the db and -wal mtimes instead. * style(usage): apply cargo fmt to opencode session sync Fixes Backend Checks cargo fmt --check failure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(usage): ignore empty OPENCODE_DB and XDG_DATA_HOME env vars An empty OPENCODE_DB collapsed the path to the data dir (dropping the opencode.db filename); an empty XDG_DATA_HOME produced a relative "opencode" path. Treat empty strings as unset, per the XDG spec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(usage): harden OpenCode session sync error handling and labeling - Map '_opencode_session' provider_id to 'OpenCode (Session)' display name - Return accurate inserted flag from insert_opencode_message (was always true) - Do not advance file/session sync_state when a session errors, so failed inserts are retried next run instead of being permanently skipped - Surface per-message insert failures into the sync result errors - Add opencode_session data-source icon and i18n labels (zh/zh-TW/en/ja) - Add provider-stats labeling test for opencode session rows * fix(usage): only import finalized OpenCode messages An in-progress assistant message holds partial tokens; OpenCode updates the same message_id with final values later. Since request_id is fixed and the insert uses INSERT OR IGNORE, a partial row could never be corrected. Skip messages without time.completed so each turn is imported once with final usage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(usage): keep OpenCode incomplete sessions retryable --------- Co-authored-by: Eira Hazel <kip3vx9ma@mozmail.com> Co-authored-by: Eria hazel <git config --global user.email your@email.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Jason <farion1231@gmail.com> |
||
|
|
2a24da517f |
feat: 新增 S3 兼容云存储同步 (#1351)
* Add S3 Cloud Sync design document Design for adding AWS S3 as a new Cloud Sync backend alongside WebDAV. Hybrid approach: extract shared sync protocol, add independent S3 transport. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add S3 cloud sync implementation design (reqwest + Sig V4) Updated design based on 2026-03-06 draft: switches from rust-s3 crate to hand-rolled AWS Sig V4 on existing reqwest for broader S3-compatible service support (AWS, MinIO, R2, Alibaba OSS, Tencent COS, Huawei OBS). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add S3 cloud sync implementation plan (11 tasks, TDD) Detailed step-by-step plan covering: sync_protocol extraction, S3 Sig V4 transport, settings, sync/auto-sync modules, Tauri commands, frontend presets/dynamic form, and i18n. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * deps: add hmac crate for S3 Sig V4 signing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract sync_protocol.rs from webdav_sync.rs for shared use Move transport-agnostic sync protocol logic (constants, types, snapshot building, manifest validation, artifact verification, snapshot application, utilities) into a new shared sync_protocol module so both WebDAV and the upcoming S3 transport can reuse it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use transport-neutral error keys in sync_protocol Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 transport layer with AWS Sig V4 signing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3SyncSettings to AppSettings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync module with upload/download/fetch Implements the S3 sync protocol layer (s3_sync.rs) that combines the shared sync_protocol with the S3 transport. Mirrors the WebDAV sync module structure with independent sync mutex, connection check, upload, download, fetch_remote_info, and sync status persistence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 auto sync worker with debounce Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync Tauri commands and auto sync worker startup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync TypeScript types and API layer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync i18n translations (en/zh/ja) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync presets and dynamic form to sync settings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: preserve HTTP scheme for S3 custom endpoints (MinIO support) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add live S3 integration tests (env-var driven, --ignored) Run with: S3_TEST_AK=... S3_TEST_SK=... S3_TEST_BUCKET=... cargo test --lib services::s3::integration_tests -- --ignored Verifies test_connection, put_object, get_object, head_object, and 404 handling against a real S3 bucket using the project's own Sig V4 signing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove internal design docs before PR * fix: wire S3 auto-sync to DB hook & sync UI state on async load - P1: Add s3_auto_sync::notify_db_changed call in SQLite update_hook so S3 auto-sync worker receives DB change signals (was only wired for WebDAV, leaving S3 worker idle) - P2: Add useEffect to update syncType selector when s3Config loads asynchronously, preventing stale "webdav" default for S3 users * fix: satisfy clippy for s3 sync * fix: address s3 sync review feedback --------- Co-authored-by: Keith (via OpenClaw) <keithyt06@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Jason <farion1231@gmail.com> |
||
|
|
c1dff06625 |
feat: 新增 ZenMux Token Plan 供应商,支持手动凭证与 USD 额度富展示 (#2709)
* feat(Token plan): 增加 ZenMux 支持 * chore: format code with prettier * chore: format code with cargo fmt --------- Co-authored-by: 明桓 <jihaodong.jhd@oceanbase.com> Co-authored-by: Jason <farion1231@gmail.com> |
||
|
|
afa09e127e |
fix(usage): resolve per-app credentials for native balance/coding-plan queries (#3355)
* fix(usage): resolve per-app credentials for native balance/coding-plan queries
The native usage-query paths (balance + coding_plan) in
`query_provider_usage_inner` read credentials only from `env.ANTHROPIC_*`.
That matches Claude providers, but Codex stores its key in
`auth.OPENAI_API_KEY` with the base URL inside a TOML `config` string,
Hermes/OpenClaw flatten them at the top level, and OpenCode nests them under
`options`. So the card "refresh usage" / auto-query returned empty
credentials and failed ("查询失败") for those apps, even though the
config-page "Test" button worked (the frontend extracts per-app correctly).
Introduce a single per-app resolver `Provider::resolve_usage_credentials`
that mirrors the frontend `getProviderCredentials`, and route both native
branches through it. Add `extract_codex_base_url` to `codex_config` as the
canonical Codex TOML base-URL parser and make the proxy adapter delegate to
it (removing a duplicate copy). Align the frontend `getProviderCredentials`
to cover OpenCode and Claude Desktop and to use the same OpenRouter/Google
key fallbacks as the backend.
Fixes #3158
Fixes #3100
Fixes #2625
* refactor(usage): explicit AppType arms + frontend trailing-slash trim
Address two review nits on the per-app credential resolver:
- provider.rs: replace the catch-all `_` arm in resolve_usage_credentials with an explicit `AppType::Claude | AppType::ClaudeDesktop` arm, so a new AppType variant fails to compile here instead of silently defaulting to the Anthropic env shape.
- UsageScriptModal.tsx: normalize getProviderCredentials baseUrl through a single trailing-slash trim so the frontend 'Test' path matches the backend resolver (trim_end_matches), keeping front/back truly in lockstep.
No behavior change for well-formed configs; tests/typecheck/fmt/clippy clean.
* fix(usage): skip empty primary credential fields in fallback chain
`obj.get(key)` returns `Some` for a present-but-empty field, so the
`.or_else()` fallback chains for the Claude/ClaudeDesktop and Gemini api-key
lookups only skipped *absent* keys, not empty ones. Presets seed fields like
`ANTHROPIC_AUTH_TOKEN` as present-but-empty placeholders, so a provider whose
real key lives in a fallback field (`ANTHROPIC_API_KEY` / `OPENROUTER_API_KEY`
/ `GOOGLE_API_KEY`, or `GOOGLE_API_KEY` for Gemini) resolved to an empty key on
the native balance/coding-plan path — while the frontend `a || b` (which skips
empty strings) still found it, reproducing the same Test-works / refresh-fails
divergence this PR removes.
Add a `first_non_empty` helper that skips present-but-empty values, matching
the frontend `||` semantics, and use it for both fallback chains. Tests cover
empty primary + populated fallback for Claude and Gemini.
|
||
|
|
0960fd7179 |
fix: Claude Desktop 官方供应商添加报错 #3402 (#3405)
* fix: Claude Desktop 官方供应商添加时缺少 ANTHROPIC_BASE_URL 报错 根因:前端 mutation 为 claude-desktop 生成随机 UUID 作为 provider id, 后端 is_official_provider 通过 id 匹配跳过校验,随机 UUID 不匹配导致 走入普通 direct 模式校验并要求 ANTHROPIC_BASE_URL。 修复: - 前端:claude-desktop + category=official 时使用固定 id "claude-desktop-official" - 后端:validate_provider / validate_direct_provider / validate_proxy_provider / apply_provider_to_paths 增加 category=="official" 兜底检查 Fixes #3402 * fix: restrict Claude Desktop official provider detection * fix: add Claude Desktop official provider via seed --------- Co-authored-by: 金恩光 <enguang.jin@gmail.com> Co-authored-by: Jason <farion1231@gmail.com> |
||
|
|
ee69c83687 |
Fix garbled output and false "not runnable" in Windows version probe
The version probe ran `cmd /C "\"{path}\" --version"` on Windows. Rust's
own arg quoting plus the manual quotes produced nested quotes that cmd
(without /S) misparsed, so it reported even working tools as an
unrecognized command -> non-zero exit -> shown as "installed but not
runnable". cmd's error is emitted in the OEM code page (e.g. GBK on
zh-CN) and was decoded as UTF-8, turning it into mojibake.
- Add decode_command_output: try strict UTF-8, then OEM (GetOEMCP), then
ANSI (GetACP) code page via MultiByteToWideChar; route all command
output decoding through it (probes, lifecycle output, osascript,
terminal launchers).
- Add run_windows_tool_version_command: run .exe directly (no cmd);
invoke .cmd/.bat via `cmd /D /S /C call <quoted> --version` with
raw_arg to bypass Rust's quoting and keep deterministic quote handling.
- Add windows-sys Win32_Globalization dependency.
|