mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 11:43:57 +08:00
22c010079a36112d188fa321378f34988fbc01d5
373 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
22c010079a | fix(provider): enforce durable identity boundaries | ||
|
|
37e78166c1 | refactor(provider): complete prerequisite A ownership | ||
|
|
2bc92e0f79 |
refactor(provider): certify typed write ownership
Freeze prerequisite A as a component-level certification unit. Add the immutable v5 certification suite, split create/update row DTOs, preserve immutable creation time, map strict-create races to AppError::Conflict, make aggregate compensation insert-or-restore, and enforce reconcile preconditions through a single-lock transaction primitive. Old save_provider callsite classification remains exhaustively recorded in 4f78451405575158ff6562c7021c7f31f2860780; this checkpoint does not add or reclassify an omitted legacy callsite. It tightens the remaining reconciliation classifications there: default live import is [create]; OpenCode/OpenClaw/Hermes existing branches are [update] and absent branches are [create]; universal Claude/Codex/Gemini branches are [create/update] selected from an observed fingerprint. The sealed compensation helper remains the only [restore] path. The old reconcile_provider_record symbol is deleted. Remaining update_provider_settings_config callsites are classified as [update]: codex_history_migration updates an already-read Codex row; proxy token synchronization updates already-read Claude, Codex, Gemini, and GrokBuild rows. Each now uses ProviderKey plus ProviderRowUpdate, explicitly removes hydrated endpoint projections, preserves endpoint authority, and fails on a missing row instead of silently succeeding. |
||
|
|
4f78451405 |
storage(pi): isolate typed provider writes and canonical restore
Replace the generic Provider save/upsert surface with strict typed create/update/rename/endpoint operations. Keep aggregate hydration read-only, preserve nullable endpoint timestamps end to end, and make service create own the initial endpoint set atomically. Restore SQL and binary backups only through UntrustedScratch, migrate and copy fixed data columns into a fresh CanonicalStage, validate the canonical result, then publish through the Backup API. The imported schema is never eligible to become the live schema. Old save_provider callsite classification ========================================== Inventory authority: abandoned 5a385fc8 tree. The old definition at src-tauri/src/database/dao/providers.rs:180 is deleted and is not a callsite. Production callsites: - src-tauri/src/commands/provider.rs:253 [create] Claude Desktop import creates one absent aggregate; it now strict-inserts the row and initial endpoints in one transaction. - src-tauri/src/database/dao/providers.rs:638 [create] official seed first proves absence, then strict-creates; a racing insert is a conflict. - src-tauri/src/database/dao/providers.rs:704 [create] on-demand seed first proves absence, then strict-creates; it cannot overwrite an existing row. - src-tauri/src/services/omo.rs:291 [create] OMO import constructs a new aggregate and strict-creates it; OMO is not eligible for rename. - src-tauri/src/services/provider/endpoints.rs:85 [update] endpoint last-used is not a Provider-row save; it now calls the exact touch endpoint operation. - src-tauri/src/services/provider/live.rs:1567 [create/update] default live import is reconciliation: read first, then strict create or strict update. - src-tauri/src/services/provider/live.rs:1743 [update] an existing OpenCode live provider follows the strict row-update branch. - src-tauri/src/services/provider/live.rs:1770 [create] a new OpenCode live provider follows the strict aggregate-create branch. - src-tauri/src/services/provider/live.rs:1825 [update] an existing OpenClaw live provider follows the strict row-update branch. - src-tauri/src/services/provider/live.rs:1858 [create] a new OpenClaw live provider follows the strict aggregate-create branch. - src-tauri/src/services/provider/live.rs:1900 [update] an existing Hermes live provider follows the strict row-update branch. - src-tauri/src/services/provider/live.rs:1926 [create] a new Hermes live provider follows the strict aggregate-create branch. - src-tauri/src/services/provider/mod.rs:2568 [create] ProviderService::add owns strict aggregate creation and all initial endpoints. - src-tauri/src/services/provider/mod.rs:2680 [rename] an additive DB-only key change now uses the dedicated transactional rename after eligibility checks. - src-tauri/src/services/provider/mod.rs:2711 [update] OMO edit updates exactly the existing main row after its live-file coordination. - src-tauri/src/services/provider/mod.rs:2740 [update] additive-provider edit updates exactly the existing main row after resolving live ownership. - src-tauri/src/services/provider/mod.rs:2750 [update] switch-mode edit updates exactly the existing main row and never inserts. - src-tauri/src/services/provider/mod.rs:2948 [update] remove-from-live changes only the existing provider's live-managed marker. - src-tauri/src/services/provider/mod.rs:3120 [update] switch backfill updates only the existing current provider row. - src-tauri/src/services/provider/mod.rs:3174 [update] successful additive switch changes only the existing live-managed marker. - src-tauri/src/services/provider/mod.rs:3315 [update] common-config migration updates only each already-read existing row. - src-tauri/src/services/provider/mod.rs:3895 [update] Gemini credential scrub updates only each already-read existing row. - src-tauri/src/services/provider/mod.rs:4082 [update] sort ordering is routed to the dedicated sort-index state operation, not row replacement. - src-tauri/src/services/provider/mod.rs:4636 [create/update] universal-to- Claude reconciliation reads the target and selects strict create or update. - src-tauri/src/services/provider/mod.rs:4651 [create/update] universal-to- Codex reconciliation reads the target and selects strict create or update. - src-tauri/src/services/provider/mod.rs:4665 [create/update] universal-to- Gemini reconciliation reads the target and selects strict create or update. Required indirect ownership paths: - src-tauri/src/deeplink/provider.rs [create] the old indirect flow called ProviderService::add and then appended endpoints one by one. It now supplies every non-primary endpoint to one strict aggregate create, so hydration is complete atomically and a duplicate is zero-side-effect. - [restore] no old generic-save callsite is reclassified as restore. Exact aggregate replacement exists only as the sealed restore_provider_aggregate_on_tx compensation primitive. Test-only callsites: Every item below is classified [test]. Each is fixture setup, not a production write authority, and is migrated to a real ProviderService entry where the behavior is under test or to the cfg(test)-only typed fixture reconciler where the test merely needs pre-existing rows. - src-tauri/src/codex_history_migration.rs:1442 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:1452 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2174 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2176 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2199 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2219 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2247 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2267 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2288 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2320 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2393 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2449 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2498 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2555 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2604 [test] migration fixture setup. - src-tauri/src/codex_history_migration.rs:2625 [test] migration fixture setup. - src-tauri/src/database/dao/providers.rs:754 [test] DAO fixture setup. - src-tauri/src/proxy/provider_router.rs:351 [test] router fixture setup. - src-tauri/src/proxy/provider_router.rs:352 [test] router fixture setup. - src-tauri/src/proxy/provider_router.rs:377 [test] router fixture setup. - src-tauri/src/proxy/provider_router.rs:378 [test] router fixture setup. - src-tauri/src/proxy/provider_router.rs:410 [test] router fixture setup. - src-tauri/src/proxy/provider_router.rs:411 [test] router fixture setup. - src-tauri/src/proxy/provider_router.rs:447 [test] router fixture setup. - src-tauri/src/proxy/provider_router.rs:448 [test] router fixture setup. - src-tauri/src/proxy/provider_router.rs:488 [test] router fixture setup. - src-tauri/src/services/provider/mod.rs:485 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:586 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:813 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:825 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:1472 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:1607 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:1737 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:1945 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:1952 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:1978 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:2006 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:2056 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:2130 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:2167 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:2207 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:2235 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:2270 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:2320 [test] service fixture setup. - src-tauri/src/services/provider/mod.rs:2362 [test] service fixture setup. - src-tauri/src/services/proxy.rs:3762 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:3948 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:4034 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:4095 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:4114 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:4263 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:4341 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:4421 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:4533 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:4651 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:4787 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:5264 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:5320 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:5385 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:5387 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:5460 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:5462 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:5611 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:5613 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:5615 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:5698 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:5700 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:5998 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:6000 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:6173 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:6175 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:6417 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:6419 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:6553 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:6555 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:6635 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:6637 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:6919 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:7173 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:7175 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:7240 [test] proxy fixture setup. - src-tauri/src/services/proxy.rs:7242 [test] proxy fixture setup. - src-tauri/tests/profile_roundtrip.rs:112 [test] profile fixture create. - src-tauri/tests/profile_roundtrip.rs:116 [test] profile fixture create. - src-tauri/tests/profile_roundtrip.rs:126 [test] profile fixture create. - src-tauri/tests/profile_roundtrip.rs:133 [test] profile fixture create. - src-tauri/tests/profile_roundtrip.rs:292 [test] profile fixture create. - src-tauri/tests/profile_roundtrip.rs:501 [test] profile fixture create. - src-tauri/tests/profile_roundtrip.rs:505 [test] profile fixture create. - src-tauri/tests/profile_roundtrip.rs:670 [test] profile fixture create. - src-tauri/tests/profile_roundtrip.rs:677 [test] profile fixture create. - src-tauri/tests/profile_roundtrip.rs:762 [test] Linux Desktop fixture create. - src-tauri/tests/profile_roundtrip.rs:769 [test] Linux Desktop fixture create. - src-tauri/tests/provider_commands.rs:69 [test] command fixture create. - src-tauri/tests/provider_service.rs:2927 [test] service fixture create. |
||
|
|
4bfb3fc30d | fix(usage): dedupe Claude Desktop proxy and session logs (#5951) | ||
|
|
c49cf96a16 |
fix(grokbuild): complete proxy and deep-link integrations (#5677)
* fix(grokbuild): complete proxy integration * fix(deeplink): preview GrokBuild configs safely * test(app): stabilize provider integration suite * fix(grokbuild): address review feedback * fix(grokbuild): resolve remaining review findings * fix(grokbuild): use native sessions and harden previews |
||
|
|
56fb46c093 |
perf(codex): cache parent rollout timelines across fork cutoffs (#5626)
* perf(codex): cache parent rollout timelines * fix(codex): harden parent timeline cache * fix(codex): tighten replay cache invalidation --------- Co-authored-by: Ayanami <ay@nami.ltd> Co-authored-by: SaladDay <92240037+SaladDay@users.noreply.github.com> |
||
|
|
87b0e3fb85 |
fix(test): pin zip extraction temp dir instead of hijacking TMPDIR
The two cleanup-guard tests introduced in
|
||
|
|
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>
|
||
|
|
cd161f4401 |
feat(usage): import Grok Build official-mode usage from session logs
Grok CLI's official OAuth mode cannot be routed through the local proxy
(empty config is the mode switch, so there is no injection point), which
left official-mode usage invisible. Add session_usage_grokbuild to
import usage from ~/.grok/sessions updates.jsonl:
- Only turn_completed events carry usage; each event is the independent
per-turn total (accumulated across inference loops within one prompt),
so events are imported at face value. Do not reintroduce differencing
of adjacent events: counters reset every turn and differencing would
massively under-record.
- Cost priority: reported costUsdTicks (1 tick = 1e-10 USD) wins when
complete, because the backfill only repairs rows with total <= 0 and
can never correct a positive mispriced value; local pricing fills the
breakdown and raises a drift warning above max(1% of reported, 1e-6).
costIsPartial marks the reported value a lower bound: prefer a full
local recompute when the model is priced, else record the lower bound.
- Idempotency key grok_session:{session}:{prompt_id}:{model} anchors on
the upstream per-turn UUID (index fallback only when prompt_id is
empty), so rewind truncation cannot shift keys and double count;
orphan rows from truncated turns are kept since the tokens were spent.
- Anti double-count vs proxy takeover: 10min settle window plus a
time-window guard over recent grokbuild proxy activity; guarded skips
never mark files as synced.
- Seed grok-4.5-build pricing 2/6/0.30, back-derived from exact
costUsdTicks samples (cache read bills at 0.30, not the listed 0.50).
- Map _grok_session to a friendly provider display name and refresh the
takeover-capability comment in services/proxy.rs.
|
||
|
|
3cf84ca362 |
fix(usage): centralize cache-inclusive app set and cover grokbuild in cost backfill
The cost backfill hardcoded codex|gemini as cache-inclusive apps, so grokbuild TOTAL-semantics rows were priced on full input tokens with cache reads double-counted. Converge the writer (proxy logger and calculator) and the backfill recompute onto a single sql_helpers::is_cache_inclusive_app predicate backed by the existing CACHE_INCLUSIVE_APP_TYPES constant, and add a regression test for the grokbuild backfill path. |
||
|
|
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. |
||
|
|
df3e07edbb |
fix(codex): strip forked history via parent-rollout token-prefix alignment
Rewrite the Codex session importer: identify forks by forked_from_id or subagent thread_spawn.parent_thread_id (conflicts defer), anchor thread identity to the rollout filename UUID, and align child token_count signatures against the parent rollout as a strict prefix so replayed parent history only establishes the cumulative baseline and is never imported as child usage. Ambiguous alignments defer without advancing the cursor and recover once the parent appears; parent signatures are cached per (path, cutoff). Fixes the 3x-20x double counting behind #5335/#5433 while keeping reference-type subagents fully counted (#5381). |
||
|
|
a10b569a53 |
feat(usage): add suspected-duplicate probe for Codex session imports
Detect codex_session rows sharing the same model/token fingerprint within the dedup window under a different request_id. Predicates reuse the COALESCE(data_source,'proxy') shape so the existing expression index serves the lookup. Wired up by the Codex importer rewrite. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
13e7c1fcc4 |
fix(usage): account for Anthropic cache write TTLs
Parse and retain Anthropic's ephemeral 5-minute and 1-hour cache-creation token buckets while preserving the existing aggregate cache-write metric for compatibility. Price 1-hour writes at the documented premium relative to the configured 5-minute write rate, clamp inconsistent provider details safely, and include TTL buckets in usage diagnostics. Persist 1-hour cache-write tokens with schema version 14 so zero-cost backfills and later pricing updates retain the original TTL semantics. Keep session import paths compatible through zero-valued detail fields. |
||
|
|
f991726ff0 |
fix(usage): account for cache-write tokens across schema versions
Parse cache_write_tokens from OpenAI usage details and preserve cache creation data across Chat, Responses, and Anthropic conversion paths. Add explicit input-token semantics to request logs and rollups so legacy rows subtract cache reads only while new total-inclusive rows subtract both cache reads and writes. Migrate v12 databases, normalize rollups to fresh input, and cover historical backfill behavior with regression tests. |
||
|
|
06039540ff | refactor(health-check): remove per-provider test config | ||
|
|
c6197ae324 |
fix(proxy): inject a single auth placeholder on managed Claude takeover (#5095)
Switching the Claude provider from a third-party (DeepSeek/MiMo/...) to a managed Codex provider wrote both ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN as PROXY_MANAGED into ~/.claude/settings.json, making Claude Code warn "Both ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY set - auth may not work as expected" on every run. The double key is an accretion artifact: the ManagedAccount takeover policy originally injected only ANTHROPIC_API_KEY (Copilot, #1049), and the Codex fix that preserved ANTHROPIC_AUTH_TOKEN to avoid the login prompt (#3784 / PR #3789) added the token on top without removing the API key insert. Inject exactly one placeholder instead: AUTH_TOKEN for Codex-managed providers (keeps the #3784 fix), API_KEY for Copilot (keeps the #1049 behavior). Local-proxy auth is unaffected - the forwarder resolves the PROXY_MANAGED placeholder from either header. Updates the three Codex takeover tests to assert API_KEY is absent and adds a regression test for the exact third-party -> Codex switch from the report. Fixes #4919 |
||
|
|
f39d463c44 |
fix(usage): 修复 Codex 子代理使用量未计入统计 (#5187)
* fix(usage): count Codex subagent session tokens * fix(usage): address Codex subagent review feedback * fix(usage): preserve Codex migration history * fix(usage): harden Codex subagent migration * fix(usage): reimport late Codex subagents * fix(usage): guard late Codex rollup reimport * refactor(usage): simplify Codex subagent accounting fix |
||
|
|
50270d5e42 |
fix: exclude Fable model env from Claude common config (#4272) (#5206)
* Add test for Fable model env key exclusion Add regression test to ensure Fable model env keys are excluded from common config. * Add Fable model to CLAUDE_MODEL_OVERRIDE_ENV_KEYS * Add files via upload * Delete .github/workflows/build-windows-unsigned.yml |
||
|
|
99e11e0851 |
feat(codex): support native Anthropic Messages protocol as upstream (#5071)
* feat(codex): support native Anthropic Messages protocol as upstream Allow gateways that only expose the native Anthropic Messages protocol (/v1/messages) to be used by Codex: the local proxy performs bidirectional request/response/streaming conversion between Responses and Anthropic. Backend: - Add two conversion modules: transform_codex_anthropic / streaming_codex_anthropic - codex.rs: add routing detection and auth: ANTHROPIC_AUTH_TOKEN→Bearer (default), ANTHROPIC_API_KEY→x-api-key, mutually exclusive - handlers.rs: add handle_codex_anthropic_to_responses_transform - forwarder.rs: support optional Claude Code client fingerprint impersonation (User-Agent / anthropic-beta / x-app / system prompt first-line injection) and /responses→/v1/messages rewriting - codex_config: the anthropic format reuses the NativeResponses profile to strip custom tools - ProviderMeta: add impersonateClaudeCode Frontend: - CodexApiFormat: add "anthropic"; the form adds auth field selection and an impersonation toggle - Add en/ja/zh/zh-TW copy Robustness: - Downgrade when tool history / forced tool_choice conflicts with extended thinking, avoiding upstream 400s - Emit cache_creation_input_tokens in usage and use saturating_add to guard against overflow - Append a unique suffix to non-streaming/streaming output-item ids to avoid multi-segment text/thinking overwriting each other * fix(codex): harden Anthropic bridge against empty text blocks and truncated streams - Drop empty/whitespace-only text content blocks when rebuilding Anthropic messages from Responses history; Anthropic 400s on empty text blocks (e.g. an empty assistant text emitted alongside a tool_use), which broke follow-up and tool-result requests. Also drop messages left without content. - Do not report a truncated Anthropic stream as completed: when the SSE connection ends before message_stop with no stop_reason, emit an incomplete response if partial output exists, or a failed (stream_truncated) response otherwise, mirroring the chat converter's EOF handling. * fix(codex): resolve Anthropic-bridge review findings Blocking: 1. Defer stripping the [1m] long-context marker until after catalog matching and model write-back, re-stripping it on the final Codex→Anthropic body and setting a flag to emit the context-1m-2025-08-07 beta header, so the marker is no longer lost or overridden by the default model. 2. Gate Anthropic thinking on the trailing turn only (via trailing_turn_allows_thinking) instead of scanning full history, so a Codex session that resends history each round no longer permanently loses thinking after the first tool call. 3. Inject 5m ephemeral cache_control on the Codex→Anthropic body by reusing cache_injector (handling the system string→array conversion), so system/tools/history are cached instead of re-sent at full price every round. 4. Add a shared base_url_is_full_endpoint helper (normalizing whitespace/query/fragment/trailing slash) used by both the Anthropic and Chat paths, so a base URL already ending in /v1/messages is treated as a full endpoint instead of double-appending to /v1/messages/v1/messages. 5. Align the catalog tool-profile predicate with the routing predicate so resolve_codex_catalog_tool_profile returns the Anthropic profile whenever the request converts to Anthropic, preventing freeform tools like apply_patch from being silently filtered by a ProxyChat catalog. 6. Explicitly disable native web_search for the Anthropic profile (including the no-catalog branch) via set_codex_native_web_search_field, so Codex no longer treats it as available while the transform silently drops it. 7. Only forward tool_choice when tools survive filtering, dropping it otherwise, to avoid a non-retryable 400 from upstream when tool_choice is sent with no tools. 8. Lower the fallback default to max_tokens=8192 (only when max_output_tokens is omitted) and clamp the thinking budget to max_tokens/2, disabling thinking below the 1024 floor, to avoid hard 400s on low-output-ceiling models/gateways. Minor: 9. Centralize the Codex/OpenAI fingerprint-header denylist in is_codex_client_fingerprint_header so impersonating Claude Code uniformly drops originator/session_id/conversation_id/chatgpt-account-id/x-client-request-id/openai-* and the x-stainless-*/x-codex-* prefixes. 10. Retain content_block_start.input as start_input and fall back to it at block close when no input_json_delta arrived, so a gateway that carries the full tool input on the start event no longer yields empty tool arguments. 11. Extract a shared codex_responses_sse module as the single Responses SSE envelope builder that both the chat and anthropic streaming emitters delegate to, with byte-for-byte-unchanged wire output, so future event-format fixes touch one place instead of two. * fix(codex): add per-provider max_output_tokens override for Codex→Anthropic path Codex does not forward model_max_output_tokens in the request body, causing the proxy to fall back to a conservative 8192 default. This truncates long or thinking-heavy responses (stop_reason=max_tokens). - Add maxOutputTokens field to ProviderMeta (Rust + TypeScript) - Inject the value into the Anthropic request body before transform, taking precedence over request-supplied and default values - Add numeric input in Codex form fields (Anthropic format only) - Add i18n entries for label, placeholder, and hint (en/zh/zh-TW/ja) - Include roundtrip and omission unit tests for the new field * fix(codex): harden Anthropic protocol bridge * fix(codex): address Anthropic bridge review * fix(codex): preserve flattened Anthropic inputs * fix(codex): harden Anthropic recovery paths * fix(ci): satisfy Rust clippy --------- Co-authored-by: Jason <farion1231@gmail.com> |
||
|
|
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. |
||
|
|
11c173c730 |
fix(mcp): stop cross-app failures from blocking MCP re-projection
sync_all_enabled iterated AppType::all() with `?`, so one app's corrupt live file (e.g. a broken ~/.claude.json, which passes the existence gate but fails to parse) blocked every app behind it in the iteration order -- and bubbled the error into whatever operation triggered the sync: - switch/save had just rewritten only the target app's live file, yet a broken unrelated file failed the whole operation after DB and live were already updated, reporting a false "switch failed" to the user. Both now project only the target app (sync_enabled_for_app) and degrade projection failure to a warning: the primary operation has already taken effect, and the projection self-heals on the next switch or MCP toggle. - sync_current_provider_for_app_to_live syncs a single app; it now projects that app only, keeping failures (which can only concern the target app) as errors. - sync_current_to_live (config import / cloud-sync restore) keeps the all-apps sweep, but sync_all_enabled is now best-effort: it projects every app, collects failures, and reports them aggregated. Its error is held until after skill sync so an MCP failure no longer skips it. |
||
|
|
1f36f0cfed |
feat(provider): extend switch-time common-config autosync to Codex
Claude already re-extracts the live config into the shared common-config snippet right before switching away, so shared tweaks (plugins, preferences) made in live propagate to all opted-in providers. Codex was gated out because its TOML pipeline leaked provider-specific and injected content into the snippet. With the extractor now stripping all injection artifacts and routing fields, and backfill stripping the MCP projection, open the gate to Claude + Codex. The autosync-before-strip ordering also self-heals stale snippet values previously baked into provider snapshots: the re-extracted snippet matches the live values, so the value-match strip removes them on the same switch. End-to-end tests cover: new shared keys captured, deletions synced, secrets/injected artifacts never entering the snippet, and provider A's key not leaking into provider B's live. |
||
|
|
6d2ee2472f |
fix(provider): re-project Codex MCP after unified-session toggle rewrites live config
Toggling unify_codex_session_history rewrites the current official provider's live config.toml in full (intended design), which drops the [mcp_servers] projection -- and nothing put it back, so enabled MCP servers silently vanished until the next provider switch (#C2). Re-project after the rewrite, with two deliberate choices: - Project Codex only (new McpService::sync_enabled_for_app) instead of sync_all_enabled: the all-apps sync short-circuits in AppType::all() order, so a corrupt ~/.claude.json would error before Codex is ever reached and the freshly wiped [mcp_servers] would stay missing. Only Codex's live file was rewritten here, so only Codex needs re-projection. - Degrade projection failure to a warning: by this point the live file already carries the new bucket state, so the toggle has taken effect. Propagating the error would make save_settings roll back the setting, creating the exact "setting=old, live=new bucket" session split the rollback exists to prevent. The projection self-heals on the next switch or any MCP toggle. |
||
|
|
473c2aaa9f |
fix(provider): exclude injected artifacts and routing fields from Codex common-config extraction
extract_codex_common_config kept several fields in the shared snippet that must never cross providers: - [mcp_servers] and the legacy [mcp.servers] form: owned by the DB mcp_servers table; once in the snippet they get merged into every opted-in provider and no sync path can ever clean them up. - top-level experimental_bearer_token: normally lives inside [model_providers.<id>] (stripped with the whole table), but three fallbacks write it at top level -- leaking the API key into the shared snippet. - model_catalog_json: per-provider catalog projection pointer. - web_search, only when it equals the injected "disabled" sentinel; a user-set value remains a shareable preference. - top-level wire_api: same provider-routing semantics as top-level base_url (the fallback target when no model_provider is set); leaking it would rewrite the next provider's protocol selection. This makes the extractor safe as the source for switch-time common-config autosync. |
||
|
|
93f56198da |
fix(codex): strip synced [mcp_servers] from provider snapshots on backfill
MCP servers are owned by the DB mcp_servers table; the [mcp_servers] section in live config.toml is only a projection re-synced after every live write. When switching away baked that projection into the stored provider snapshot, servers deleted in the app were resurrected the next time that provider was activated -- per-entry reconcile only knows rows that still exist in the DB, so it can never clean up such orphans. Strip [mcp_servers] (and the legacy [mcp.servers] form) from the live settings during switch-away backfill. Previously polluted snapshots self-heal the next time the user switches away from them. |
||
|
|
e78aa8a7c3 |
fix: sync openclaw and hermes live provider updates (#5098)
* fix: sync openclaw live provider updates * fix: sync hermes live provider updates * fix test: hermes live import stores models as array after denormalize The test import_hermes_providers_from_live_updates_existing_provider_from_live seeded models as a dict, but import_hermes_providers_from_live reads via get_providers() which calls denormalize_provider_models_for_read(), converting models from YAML dict to UI-friendly array. The test assertion accessed models as dict -> Null -> assertion failure. Fix: access models as array by index, matching the actual storage format after live import. Also verify the id field is preserved in the denormalized output. |
||
|
|
e191af4aa1 |
fix: OpenCode live provider import updates (#4712)
* fix: sync existing opencode providers from live config * fix: preserve OpenCode provider display names * docs: update comments and log messages to reflect new update behavior - fix stale comment in lib.rs that said existing providers are skipped - change log message from 'Imported' to 'Synced' since count now includes both new imports and updates |
||
|
|
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 | ||
|
|
4f45601f9f | feat(profiles): unconditionally disable proxy takeover before applying profile | ||
|
|
4cf6f175a7 |
feat(profiles): autosave previous profile state on switch
When switching to a different project, the current configuration of the previously active project is automatically captured into that project's snapshot before the target project's snapshot is applied. Scope is limited to the active group (Claude/Codex); autosave failures are collected as warnings and do not block the switch. - Move autosave logic into ProfileService::apply for consistent behavior across UI, tray, and tests - Add integration test covering bidirectional autosave roundtrip Refs: profile switch B方案 |