diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml
new file mode 100644
index 0000000000..7599c771cb
--- /dev/null
+++ b/.github/workflows/native.yml
@@ -0,0 +1,441 @@
+name: Native
+
+# Phase 5 of the desktop migration contract (see the desktop migration contract §5.1).
+# Gates the Rust/Tauri desktop app (apps/native/) — a separate cargo
+# workspace, independent of the Bun workspaces the other workflows cover.
+#
+# Path-filtered (not the `changes`-job PR-diff pattern test.yml/e2e.yml/
+# sandbox-daemon.yml use) because this workflow is NOT yet a required
+# branch-protection check — apps/native is still behind the migration flag
+# (the desktop migration contract's `desktopAppAvailable`, default off) and this branch hasn't
+# merged to main. A never-scheduled REQUIRED check blocks merges forever if
+# skipped via workflow-level `paths:` (see test.yml's comment on its
+# `changes` job) — but an opt-in, non-required check like this one is exactly
+# what daemon-e2e-windows.yml already does with plain `paths:`, so this
+# mirrors that convention. If/when this becomes a required check, switch to
+# the `changes`-job pattern first.
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - "apps/native/**"
+ - "apps/web/**"
+ - "packages/sandbox/daemon/**"
+ - "packages/shared/**"
+ - ".github/workflows/native.yml"
+ pull_request:
+ branches:
+ - main
+ paths:
+ - "apps/native/**"
+ - "apps/web/**"
+ - "packages/sandbox/daemon/**"
+ - "packages/shared/**"
+ - ".github/workflows/native.yml"
+ # stub-seam-nightly keeps the DAEMON_E2E_CMD swap seam (packages/sandbox/
+ # daemon/e2e-stub/) from rotting even on weeks with no apps/native PRs —
+ # see the desktop migration contract §5.1's "nightly job running the daemon-e2e stub-seam
+ # check". `schedule` + `workflow_dispatch` fire regardless of `paths:`
+ # (path filters only apply to push/pull_request), which is intentional
+ # here — every job below is gated by `if:` on `github.event_name` instead
+ # (see each job).
+ schedule:
+ - cron: "17 3 * * *" # nightly, arbitrary off-hour offset to dodge the top-of-hour stampede
+ workflow_dispatch: {}
+
+concurrency:
+ group: native-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ BUN_VERSION: "1.3.14"
+
+jobs:
+ # ---------------------------------------------------------------------
+ # 1. rust-checks — fmt / clippy / cargo test across the whole cargo
+ # workspace (crates/local-api, crates/harness, crates/upstream,
+ # src-tauri). macOS-only (matches the other jobs and the app's actual
+ # v1 target — the desktop migration contract's "v1 is macOS-only"); no ubuntu leg because
+ # src-tauri's deps (tao/wry/muda/objc2/...) are macOS-framework-linked
+ # and won't compile on Linux without a large extra apt-get surface
+ # this phase doesn't need to carry.
+ # ---------------------------------------------------------------------
+ rust-checks:
+ if: github.event_name != 'schedule'
+ runs-on: macos-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ components: rustfmt, clippy
+
+ - name: Cache cargo registry + target dir
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: apps/native
+
+ - name: cargo fmt --check
+ working-directory: apps/native
+ run: cargo fmt --all -- --check
+
+ - name: Placeholder frontendDist for generate_context!
+ # `tauri::generate_context!` (compiled by clippy + test below) panics if
+ # the configured `frontendDist` (../../web/dist/native) is absent. This
+ # job intentionally does NOT build the frontend, so drop a minimal
+ # placeholder — the real bundle is produced in the `tauri-build` job.
+ run: |
+ mkdir -p apps/web/dist/native
+ printf '
' > apps/web/dist/native/index.html
+
+ # `tauri-build` resolves `externalBin` in `src-tauri/build.rs`, so ANY
+ # cargo command touching the workspace needs the sidecar on disk — not
+ # just `tauri build`. It is git-ignored and fetched at build time, so
+ # without this every cargo job fails with "resource path
+ # binaries/rclone- doesn't exist".
+ - name: Cache bundled rclone
+ uses: actions/cache@v4
+ with:
+ path: apps/native/src-tauri/binaries
+ key: rclone-${{ runner.os }}-${{ hashFiles('apps/native/scripts/fetch-rclone.sh') }}
+
+ - name: Fetch bundled rclone
+ working-directory: apps/native
+ run: ./scripts/fetch-rclone.sh
+
+ - name: cargo clippy -D warnings
+ working-directory: apps/native
+ run: cargo clippy --workspace --all-targets -- -D warnings
+
+ - name: cargo test --workspace
+ working-directory: apps/native
+ run: cargo test --workspace
+
+ # ---------------------------------------------------------------------
+ # 2. daemon-e2e-vs-rust — the parity oracle: build the release local-api
+ # binary, point packages/sandbox's curated 5-file daemon e2e suite at
+ # it via DAEMON_E2E_CMD, and assert the EXACT documented 133 pass / 7
+ # fail outcome — not just the counts. apps/native/docs/
+ # the native parity contract pins the 7 failing test names as intentional
+ # auth/CORS tightening (+ one harness self-check artifact); a NEW
+ # failure (regression) or a VANISHED one (doc gone stale) both fail
+ # this job via apps/native/scripts/ci/check-junit-allowlist.mjs.
+ #
+ # `daemon.proxy.e2e.test.ts` + `daemon.ws-proxy.e2e.test.ts` (12 tests,
+ # formerly "all pass") are DELIBERATELY excluded from this curated set
+ # as of the port-router split (`local_api::ServerHandle`'s dedicated
+ # PREVIEW listener) — this harness (`daemon.e2e.helpers.ts`, owned by
+ # `packages/sandbox`, out of bounds to edit here) hard-codes a SINGLE
+ # port for the whole daemon and has no way to discover/target a SECOND
+ # one, so it structurally cannot drive local-api's now-separate preview
+ # listener. See `the native parity contract`'s "Reverse proxy +
+ # WS proxy" exclusion section for the full rationale and this crate's
+ # own substitute oracle coverage (`apps/native/e2e/git-sandbox.e2e.test.ts`,
+ # `real-ui-passthrough.e2e.test.ts`).
+ # ---------------------------------------------------------------------
+ daemon-e2e-vs-rust:
+ if: github.event_name != 'schedule'
+ runs-on: macos-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo registry + target dir
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: apps/native
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: ${{ env.BUN_VERSION }}
+
+ - name: Install dependencies
+ run: bun install
+
+ # The fs `grep` route (in the curated suite's daemon.e2e.test.ts)
+ # shells out to ripgrep; install it so the test asserts results
+ # strictly instead of tolerating "grep unavailable" — same reasoning
+ # as sandbox-daemon.yml / daemon-e2e-windows.yml's ripgrep installs.
+ - name: Install ripgrep
+ run: brew install ripgrep
+
+ - name: Build release local-api binary
+ working-directory: apps/native
+ run: cargo build --release --bin local-api
+
+ - name: Run curated daemon e2e subset against local-api
+ id: daemon-parity
+ # 7 documented failures are EXPECTED here (see the native parity contract) —
+ # `|| true` keeps this step green so the next step (the actual
+ # gate) can inspect the junit output and fail precisely on a SET
+ # mismatch, not merely on bun test's own non-zero exit.
+ #
+ # `daemon.proxy.e2e.test.ts` / `daemon.ws-proxy.e2e.test.ts` are NOT
+ # in this list — see the job header comment above and
+ # the native parity contract for why (the port-router split moved that
+ # traffic to a second listener this single-port harness can't
+ # reach).
+ run: |
+ LOCAL_API_BIN="$GITHUB_WORKSPACE/apps/native/target/release/local-api"
+ export DAEMON_E2E_CMD="[\"$LOCAL_API_BIN\"]"
+ cd packages/sandbox
+ bun test daemon/daemon.e2e.test.ts daemon/daemon.sse-shapes.e2e.test.ts \
+ daemon/daemon.git.e2e.test.ts daemon/daemon.tools.e2e.test.ts \
+ daemon/daemon.dispatch.e2e.test.ts \
+ --reporter=junit --reporter-outfile="$RUNNER_TEMP/daemon-parity.xml" || true
+
+ - name: Assert exact fail-set match against the pinned allowlist
+ run: |
+ bun run --cwd apps/native check:junit-allowlist -- \
+ "$RUNNER_TEMP/daemon-parity.xml" \
+ "$GITHUB_WORKSPACE/apps/native/scripts/ci/daemon-parity-allowlist.json"
+
+ - name: Upload junit report (debugging aid)
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: daemon-parity-junit
+ path: ${{ runner.temp }}/daemon-parity.xml
+ if-no-files-found: warn
+ retention-days: 7
+
+ # ---------------------------------------------------------------------
+ # 3. contract-suite — apps/native/e2e's own black-box contract suite
+ # (LOCAL_API_E2E_CMD) against the release local-api binary. Unlike the
+ # parity oracle above, this suite is OWNED by this implementation
+ # (the native local-API contract is the spec it pins), so
+ # the bar is simply "all green" — no allowlist needed.
+ # ---------------------------------------------------------------------
+ contract-suite:
+ if: github.event_name != 'schedule'
+ runs-on: macos-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo registry + target dir
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: apps/native
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: ${{ env.BUN_VERSION }}
+
+ - name: Install dependencies
+ run: bun install
+
+ - name: Build release local-api binary
+ working-directory: apps/native
+ run: cargo build --release --bin local-api
+
+ - name: Run local-api contract suite
+ working-directory: apps/native
+ run: |
+ export LOCAL_API_E2E_CMD="[\"$GITHUB_WORKSPACE/apps/native/target/release/local-api\"]"
+ bun test e2e
+
+ # ---------------------------------------------------------------------
+ # 4. tauri-build — bundles the debug .app, uploads it as an artifact, and
+ # boot-smokes it. macOS code-signing / notarization / updater-artifact
+ # signing are handled ENTIRELY by the Tauri CLI itself once the
+ # matching env vars are non-empty (https://v2.tauri.app/distribute/
+ # sign/macos/) — this job does not hand-roll `security`/`notarytool`
+ # calls, it just wires the documented secret names through as env and
+ # lets `tauri build` no-op into an unsigned/ad-hoc build when they're
+ # absent (the fork-safe default: every secret below is empty on a
+ # fork PR, so signing/notarization are skipped, never failed).
+ # ---------------------------------------------------------------------
+ tauri-build:
+ if: github.event_name != 'schedule'
+ runs-on: macos-latest
+ timeout-minutes: 45
+ env:
+ # --- macOS code signing + notarization (Apple Developer ID) ---------
+ # All optional. Human-required setup (cert enrollment, notarization
+ # credentials) — see the native release contract (§5.3,
+ # dormant scaffolding). Required secret names, each consumed only by
+ # the `tauri build` step below via env:
+ # APPLE_CERTIFICATE - base64-encoded .p12 Developer ID cert
+ # APPLE_CERTIFICATE_PASSWORD - password for the .p12 above
+ # APPLE_SIGNING_IDENTITY - cert Common Name, e.g. "Developer ID
+ # Application: ()" —
+ # not in the brief's original list but
+ # REQUIRED by Tauri's signing flow
+ # alongside the cert itself; documented
+ # here so it isn't missed when the
+ # other 5 Apple secrets are configured
+ # APPLE_ID - Apple ID used for notarization
+ # APPLE_PASSWORD - app-specific password for APPLE_ID
+ # APPLE_TEAM_ID - Developer Team ID
+ # DELIBERATELY NOT WIRED HERE. This job only ever produces a DEBUG
+ # bundle for boot-smoke, which runs the binary directly and gains
+ # nothing from a Developer ID. Exporting the secrets made the Tauri CLI
+ # attempt `security import` on every build in this job — including the
+ # one boot-smoke launches itself — and it failed with "failed codesign
+ # application: failed to import keychain certificate", from a cert that
+ # is present but not importable. Wiring them cost a green CI and bought
+ # nothing. Signing belongs to the separate, secret-gated release job.
+ # --- Tauri updater artifact signing (separate keypair; unrelated to
+ # Apple code signing above) — only takes effect once
+ # tauri.conf.json5 configures the `updater` bundle target, which
+ # is still dormant (the native release contract
+ # §"Updater"). Wired here now so no further workflow edit is
+ # needed once that config lands.
+ TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
+ TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo registry + target dir
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: apps/native
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: ${{ env.BUN_VERSION }}
+
+ - name: Install dependencies
+ run: bun install
+
+ # rclone is bundled as an `externalBin` (the org filesystem's NFS
+ # mount) and deliberately NOT committed — 78 MB per architecture.
+ # Keyed on the fetch script's hash, so bumping the pinned version in
+ # that script busts the cache automatically.
+ - name: Cache bundled rclone
+ uses: actions/cache@v4
+ with:
+ path: apps/native/src-tauri/binaries
+ key: rclone-${{ runner.os }}-${{ hashFiles('apps/native/scripts/fetch-rclone.sh') }}
+
+ # Explicit for the same reason as the frontend bundle below:
+ # `beforeBuildCommand` also runs it, but a dedicated step attributes a
+ # download or checksum failure to itself instead of to `tauri build`.
+ - name: Fetch bundled rclone
+ working-directory: apps/native
+ run: ./scripts/fetch-rclone.sh
+
+ # Explicit step (not relying solely on tauri.conf.json5's own
+ # `beforeBuildCommand: bun run build:native`) for a clearer CI step
+ # boundary/log — slightly redundant with that hook, harmless (the
+ # second invocation is a fast no-op rebuild of already-fresh output).
+ - name: Build desktop frontend bundle
+ run: bun run --cwd apps/web build:native
+
+ - name: tauri build --debug
+ working-directory: apps/native
+ # This build is NEVER signed, whether or not the secrets exist.
+ # boot-smoke runs the binary directly and a Developer ID buys it
+ # nothing, while attempting the import couples this job to the health
+ # of six repo secrets — which is exactly how it has been failing:
+ # "failed codesign application: failed to import keychain certificate",
+ # from a cert that is present but not importable. Signing belongs to
+ # the separate, secret-gated release job (see the native release
+ # contract).
+ run: bunx @tauri-apps/cli@latest build --debug
+
+ - name: Upload .app artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: decocms-desktop-debug-app
+ path: apps/native/target/debug/bundle/macos/decocms-desktop.app
+ if-no-files-found: error
+ retention-days: 14
+
+ # Boot-smoke consideration (the desktop migration contract §5.1): CI macOS runners
+ # are full logged-in GUI VMs (not headless like Linux — no Xvfb
+ # needed), and DESKTOP_SELFTEST=1 builds the "main" window
+ # `.visible(false)` specifically so this never pops a real window
+ # (see the native Tauri integration's "Self-test mode" section) — confirmed
+ # locally: `bun run --cwd apps/native smoke:boot` passes end-to-end
+ # (all 7 self-test checks + the orphan-process check) without ever
+ # showing a window. Included here rather than documented as
+ # local-only. Residual risk not fully provable from a dev machine:
+ # `authStatusInvoke`'s status check touches macOS Keychain via the
+ # `keyring` crate (crates/upstream) — GH's macos-latest runners
+ # provide an unlocked default login keychain so a read-only
+ # "no stored session" status check is expected to work the same way
+ # it does locally, but if this step ever starts failing ONLY in CI
+ # with a Keychain-access error, add a
+ # `security unlock-keychain -p runner ~/Library/Keychains/login.keychain-db`
+ # step immediately before this one rather than reverting it to
+ # local-only.
+ - name: Boot smoke (DESKTOP_SELFTEST=1, hidden window)
+ run: bun run --cwd apps/native smoke:boot
+
+ # ---------------------------------------------------------------------
+ # 5. stub-seam-nightly — keeps the DAEMON_E2E_CMD swap-seam mechanics
+ # (spawn / env contract / health-poll / SIGKILL-teardown) exercised
+ # even on weeks with no apps/native PRs, per the desktop migration contract §5.1.
+ # Runs the plain Node stub (packages/sandbox/daemon/e2e-stub/
+ # stub-daemon.mjs), not the Rust binary — ubuntu-latest is fine since
+ # nothing here is macOS-specific (no cargo build at all). Asserts the
+ # documented 76 pass / 27 fail table from the daemon parity contract via the
+ # same exact-set allowlist checker as job 2 — this table already
+ # documents (and this job tolerates, by pinning it into the allowlist
+ # like every other entry) the one env-leakage self-check failure
+ # (`swappable spawn target > defaults to ... when DAEMON_E2E_CMD is
+ # unset`) the README's "Friction found in the seam" section explains.
+ # ---------------------------------------------------------------------
+ stub-seam-nightly:
+ if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: ${{ env.BUN_VERSION }}
+
+ - name: Install dependencies
+ run: bun install
+
+ - name: Run daemon.e2e.test.ts against the e2e-stub seam probe
+ # 27 documented failures are EXPECTED (this stub implements only a
+ # slice of the daemon's route surface — see the daemon parity contract's
+ # "What stub-daemon.mjs implements"). `|| true` for the same reason
+ # as job 2's parity step — the next step is the real gate.
+ run: |
+ STUB="$GITHUB_WORKSPACE/packages/sandbox/daemon/e2e-stub/stub-daemon.mjs"
+ export DAEMON_E2E_CMD="[\"node\", \"$STUB\"]"
+ cd packages/sandbox
+ bun test daemon/daemon.e2e.test.ts \
+ --reporter=junit --reporter-outfile="$RUNNER_TEMP/stub-seam.xml" || true
+
+ - name: Assert exact fail-set match against the pinned allowlist
+ run: |
+ bun run --cwd apps/native check:junit-allowlist -- \
+ "$RUNNER_TEMP/stub-seam.xml" \
+ "$GITHUB_WORKSPACE/apps/native/scripts/ci/stub-seam-allowlist.json"
+
+ - name: Upload junit report (debugging aid)
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: stub-seam-junit
+ path: ${{ runner.temp }}/stub-seam.xml
+ if-no-files-found: warn
+ retention-days: 7
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 97a1a687b8..0f4142b01b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -179,12 +179,18 @@ jobs:
test -f apps/api/dist/server/migrate.js
test -f apps/api/dist/server/cli.js
+ # `build:web` rather than `build` so VITE_TAURI_APP=0 is stated, not
+ # inherited from the variable happening to be unset — the desktop-free
+ # assertion below is only meaningful against a known browser build.
- name: Build web app
- run: bun run --cwd=apps/web build
+ run: bun run --cwd=apps/web build:web
- name: Verify web output
run: test -f apps/web/dist/index.html
+ - name: Assert no Tauri desktop code in the browser bundle
+ run: bun run check:web-bundle
+
# Playwright component tests for the sections-editor field widgets
# (apps/web/ct). Self-contained — real chromium, no server/DB — so it lives
# here rather than e2e.yml, gated to web changes only.
diff --git a/.gitignore b/.gitignore
index e8cb09f66f..cc7ac42f8c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -100,6 +100,8 @@ packages/e2e/playwright-report/
# so we can un-ignore specific subdirs below — git won't traverse into an
# ignored directory.
/docs/*
+# Committed engineering plans (desktop migration etc.)
+!/docs/plans/
# Superpowers workspace artifacts (brainstorming + writing-plans output)
# are kept local to the working tree, not committed.
diff --git a/AGENTS.md b/AGENTS.md
index 6a9cf0d031..3a4249b2ad 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -25,8 +25,24 @@ bun run --cwd=apps/api dev:server
# Run documentation site locally
bun run docs:dev
+
+# Native desktop app (Tauri) dev loop — HMR via Vite on port 4420
+bun run --cwd=apps/native dev
```
+**One-time macOS setup for `apps/native` devs**: run
+`bun run --cwd=apps/native dev:signing:setup` once. It creates or reuses the
+local self-signed `decocms-dev` code-signing identity; no Apple Developer
+account is required. It also builds, signs, and installs one fixed
+`decocms-keychain-helper` under the user's Application Support directory.
+Debug app rebuilds talk to that unchanged helper over JSON stdin/stdout, so
+Keychain sees one stable executable; tokens never use argv, logs, or a
+filesystem fallback. The native dev runner still signs the app itself and
+fails closed if signing drifts, but the fixed helper—not the app's self-signed
+designated requirement—is what makes debug Keychain access stable. Debug
+sessions stay in the Keychain-only `com.decocms.studio.dev` namespace; release
+sessions stay in `com.decocms.studio`.
+
### Testing & Quality
```bash
# Run all tests (Bun test runner)
diff --git a/README.md b/README.md
index 68ffdd2be7..3b1840721b 100644
--- a/README.md
+++ b/README.md
@@ -183,6 +183,7 @@ Every tool call gets input/output validation, access control, audit logging, and
| --- | --- |
| [`apps/api`](./apps/api/README.md) | Hono API, authentication, tools, storage, migrations, and the `deco` CLI |
| [`apps/docs`](./apps/docs/README.md) | Astro documentation site |
+| [`apps/native`](./apps/native/README.md) | Tauri desktop app and local Rust runtime |
| [`apps/web`](./apps/web/README.md) | Vite and React 19 administration interface |
### Packages
diff --git a/apps/api/package.json b/apps/api/package.json
index bdb7e3b75d..960d3ec916 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -113,7 +113,7 @@
"typescript": "^7.0.2",
"typescript5": "npm:typescript@^5.9.3",
"vite": "^7.2.1",
- "zod": "^4.0.0"
+ "zod": "4.3.6"
},
"homepage": "https://github.com/decocms/studio",
"keywords": [
diff --git a/apps/api/src/api/routes/desktop-session-bridge.ts b/apps/api/src/api/routes/desktop-session-bridge.ts
index 8ca824a86a..efeda6d9d2 100644
--- a/apps/api/src/api/routes/desktop-session-bridge.ts
+++ b/apps/api/src/api/routes/desktop-session-bridge.ts
@@ -2,7 +2,7 @@
* Desktop system-browser session bridge.
*
* `POST /api/auth/desktop/session-from-oauth` — the one Studio-side change
- * `apps/desktop/docs/real-ui-auth-recon.md` concluded was unavoidable to fix
+ * the native authentication contract concluded was unavoidable to fix
* the Google/GitHub/SAML system-browser desktop login path: an MCP OAuth
* access token (the `oauthAccessToken` row both desktop login paths
* ultimately hold) satisfies org-scoped `/api/:org/*` routes but is REJECTED
@@ -20,7 +20,7 @@
* `Set-Cookie` header — this caller is a native app process, not a browser
* that would capture one) so it can forward that value itself, byte for
* byte, as `Cookie: better-auth.session_token=` — see
- * `apps/desktop/crates/upstream/src/login.rs`'s
+ * `apps/native/crates/upstream/src/login.rs`'s
* `mint_session_from_access_token`, the Rust-side caller.
*
* ## Auth guard
diff --git a/apps/native/.cargo/config.toml b/apps/native/.cargo/config.toml
new file mode 100644
index 0000000000..8c7f34e3ca
--- /dev/null
+++ b/apps/native/.cargo/config.toml
@@ -0,0 +1,11 @@
+# macOS dev-build signing hook. Cargo routes each locally-executed binary
+# through the runner, but the script signs only `decocms-desktop`; test binaries
+# pass through unchanged. The app uses the local self-signed `decocms-dev`
+# identity with an explicit certificate-hash + bundle-identifier requirement so
+# Keychain ACLs remain stable across rebuilds. Signing fails closed instead of
+# launching an ad-hoc binary.
+[target.aarch64-apple-darwin]
+runner = "scripts/dev-runner.sh"
+
+[target.x86_64-apple-darwin]
+runner = "scripts/dev-runner.sh"
diff --git a/apps/native/.gitignore b/apps/native/.gitignore
new file mode 100644
index 0000000000..124e199b8d
--- /dev/null
+++ b/apps/native/.gitignore
@@ -0,0 +1,4 @@
+/target/
+**/target/
+/.dev-signing-identity
+/src-tauri/binaries/
diff --git a/apps/native/Cargo.lock b/apps/native/Cargo.lock
new file mode 100644
index 0000000000..c179215575
--- /dev/null
+++ b/apps/native/Cargo.lock
@@ -0,0 +1,6960 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures 0.2.17",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
+
+[[package]]
+name = "apple-native-keyring-store"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "797f94b6a53d7d10b56dc18290e0d40a2158352f108bb4ff32350825081a9f29"
+dependencies = [
+ "keyring-core",
+ "log",
+ "security-framework",
+]
+
+[[package]]
+name = "arc-swap"
+version = "1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "atk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
+dependencies = [
+ "atk-sys",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "atk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.17.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.43.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+ "pkg-config",
+]
+
+[[package]]
+name = "axum"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "axum-macros",
+ "base64 0.22.1",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sha1",
+ "sync_wrapper",
+ "tokio",
+ "tokio-tungstenite 0.24.0",
+ "tower 0.5.3",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-macros"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "axum-server"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ab4a3ec9ea8a657c72d99a03a824af695bd0fb5ec639ccbd9cd3543b41a5f9"
+dependencies = [
+ "arc-swap",
+ "bytes",
+ "fs-err",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-util",
+ "pin-project-lite",
+ "rustls",
+ "rustls-pemfile",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block2"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "block2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2 0.6.4",
+]
+
+[[package]]
+name = "blocking"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "brotli"
+version = "8.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "5.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bs58"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "bstr"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530"
+dependencies = [
+ "memchr",
+ "serde_core",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cairo-rs"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
+dependencies = [
+ "bitflags 2.13.1",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "camino"
+version = "1.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "cargo_toml"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
+dependencies = [
+ "serde",
+ "toml 0.9.12+spec-1.1.0",
+]
+
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.67"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfb"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
+dependencies = [
+ "byteorder",
+ "fnv",
+ "uuid",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "chacha20"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+dependencies = [
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "cookie"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+dependencies = [
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-graphics"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
+dependencies = [
+ "bitflags 2.13.1",
+ "core-foundation 0.10.1",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
+dependencies = [
+ "bitflags 2.13.1",
+ "core-foundation 0.10.1",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "phf",
+ "smallvec",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "ctor"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98"
+dependencies = [
+ "ctor-proc-macro",
+ "dtor",
+]
+
+[[package]]
+name = "ctor-proc-macro"
+version = "0.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+
+[[package]]
+name = "dbus"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "decocms-desktop"
+version = "0.1.0"
+dependencies = [
+ "async-trait",
+ "local-api",
+ "rand 0.8.7",
+ "rcgen",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-build",
+ "tauri-plugin-mcp-bridge",
+ "tauri-plugin-opener",
+ "tempfile",
+ "thiserror 1.0.69",
+ "time",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+ "upstream",
+ "urlencoding",
+ "uuid",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.6.2",
+ "libc",
+ "objc2 0.6.4",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "dlopen2"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4"
+dependencies = [
+ "dlopen2_derive",
+ "libc",
+ "once_cell",
+ "winapi",
+]
+
+[[package]]
+name = "dlopen2_derive"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "dom_query"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
+dependencies = [
+ "bit-set",
+ "cssparser",
+ "foldhash",
+ "html5ever",
+ "precomputed-hash",
+ "selectors",
+ "tendril",
+]
+
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
+[[package]]
+name = "dpi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "dtoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "dtor"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4"
+dependencies = [
+ "dtor-proc-macro",
+]
+
+[[package]]
+name = "dtor-proc-macro"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "embed-resource"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd"
+dependencies = [
+ "cc",
+ "memchr",
+ "rustc_version",
+ "toml 1.1.3+spec-1.1.0",
+ "vswhom",
+ "winreg 0.55.0",
+]
+
+[[package]]
+name = "embed_plist"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
+
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "erased-serde"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec"
+dependencies = [
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "field-offset"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
+dependencies = [
+ "memoffset",
+ "rustc_version",
+]
+
+[[package]]
+name = "filedescriptor"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
+dependencies = [
+ "libc",
+ "thiserror 1.0.69",
+ "winapi",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs-err"
+version = "3.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a"
+dependencies = [
+ "autocfg",
+ "tokio",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "fsevent-sys"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "gdk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkwayland-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkx11"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe"
+dependencies = [
+ "gdk",
+ "gdkx11-sys",
+ "gio",
+ "glib",
+ "libc",
+ "x11",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "libc",
+ "system-deps",
+ "x11",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 6.0.0",
+ "rand_core 0.10.1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gio"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
+dependencies = [
+ "bitflags 2.13.1",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-crate 2.0.2",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "globset"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a"
+dependencies = [
+ "atk",
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gio",
+ "glib",
+ "gtk-sys",
+ "gtk3-macros",
+ "libc",
+ "pango",
+ "pkg-config",
+]
+
+[[package]]
+name = "gtk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414"
+dependencies = [
+ "atk-sys",
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk3-macros"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap 2.14.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "harness"
+version = "0.1.0"
+dependencies = [
+ "bytes",
+ "futures",
+ "futures-util",
+ "portable-pty",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "hashlink"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "html5ever"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
+dependencies = [
+ "log",
+ "markup5ever",
+]
+
+[[package]]
+name = "http"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-range-header"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "system-configuration",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+ "windows-registry",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core 0.62.2",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ico"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
+dependencies = [
+ "byteorder",
+ "png 0.17.16",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "ignore"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b009b6744c1445efd7244084e25e498636412effb6760b55067553baa925cc7"
+dependencies = [
+ "crossbeam-deque",
+ "globset",
+ "log",
+ "memchr",
+ "regex-automata",
+ "same-file",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "image"
+version = "0.25.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "moxcms",
+ "num-traits",
+ "png 0.18.1",
+ "zune-core",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "infer"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
+dependencies = [
+ "cfb",
+]
+
+[[package]]
+name = "inotify"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
+dependencies = [
+ "bitflags 1.3.2",
+ "inotify-sys",
+ "libc",
+]
+
+[[package]]
+name = "inotify-sys"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "is-docker"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "is-wsl"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
+dependencies = [
+ "is-docker",
+ "once_cell",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "javascriptcore-rs"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
+dependencies = [
+ "bitflags 1.3.2",
+ "glib",
+ "javascriptcore-rs-sys",
+]
+
+[[package]]
+name = "javascriptcore-rs-sys"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys 0.3.1",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
+dependencies = [
+ "jni-sys 0.4.1",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
+dependencies = [
+ "getrandom 0.4.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-patch"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08"
+dependencies = [
+ "jsonptr",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "json5"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+dependencies = [
+ "pest",
+ "pest_derive",
+ "serde",
+]
+
+[[package]]
+name = "jsonptr"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "keyboard-types"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
+dependencies = [
+ "bitflags 2.13.1",
+ "serde",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "keyring"
+version = "4.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0298a59b384c540e408a600c8b375a09b49c3f97debc080e2c30675d79a6368a"
+dependencies = [
+ "apple-native-keyring-store",
+ "keyring-core",
+ "windows-native-keyring-store",
+ "zbus-secret-service-keyring-store",
+]
+
+[[package]]
+name = "keyring-core"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "kqueue"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5"
+dependencies = [
+ "kqueue-sys",
+ "libc",
+]
+
+[[package]]
+name = "kqueue-sys"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
+dependencies = [
+ "bitflags 2.13.1",
+ "libc",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libappindicator"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
+dependencies = [
+ "glib",
+ "gtk",
+ "gtk-sys",
+ "libappindicator-sys",
+ "log",
+]
+
+[[package]]
+name = "libappindicator-sys"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
+dependencies = [
+ "gtk-sys",
+ "libloading",
+ "once_cell",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libdbus-sys"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
+dependencies = [
+ "pkg-config",
+]
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "local-api"
+version = "0.1.0"
+dependencies = [
+ "async-trait",
+ "axum",
+ "axum-server",
+ "bytes",
+ "futures",
+ "futures-util",
+ "globset",
+ "harness",
+ "hyper",
+ "hyper-util",
+ "ignore",
+ "notify",
+ "rand 0.8.7",
+ "regex",
+ "reqwest 0.12.28",
+ "rusqlite",
+ "rustls",
+ "serde",
+ "serde_json",
+ "sha2",
+ "subtle",
+ "tempfile",
+ "thiserror 1.0.69",
+ "tokio",
+ "tokio-tungstenite 0.23.1",
+ "tower 0.4.13",
+ "tower-http 0.5.2",
+ "tracing",
+ "tracing-subscriber",
+ "upstream",
+ "urlencoding",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "markup5ever"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862"
+dependencies = [
+ "log",
+ "tendril",
+ "web_atoms",
+]
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "moxcms"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
+dependencies = [
+ "num-traits",
+ "pxfm",
+]
+
+[[package]]
+name = "muda"
+version = "0.19.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878"
+dependencies = [
+ "crossbeam-channel",
+ "dpi",
+ "gtk",
+ "keyboard-types",
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "once_cell",
+ "png 0.18.1",
+ "serde",
+ "thiserror 2.0.18",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.13.1",
+ "jni-sys 0.3.1",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys 0.3.1",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "nix"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
+dependencies = [
+ "bitflags 2.13.1",
+ "cfg-if",
+ "cfg_aliases 0.1.1",
+ "libc",
+]
+
+[[package]]
+name = "notify"
+version = "6.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
+dependencies = [
+ "bitflags 2.13.1",
+ "crossbeam-channel",
+ "filetime",
+ "fsevent-sys",
+ "inotify",
+ "kqueue",
+ "libc",
+ "log",
+ "mio 0.8.11",
+ "walkdir",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "objc-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+
+[[package]]
+name = "objc2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
+dependencies = [
+ "objc2-encode",
+ "objc2-exception-helper",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.5.1",
+ "libc",
+ "objc2 0.5.2",
+ "objc2-core-data 0.2.2",
+ "objc2-core-image 0.2.2",
+ "objc2-foundation 0.2.2",
+ "objc2-quartz-core 0.2.2",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.6.2",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-core-location 0.2.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-contacts"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
+dependencies = [
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
+dependencies = [
+ "bitflags 2.13.1",
+ "dispatch2",
+ "objc2 0.6.4",
+]
+
+[[package]]
+name = "objc2-core-graphics"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
+dependencies = [
+ "bitflags 2.13.1",
+ "dispatch2",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-io-surface",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
+dependencies = [
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-core-location"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-contacts",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-location"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
+dependencies = [
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-core-text"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-exception-helper"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.5.1",
+ "libc",
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.6.2",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-io-surface"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-link-presentation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-symbols"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
+dependencies = [
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-cloud-kit 0.2.2",
+ "objc2-core-data 0.2.2",
+ "objc2-core-image 0.2.2",
+ "objc2-core-location 0.2.2",
+ "objc2-foundation 0.2.2",
+ "objc2-link-presentation",
+ "objc2-quartz-core 0.2.2",
+ "objc2-symbols",
+ "objc2-uniform-type-identifiers",
+ "objc2-user-notifications 0.2.2",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.6.2",
+ "objc2 0.6.4",
+ "objc2-cloud-kit 0.3.2",
+ "objc2-core-data 0.3.2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-image 0.3.2",
+ "objc2-core-location 0.3.2",
+ "objc2-core-text",
+ "objc2-foundation 0.3.2",
+ "objc2-quartz-core 0.3.2",
+ "objc2-user-notifications 0.3.2",
+]
+
+[[package]]
+name = "objc2-uniform-type-identifiers"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-user-notifications"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-core-location 0.2.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-user-notifications"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
+dependencies = [
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-web-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68bc69301064cebefc6c4c90ce9cba69225239e4b8ff99d445a2b5563797da65"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-web-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.6.2",
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "open"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5"
+dependencies = [
+ "dunce",
+ "is-wsl",
+ "libc",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "pango"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "pem"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
+dependencies = [
+ "base64 0.22.1",
+ "serde_core",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pest"
+version = "2.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9"
+dependencies = [
+ "memchr",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210"
+dependencies = [
+ "pest",
+]
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+ "serde",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "plist"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85"
+dependencies = [
+ "base64 0.22.1",
+ "indexmap 2.14.0",
+ "quick-xml",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "png"
+version = "0.17.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "png"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+dependencies = [
+ "bitflags 2.13.1",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "portable-pty"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e"
+dependencies = [
+ "anyhow",
+ "bitflags 1.3.2",
+ "downcast-rs",
+ "filedescriptor",
+ "lazy_static",
+ "libc",
+ "log",
+ "nix",
+ "serial2",
+ "shared_library",
+ "shell-words",
+ "winapi",
+ "winreg 0.10.1",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
+dependencies = [
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit 0.25.13+spec-1.1.0",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "pxfm"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
+
+[[package]]
+name = "quick-xml"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases 0.2.2",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
+dependencies = [
+ "bytes",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand 0.10.2",
+ "rand_pcg",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases 0.2.2",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "rcgen"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
+dependencies = [
+ "pem",
+ "ring",
+ "rustls-pki-types",
+ "time",
+ "yasna",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.13.1",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "tower 0.5.3",
+ "tower-http 0.6.11",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams 0.4.2",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "sync_wrapper",
+ "tokio",
+ "tokio-util",
+ "tower 0.5.3",
+ "tower-http 0.6.11",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams 0.5.0",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rusqlite"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
+dependencies = [
+ "bitflags 2.13.1",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink",
+ "libsqlite3-sys",
+ "smallvec",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.13.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
+dependencies = [
+ "aws-lc-rs",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "aws-lc-rs",
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
+dependencies = [
+ "dyn-clone",
+ "indexmap 1.9.3",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "secret-service"
+version = "5.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a62d7f86047af0077255a29494136b9aaaf697c76ff70b8e49cded4e2623c14"
+dependencies = [
+ "aes",
+ "cbc",
+ "futures-util",
+ "generic-array",
+ "getrandom 0.2.17",
+ "hkdf",
+ "num",
+ "once_cell",
+ "serde",
+ "sha2",
+ "zbus",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags 2.13.1",
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "selectors"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
+dependencies = [
+ "bitflags 2.13.1",
+ "cssparser",
+ "derive_more",
+ "log",
+ "new_debug_unreachable",
+ "phf",
+ "phf_codegen",
+ "precomputed-hash",
+ "rustc-hash",
+ "servo_arc",
+ "smallvec",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-untagged"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
+dependencies = [
+ "base64 0.22.1",
+ "bs58",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.14.0",
+ "schemars 0.9.0",
+ "schemars 1.2.1",
+ "serde_core",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "serial2"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9eb6ea5562eeaed6936b8b54e086aa0f88b9e5b1bef45beb038e2519fa1185b1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "serialize-to-javascript"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
+dependencies = [
+ "serde",
+ "serde_json",
+ "serialize-to-javascript-impl",
+]
+
+[[package]]
+name = "serialize-to-javascript-impl"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930"
+dependencies = [
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shared_library"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
+dependencies = [
+ "lazy_static",
+ "libc",
+]
+
+[[package]]
+name = "shell-words"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
+name = "siphasher"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "softbuffer"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
+dependencies = [
+ "bytemuck",
+ "js-sys",
+ "ndk",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation 0.3.2",
+ "objc2-quartz-core 0.3.2",
+ "raw-window-handle",
+ "redox_syscall",
+ "tracing",
+ "wasm-bindgen",
+ "web-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "soup3"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
+dependencies = [
+ "futures-channel",
+ "gio",
+ "glib",
+ "libc",
+ "soup3-sys",
+]
+
+[[package]]
+name = "soup3-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "string_cache"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
+dependencies = [
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared",
+ "precomputed-hash",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "swift-rs"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7"
+dependencies = [
+ "base64 0.21.7",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
+dependencies = [
+ "bitflags 2.13.1",
+ "core-foundation 0.9.4",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck 0.5.0",
+ "pkg-config",
+ "toml 0.8.2",
+ "version-compare",
+]
+
+[[package]]
+name = "tao"
+version = "0.35.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2 0.6.2",
+ "core-foundation 0.10.1",
+ "core-graphics",
+ "crossbeam-channel",
+ "dbus",
+ "dispatch2",
+ "dlopen2",
+ "dpi",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gtk",
+ "jni",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-sys",
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "objc2-foundation 0.3.2",
+ "objc2-ui-kit 0.3.2",
+ "once_cell",
+ "parking_lot",
+ "percent-encoding",
+ "raw-window-handle",
+ "tao-macros",
+ "unicode-segmentation",
+ "url",
+ "windows",
+ "windows-core 0.61.2",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "tauri"
+version = "2.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "cookie",
+ "dirs",
+ "dunce",
+ "embed_plist",
+ "getrandom 0.3.4",
+ "glob",
+ "gtk",
+ "heck 0.5.0",
+ "http",
+ "jni",
+ "libc",
+ "log",
+ "mime",
+ "muda",
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "objc2-foundation 0.3.2",
+ "objc2-ui-kit 0.3.2",
+ "objc2-web-kit 0.3.2",
+ "percent-encoding",
+ "plist",
+ "raw-window-handle",
+ "reqwest 0.13.4",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "serialize-to-javascript",
+ "swift-rs",
+ "tauri-build",
+ "tauri-macros",
+ "tauri-runtime",
+ "tauri-runtime-wry",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "tokio",
+ "tray-icon",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "window-vibrancy",
+ "windows",
+]
+
+[[package]]
+name = "tauri-build"
+version = "2.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632"
+dependencies = [
+ "anyhow",
+ "cargo_toml",
+ "dirs",
+ "glob",
+ "heck 0.5.0",
+ "json-patch",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "tauri-winres",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-codegen"
+version = "2.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5"
+dependencies = [
+ "base64 0.22.1",
+ "brotli",
+ "ico",
+ "json-patch",
+ "plist",
+ "png 0.17.16",
+ "proc-macro2",
+ "quote",
+ "semver",
+ "serde",
+ "serde_json",
+ "sha2",
+ "syn 2.0.119",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "time",
+ "url",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-macros"
+version = "2.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "tauri-codegen",
+ "tauri-utils",
+]
+
+[[package]]
+name = "tauri-plugin"
+version = "2.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020"
+dependencies = [
+ "anyhow",
+ "glob",
+ "plist",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-plugin-mcp-bridge"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee9c726f2293f54dc9a61739886967a75c9f1701d17a3dcb8a4544f7353d4861"
+dependencies = [
+ "base64 0.22.1",
+ "block2 0.5.1",
+ "futures-util",
+ "image",
+ "jni",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+ "objc2-ui-kit 0.2.2",
+ "objc2-web-kit 0.2.2",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 1.0.69",
+ "tokio",
+ "tokio-tungstenite 0.28.0",
+ "uuid",
+ "webview2-com",
+ "windows",
+ "windows-core 0.61.2",
+]
+
+[[package]]
+name = "tauri-plugin-opener"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29"
+dependencies = [
+ "dunce",
+ "glob",
+ "objc2-app-kit 0.3.2",
+ "objc2-foundation 0.3.2",
+ "open",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.18",
+ "url",
+ "windows",
+ "zbus",
+]
+
+[[package]]
+name = "tauri-runtime"
+version = "2.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8"
+dependencies = [
+ "cookie",
+ "dpi",
+ "gtk",
+ "http",
+ "jni",
+ "objc2 0.6.4",
+ "objc2-ui-kit 0.3.2",
+ "objc2-web-kit 0.3.2",
+ "raw-window-handle",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+]
+
+[[package]]
+name = "tauri-runtime-wry"
+version = "2.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f"
+dependencies = [
+ "gtk",
+ "http",
+ "jni",
+ "log",
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "softbuffer",
+ "tao",
+ "tauri-runtime",
+ "tauri-utils",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+ "wry",
+]
+
+[[package]]
+name = "tauri-utils"
+version = "2.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887"
+dependencies = [
+ "anyhow",
+ "brotli",
+ "cargo_metadata",
+ "ctor",
+ "dom_query",
+ "dunce",
+ "glob",
+ "http",
+ "infer",
+ "json-patch",
+ "json5",
+ "log",
+ "memchr",
+ "phf",
+ "plist",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde-untagged",
+ "serde_json",
+ "serde_with",
+ "swift-rs",
+ "thiserror 2.0.18",
+ "toml 1.1.3+spec-1.1.0",
+ "url",
+ "urlpattern",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-winres"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6"
+dependencies = [
+ "dunce",
+ "embed-resource",
+ "toml 1.1.3+spec-1.1.0",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tendril"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08"
+dependencies = [
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio 1.2.2",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite 0.23.0",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite 0.24.0",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite 0.28.0",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
+dependencies = [
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "indexmap 2.14.0",
+ "serde_core",
+ "serde_spanned 1.1.1",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.15",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c"
+dependencies = [
+ "indexmap 2.14.0",
+ "serde_core",
+ "serde_spanned 1.1.1",
+ "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap 2.14.0",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
+dependencies = [
+ "indexmap 2.14.0",
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.13+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
+dependencies = [
+ "indexmap 2.14.0",
+ "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_parser",
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+dependencies = [
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project",
+ "pin-project-lite",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
+dependencies = [
+ "bitflags 2.13.1",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "http-range-header",
+ "httpdate",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags 2.13.1",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower 0.5.3",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "tray-icon"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc"
+dependencies = [
+ "crossbeam-channel",
+ "dirs",
+ "libappindicator",
+ "muda",
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation 0.3.2",
+ "once_cell",
+ "png 0.18.1",
+ "serde",
+ "thiserror 2.0.18",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "tungstenite"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand 0.8.7",
+ "sha1",
+ "thiserror 1.0.69",
+ "utf-8",
+]
+
+[[package]]
+name = "tungstenite"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand 0.8.7",
+ "sha1",
+ "thiserror 1.0.69",
+ "utf-8",
+]
+
+[[package]]
+name = "tungstenite"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
+dependencies = [
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand 0.9.5",
+ "sha1",
+ "thiserror 2.0.18",
+ "utf-8",
+]
+
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
+[[package]]
+name = "uds_windows"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-ucd-ident"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "upstream"
+version = "0.1.0"
+dependencies = [
+ "async-trait",
+ "axum",
+ "base64 0.22.1",
+ "dirs",
+ "keyring",
+ "rand 0.8.7",
+ "reqwest 0.12.28",
+ "serde",
+ "serde_json",
+ "sha2",
+ "tempfile",
+ "thiserror 1.0.69",
+ "tokio",
+ "tracing",
+ "urlencoding",
+ "uuid",
+ "wait-timeout",
+]
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
+name = "urlpattern"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
+dependencies = [
+ "regex",
+ "serde",
+ "unic-ucd-ident",
+ "url",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "uuid"
+version = "1.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
+dependencies = [
+ "getrandom 0.4.3",
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version-compare"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vswhom"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
+dependencies = [
+ "libc",
+ "vswhom-sys",
+]
+
+[[package]]
+name = "vswhom-sys"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web_atoms"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297"
+dependencies = [
+ "phf",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+]
+
+[[package]]
+name = "webkit2gtk"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-rs",
+ "gdk",
+ "gdk-sys",
+ "gio",
+ "gio-sys",
+ "glib",
+ "glib-sys",
+ "gobject-sys",
+ "gtk",
+ "gtk-sys",
+ "javascriptcore-rs",
+ "libc",
+ "once_cell",
+ "soup3",
+ "webkit2gtk-sys",
+]
+
+[[package]]
+name = "webkit2gtk-sys"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-sys-rs",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "javascriptcore-rs-sys",
+ "libc",
+ "pkg-config",
+ "soup3-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "webview2-com"
+version = "0.38.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
+dependencies = [
+ "webview2-com-macros",
+ "webview2-com-sys",
+ "windows",
+ "windows-core 0.61.2",
+ "windows-implement",
+ "windows-interface",
+]
+
+[[package]]
+name = "webview2-com-macros"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "webview2-com-sys"
+version = "0.38.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
+dependencies = [
+ "thiserror 2.0.18",
+ "windows",
+ "windows-core 0.61.2",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "window-vibrancy"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
+dependencies = [
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "raw-window-handle",
+ "windows-sys 0.59.0",
+ "windows-version",
+]
+
+[[package]]
+name = "windows"
+version = "0.61.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
+dependencies = [
+ "windows-collections",
+ "windows-core 0.61.2",
+ "windows-future",
+ "windows-link 0.1.3",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
+dependencies = [
+ "windows-core 0.61.2",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link 0.1.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link 0.2.1",
+ "windows-result 0.4.1",
+ "windows-strings 0.5.1",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-native-keyring-store"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8"
+dependencies = [
+ "byteorder",
+ "keyring-core",
+ "regex",
+ "windows-sys 0.61.2",
+ "zeroize",
+]
+
+[[package]]
+name = "windows-numerics"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-registry"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
+dependencies = [
+ "windows-link 0.2.1",
+ "windows-result 0.4.1",
+ "windows-strings 0.5.1",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-version"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winreg"
+version = "0.55.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "wry"
+version = "0.55.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514"
+dependencies = [
+ "base64 0.22.1",
+ "block2 0.6.2",
+ "cookie",
+ "crossbeam-channel",
+ "dirs",
+ "dom_query",
+ "dpi",
+ "dunce",
+ "gdkx11",
+ "gtk",
+ "http",
+ "javascriptcore-rs",
+ "jni",
+ "libc",
+ "ndk",
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "objc2-ui-kit 0.3.2",
+ "objc2-web-kit 0.3.2",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "sha2",
+ "soup3",
+ "tao-macros",
+ "thiserror 2.0.18",
+ "url",
+ "webkit2gtk",
+ "webkit2gtk-sys",
+ "webview2-com",
+ "windows",
+ "windows-core 0.61.2",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "x11"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "yasna"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
+dependencies = [
+ "time",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zbus"
+version = "5.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 1.0.4",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus-secret-service-keyring-store"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ccede190ba363386a24e8021c7f3848393976609ec9f5d1f8c6c09ef37075b4"
+dependencies = [
+ "keyring-core",
+ "secret-service",
+ "zbus",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
+dependencies = [
+ "serde",
+ "winnow 1.0.4",
+ "zvariant",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+
+[[package]]
+name = "zune-core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
+
+[[package]]
+name = "zune-jpeg"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
+dependencies = [
+ "zune-core",
+]
+
+[[package]]
+name = "zvariant"
+version = "5.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 1.0.4",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.119",
+ "winnow 1.0.4",
+]
diff --git a/apps/native/Cargo.toml b/apps/native/Cargo.toml
new file mode 100644
index 0000000000..4fc7fbcd21
--- /dev/null
+++ b/apps/native/Cargo.toml
@@ -0,0 +1,90 @@
+[workspace]
+resolver = "2"
+members = ["crates/local-api", "crates/harness", "crates/upstream", "src-tauri"]
+
+[workspace.package]
+edition = "2021"
+version = "0.1.0"
+license = "MIT"
+publish = false
+
+# Warn locally (fast inner-loop feedback); CI runs
+# `cargo clippy -- -D warnings` from apps/native/, which promotes every
+# warning below to a hard failure. Do not add `deny` here — a `deny` in the
+# manifest breaks `cargo build`/`check` too, not just `clippy`, which is
+# stricter than intended for local iteration.
+[workspace.lints.rust]
+unsafe_code = "deny"
+
+[workspace.lints.clippy]
+all = { level = "warn", priority = -1 }
+
+# --- Dependency versions, pinned once here and inherited via `.workspace = true`
+# in crates/local-api/Cargo.toml. Over-provisioned on purpose (bootstrap
+# directive): every family implementer building on this skeleton pulls from
+# this table instead of editing it — Cargo.toml is a shared file family
+# owners must not touch (see the native module-ownership contract).
+[workspace.dependencies]
+axum = { version = "0.7", features = ["ws", "macros"] }
+# Local TLS: the app terminates HTTPS on the user's own machine so its origin
+# is a secure context (Web Crypto) AND a real domain (per-sandbox cookie
+# jars). `rcgen` mints the CA/leaf on first run; nothing is shipped.
+rcgen = { version = "0.13", default-features = false, features = ["pem", "crypto", "ring"] }
+axum-server = { version = "0.7", features = ["tls-rustls"] }
+time = { version = "0.3", default-features = false, features = ["std"] }
+tokio = { version = "1", features = ["full"] }
+tower = { version = "0.4", features = ["util"] }
+tower-http = { version = "0.5", features = ["trace", "cors", "fs", "timeout"] }
+hyper = { version = "1", features = ["full"] }
+hyper-util = { version = "0.1", features = ["full", "client-legacy", "http1"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+thiserror = "1"
+rusqlite = { version = "0.31", features = ["bundled"] }
+reqwest = { version = "0.12", default-features = false, features = [
+ "json",
+ "stream",
+ "rustls-tls",
+] }
+globset = "0.4"
+walkdir = "2"
+ignore = "0.4"
+notify = "6"
+regex = "1"
+subtle = "2"
+rand = "0.8"
+futures = "0.3"
+futures-util = "0.3"
+bytes = "1"
+urlencoding = "2"
+uuid = { version = "1", features = ["v4"] }
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+async-trait = "0.1"
+# Inbound WS upgrades go through axum's own `ws` feature (above); this is for
+# a family that needs a WS *client* role (e.g. proxying an outbound upgrade).
+tokio-tungstenite = "0.23"
+tempfile = "3"
+# Phase 3 (`src-tauri/`, the Tauri shell) additions — distinct lines to
+# minimize merge friction with `crates/upstream` (built in parallel), which
+# is expected to add its own (`keyring`, etc.) below these rather than
+# interleaving.
+tauri = { version = "2", features = ["config-json5"] }
+# Dev-only MCP bridge for @hypothesi/tauri-mcp-server (registered under
+# `#[cfg(debug_assertions)]` in src-tauri/src/lib.rs — never in release).
+tauri-plugin-mcp-bridge = "0.12"
+tauri-plugin-opener = "2"
+tauri-build = { version = "2", features = ["config-json5"] }
+# `crates/upstream`'s OAuth/Keychain-specific deps (`keyring`, `sha2`,
+# `base64`, `dirs`) are declared directly in `crates/upstream/Cargo.toml`
+# instead of here, following the same "single-consumer dep stays local"
+# precedent `crates/harness/Cargo.toml` set for `portable-pty` — see that
+# crate's Cargo.toml doc comment.
+
+# Phase 2's `crates/harness` PTY spawn (see
+# the desktop migration contract) declares `portable-pty` (and
+# `libc`, for process-group signaling) directly in its OWN
+# `crates/harness/Cargo.toml` rather than here — this table stays the
+# family-agnostic dependency set every OTHER crate inherits via
+# `.workspace = true`; local-api itself never spawns a PTY directly and has
+# no reason to pull either dependency in.
diff --git a/apps/native/README.md b/apps/native/README.md
new file mode 100644
index 0000000000..dae78873fb
--- /dev/null
+++ b/apps/native/README.md
@@ -0,0 +1,153 @@
+# Studio Native
+
+Packages the existing Studio web application as a Tauri desktop app with a
+local Rust runtime.
+
+| Attribute | Value |
+| --- | --- |
+| Workspace | `@decocms/native` (`apps/native`) |
+| Kind | Tauri desktop application and local runtime |
+| Runtime | Rust, WebKit, and the bundled React application |
+| Distribution | Private workspace package; signed macOS application |
+
+## Overview
+
+Studio Native embeds the production `apps/web` UI without forking its product
+shell. An in-process Axum server serves the bundled UI and native API from one
+stable loopback origin, proxies upstream Studio requests, and intercepts the
+local-only thread, harness, sandbox, filesystem, Git, task, and preview
+surfaces.
+
+The current release targets macOS. The Rust crates and browser contracts are
+kept portable so Windows can be added without redesigning the application
+boundary.
+
+## Responsibilities
+
+- Start and supervise the local Rust API inside the Tauri process.
+- Serve the native Vite bundle and API from one `localhost` origin.
+- Authenticate local control requests with an HttpOnly `SameSite=Strict`
+ cookie.
+- Store upstream Studio sessions in the operating-system credential store.
+- Detect and run the user's Claude Code and Codex installations.
+- Persist native threads and provider session identifiers in SQLite.
+- Create, recover, start, stop, and restart local Git-backed sandboxes.
+- Retain subprocess output in bounded files and replay their tails to xterm.js.
+- Proxy sandbox previews without stripping the sandbox application's cookies or
+ authorization headers.
+
+## Usage
+
+From the repository root, install dependencies and start the native HMR loop:
+
+```bash
+bun install
+bun run --cwd=apps/native dev
+```
+
+The command starts native-mode Vite from `apps/web` on
+`http://localhost:4420`, builds and launches the Rust shell, and proxies native
+control routes to the embedded API.
+
+Build the native web bundle independently with:
+
+```bash
+bun run --cwd=apps/web build:native
+```
+
+Build the Tauri application with:
+
+```bash
+bunx tauri build --config apps/native/src-tauri/tauri.conf.json5
+```
+
+## Architecture
+
+```text
+Tauri window
+ |
+ v
+http://localhost:
+ |
+ +-- bundled apps/web UI
+ +-- local control API
+ +-- upstream Studio proxy ----> studio.decocms.com
+ +-- sandbox preview proxy ----> user dev server
+ |
+ +-- SQLite threads and sandbox registry
+ +-- Keychain session
+ +-- Claude Code / Codex processes
+```
+
+Key paths:
+
+| Path | Purpose |
+| --- | --- |
+| `src-tauri/` | Tauri shell, stable control origin, bundled assets, CSP, and native commands |
+| `crates/local-api/` | Axum control API, proxy, thread store, sandbox manager, and setup pipeline |
+| `crates/harness/` | Claude Code and Codex detection, process execution, and stream translation |
+| `crates/upstream/` | OAuth, token refresh, Keychain access, and upstream session handling |
+| `e2e/` | Black-box native API, sandbox, provider-resume, and recovery tests |
+| `scripts/` | Dev signing, boot smoke, and release helpers |
+
+The packaged UI is built by `apps/web`; the hosted Hono backend remains in
+`apps/api`. Browser-safe contracts shared by both live in `@decocms/shared`.
+
+## Development
+
+Run the focused verification gates from the repository root:
+
+```bash
+bun run --cwd=apps/web check
+bun run --cwd=apps/api check
+bun run --cwd=apps/native check
+cd apps/native && cargo fmt --check
+cd apps/native && cargo clippy --workspace --all-targets -- -D warnings
+cd apps/native && cargo test --workspace
+bun run --cwd=apps/native smoke:boot
+```
+
+### Dev sessions & the Keychain
+
+On each macOS development machine, create the stable self-signed development
+identity once:
+
+```bash
+bun run --cwd=apps/native dev:signing:setup
+```
+
+The Cargo runner signs each development binary with that identity. This gives
+Keychain entries a stable designated requirement across Rust rebuilds, so a
+saved Studio session remains readable. The shipped app continues to use its
+release signing identity; there is no filesystem token-store fallback.
+
+### Verification matrix
+
+The native E2E suite can run against the built `local-api` binary through
+`LOCAL_API_E2E_CMD`. Provider-resume tests use deterministic Claude Code and
+Codex fixtures to prove that the persisted provider session ID is reused and
+only the newest user message is sent when a run resumes.
+
+Run `bun run fmt` after changes.
+
+## Boundaries
+
+- Keep product UI and user-facing behavior in `apps/web`; native code should
+ intercept transport and local capabilities rather than create a second UI.
+- Keep hosted Hono routes and server persistence in `apps/api`.
+- Keep browser-safe wire contracts in `@decocms/shared`; Rust mirrors those
+ contracts at the process boundary.
+- Never expose upstream access or refresh tokens to webview JavaScript.
+- Do not authorize sandbox preview traffic with the control cookie or rewrite
+ its application cookies. Preview requests are a transparent, dedicated
+ proxy surface.
+- Sandboxes may persist identity and logs, but child processes are always
+ re-established after an application restart rather than orphaned.
+- Subprocess logs are file-backed and bounded; memory holds only live fan-out.
+
+## Related documentation
+
+- [Studio Web](../web/README.md)
+- [Studio API](../api/README.md)
+- [Repository guidelines](../../AGENTS.md)
+- [Testing strategy](../../TESTING.md)
diff --git a/apps/native/crates/harness/Cargo.toml b/apps/native/crates/harness/Cargo.toml
new file mode 100644
index 0000000000..8b9dcf2220
--- /dev/null
+++ b/apps/native/crates/harness/Cargo.toml
@@ -0,0 +1,47 @@
+[package]
+name = "harness"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+publish.workspace = true
+
+[lints]
+workspace = true
+
+[dependencies]
+# Shared workspace-pinned deps (see apps/native/Cargo.toml's
+# `[workspace.dependencies]` table) — consumed via `.workspace = true`,
+# never edited here.
+serde = { workspace = true }
+serde_json = { workspace = true }
+tokio = { workspace = true }
+tracing = { workspace = true }
+futures = { workspace = true }
+futures-util = { workspace = true }
+bytes = { workspace = true }
+
+# Crate-local dependency, NOT in the shared workspace table (see that
+# table's trailing comment) — declared directly here since this crate is
+# its only consumer.
+#
+# `portable-pty` — wezterm's PTY crate (see the native implementation contract
+# section 4 for the full "why portable-pty over node-pty" writeup): spawns
+# the child via `std::process::Command` from inside our own signed binary
+# (no separate `spawn-helper` executable to lose its execute bit or fail
+# Gatekeeper), used for the PTY-backed half of `spawn::spawn` — see that
+# module's doc comment for why it is NOT the path harness dispatch
+# actually takes (plain pipes are; PTY is implemented for completeness /
+# any future TTY-requiring CLI, per the Phase 2 TODO's explicit ask).
+#
+# No `libc`/`nix` dependency: the workspace denies `unsafe_code` (see
+# `apps/native/Cargo.toml`'s `[workspace.lints.rust]`), and every raw
+# `kill(2)`/`setpgid(2)` FFI call requires `unsafe`. Process-GROUP
+# signaling (`spawn.rs::kill_process_group`) instead shells out to the
+# `kill(1)` binary (`kill -TERM -`) — POSIX-portable, zero `unsafe`,
+# no extra dependency. Process-group CREATION uses
+# `std::os::unix::process::CommandExt::process_group`, which is a safe,
+# stable std API (since Rust 1.64) — no FFI needed for that half either.
+portable-pty = "0.9"
+
+[dev-dependencies]
+tempfile = { workspace = true }
diff --git a/apps/native/crates/harness/src/detect.rs b/apps/native/crates/harness/src/detect.rs
new file mode 100644
index 0000000000..c855ae3fca
--- /dev/null
+++ b/apps/native/crates/harness/src/detect.rs
@@ -0,0 +1,418 @@
+//! CLI availability detection — GROW-ONLY cache mirroring
+//! `apps/api/src/link-daemon/capabilities.ts`'s semantics EXACTLY:
+//! `mergeProbedCapabilities` there never un-advertises a capability once
+//! seen ("a transient probe failure (CLI busy, SDK hiccup) must never
+//! un-advertise a capability mid-run — dispatch routing would start
+//! bouncing threads that are actively streaming"). This module ports that
+//! same invariant to Rust: once a harness flips to `true`, no later probe
+//! — however it fails — can flip it back.
+//!
+//! "Detected" means the CLI is installed AND has a confirmed logged-in
+//! session — FAIL-CLOSED: an installed-but-not-logged-in CLI is not usable,
+//! so it does NOT count as detected. `probe()` requires BOTH a successful
+//! `--version` probe (installed) AND a passing auth probe (logged in):
+//! - claude via ` auth status --json`'s `"loggedIn":true` (mirrors
+//! `capabilities.ts::detectClaudeCode`'s account-email check, adapted
+//! since this crate drives the CLI's own surface rather than the
+//! `@anthropic-ai/claude-agent-sdk`),
+//! - codex via ` login status`'s "Logged in" text — which the codex
+//! CLI prints to STDERR with an empty stdout, so the auth probe merges
+//! stdout+stderr before parsing (see `run_probe_capture`).
+//!
+//! A failed/unrecognized/unparseable auth check keeps `detected` at
+//! `false` (logged as a diagnostic). The grow-only cache invariant above
+//! still holds: once a confirmed-logged-in probe flips a harness to
+//! `true`, no later probe flips it back — but a CLI never reaches `true`
+//! in the first place until login is confirmed.
+//!
+//! Because detection is now auth-gated, the SHARED CONTRACT surface
+//! `apps/native/e2e/fixtures/stub-harness.mjs` answers the auth probe as
+//! logged-in too (claude: `{"loggedIn":true}` on stdout for `auth status
+//! --json`; codex: "Logged in using ChatGPT" on stderr for `login status`)
+//! in ADDITION to `--version`, so the differential/parity tests still see
+//! it as detected. Both probes are independently swappable via
+//! [`resolve::resolve_argv`] (the SHARED CONTRACT env overrides).
+
+use std::sync::OnceLock;
+use std::time::Duration;
+
+use tokio::sync::RwLock;
+
+use crate::resolve::{self, HarnessId};
+
+/// Wall-clock cap on a single probe subprocess (version OR auth check).
+/// Generous for a binary that's actually installed and responsive, bounded
+/// so a hung/misbehaving binary on someone's PATH can't wedge detection.
+pub const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
+/// How often the background task re-probes while at least one harness is
+/// still undetected. Matches the order of magnitude of
+/// `capabilities.ts::startCapabilityReprobe`'s default (60s).
+pub const REFRESH_INTERVAL: Duration = Duration::from_secs(60);
+
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
+pub struct Detection {
+ pub claude_code: bool,
+ pub codex: bool,
+}
+
+impl Detection {
+ pub fn get(self, harness: HarnessId) -> bool {
+ match harness {
+ HarnessId::ClaudeCode => self.claude_code,
+ HarnessId::Codex => self.codex,
+ }
+ }
+
+ fn set(&mut self, harness: HarnessId, value: bool) {
+ match harness {
+ HarnessId::ClaudeCode => self.claude_code = value,
+ HarnessId::Codex => self.codex = value,
+ }
+ }
+
+ fn all_detected(self) -> bool {
+ self.claude_code && self.codex
+ }
+
+ /// Grow-only merge: `self ||= probed`, per-field. Never flips a `true`
+ /// back to `false`. Returns whether anything NEW became detected.
+ fn merge(&mut self, probed: Detection) -> bool {
+ let mut grew = false;
+ for harness in HarnessId::ALL {
+ if !self.get(harness) && probed.get(harness) {
+ self.set(harness, true);
+ grew = true;
+ }
+ }
+ grew
+ }
+}
+
+static CACHE: OnceLock> = OnceLock::new();
+static REFRESH_STARTED: OnceLock<()> = OnceLock::new();
+
+fn cache_lock() -> &'static RwLock {
+ CACHE.get_or_init(|| RwLock::new(Detection::default()))
+}
+
+/// Read the cache without probing. `Detection::default()` (nothing
+/// detected) if nothing has probed yet.
+pub async fn cached() -> Detection {
+ *cache_lock().read().await
+}
+
+/// Ensures the cache has been populated at least once, paying for a
+/// bounded probe pass on the FIRST call (cold start) and starting the
+/// background re-probe loop as a side effect. Every call after the first
+/// reads the warm cache instantly — a request never blocks on a probe it
+/// didn't cause. Concurrent cold-start callers all pay for their own probe
+/// (idempotent — the grow-only merge makes redundant probing harmless,
+/// just slightly wasteful), same tradeoff the Phase 1 `routes/models.rs`
+/// bootstrap already made.
+pub async fn ensure_detected() -> Detection {
+ let current = cached().await;
+ if current.all_detected() {
+ start_background_reprobe();
+ return current;
+ }
+ let probed = probe_missing(current).await;
+ let merged = {
+ let mut guard = cache_lock().write().await;
+ guard.merge(probed);
+ *guard
+ };
+ start_background_reprobe();
+ merged
+}
+
+/// Starts the periodic re-probe loop exactly once per process. A no-op
+/// once every harness is detected (matches `capabilities.ts`'s "probing
+/// stops being useful once every CLI capability is present" comment) —
+/// each tick still fires, but `probe_missing` skips a harness already
+/// `true`, so a fully-detected process settles into a near-free timer.
+fn start_background_reprobe() {
+ if REFRESH_STARTED.set(()).is_err() {
+ return;
+ }
+ tokio::spawn(async move {
+ loop {
+ tokio::time::sleep(REFRESH_INTERVAL).await;
+ let current = cached().await;
+ if current.all_detected() {
+ continue;
+ }
+ let probed = probe_missing(current).await;
+ let mut guard = cache_lock().write().await;
+ guard.merge(probed);
+ }
+ });
+}
+
+/// Probes only the harnesses NOT already `true` in `current` — matches
+/// `capabilities.ts::missingCliCapabilities` gating re-probes to what's
+/// actually missing, so a confirmed-detected CLI never pays for another
+/// probe subprocess.
+async fn probe_missing(current: Detection) -> Detection {
+ let (claude_code, codex) = tokio::join!(
+ async {
+ if current.claude_code {
+ true
+ } else {
+ probe(HarnessId::ClaudeCode).await
+ }
+ },
+ async {
+ if current.codex {
+ true
+ } else {
+ probe(HarnessId::Codex).await
+ }
+ },
+ );
+ Detection { claude_code, codex }
+}
+
+async fn probe(harness: HarnessId) -> bool {
+ // Fail-CLOSED: detected == installed AND logged in. Short-circuit the
+ // auth probe when `--version` already failed (nothing to be logged in
+ // to).
+ if !version_ok(harness).await {
+ return false;
+ }
+ if !auth_ok(harness).await {
+ tracing::debug!(
+ harness = harness.wire_id(),
+ "CLI answered --version but its auth-status probe did not confirm a logged-in session \
+ (not logged in, or the auth surface is unrecognized/unsupported) — NOT counted as \
+ detected (fail-closed: an installed-but-not-logged-in CLI is not usable)"
+ );
+ return false;
+ }
+ true
+}
+
+async fn version_ok(harness: HarnessId) -> bool {
+ let Ok(argv) = resolve::resolve_argv(harness) else {
+ return false;
+ };
+ run_probe_capture(&argv, &["--version"], PROBE_TIMEOUT)
+ .await
+ .is_some()
+}
+
+async fn auth_ok(harness: HarnessId) -> bool {
+ let Ok(argv) = resolve::resolve_argv(harness) else {
+ return false;
+ };
+ match harness {
+ HarnessId::ClaudeCode => {
+ let Some(captured) =
+ run_probe_capture(&argv, &["auth", "status", "--json"], PROBE_TIMEOUT).await
+ else {
+ return false;
+ };
+ claude_auth_status_indicates_logged_in(&captured)
+ }
+ HarnessId::Codex => {
+ let Some(captured) =
+ run_probe_capture(&argv, &["login", "status"], PROBE_TIMEOUT).await
+ else {
+ return false;
+ };
+ codex_login_status_indicates_logged_in(&captured)
+ }
+ }
+}
+
+/// Parses `claude auth status --json`'s output. Pure (no subprocess) so it
+/// is unit-tested directly against captured output rather than requiring
+/// a real login/logout cycle in CI. `text` is the MERGED stdout+stderr the
+/// probe captured, so rather than parsing the whole capture (which fails
+/// the moment any stderr noise precedes/follows the JSON), extract the
+/// JSON object — first `{` through last `}` — and parse `loggedIn` from
+/// that substring. A clean stdout-only JSON still parses (the whole string
+/// is the object).
+fn claude_auth_status_indicates_logged_in(text: &str) -> bool {
+ let (Some(start), Some(end)) = (text.find('{'), text.rfind('}')) else {
+ return false;
+ };
+ if end < start {
+ return false;
+ }
+ serde_json::from_str::(&text[start..=end])
+ .ok()
+ .and_then(|v| v.get("loggedIn").and_then(serde_json::Value::as_bool))
+ .unwrap_or(false)
+}
+
+/// Parses `codex login status`'s plain-text output ("Logged in using
+/// ChatGPT" / "Logged in using an API key - ..." vs "Not logged in" — the
+/// codex CLI has no `--json` flag for this subcommand, and prints this line
+/// to STDERR). Pure for the same reason as the claude parser above. `text`
+/// is the MERGED stdout+stderr, so check whether ANY line/segment is
+/// "Logged in" (capital L) — true for "Logged in using ChatGPT" even when
+/// it arrives via stderr after (empty) stdout, false for "Not logged in".
+fn codex_login_status_indicates_logged_in(text: &str) -> bool {
+ text.lines()
+ .any(|line| line.trim_start().starts_with("Logged in"))
+}
+
+/// Spawn `` with stdin closed (never let a probe hang reading
+/// stdin — see `run.rs`'s module doc for why every harness spawn does
+/// this), capture stdout AND stderr merged (stdout first, then stderr),
+/// bounded by `timeout`. The auth probe needs the merge because codex
+/// `login status` prints "Logged in ..." to stderr with an empty stdout;
+/// merging is harmless for the `--version` probe (which only checks
+/// success). `None` on spawn failure, non-success exit, or timeout;
+/// non-UTF8-safe-decode issues are tolerated via lossy conversion, and the
+/// child is best-effort killed on timeout so it doesn't leak as a zombie.
+async fn run_probe_capture(argv: &[String], args: &[&str], timeout: Duration) -> Option {
+ let (program, rest) = argv.split_first()?;
+ let mut cmd = tokio::process::Command::new(program);
+ cmd.args(rest);
+ cmd.args(args);
+ cmd.stdin(std::process::Stdio::null());
+ cmd.stdout(std::process::Stdio::piped());
+ cmd.stderr(std::process::Stdio::piped());
+ let mut child = cmd.spawn().ok()?;
+ let mut stdout = child.stdout.take()?;
+ let mut stderr = child.stderr.take()?;
+
+ let read_and_wait = async {
+ use tokio::io::AsyncReadExt;
+ let mut out = Vec::new();
+ let mut err = Vec::new();
+ // Drain BOTH pipes CONCURRENTLY: a child that fills its stderr
+ // pipe buffer while we're blocked reading stdout to EOF (or the
+ // reverse) would deadlock if we drained them sequentially.
+ let _ = tokio::join!(stdout.read_to_end(&mut out), stderr.read_to_end(&mut err));
+ let status = child.wait().await;
+ (out, err, status)
+ };
+
+ match tokio::time::timeout(timeout, read_and_wait).await {
+ Ok((mut out, err, Ok(status))) if status.success() => {
+ out.extend_from_slice(&err);
+ Some(String::from_utf8_lossy(&out).into_owned())
+ }
+ Ok(_) => None,
+ Err(_) => {
+ // Timed out — best-effort reap so we don't leak a zombie. The
+ // owning `Command`/`Child` was moved into `read_and_wait`'s
+ // future, which was dropped by the timeout; nothing left to
+ // kill from here on most platforms (drop already sends
+ // SIGKILL for a tokio::process::Child with kill_on_drop, the
+ // default). Nothing further to do.
+ None
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn detection_merge_is_grow_only() {
+ let mut current = Detection {
+ claude_code: true,
+ codex: false,
+ };
+ let probed_a_failure = Detection {
+ claude_code: false,
+ codex: false,
+ };
+ let grew = current.merge(probed_a_failure);
+ assert!(!grew);
+ assert!(
+ current.claude_code,
+ "a transient probe failure must never un-list a previously-detected CLI"
+ );
+ }
+
+ #[test]
+ fn detection_merge_reports_growth_when_something_new_is_detected() {
+ let mut current = Detection::default();
+ let probed = Detection {
+ claude_code: true,
+ codex: false,
+ };
+ let grew = current.merge(probed);
+ assert!(grew);
+ assert!(current.claude_code);
+ assert!(!current.codex);
+ }
+
+ #[test]
+ fn detection_merge_of_fully_detected_is_a_no_op() {
+ let mut current = Detection {
+ claude_code: true,
+ codex: true,
+ };
+ let grew = current.merge(Detection {
+ claude_code: true,
+ codex: true,
+ });
+ assert!(!grew);
+ assert!(current.all_detected());
+ }
+
+ #[test]
+ fn claude_auth_status_parses_logged_in_true() {
+ let sample = r#"{"loggedIn":true,"authMethod":"claude.ai","email":"a@b.com"}"#;
+ assert!(claude_auth_status_indicates_logged_in(sample));
+ }
+
+ #[test]
+ fn claude_auth_status_parses_logged_in_false() {
+ let sample = r#"{"loggedIn":false}"#;
+ assert!(!claude_auth_status_indicates_logged_in(sample));
+ }
+
+ #[test]
+ fn claude_auth_status_treats_garbage_as_not_logged_in() {
+ assert!(!claude_auth_status_indicates_logged_in("not json at all"));
+ assert!(!claude_auth_status_indicates_logged_in(""));
+ }
+
+ #[test]
+ fn claude_auth_status_parses_json_embedded_in_merged_stderr_noise() {
+ // Merged stdout+stderr: a stderr warning line before the JSON must
+ // not break parsing (the whole-capture `from_str` would have).
+ let merged = "warning: config deprecated\n{\"loggedIn\":true,\"email\":\"a@b.com\"}\n";
+ assert!(claude_auth_status_indicates_logged_in(merged));
+ let merged_false = "some noise\n{\"loggedIn\":false}\ntrailing noise\n";
+ assert!(!claude_auth_status_indicates_logged_in(merged_false));
+ }
+
+ #[test]
+ fn codex_login_status_recognizes_logged_in_variants() {
+ assert!(codex_login_status_indicates_logged_in(
+ "Logged in using ChatGPT"
+ ));
+ assert!(codex_login_status_indicates_logged_in(
+ "Logged in using an API key - sk-...\n"
+ ));
+ assert!(codex_login_status_indicates_logged_in(
+ " Logged in using ChatGPT"
+ ));
+ }
+
+ #[test]
+ fn codex_login_status_recognizes_logged_in_delivered_via_stderr() {
+ // codex prints "Logged in ..." to STDERR with an empty stdout; the
+ // probe merges stdout (empty) + stderr, so the parser sees the line
+ // after a leading empty-stdout segment (and possibly other stderr
+ // lines). This is the regression this whole change fixes.
+ let merged = "\nLogged in using ChatGPT\n";
+ assert!(codex_login_status_indicates_logged_in(merged));
+ let merged_with_noise = "codex-cli 1.2.3\nLogged in using ChatGPT\n";
+ assert!(codex_login_status_indicates_logged_in(merged_with_noise));
+ }
+
+ #[test]
+ fn codex_login_status_recognizes_not_logged_in() {
+ assert!(!codex_login_status_indicates_logged_in("Not logged in"));
+ assert!(!codex_login_status_indicates_logged_in(""));
+ }
+}
diff --git a/apps/native/crates/harness/src/events/claude.rs b/apps/native/crates/harness/src/events/claude.rs
new file mode 100644
index 0000000000..69d3e79a50
--- /dev/null
+++ b/apps/native/crates/harness/src/events/claude.rs
@@ -0,0 +1,1125 @@
+//! `claude -p --output-format stream-json --include-partial-messages --verbose` ndjson →
+//! `UIMessageChunk` mapping.
+//!
+//! Claude emits each partial content block twice: incremental
+//! `stream_event.event.content_block_*` frames, then a complete
+//! `assistant.message.content[]` snapshot immediately before that block's
+//! `content_block_stop`. The mapper streams the partial frames and uses
+//! the snapshot only to append a missing suffix or finalize tool input.
+//! It retains the complete-frame path as a fallback for older CLIs and
+//! the deterministic stub, which emit no partial frames.
+//! This ordering was re-verified live with Claude Code 2.1.218 on
+//! 2026-07-23; `fixtures/claude_partial.jsonl` is a trimmed deterministic
+//! representation of that capture.
+//!
+//! Event mapping:
+//! - `system.init` → [`crate::events::MappedEvent::SessionId`] (captured,
+//! no chunk) + one `{"type":"start"}` chunk.
+//! - `stream_event.content_block_*` → incremental text, reasoning, and
+//! tool-input chunks with stable per-block ids.
+//! - `assistant.message.content[]` → suffix/finalization for a matching
+//! partial block, or a complete one-shot block when no partial exists.
+//! - `user.message.content[]` (`tool_result` items) →
+//! `tool-output-available` / `tool-output-error`.
+//! - `result`, `is_error:true` → `MappedEvent::FatalError` (message from
+//! `result.result` if present, else the first `result.errors[]` entry).
+//! - `result`, `is_error:false` → one `finish` chunk carrying
+//! usage/cost/session metadata.
+//! - unknown events and unparseable lines → ignored.
+
+use std::collections::BTreeMap;
+
+use serde_json::{json, Value};
+
+use super::{chunk, EventMapper, FlushReason, MappedEvent};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum PartialBlockKind {
+ Text,
+ Reasoning,
+ Tool,
+}
+
+#[derive(Debug, Default)]
+struct BlockStatus {
+ stopped: bool,
+ snapshot_seen: bool,
+ finalized: bool,
+}
+
+#[derive(Debug)]
+enum PartialBlock {
+ Text {
+ id: String,
+ emitted: String,
+ status: BlockStatus,
+ },
+ Reasoning {
+ id: String,
+ emitted: String,
+ status: BlockStatus,
+ },
+ Tool {
+ tool_call_id: String,
+ tool_name: String,
+ input_text: String,
+ final_input: Option,
+ status: BlockStatus,
+ },
+}
+
+impl PartialBlock {
+ fn kind(&self) -> PartialBlockKind {
+ match self {
+ Self::Text { .. } => PartialBlockKind::Text,
+ Self::Reasoning { .. } => PartialBlockKind::Reasoning,
+ Self::Tool { .. } => PartialBlockKind::Tool,
+ }
+ }
+
+ fn status(&self) -> &BlockStatus {
+ match self {
+ Self::Text { status, .. }
+ | Self::Reasoning { status, .. }
+ | Self::Tool { status, .. } => status,
+ }
+ }
+
+ fn status_mut(&mut self) -> &mut BlockStatus {
+ match self {
+ Self::Text { status, .. }
+ | Self::Reasoning { status, .. }
+ | Self::Tool { status, .. } => status,
+ }
+ }
+
+ fn matches_snapshot(&self, block: &Value) -> bool {
+ match self {
+ Self::Text { emitted, .. } => {
+ let Some(text) = block.get("text").and_then(Value::as_str) else {
+ return false;
+ };
+ block.get("type").and_then(Value::as_str) == Some("text")
+ && (text.starts_with(emitted) || emitted.starts_with(text))
+ }
+ Self::Reasoning { emitted, .. } => {
+ let Some(thinking) = block.get("thinking").and_then(Value::as_str) else {
+ return false;
+ };
+ block.get("type").and_then(Value::as_str) == Some("thinking")
+ && (thinking.starts_with(emitted) || emitted.starts_with(thinking))
+ }
+ Self::Tool {
+ tool_call_id,
+ tool_name,
+ ..
+ } => {
+ if block.get("type").and_then(Value::as_str) != Some("tool_use") {
+ return false;
+ }
+ let snapshot_id = block.get("id").and_then(Value::as_str).unwrap_or("");
+ let snapshot_name = block.get("name").and_then(Value::as_str).unwrap_or("");
+ (!snapshot_id.is_empty() && snapshot_id == tool_call_id)
+ || (snapshot_id.is_empty()
+ && !snapshot_name.is_empty()
+ && snapshot_name == tool_name)
+ }
+ }
+ }
+
+ fn apply_delta(&mut self, delta: &Value) -> Vec {
+ match self {
+ Self::Text { id, emitted, .. }
+ if delta.get("type").and_then(Value::as_str) == Some("text_delta") =>
+ {
+ let text = delta.get("text").and_then(Value::as_str).unwrap_or("");
+ emitted.push_str(text);
+ if text.is_empty() {
+ vec![]
+ } else {
+ vec![chunk(
+ json!({"type": "text-delta", "id": id, "delta": text}),
+ )]
+ }
+ }
+ Self::Reasoning { id, emitted, .. }
+ if delta.get("type").and_then(Value::as_str) == Some("thinking_delta") =>
+ {
+ let thinking = delta.get("thinking").and_then(Value::as_str).unwrap_or("");
+ emitted.push_str(thinking);
+ if thinking.is_empty() {
+ vec![]
+ } else {
+ vec![chunk(
+ json!({"type": "reasoning-delta", "id": id, "delta": thinking}),
+ )]
+ }
+ }
+ Self::Tool {
+ tool_call_id,
+ input_text,
+ ..
+ } if delta.get("type").and_then(Value::as_str) == Some("input_json_delta") => {
+ let partial = delta
+ .get("partial_json")
+ .and_then(Value::as_str)
+ .unwrap_or("");
+ input_text.push_str(partial);
+ if partial.is_empty() {
+ vec![]
+ } else {
+ vec![chunk(json!({
+ "type": "tool-input-delta",
+ "toolCallId": tool_call_id,
+ "inputTextDelta": partial,
+ }))]
+ }
+ }
+ _ => vec![],
+ }
+ }
+
+ fn apply_snapshot(&mut self, block: &Value) -> Vec {
+ let mut out = Vec::new();
+ match self {
+ Self::Text {
+ id,
+ emitted,
+ status,
+ } => {
+ let snapshot = block.get("text").and_then(Value::as_str).unwrap_or("");
+ append_missing_suffix(&mut out, "text-delta", id, emitted, snapshot);
+ status.snapshot_seen = true;
+ }
+ Self::Reasoning {
+ id,
+ emitted,
+ status,
+ } => {
+ let snapshot = block.get("thinking").and_then(Value::as_str).unwrap_or("");
+ append_missing_suffix(&mut out, "reasoning-delta", id, emitted, snapshot);
+ status.snapshot_seen = true;
+ }
+ Self::Tool {
+ tool_call_id,
+ input_text,
+ final_input,
+ status,
+ ..
+ } => {
+ let input = block.get("input").cloned().unwrap_or_else(|| json!({}));
+ let snapshot_text = serde_json::to_string(&input).unwrap_or_default();
+ if let Some(suffix) = snapshot_text.strip_prefix(input_text.as_str()) {
+ if !suffix.is_empty() {
+ input_text.push_str(suffix);
+ out.push(chunk(json!({
+ "type": "tool-input-delta",
+ "toolCallId": tool_call_id,
+ "inputTextDelta": suffix,
+ })));
+ }
+ }
+ *final_input = Some(input);
+ status.snapshot_seen = true;
+ }
+ }
+ out
+ }
+
+ fn mark_stopped(&mut self) {
+ self.status_mut().stopped = true;
+ }
+
+ fn should_finalize(&self) -> bool {
+ let status = self.status();
+ status.stopped && status.snapshot_seen
+ }
+
+ fn finalize(&mut self) -> Vec {
+ if self.status().finalized {
+ return vec![];
+ }
+ self.status_mut().finalized = true;
+ match self {
+ Self::Text { id, .. } => vec![chunk(json!({"type": "text-end", "id": id}))],
+ Self::Reasoning { id, .. } => {
+ vec![chunk(json!({"type": "reasoning-end", "id": id}))]
+ }
+ Self::Tool {
+ tool_call_id,
+ tool_name,
+ input_text,
+ final_input,
+ ..
+ } => {
+ if let Some(input) = final_input.clone() {
+ vec![chunk(json!({
+ "type": "tool-input-available",
+ "toolCallId": tool_call_id,
+ "toolName": tool_name,
+ "input": input,
+ }))]
+ } else {
+ let input = serde_json::from_str(input_text)
+ .unwrap_or_else(|_| Value::String(input_text.clone()));
+ vec![chunk(json!({
+ "type": "tool-input-error",
+ "toolCallId": tool_call_id,
+ "toolName": tool_name,
+ "input": input,
+ "errorText": "Claude's stream ended before the tool input was complete",
+ }))]
+ }
+ }
+ }
+ }
+}
+
+fn append_missing_suffix(
+ out: &mut Vec,
+ chunk_type: &str,
+ id: &str,
+ emitted: &mut String,
+ snapshot: &str,
+) {
+ let Some(suffix) = snapshot.strip_prefix(emitted.as_str()) else {
+ return;
+ };
+ if suffix.is_empty() {
+ return;
+ }
+ emitted.push_str(suffix);
+ out.push(chunk(json!({
+ "type": chunk_type,
+ "id": id,
+ "delta": suffix,
+ })));
+}
+
+#[derive(Debug, Default)]
+pub struct ClaudeEventMapper {
+ session_id: Option,
+ started: bool,
+ next_id: u64,
+ partial_blocks: BTreeMap,
+ current_block_index: Option,
+}
+
+impl ClaudeEventMapper {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ fn next_part_id(&mut self) -> String {
+ let id = self.next_id;
+ self.next_id += 1;
+ id.to_string()
+ }
+
+ fn handle_top_level(&mut self, value: &Value) -> Vec {
+ match value.get("type").and_then(Value::as_str) {
+ Some("system") => self.handle_system(value),
+ Some("assistant") => self.handle_assistant(value),
+ Some("user") => self.handle_user(value),
+ Some("result") => self.handle_result(value),
+ Some("stream_event") => self.handle_stream_event(value),
+ _ => vec![],
+ }
+ }
+
+ fn handle_system(&mut self, value: &Value) -> Vec {
+ if value.get("subtype").and_then(Value::as_str) != Some("init") {
+ return vec![];
+ }
+ let mut out = Vec::new();
+ if !self.started {
+ self.started = true;
+ out.push(chunk(json!({"type": "start"})));
+ }
+ if let Some(sid) = value.get("session_id").and_then(Value::as_str) {
+ self.session_id = Some(sid.to_string());
+ out.push(MappedEvent::SessionId(sid.to_string()));
+ }
+ out
+ }
+
+ fn handle_assistant(&mut self, value: &Value) -> Vec {
+ let Some(content) = value.pointer("/message/content").and_then(Value::as_array) else {
+ return vec![];
+ };
+ let mut out = Vec::new();
+ for block in content {
+ let Some(index) = self.matching_partial_index(block) else {
+ out.extend(self.emit_complete_block(block));
+ continue;
+ };
+ let Some(partial) = self.partial_blocks.get_mut(&index) else {
+ continue;
+ };
+ out.extend(partial.apply_snapshot(block));
+ if partial.should_finalize() {
+ out.extend(partial.finalize());
+ }
+ }
+ out
+ }
+
+ fn emit_complete_block(&mut self, block: &Value) -> Vec {
+ match block.get("type").and_then(Value::as_str) {
+ Some("text") => {
+ let text = block.get("text").and_then(Value::as_str).unwrap_or("");
+ let id = self.next_part_id();
+ vec![
+ chunk(json!({"type": "text-start", "id": id})),
+ chunk(json!({"type": "text-delta", "id": id, "delta": text})),
+ chunk(json!({"type": "text-end", "id": id})),
+ ]
+ }
+ Some("thinking") => {
+ let text = block.get("thinking").and_then(Value::as_str).unwrap_or("");
+ let id = self.next_part_id();
+ vec![
+ chunk(json!({"type": "reasoning-start", "id": id})),
+ chunk(json!({"type": "reasoning-delta", "id": id, "delta": text})),
+ chunk(json!({"type": "reasoning-end", "id": id})),
+ ]
+ }
+ Some("tool_use") => {
+ let tool_call_id = block
+ .get("id")
+ .and_then(Value::as_str)
+ .unwrap_or("")
+ .to_string();
+ let tool_name = block
+ .get("name")
+ .and_then(Value::as_str)
+ .unwrap_or("")
+ .to_string();
+ let input = block.get("input").cloned().unwrap_or_else(|| json!({}));
+ let input_text = serde_json::to_string(&input).unwrap_or_default();
+ vec![
+ chunk(json!({
+ "type": "tool-input-start",
+ "toolCallId": tool_call_id,
+ "toolName": tool_name,
+ })),
+ chunk(json!({
+ "type": "tool-input-delta",
+ "toolCallId": tool_call_id,
+ "inputTextDelta": input_text,
+ })),
+ chunk(json!({
+ "type": "tool-input-available",
+ "toolCallId": tool_call_id,
+ "toolName": tool_name,
+ "input": input,
+ })),
+ ]
+ }
+ // `redacted_thinking`/any future block type: dropped rather
+ // than guessed at.
+ _ => vec![],
+ }
+ }
+
+ fn matching_partial_index(&self, block: &Value) -> Option {
+ let kind = match block.get("type").and_then(Value::as_str) {
+ Some("text") => PartialBlockKind::Text,
+ Some("thinking") => PartialBlockKind::Reasoning,
+ Some("tool_use") => PartialBlockKind::Tool,
+ _ => return None,
+ };
+
+ if let Some(index) = self.current_block_index {
+ if self
+ .partial_blocks
+ .get(&index)
+ .is_some_and(|partial| partial.kind() == kind && partial.matches_snapshot(block))
+ {
+ return Some(index);
+ }
+ }
+
+ if let Some((index, _)) = self
+ .partial_blocks
+ .iter()
+ .rev()
+ .find(|(_, partial)| partial.kind() == kind && partial.matches_snapshot(block))
+ {
+ return Some(*index);
+ }
+
+ let mut unmatched = self
+ .partial_blocks
+ .iter()
+ .filter(|(_, partial)| partial.kind() == kind && !partial.status().snapshot_seen);
+ let (index, _) = unmatched.next()?;
+ if unmatched.next().is_none() {
+ Some(*index)
+ } else {
+ None
+ }
+ }
+
+ fn handle_stream_event(&mut self, value: &Value) -> Vec {
+ let Some(event) = value.get("event") else {
+ return vec![];
+ };
+ match event.get("type").and_then(Value::as_str) {
+ Some("message_start") => self.finish_partial_message(),
+ Some("content_block_start") => self.handle_content_block_start(event),
+ Some("content_block_delta") => self.handle_content_block_delta(event),
+ Some("content_block_stop") => self.handle_content_block_stop(event),
+ Some("message_stop") => self.finish_partial_message(),
+ _ => vec![],
+ }
+ }
+
+ fn handle_content_block_start(&mut self, event: &Value) -> Vec {
+ let Some(index) = event.get("index").and_then(Value::as_u64) else {
+ return vec![];
+ };
+ let Some(block) = event.get("content_block") else {
+ return vec![];
+ };
+
+ let mut out = Vec::new();
+ if let Some(mut prior) = self.partial_blocks.remove(&index) {
+ out.extend(prior.finalize());
+ }
+
+ let partial = match block.get("type").and_then(Value::as_str) {
+ Some("text") => {
+ let id = self.next_part_id();
+ let initial = block
+ .get("text")
+ .and_then(Value::as_str)
+ .unwrap_or("")
+ .to_string();
+ out.push(chunk(json!({"type": "text-start", "id": id})));
+ if !initial.is_empty() {
+ out.push(chunk(
+ json!({"type": "text-delta", "id": id, "delta": initial}),
+ ));
+ }
+ PartialBlock::Text {
+ id,
+ emitted: initial,
+ status: BlockStatus::default(),
+ }
+ }
+ Some("thinking") => {
+ let id = self.next_part_id();
+ let initial = block
+ .get("thinking")
+ .and_then(Value::as_str)
+ .unwrap_or("")
+ .to_string();
+ out.push(chunk(json!({"type": "reasoning-start", "id": id})));
+ if !initial.is_empty() {
+ out.push(chunk(
+ json!({"type": "reasoning-delta", "id": id, "delta": initial}),
+ ));
+ }
+ PartialBlock::Reasoning {
+ id,
+ emitted: initial,
+ status: BlockStatus::default(),
+ }
+ }
+ Some("tool_use") => {
+ let tool_call_id = block
+ .get("id")
+ .and_then(Value::as_str)
+ .unwrap_or("")
+ .to_string();
+ let tool_name = block
+ .get("name")
+ .and_then(Value::as_str)
+ .unwrap_or("")
+ .to_string();
+ out.push(chunk(json!({
+ "type": "tool-input-start",
+ "toolCallId": tool_call_id,
+ "toolName": tool_name,
+ })));
+ PartialBlock::Tool {
+ tool_call_id,
+ tool_name,
+ input_text: String::new(),
+ final_input: None,
+ status: BlockStatus::default(),
+ }
+ }
+ _ => return out,
+ };
+
+ self.partial_blocks.insert(index, partial);
+ self.current_block_index = Some(index);
+ out
+ }
+
+ fn handle_content_block_delta(&mut self, event: &Value) -> Vec {
+ let Some(index) = event.get("index").and_then(Value::as_u64) else {
+ return vec![];
+ };
+ let Some(delta) = event.get("delta") else {
+ return vec![];
+ };
+ self.partial_blocks
+ .get_mut(&index)
+ .map_or_else(Vec::new, |partial| partial.apply_delta(delta))
+ }
+
+ fn handle_content_block_stop(&mut self, event: &Value) -> Vec {
+ let Some(index) = event.get("index").and_then(Value::as_u64) else {
+ return vec![];
+ };
+ if self.current_block_index == Some(index) {
+ self.current_block_index = None;
+ }
+ let Some(partial) = self.partial_blocks.get_mut(&index) else {
+ return vec![];
+ };
+ partial.mark_stopped();
+ if partial.should_finalize() {
+ partial.finalize()
+ } else {
+ vec![]
+ }
+ }
+
+ fn finish_partial_message(&mut self) -> Vec {
+ let mut out = Vec::new();
+ for partial in self.partial_blocks.values_mut() {
+ out.extend(partial.finalize());
+ }
+ self.partial_blocks.clear();
+ self.current_block_index = None;
+ out
+ }
+
+ fn handle_user(&mut self, value: &Value) -> Vec {
+ let mut out = self.finish_partial_message();
+ let Some(content) = value.pointer("/message/content").and_then(Value::as_array) else {
+ return out;
+ };
+ out.extend(
+ content
+ .iter()
+ .filter_map(|item| {
+ if item.get("type").and_then(Value::as_str) != Some("tool_result") {
+ return None;
+ }
+ let tool_call_id = item.get("tool_use_id").and_then(Value::as_str)?.to_string();
+ let is_error = item
+ .get("is_error")
+ .and_then(Value::as_bool)
+ .unwrap_or(false);
+ let content_val = item.get("content").cloned().unwrap_or(Value::Null);
+ Some(if is_error {
+ chunk(json!({
+ "type": "tool-output-error",
+ "toolCallId": tool_call_id,
+ "errorText": stringify_tool_content(&content_val),
+ }))
+ } else {
+ chunk(json!({
+ "type": "tool-output-available",
+ "toolCallId": tool_call_id,
+ "output": content_val,
+ }))
+ })
+ })
+ .collect::>(),
+ );
+ out
+ }
+
+ fn handle_result(&mut self, value: &Value) -> Vec {
+ let mut out = self.finish_partial_message();
+ // Error results can be the only frame carrying the session id (for
+ // example, an early resume failure with no preceding init). Capture it
+ // before branching on `is_error` so the durable assistant result keeps
+ // the resume anchor even though this turn itself failed.
+ if let Some(sid) = value
+ .get("session_id")
+ .and_then(Value::as_str)
+ .filter(|sid| !sid.trim().is_empty())
+ {
+ self.session_id = Some(sid.to_string());
+ out.push(MappedEvent::SessionId(sid.to_string()));
+ }
+ let is_error = value
+ .get("is_error")
+ .and_then(Value::as_bool)
+ .unwrap_or(false);
+ if is_error {
+ let message = value
+ .get("result")
+ .and_then(Value::as_str)
+ .filter(|s| !s.is_empty())
+ .map(str::to_string)
+ .or_else(|| {
+ // The stub's `fail` scenario (and, per the module doc,
+ // possibly the real CLI) carries `errors: [string]`
+ // instead of a `result` string on an error frame.
+ value
+ .get("errors")
+ .and_then(Value::as_array)
+ .and_then(|arr| arr.first())
+ .and_then(Value::as_str)
+ .map(str::to_string)
+ })
+ .or_else(|| {
+ value
+ .get("subtype")
+ .and_then(Value::as_str)
+ .map(str::to_string)
+ })
+ .unwrap_or_else(|| "claude CLI reported an error".to_string());
+ out.push(MappedEvent::FatalError { message });
+ return out;
+ }
+
+ let usage = value.get("usage").cloned().unwrap_or(Value::Null);
+ let total_cost_usd = value.get("total_cost_usd").cloned().unwrap_or(Value::Null);
+ out.push(chunk(json!({
+ "type": "finish",
+ "finishReason": "stop",
+ "messageMetadata": {
+ "usage": usage,
+ "totalCostUsd": total_cost_usd,
+ "codingAgentProvider": "claude-code",
+ "codingAgentSessionId": self.session_id,
+ },
+ })));
+ out
+ }
+}
+
+/// Best-effort text extraction from a `tool_result` content value for the
+/// `tool-output-error` chunk's `errorText` (a plain string per the AI
+/// SDK's `UIMessageChunk` shape) — the CLI's `content` field itself may be
+/// a string OR an array of content blocks.
+fn stringify_tool_content(value: &Value) -> String {
+ match value {
+ Value::String(s) => s.clone(),
+ Value::Array(items) => items
+ .iter()
+ .filter_map(|item| item.get("text").and_then(Value::as_str))
+ .collect::>()
+ .join("\n"),
+ other => other.to_string(),
+ }
+}
+
+impl EventMapper for ClaudeEventMapper {
+ fn feed_line(&mut self, line: &str) -> Vec {
+ let trimmed = line.trim();
+ if trimmed.is_empty() {
+ return vec![];
+ }
+ let Ok(value) = serde_json::from_str::(trimmed) else {
+ // A stray non-JSON line (banners, warnings) must not abort an
+ // otherwise-healthy run.
+ return vec![];
+ };
+ self.handle_top_level(&value)
+ }
+
+ fn session_id(&self) -> Option<&str> {
+ self.session_id.as_deref()
+ }
+
+ fn flush(&mut self, _reason: FlushReason) -> Vec {
+ self.finish_partial_message()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const SIMPLE_FIXTURE: &str = include_str!("fixtures/claude_simple.jsonl");
+ const ERROR_FIXTURE: &str = include_str!("fixtures/claude_error.jsonl");
+ const PARTIAL_FIXTURE: &str = include_str!("fixtures/claude_partial.jsonl");
+ const TOOL_USE_FIXTURE: &str = include_str!("fixtures/claude_tool_use.jsonl");
+
+ fn feed_all(mapper: &mut ClaudeEventMapper, fixture: &str) -> Vec {
+ fixture
+ .lines()
+ .flat_map(|line| mapper.feed_line(line))
+ .collect()
+ }
+
+ #[test]
+ fn simple_fixture_yields_start_session_text_and_finish() {
+ let mut mapper = ClaudeEventMapper::new();
+ let events = feed_all(&mut mapper, SIMPLE_FIXTURE);
+
+ assert_eq!(events.first(), Some(&chunk(json!({"type": "start"}))));
+ assert!(events
+ .iter()
+ .any(|e| matches!(e, MappedEvent::SessionId(_))));
+ assert!(mapper.session_id().is_some());
+
+ let text_deltas: Vec<&str> = events
+ .iter()
+ .filter_map(|e| match e {
+ MappedEvent::Chunk(v) if v["type"] == "text-delta" => v["delta"].as_str(),
+ _ => None,
+ })
+ .collect();
+ assert_eq!(text_deltas.join(""), "pong");
+
+ let finish = events
+ .iter()
+ .find(|e| matches!(e, MappedEvent::Chunk(v) if v["type"] == "finish"))
+ .expect("a finish chunk");
+ let MappedEvent::Chunk(finish) = finish else {
+ unreachable!()
+ };
+ assert_eq!(finish["finishReason"], "stop");
+ assert_eq!(
+ finish["messageMetadata"]["codingAgentProvider"],
+ "claude-code"
+ );
+
+ assert!(!events
+ .iter()
+ .any(|e| matches!(e, MappedEvent::FatalError { .. })));
+ }
+
+ #[test]
+ fn start_chunk_is_emitted_exactly_once() {
+ let mut mapper = ClaudeEventMapper::new();
+ let events = feed_all(&mut mapper, SIMPLE_FIXTURE);
+ let starts = events
+ .iter()
+ .filter(|e| matches!(e, MappedEvent::Chunk(v) if v["type"] == "start"))
+ .count();
+ assert_eq!(starts, 1);
+ }
+
+ #[test]
+ fn tool_use_fixture_yields_tool_input_and_output_chunks() {
+ let mut mapper = ClaudeEventMapper::new();
+ let events = feed_all(&mut mapper, TOOL_USE_FIXTURE);
+
+ let available = events
+ .iter()
+ .find(|e| matches!(e, MappedEvent::Chunk(v) if v["type"] == "tool-input-available"))
+ .expect("a tool-input-available chunk");
+ let MappedEvent::Chunk(available) = available else {
+ unreachable!()
+ };
+ assert_eq!(available["toolCallId"], "toolu_01ABC");
+ assert_eq!(available["toolName"], "Bash");
+ assert_eq!(available["input"]["command"], "echo hi");
+
+ let output = events
+ .iter()
+ .find(|e| matches!(e, MappedEvent::Chunk(v) if v["type"] == "tool-output-available"))
+ .expect("a tool-output-available chunk");
+ let MappedEvent::Chunk(output) = output else {
+ unreachable!()
+ };
+ assert_eq!(output["toolCallId"], "toolu_01ABC");
+ }
+
+ #[test]
+ fn result_is_error_true_with_result_field_yields_a_fatal_error() {
+ let mut mapper = ClaudeEventMapper::new();
+ let events = feed_all(&mut mapper, ERROR_FIXTURE);
+ assert_eq!(
+ events.last(),
+ Some(&MappedEvent::FatalError {
+ message: "API Error: 529 overloaded".to_string()
+ })
+ );
+ }
+
+ #[test]
+ fn result_is_error_true_with_errors_array_falls_back_to_the_first_entry() {
+ let mut mapper = ClaudeEventMapper::new();
+ let events = mapper.feed_line(
+ r#"{"type":"result","subtype":"error_during_execution","is_error":true,"errors":["stub-harness: induced failure"]}"#,
+ );
+ assert_eq!(
+ events,
+ vec![MappedEvent::FatalError {
+ message: "stub-harness: induced failure".to_string()
+ }]
+ );
+ }
+
+ #[test]
+ fn error_result_captures_session_before_reporting_the_failure() {
+ let mut mapper = ClaudeEventMapper::new();
+ let events = mapper.feed_line(
+ r#"{"type":"result","subtype":"error_during_execution","is_error":true,"result":"failed","session_id":"error-session"}"#,
+ );
+ assert_eq!(
+ events,
+ vec![
+ MappedEvent::SessionId("error-session".to_string()),
+ MappedEvent::FatalError {
+ message: "failed".to_string()
+ }
+ ]
+ );
+ assert_eq!(mapper.session_id(), Some("error-session"));
+ }
+
+ #[test]
+ fn malformed_json_line_is_ignored_not_fatal() {
+ let mut mapper = ClaudeEventMapper::new();
+ let events = mapper.feed_line("Reading additional input from stdin...");
+ assert_eq!(events, vec![]);
+ }
+
+ #[test]
+ fn blank_line_is_ignored() {
+ let mut mapper = ClaudeEventMapper::new();
+ assert_eq!(mapper.feed_line(""), vec![]);
+ assert_eq!(mapper.feed_line(" "), vec![]);
+ }
+
+ #[test]
+ fn unknown_top_level_type_is_ignored() {
+ let mut mapper = ClaudeEventMapper::new();
+ let events = mapper.feed_line(r#"{"type":"rate_limit_event","rate_limit_info":{}}"#);
+ assert_eq!(events, vec![]);
+ }
+
+ #[test]
+ fn partial_text_is_streamed_and_the_identical_snapshot_is_deduplicated() {
+ let mut mapper = ClaudeEventMapper::new();
+ let fixture = [
+ r#"{"type":"stream_event","event":{"type":"message_start","message":{"content":[]}}}"#,
+ r#"{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}}"#,
+ r#"{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"pong"}}}"#,
+ r#"{"type":"assistant","message":{"content":[{"type":"text","text":"pong"}]}}"#,
+ r#"{"type":"stream_event","event":{"type":"content_block_stop","index":0}}"#,
+ r#"{"type":"stream_event","event":{"type":"message_stop"}}"#,
+ ]
+ .join("\n");
+ let events = feed_all(&mut mapper, &fixture);
+
+ let text_deltas: Vec<&str> = events
+ .iter()
+ .filter_map(|event| match event {
+ MappedEvent::Chunk(value) if value["type"] == "text-delta" => {
+ value["delta"].as_str()
+ }
+ _ => None,
+ })
+ .collect();
+ assert_eq!(text_deltas, vec!["pong"]);
+ assert_eq!(
+ events
+ .iter()
+ .filter(|event| matches!(
+ event,
+ MappedEvent::Chunk(value) if value["type"] == "text-start"
+ ))
+ .count(),
+ 1
+ );
+ assert_eq!(
+ events
+ .iter()
+ .filter(|event| matches!(
+ event,
+ MappedEvent::Chunk(value) if value["type"] == "text-end"
+ ))
+ .count(),
+ 1
+ );
+ }
+
+ #[test]
+ fn eof_closes_a_partial_text_block_exactly_once() {
+ let mut mapper = ClaudeEventMapper::new();
+ let events = mapper.feed_line(
+ r#"{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"text","text":"hel"}}}"#,
+ );
+ assert!(events.iter().any(
+ |event| matches!(event, MappedEvent::Chunk(value) if value["type"] == "text-start")
+ ));
+
+ let flushed = mapper.flush(FlushReason::EndOfStream);
+ assert_eq!(flushed, vec![chunk(json!({"type": "text-end", "id": "0"}))]);
+ assert!(mapper.flush(FlushReason::EndOfStream).is_empty());
+ }
+
+ #[test]
+ fn cancellation_closes_a_partial_reasoning_block() {
+ let mut mapper = ClaudeEventMapper::new();
+ mapper.feed_line(
+ r#"{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}}"#,
+ );
+ mapper.feed_line(
+ r#"{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"still thinking"}}}"#,
+ );
+
+ assert_eq!(
+ mapper.flush(FlushReason::Cancelled),
+ vec![chunk(json!({"type": "reasoning-end", "id": "0"}))]
+ );
+ }
+
+ #[test]
+ fn failed_stream_turns_partial_tool_input_into_tool_input_error() {
+ let mut mapper = ClaudeEventMapper::new();
+ mapper.feed_line(
+ r#"{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_cut","name":"Bash","input":{}}}}"#,
+ );
+ mapper.feed_line(
+ r#"{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"command\":"}}}"#,
+ );
+
+ let flushed = mapper.flush(FlushReason::Failed);
+ assert_eq!(
+ flushed,
+ vec![chunk(json!({
+ "type": "tool-input-error",
+ "toolCallId": "toolu_cut",
+ "toolName": "Bash",
+ "input": "{\"command\":",
+ "errorText": "Claude's stream ended before the tool input was complete",
+ }))]
+ );
+ assert!(!flushed.iter().any(
+ |event| matches!(event, MappedEvent::Chunk(value) if value["type"] == "tool-input-available")
+ ));
+ }
+
+ #[test]
+ fn partial_fixture_appends_snapshot_suffixes_and_finalizes_reasoning_and_tools() {
+ let mut mapper = ClaudeEventMapper::new();
+ let events = feed_all(&mut mapper, PARTIAL_FIXTURE);
+
+ let text = events
+ .iter()
+ .filter_map(|event| match event {
+ MappedEvent::Chunk(value) if value["type"] == "text-delta" => {
+ value["delta"].as_str()
+ }
+ _ => None,
+ })
+ .collect::();
+ assert_eq!(text, "hello");
+
+ let reasoning = events
+ .iter()
+ .filter_map(|event| match event {
+ MappedEvent::Chunk(value) if value["type"] == "reasoning-delta" => {
+ value["delta"].as_str()
+ }
+ _ => None,
+ })
+ .collect::();
+ assert_eq!(reasoning, "Checking the task");
+
+ let tool_input_text = events
+ .iter()
+ .filter_map(|event| match event {
+ MappedEvent::Chunk(value) if value["type"] == "tool-input-delta" => {
+ value["inputTextDelta"].as_str()
+ }
+ _ => None,
+ })
+ .collect::();
+ assert_eq!(tool_input_text, r#"{"command":"echo hi"}"#);
+
+ let tool_available: Vec<&Value> = events
+ .iter()
+ .filter_map(|event| match event {
+ MappedEvent::Chunk(value) if value["type"] == "tool-input-available" => Some(value),
+ _ => None,
+ })
+ .collect();
+ assert_eq!(tool_available.len(), 1);
+ assert_eq!(tool_available[0]["toolCallId"], "toolu_partial");
+ assert_eq!(tool_available[0]["input"]["command"], "echo hi");
+ assert!(events
+ .iter()
+ .filter_map(|event| match event {
+ MappedEvent::Chunk(value) if value["type"] == "tool-input-start" => {
+ value["toolCallId"].as_str()
+ }
+ _ => None,
+ })
+ .all(|id| id == "toolu_partial"));
+
+ for (start, end) in [
+ ("text-start", "text-end"),
+ ("reasoning-start", "reasoning-end"),
+ ] {
+ assert_eq!(
+ events
+ .iter()
+ .filter(|event| matches!(
+ event,
+ MappedEvent::Chunk(value) if value["type"] == start
+ ))
+ .count(),
+ 1
+ );
+ assert_eq!(
+ events
+ .iter()
+ .filter(|event| matches!(
+ event,
+ MappedEvent::Chunk(value) if value["type"] == end
+ ))
+ .count(),
+ 1
+ );
+ let start_id = events.iter().find_map(|event| match event {
+ MappedEvent::Chunk(value) if value["type"] == start => value["id"].as_str(),
+ _ => None,
+ });
+ let end_id = events.iter().find_map(|event| match event {
+ MappedEvent::Chunk(value) if value["type"] == end => value["id"].as_str(),
+ _ => None,
+ });
+ assert_eq!(start_id, end_id);
+ }
+ }
+
+ #[test]
+ fn multiple_assistant_frames_each_contribute_their_own_text_block() {
+ // Mirrors stub-harness.mjs's SCENARIO:simple: 3 separate
+ // `assistant` frames, each with ONE new text block — not an
+ // accumulating snapshot.
+ let mut mapper = ClaudeEventMapper::new();
+ mapper.feed_line(r#"{"type":"system","subtype":"init","session_id":"s1"}"#);
+ let e1 = mapper.feed_line(
+ r#"{"type":"assistant","message":{"content":[{"type":"text","text":"Hello"}]}}"#,
+ );
+ let e2 = mapper.feed_line(
+ r#"{"type":"assistant","message":{"content":[{"type":"text","text":" from"}]}}"#,
+ );
+ let e3 = mapper.feed_line(
+ r#"{"type":"assistant","message":{"content":[{"type":"text","text":" stub"}]}}"#,
+ );
+ let all: Vec = [e1, e2, e3].concat();
+ let deltas: Vec<&str> = all
+ .iter()
+ .filter_map(|e| match e {
+ MappedEvent::Chunk(v) if v["type"] == "text-delta" => v["delta"].as_str(),
+ _ => None,
+ })
+ .collect();
+ assert_eq!(deltas, vec!["Hello", " from", " stub"]);
+ // Each block gets its OWN part id (start/end pair), not shared.
+ let starts = all
+ .iter()
+ .filter(|e| matches!(e, MappedEvent::Chunk(v) if v["type"] == "text-start"))
+ .count();
+ assert_eq!(starts, 3);
+ }
+}
diff --git a/apps/native/crates/harness/src/events/codex.rs b/apps/native/crates/harness/src/events/codex.rs
new file mode 100644
index 0000000000..7c5f97c712
--- /dev/null
+++ b/apps/native/crates/harness/src/events/codex.rs
@@ -0,0 +1,425 @@
+//! `codex exec --json --skip-git-repo-check ` ndjson →
+//! `UIMessageChunk` mapping.
+//!
+//! Wire shapes for `thread.started`/`turn.started`/`item.completed`
+//! (`agent_message` item)/`turn.completed` were captured live on this
+//! machine (codex-cli 0.144.5, 2026-07-18) — see
+//! `events/fixtures/codex_exec.jsonl` (real capture, verbatim). Codex's
+//! `--json` protocol also documents `item.started`/`item.updated` (for
+//! items that stream incrementally) and richer item types
+//! (`reasoning`, `command_execution`, `file_change`, `mcp_tool_call`,
+//! `web_search`, `todo_list`) plus `turn.failed`/top-level `error` — none
+//! of those were exercised by the trivial "reply with one word" smoke
+//! prompt (a real capture would need to spend real API credits driving an
+//! actual tool call, which this crate doesn't need to do twice to build a
+//! reasonable mapping). `events/fixtures/codex_tool_use.jsonl` and
+//! `events/fixtures/codex_error.jsonl` are HAND-CONSTRUCTED to the
+//! documented item/turn shapes; unrecognized item types fall through a
+//! generic best-effort mapping (see `handle_item` below) rather than being
+//! silently dropped, so a future item type this mapper hasn't been taught
+//! about yet still surfaces SOMETHING instead of vanishing.
+//!
+//! Unlike claude's Anthropic-Messages-API deltas, codex's `--json` items
+//! arrive essentially as a WHOLE (`item.completed`) rather than as
+//! incremental token deltas — so each `agent_message`/`reasoning` item
+//! synthesizes a start+delta(full text)+end triplet in one line rather
+//! than genuinely streaming word-by-word. `item.started`/`item.updated`
+//! ARE handled (for forward-compat with a codex version that does stream
+//! them): the mapper tracks each item's last-seen text and emits only the
+//! GROWN suffix as the delta, so a future incrementally-updated item
+//! degrades gracefully into real deltas instead of repeating the whole
+//! text on every update.
+//!
+//! Event mapping:
+//! - `thread.started` → [`crate::events::MappedEvent::SessionId`]
+//! (`thread_id` is codex's resume token — `codex exec resume
+//! `).
+//! - `turn.started` → one `{"type":"start"}` chunk.
+//! - `item.started` / `item.updated` / `item.completed` → see
+//! [`handle_item`].
+//! - `turn.completed` → one `finish` chunk carrying usage metadata.
+//! - `turn.failed` / top-level `error` → `MappedEvent::FatalError`.
+//! - anything else / an unparseable line → `Ignored`.
+
+use std::collections::HashMap;
+
+use serde_json::{json, Value};
+
+use super::{chunk, EventMapper, MappedEvent};
+
+#[derive(Debug, Default)]
+pub struct CodexEventMapper {
+ /// Per-item last-seen text, keyed by item id — lets `item.updated`
+ /// (if a future codex version streams it) emit only the grown suffix.
+ item_text: HashMap,
+ /// Item ids whose start chunk has already been emitted (so a repeated
+ /// `item.updated` for the same id doesn't re-open it).
+ item_started: std::collections::HashSet,
+ session_id: Option,
+ turn_started: bool,
+}
+
+impl CodexEventMapper {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ fn handle_top_level(&mut self, value: &Value) -> Vec {
+ match value.get("type").and_then(Value::as_str) {
+ Some("thread.started") => self.handle_thread_started(value),
+ Some("turn.started") => {
+ if self.turn_started {
+ return vec![];
+ }
+ self.turn_started = true;
+ vec![chunk(json!({"type": "start"}))]
+ }
+ Some("item.started") | Some("item.updated") => value
+ .get("item")
+ .map(|item| self.handle_item(item, false))
+ .unwrap_or_default(),
+ Some("item.completed") => value
+ .get("item")
+ .map(|item| self.handle_item(item, true))
+ .unwrap_or_default(),
+ Some("turn.completed") => Self::handle_turn_completed(value),
+ Some("turn.failed") => Self::handle_turn_failed(value),
+ Some("error") => Some(MappedEvent::FatalError {
+ message: value
+ .get("message")
+ .and_then(Value::as_str)
+ .unwrap_or("codex CLI reported an error")
+ .to_string(),
+ })
+ .into_iter()
+ .collect(),
+ _ => vec![],
+ }
+ }
+
+ fn handle_thread_started(&mut self, value: &Value) -> Vec {
+ let Some(id) = value.get("thread_id").and_then(Value::as_str) else {
+ return vec![];
+ };
+ self.session_id = Some(id.to_string());
+ vec![MappedEvent::SessionId(id.to_string())]
+ }
+
+ /// Maps ONE item (from `item.started`/`item.updated`/`item.completed`)
+ /// to zero or more chunks. `agent_message`/`reasoning` map to
+ /// text/reasoning start+delta(+end on completion); every other item
+ /// type (`command_execution`, `file_change`, `mcp_tool_call`,
+ /// `web_search`, `todo_list`, and anything not yet known) maps to a
+ /// generic tool-call shape: `toolName` is the item's own `type`,
+ /// `input` is the item object minus `id`/`type`/`status` (the
+ /// "control" fields), `output` populated from the item's own status
+ /// once it's `completed`.
+ fn handle_item(&mut self, item: &Value, is_completed_event: bool) -> Vec {
+ let Some(id) = item.get("id").and_then(Value::as_str) else {
+ return vec![];
+ };
+ let item_type = item.get("type").and_then(Value::as_str).unwrap_or("");
+ // "Completed" if either the wrapping event says so (the
+ // `item.completed` variant) OR the item carries its own terminal
+ // `status` field — defensive coverage for whichever convention a
+ // given codex version actually uses; the real capture this
+ // mapper was built against carries no `status` field at all on
+ // its `item.completed` item, so `is_completed_event` alone must
+ // be sufficient.
+ let completed =
+ is_completed_event || item.get("status").and_then(Value::as_str) == Some("completed");
+
+ match item_type {
+ "agent_message" => self.handle_text_like_item(id, item, "text", completed),
+ "reasoning" => self.handle_text_like_item(id, item, "reasoning", completed),
+ // Generic (tool-shaped) items only surface once complete — an
+ // `item.started`/`item.updated` for e.g. a `command_execution`
+ // rarely carries a full `input`/`output` yet, and emitting the
+ // same object twice (once on started, again on completed)
+ // would duplicate the chunk.
+ other if completed => self.handle_generic_tool_item(id, other, item),
+ _ => vec![],
+ }
+ }
+
+ /// `agent_message`/`reasoning` items: emit a `-start` the first time
+ /// this item id is seen, a `-delta` for whatever text GREW since the
+ /// last update (the whole text on first sight), and a `-end` only once
+ /// the item reaches a terminal status (or unconditionally for
+ /// `item.completed`, since that event has no further updates coming).
+ fn handle_text_like_item(
+ &mut self,
+ id: &str,
+ item: &Value,
+ kind: &str,
+ completed: bool,
+ ) -> Vec {
+ let text = item
+ .get("text")
+ .or_else(|| item.get("summary"))
+ .and_then(Value::as_str)
+ .unwrap_or_default();
+ let mut out = Vec::new();
+
+ let first_sight = self.item_started.insert(id.to_string());
+ if first_sight {
+ out.push(chunk(json!({"type": format!("{kind}-start"), "id": id})));
+ }
+
+ let previous = self.item_text.get(id).cloned().unwrap_or_default();
+ if let Some(grown) = text.strip_prefix(&previous) {
+ if !grown.is_empty() {
+ out.push(chunk(
+ json!({"type": format!("{kind}-delta"), "id": id, "delta": grown}),
+ ));
+ }
+ } else if text != previous {
+ // Text didn't grow as a simple suffix (a rewrite, not an
+ // append) — emit the new text wholesale rather than a
+ // misleading diff.
+ out.push(chunk(
+ json!({"type": format!("{kind}-delta"), "id": id, "delta": text}),
+ ));
+ }
+ self.item_text.insert(id.to_string(), text.to_string());
+
+ if completed {
+ out.push(chunk(json!({"type": format!("{kind}-end"), "id": id})));
+ }
+ out
+ }
+
+ /// Any item type that isn't `agent_message`/`reasoning` — a generic,
+ /// best-effort tool-call mapping so a not-yet-specifically-handled
+ /// item (or a genuinely new item type a future codex version adds)
+ /// still surfaces as SOMETHING instead of silently vanishing (see
+ /// module doc).
+ fn handle_generic_tool_item(
+ &self,
+ id: &str,
+ tool_name: &str,
+ item: &Value,
+ ) -> Vec {
+ let mut input = item.clone();
+ if let Value::Object(map) = &mut input {
+ map.remove("id");
+ map.remove("type");
+ }
+ vec![chunk(json!({
+ "type": "tool-input-available",
+ "toolCallId": id,
+ "toolName": tool_name,
+ "input": input,
+ }))]
+ }
+
+ fn handle_turn_completed(value: &Value) -> Vec {
+ let usage = value.get("usage").cloned().unwrap_or(Value::Null);
+ vec![chunk(json!({
+ "type": "finish",
+ "finishReason": "stop",
+ "messageMetadata": {
+ "usage": usage,
+ "codingAgentProvider": "codex",
+ },
+ }))]
+ }
+
+ fn handle_turn_failed(value: &Value) -> Vec {
+ let message = value
+ .pointer("/error/message")
+ .and_then(Value::as_str)
+ .unwrap_or("codex CLI reported a turn failure")
+ .to_string();
+ vec![MappedEvent::FatalError { message }]
+ }
+}
+
+impl EventMapper for CodexEventMapper {
+ fn feed_line(&mut self, line: &str) -> Vec {
+ let trimmed = line.trim();
+ if trimmed.is_empty() {
+ return vec![];
+ }
+ let Ok(value) = serde_json::from_str::(trimmed) else {
+ // Real captured behavior: codex prints a human
+ // "Reading additional input from stdin..." banner to stdout
+ // BEFORE its JSON stream when stdin isn't closed — see
+ // `run.rs`'s module doc for why every harness spawn closes
+ // stdin, and this fallback for defense in depth regardless.
+ return vec![];
+ };
+ self.handle_top_level(&value)
+ }
+
+ fn session_id(&self) -> Option<&str> {
+ self.session_id.as_deref()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const EXEC_FIXTURE: &str = include_str!("fixtures/codex_exec.jsonl");
+ const TOOL_USE_FIXTURE: &str = include_str!("fixtures/codex_tool_use.jsonl");
+ const ERROR_FIXTURE: &str = include_str!("fixtures/codex_error.jsonl");
+
+ fn feed_all(mapper: &mut CodexEventMapper, fixture: &str) -> Vec {
+ fixture
+ .lines()
+ .flat_map(|line| mapper.feed_line(line))
+ .collect()
+ }
+
+ #[test]
+ fn exec_fixture_yields_session_start_text_and_finish() {
+ let mut mapper = CodexEventMapper::new();
+ let events = feed_all(&mut mapper, EXEC_FIXTURE);
+
+ assert_eq!(
+ events.first(),
+ Some(&MappedEvent::SessionId(
+ "019f7325-3e34-7b30-863a-861396b02def".to_string()
+ ))
+ );
+ assert_eq!(
+ mapper.session_id(),
+ Some("019f7325-3e34-7b30-863a-861396b02def")
+ );
+
+ assert!(events.contains(&chunk(json!({"type": "start"}))));
+ assert!(events.contains(&chunk(json!({"type": "text-start", "id": "item_0"}))));
+ assert!(events.contains(&chunk(
+ json!({"type": "text-delta", "id": "item_0", "delta": "pong"})
+ )));
+ assert!(events.contains(&chunk(json!({"type": "text-end", "id": "item_0"}))));
+
+ let finish = events
+ .iter()
+ .find(|e| matches!(e, MappedEvent::Chunk(v) if v["type"] == "finish"))
+ .expect("a finish chunk");
+ let MappedEvent::Chunk(finish) = finish else {
+ unreachable!()
+ };
+ assert_eq!(finish["messageMetadata"]["codingAgentProvider"], "codex");
+ assert_eq!(finish["messageMetadata"]["usage"]["output_tokens"], 106);
+
+ assert!(!events
+ .iter()
+ .any(|e| matches!(e, MappedEvent::FatalError { .. })));
+ }
+
+ #[test]
+ fn agent_message_start_is_emitted_only_once() {
+ let mut mapper = CodexEventMapper::new();
+ let events = feed_all(&mut mapper, EXEC_FIXTURE);
+ let starts = events
+ .iter()
+ .filter(|e| matches!(e, MappedEvent::Chunk(v) if v["type"] == "text-start"))
+ .count();
+ assert_eq!(starts, 1);
+ }
+
+ #[test]
+ fn tool_use_fixture_maps_reasoning_and_generic_command_execution() {
+ let mut mapper = CodexEventMapper::new();
+ let events = feed_all(&mut mapper, TOOL_USE_FIXTURE);
+
+ assert!(events.contains(&chunk(json!({"type": "reasoning-start", "id": "item_0"}))));
+ assert!(events.contains(&chunk(
+ json!({"type": "reasoning-delta", "id": "item_0", "delta": "Figuring out what to run."})
+ )));
+ assert!(events.contains(&chunk(json!({"type": "reasoning-end", "id": "item_0"}))));
+
+ let tool_call = events
+ .iter()
+ .find(|e| matches!(e, MappedEvent::Chunk(v) if v["toolCallId"] == "item_1"))
+ .expect("a generic tool-input-available chunk for command_execution");
+ let MappedEvent::Chunk(tool_call) = tool_call else {
+ unreachable!()
+ };
+ assert_eq!(tool_call["type"], "tool-input-available");
+ assert_eq!(tool_call["toolName"], "command_execution");
+ assert_eq!(tool_call["input"]["command"], "echo hi");
+ assert_eq!(tool_call["input"]["exit_code"], 0);
+ // Control fields id/type are stripped from `input`.
+ assert!(tool_call["input"].get("id").is_none());
+ assert!(tool_call["input"].get("type").is_none());
+ }
+
+ #[test]
+ fn turn_failed_yields_a_fatal_error_with_the_reported_message() {
+ let mut mapper = CodexEventMapper::new();
+ let events = feed_all(&mut mapper, ERROR_FIXTURE);
+ assert_eq!(
+ events.last(),
+ Some(&MappedEvent::FatalError {
+ message: "stream disconnected before completion".to_string()
+ })
+ );
+ }
+
+ #[test]
+ fn top_level_error_type_is_also_fatal() {
+ let mut mapper = CodexEventMapper::new();
+ let events = mapper.feed_line(r#"{"type":"error","message":"boom"}"#);
+ assert_eq!(
+ events,
+ vec![MappedEvent::FatalError {
+ message: "boom".to_string()
+ }]
+ );
+ }
+
+ #[test]
+ fn malformed_json_line_is_ignored_not_fatal() {
+ let mut mapper = CodexEventMapper::new();
+ let events = mapper.feed_line("Reading additional input from stdin...");
+ assert_eq!(events, vec![]);
+ }
+
+ #[test]
+ fn incremental_item_updated_emits_only_the_grown_suffix() {
+ let mut mapper = CodexEventMapper::new();
+ mapper.feed_line(r#"{"type":"thread.started","thread_id":"t1"}"#);
+ mapper.feed_line(r#"{"type":"turn.started"}"#);
+ let first = mapper.feed_line(
+ r#"{"type":"item.updated","item":{"id":"item_0","type":"agent_message","text":"pon","status":"in_progress"}}"#,
+ );
+ assert!(first.contains(&chunk(json!({"type":"text-start","id":"item_0"}))));
+ assert!(first.contains(&chunk(
+ json!({"type":"text-delta","id":"item_0","delta":"pon"})
+ )));
+
+ let second = mapper.feed_line(
+ r#"{"type":"item.updated","item":{"id":"item_0","type":"agent_message","text":"pong","status":"in_progress"}}"#,
+ );
+ // Only the grown suffix "g", no repeated start.
+ assert_eq!(
+ second,
+ vec![chunk(
+ json!({"type":"text-delta","id":"item_0","delta":"g"})
+ )]
+ );
+
+ let third = mapper.feed_line(
+ r#"{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"pong"}}"#,
+ );
+ assert_eq!(third, vec![chunk(json!({"type":"text-end","id":"item_0"}))]);
+ }
+
+ #[test]
+ fn unknown_item_type_still_surfaces_generically() {
+ let mut mapper = CodexEventMapper::new();
+ let events = mapper.feed_line(
+ r#"{"type":"item.completed","item":{"id":"item_9","type":"some_future_item","payload":42}}"#,
+ );
+ let MappedEvent::Chunk(c) = &events[0] else {
+ panic!("expected a chunk")
+ };
+ assert_eq!(c["type"], "tool-input-available");
+ assert_eq!(c["toolName"], "some_future_item");
+ assert_eq!(c["input"]["payload"], 42);
+ }
+}
diff --git a/apps/native/crates/harness/src/events/fixtures/claude_error.jsonl b/apps/native/crates/harness/src/events/fixtures/claude_error.jsonl
new file mode 100644
index 0000000000..0f2fd588d3
--- /dev/null
+++ b/apps/native/crates/harness/src/events/fixtures/claude_error.jsonl
@@ -0,0 +1,2 @@
+{"type":"system","subtype":"init","cwd":"/private/tmp","session_id":"error-session","tools":["Bash"],"model":"claude-fable-5"}
+{"type":"result","subtype":"error_during_execution","is_error":true,"result":"API Error: 529 overloaded","session_id":"error-session"}
diff --git a/apps/native/crates/harness/src/events/fixtures/claude_partial.jsonl b/apps/native/crates/harness/src/events/fixtures/claude_partial.jsonl
new file mode 100644
index 0000000000..6ca1cc00cf
--- /dev/null
+++ b/apps/native/crates/harness/src/events/fixtures/claude_partial.jsonl
@@ -0,0 +1,17 @@
+{"type":"system","subtype":"init","session_id":"partial-session"}
+{"type":"stream_event","event":{"type":"message_start","message":{"content":[]}},"session_id":"partial-session"}
+{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""}},"session_id":"partial-session"}
+{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"Checking"}},"session_id":"partial-session"}
+{"type":"assistant","message":{"content":[{"type":"thinking","thinking":"Checking the task","signature":"fixture"}]},"session_id":"partial-session"}
+{"type":"stream_event","event":{"type":"content_block_stop","index":0},"session_id":"partial-session"}
+{"type":"stream_event","event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_partial","name":"Bash","input":{}}},"session_id":"partial-session"}
+{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"command\":"}},"session_id":"partial-session"}
+{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\"echo hi\"}"}},"session_id":"partial-session"}
+{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_partial","name":"Bash","input":{"command":"echo hi"}}]},"session_id":"partial-session"}
+{"type":"stream_event","event":{"type":"content_block_stop","index":1},"session_id":"partial-session"}
+{"type":"stream_event","event":{"type":"content_block_start","index":2,"content_block":{"type":"text","text":""}},"session_id":"partial-session"}
+{"type":"stream_event","event":{"type":"content_block_delta","index":2,"delta":{"type":"text_delta","text":"hel"}},"session_id":"partial-session"}
+{"type":"assistant","message":{"content":[{"type":"text","text":"hello"}]},"session_id":"partial-session"}
+{"type":"stream_event","event":{"type":"content_block_stop","index":2},"session_id":"partial-session"}
+{"type":"stream_event","event":{"type":"message_stop"},"session_id":"partial-session"}
+{"type":"result","subtype":"success","is_error":false,"result":"hello","session_id":"partial-session","usage":{"input_tokens":1,"output_tokens":2}}
diff --git a/apps/native/crates/harness/src/events/fixtures/claude_simple.jsonl b/apps/native/crates/harness/src/events/fixtures/claude_simple.jsonl
new file mode 100644
index 0000000000..03aa323835
--- /dev/null
+++ b/apps/native/crates/harness/src/events/fixtures/claude_simple.jsonl
@@ -0,0 +1,5 @@
+{"type":"system","subtype":"init","cwd":"/private/tmp","session_id":"770886ee-0c27-444d-b6b1-85fa370466e7","tools":["Bash","Read","Write"],"model":"claude-fable-5","permissionMode":"default","apiKeySource":"none","claude_code_version":"2.1.214"}
+{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1784359800,"rateLimitType":"five_hour"},"uuid":"013f1974-40e0-4fa7-9cfe-60b3a46043e4","session_id":"770886ee-0c27-444d-b6b1-85fa370466e7"}
+{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011Cd8mPPW3FXy5S3H8FmUHF","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAISiQIKiAEIDxgCKkD0"}],"stop_reason":null,"usage":{"input_tokens":2,"output_tokens":2}},"parent_tool_use_id":null,"session_id":"770886ee-0c27-444d-b6b1-85fa370466e7","uuid":"7c58a441-0e8c-48c4-b132-f7253cfe701a","timestamp":"2026-07-18T02:52:44.744Z"}
+{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011Cd8mPPW3FXy5S3H8FmUHF","type":"message","role":"assistant","content":[{"type":"text","text":"pong"}],"stop_reason":null,"usage":{"input_tokens":2,"output_tokens":17}},"parent_tool_use_id":null,"session_id":"770886ee-0c27-444d-b6b1-85fa370466e7","uuid":"5bc95ffd-c667-4c74-90aa-7bdabae3c79c","timestamp":"2026-07-18T02:52:44.753Z"}
+{"type":"result","subtype":"success","is_error":false,"duration_ms":2862,"num_turns":1,"result":"pong","stop_reason":"end_turn","session_id":"770886ee-0c27-444d-b6b1-85fa370466e7","total_cost_usd":0.170644,"usage":{"input_tokens":2,"output_tokens":17},"permission_denials":[],"terminal_reason":"completed"}
diff --git a/apps/native/crates/harness/src/events/fixtures/claude_tool_use.jsonl b/apps/native/crates/harness/src/events/fixtures/claude_tool_use.jsonl
new file mode 100644
index 0000000000..402074bd60
--- /dev/null
+++ b/apps/native/crates/harness/src/events/fixtures/claude_tool_use.jsonl
@@ -0,0 +1,5 @@
+{"type":"system","subtype":"init","cwd":"/private/tmp","session_id":"tool-use-session","tools":["Bash"],"model":"claude-fable-5"}
+{"type":"assistant","message":{"id":"msg_tool1","role":"assistant","content":[{"type":"tool_use","id":"toolu_01ABC","name":"Bash","input":{"command":"echo hi"}}],"usage":{"input_tokens":1,"output_tokens":1}},"session_id":"tool-use-session"}
+{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01ABC","content":[{"type":"text","text":"hi\n"}],"is_error":false}]},"session_id":"tool-use-session"}
+{"type":"assistant","message":{"id":"msg_tool1","role":"assistant","content":[{"type":"text","text":"Ran `echo hi`. Output: hi"}],"usage":{"input_tokens":1,"output_tokens":10}},"session_id":"tool-use-session"}
+{"type":"result","subtype":"success","is_error":false,"result":"Ran `echo hi`. Output: hi","session_id":"tool-use-session","total_cost_usd":0.01,"usage":{"input_tokens":1,"output_tokens":10}}
diff --git a/apps/native/crates/harness/src/events/fixtures/codex_error.jsonl b/apps/native/crates/harness/src/events/fixtures/codex_error.jsonl
new file mode 100644
index 0000000000..df8732e1ba
--- /dev/null
+++ b/apps/native/crates/harness/src/events/fixtures/codex_error.jsonl
@@ -0,0 +1,3 @@
+{"type":"thread.started","thread_id":"error-thread"}
+{"type":"turn.started"}
+{"type":"turn.failed","error":{"message":"stream disconnected before completion"}}
diff --git a/apps/native/crates/harness/src/events/fixtures/codex_exec.jsonl b/apps/native/crates/harness/src/events/fixtures/codex_exec.jsonl
new file mode 100644
index 0000000000..705cdaae2f
--- /dev/null
+++ b/apps/native/crates/harness/src/events/fixtures/codex_exec.jsonl
@@ -0,0 +1,4 @@
+{"type":"thread.started","thread_id":"019f7325-3e34-7b30-863a-861396b02def"}
+{"type":"turn.started"}
+{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"pong"}}
+{"type":"turn.completed","usage":{"input_tokens":10522,"cached_input_tokens":5120,"output_tokens":106,"reasoning_output_tokens":99}}
diff --git a/apps/native/crates/harness/src/events/fixtures/codex_tool_and_error.jsonl b/apps/native/crates/harness/src/events/fixtures/codex_tool_and_error.jsonl
new file mode 100644
index 0000000000..f12a40eacd
--- /dev/null
+++ b/apps/native/crates/harness/src/events/fixtures/codex_tool_and_error.jsonl
@@ -0,0 +1,9 @@
+{"type":"thread.started","thread_id":"tool-use-thread"}
+{"type":"turn.started"}
+{"type":"item.completed","item":{"id":"item_0","type":"reasoning","text":"Figuring out what to run."}}
+{"type":"item.completed","item":{"id":"item_1","type":"command_execution","command":"echo hi","aggregated_output":"hi\n","exit_code":0,"status":"completed"}}
+{"type":"item.completed","item":{"id":"item_2","type":"agent_message","text":"hi"}}
+{"type":"turn.completed","usage":{"input_tokens":5,"output_tokens":5}}
+{"type":"thread.started","thread_id":"error-thread"}
+{"type":"turn.started"}
+{"type":"turn.failed","error":{"message":"stream disconnected before completion"}}
diff --git a/apps/native/crates/harness/src/events/fixtures/codex_tool_use.jsonl b/apps/native/crates/harness/src/events/fixtures/codex_tool_use.jsonl
new file mode 100644
index 0000000000..998122ea86
--- /dev/null
+++ b/apps/native/crates/harness/src/events/fixtures/codex_tool_use.jsonl
@@ -0,0 +1,6 @@
+{"type":"thread.started","thread_id":"tool-use-thread"}
+{"type":"turn.started"}
+{"type":"item.completed","item":{"id":"item_0","type":"reasoning","text":"Figuring out what to run."}}
+{"type":"item.completed","item":{"id":"item_1","type":"command_execution","command":"echo hi","aggregated_output":"hi\n","exit_code":0,"status":"completed"}}
+{"type":"item.completed","item":{"id":"item_2","type":"agent_message","text":"hi"}}
+{"type":"turn.completed","usage":{"input_tokens":5,"output_tokens":5}}
diff --git a/apps/native/crates/harness/src/events/mod.rs b/apps/native/crates/harness/src/events/mod.rs
new file mode 100644
index 0000000000..fd6702566d
--- /dev/null
+++ b/apps/native/crates/harness/src/events/mod.rs
@@ -0,0 +1,86 @@
+//! ndjson → `UIMessageChunk`-shaped-JSON mapping, one submodule per CLI's
+//! wire format. See `crate` module doc's "Unknown-event policy" for what
+//! happens to a line neither mapper recognizes.
+//!
+//! Both mappers are STATEFUL (`content_block_start`/`_delta`/`_stop` for
+//! claude, `item.started`/`item.completed` for codex all need to remember
+//! what's "open" across lines) but PURE otherwise — `feed_line` takes a
+//! `&str`, returns `Vec`, does no I/O. That's what makes them
+//! unit-testable by feeding captured fixture lines (`events/fixtures/*`)
+//! without spawning any process; `run.rs` is the only place that wires a
+//! mapper to a real child process's stdout.
+
+pub mod claude;
+pub mod codex;
+
+use serde_json::Value;
+
+/// One decoded outcome from a single ndjson line of harness output.
+#[derive(Debug, Clone, PartialEq)]
+pub enum MappedEvent {
+ /// → dispatch's `{"type":"ui-message-chunk","chunk":}`.
+ Chunk(Value),
+ /// The harness itself reported a terminal fatal error (Claude's
+ /// `result.is_error`, Codex's `turn.failed`/top-level `error`) — NOT a
+ /// JSON-parse failure on our side. Maps to dispatch's
+ /// `{"type":"error",...}` immediately followed by `done`; the run
+ /// stops reading further lines once this is returned.
+ FatalError { message: String },
+ /// A session/thread id became known — the resume token for the NEXT
+ /// turn (`input.harness.sessionId` on a future dispatch). Not itself a
+ /// chunk; `run.rs` threads it into the terminal `finish` chunk's
+ /// metadata and (in local-api) the persisted `Run` row.
+ SessionId(String),
+ /// Nothing worth surfacing — see the crate doc's unknown-event policy.
+ Ignored,
+}
+
+/// Builds a `MappedEvent::Chunk` from an already-shaped `UIMessageChunk`
+/// JSON value. Trivial, but centralizes the "one chunk in, one event out"
+/// idiom both mappers use dozens of times.
+pub(crate) fn chunk(value: Value) -> MappedEvent {
+ MappedEvent::Chunk(value)
+}
+
+/// Why the subprocess-backed event stream is being closed.
+///
+/// Mappers use this only to close protocol parts that were opened by an
+/// incremental event but never received their normal terminator. The reason is
+/// deliberately coarse: callers must not infer process policy from a mapper;
+/// it exists so a malformed/truncated tool input can be surfaced as an error
+/// instead of being promoted to an available tool call.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum FlushReason {
+ EndOfStream,
+ Cancelled,
+ Failed,
+}
+
+/// Feeds one line at a time (as a real spawned process's stdout would
+/// arrive, split on `\n`) into a mapper. Implemented by both
+/// [`claude::ClaudeEventMapper`] and [`codex::CodexEventMapper`].
+pub trait EventMapper {
+ /// `line` has its trailing newline already stripped. A line that
+ /// isn't valid JSON (has been observed in the wild — see
+ /// `codex_error.jsonl`'s sibling fixture's real capture printing a
+ /// human `"Reading additional input from stdin..."` banner before its
+ /// first JSON line when stdin isn't closed) is treated as
+ /// `MappedEvent::Ignored`, never a fatal error — a stray banner line
+ /// must not abort an otherwise-healthy run.
+ fn feed_line(&mut self, line: &str) -> Vec;
+
+ /// The session/thread id captured so far, if any — convenience
+ /// accessor so `run.rs` can read it without re-deriving it from every
+ /// `MappedEvent::SessionId` it already forwarded.
+ fn session_id(&self) -> Option<&str>;
+
+ /// Closes any incremental UI-message parts still open when the
+ /// subprocess stream ends. Called for clean EOF, cancellation, and every
+ /// fatal/nonzero path. Most mappers have nothing to flush; Claude's
+ /// partial-message protocol needs this to pair every `*-start` with an
+ /// `*-end`, and to turn a truncated tool input into
+ /// `tool-input-error`.
+ fn flush(&mut self, _reason: FlushReason) -> Vec {
+ Vec::new()
+ }
+}
diff --git a/apps/native/crates/harness/src/lib.rs b/apps/native/crates/harness/src/lib.rs
new file mode 100644
index 0000000000..3319813301
--- /dev/null
+++ b/apps/native/crates/harness/src/lib.rs
@@ -0,0 +1,63 @@
+//! `harness` — CLI detection, model tiers, process spawn, and ndjson→SSE
+//! stream translation for the two harness CLIs desktop v1 supports
+//! (`claude`, `codex` — see the desktop migration contract decision
+//! 1: no Decopilot in the desktop app). Consumed by
+//! `crates/local-api`'s `routes/dispatch.rs` (real streaming) and
+//! `routes/models.rs` (`GET /models` detection + tiers).
+//!
+//! ## Module map
+//!
+//! - [`resolve`] — binary resolution. Implements the SHARED CONTRACT env
+//! overrides (`LOCAL_API_CLAUDE_BIN` / `LOCAL_API_CODEX_BIN`, absolute
+//! path or argv JSON array) with a PATH fallback to `claude`/`codex`.
+//! - [`tiers`] — the model-tier tables ported verbatim (same ids/labels)
+//! from `packages/harness/src/{claude-code,codex}/model/agent-tiers.ts`,
+//! plus the composite-id → CLI `--model` flag resolvers ported from
+//! `packages/harness/src/{claude-code,codex}/model/index.ts`.
+//! - [`detect`] — CLI availability probing with GROW-ONLY caching, mirroring
+//! `apps/api/src/link-daemon/capabilities.ts`'s semantics: a transient
+//! probe failure never un-lists a CLI that was previously detected.
+//! - [`spawn`] — the one process-spawn abstraction (PTY via `portable-pty`
+//! OR plain pipes), 128+signal exit-code convention, process-group kill.
+//! See that module's doc comment for why harness dispatch defaults to
+//! plain pipes (investigated + documented, not just asserted).
+//! - [`watchdog`] — the parent-liveness watchdog script and anchored
+//! process-group helpers, shared with `crates/local-api`'s
+//! `ProcessGroupChild` so crash-recovery semantics cannot drift between
+//! the two spawn families (see that module's doc comment).
+//! - [`events`] — incremental ndjson→`UIMessageChunk`-shaped-JSON mapping
+//! for each CLI's wire format (`events::claude`, `events::codex`).
+//! - [`run`] — ties the above together: resolve → spawn → parse → yield,
+//! with cancellation support. The type `local-api`'s dispatch route
+//! actually drives.
+//! - [`parts`] — reconstructs a UIMessage-shaped `parts` array from a
+//! run's chunk stream, for persisting the assistant `Message` row (see
+//! that module's doc for the resume-token convention it introduces).
+//!
+//! ## Unknown-event policy
+//!
+//! the native local-API contract's Dispatch Lifecycle section
+//! pins exactly three SSE event shapes for `POST /_sandbox/dispatch`:
+//! `ui-message-chunk`, `error`, and `done` — it defines no "escape hatch"
+//! wire shape for an event this crate doesn't recognize. So: every ndjson
+//! line this crate can't confidently map to one of those three lands as
+//! [`events::MappedEvent::Ignored`] and is silently dropped, never
+//! forwarded raw. Forwarding an unrecognized shape under the
+//! `ui-message-chunk` envelope would violate its "opaque `UIMessageChunk`"
+//! contract (the desktop frontend, Phase 3, expects genuine AI-SDK-shaped
+//! chunks) — dropping is the conservative reading of "follow the contract
+//! doc exactly" when the doc simply doesn't define that hatch.
+#![forbid(unsafe_code)]
+
+pub mod detect;
+pub mod events;
+pub mod parts;
+pub mod resolve;
+pub mod run;
+pub mod spawn;
+pub mod tiers;
+pub mod title;
+pub mod watchdog;
+
+pub use resolve::{resolve_argv, resolve_checked, HarnessId, ResolveError};
+pub use tiers::{ModelTier, CLAUDE_CODE_TIERS, CODEX_TIERS};
diff --git a/apps/native/crates/harness/src/parts.rs b/apps/native/crates/harness/src/parts.rs
new file mode 100644
index 0000000000..ef3d7436c3
--- /dev/null
+++ b/apps/native/crates/harness/src/parts.rs
@@ -0,0 +1,453 @@
+//! Reconstructs a UIMessage-shaped `parts` array — the same storage shape
+//! the native local-API contract's Threads-store section pins
+//! for `Message.parts` (`unknown[]`, "opaque to local-api, ... the same
+//! 'UIMessage parts' shape the mesh chat wire already uses") — from the
+//! sequence of `UIMessageChunk`-shaped JSON values a [`crate::run::RunHandle`]
+//! yields as [`crate::run::RunEvent::Chunk`]. Pure/stateful, no I/O:
+//! `local-api`'s dispatch route feeds it every chunk it relays over SSE
+//! and persists the final [`PartsAccumulator::finish`] array as the
+//! assistant `Message` row once the run ends.
+//!
+//! Not itself part of the pinned contract (that doc only pins the STORAGE
+//! shape and explicitly treats `parts`' contents as opaque) — the exact
+//! part shapes below (`text`, `reasoning`, `tool-`) are this crate's
+//! own reasonable-effort UIMessage-part reconstruction, introduced in
+//! Phase 2 as the bridge between dispatch's ephemeral SSE stream and the
+//! durable thread store. No test in either e2e suite pins this exact
+//! shape (dispatch's real-harness streaming persistence has no e2e
+//! coverage yet — see the Phase 2 report); this module's own unit tests
+//! are the correctness gate for now.
+//!
+//! ## Resume token convention
+//!
+//! The contract's `Run` entity has no field for the harness CLI's own
+//! session/thread id (needed to `--resume`/`resume ` on the NEXT
+//! turn) — a Phase 0/1 schema gap; widening the pinned `Run`/`Message`
+//! shape is a contract-doc change, out of this Phase 2 module's scope.
+//! Instead, when a session id is known, [`PartsAccumulator::finish`]
+//! appends ONE final pseudo-part:
+//! `{"type":"data-harness-session","harnessId":...,"sessionId":...}` —
+//! fits the EXISTING `parts: unknown[]` column with no schema change; a
+//! future turn recovers it by scanning assistant messages newest-first for
+//! the latest valid token matching the thread's pinned harness. Native chat
+//! also checkpoints a newly observed token on its active SQLite queue claim,
+//! so crash recovery can append this same part before retiring an interrupted
+//! turn.
+
+use std::collections::HashMap;
+
+use serde_json::{json, Value};
+
+#[derive(Debug, Clone)]
+enum OpenPart {
+ Text(String),
+ Reasoning(String),
+ Tool {
+ tool_name: String,
+ input: Option,
+ raw_input: Option,
+ output: Option,
+ error_text: Option,
+ state: &'static str,
+ },
+}
+
+#[derive(Debug, Default)]
+pub struct PartsAccumulator {
+ /// First-seen order of part/tool-call ids — preserves the sequence
+ /// the harness actually produced them in, independent of `HashMap`'s
+ /// unordered iteration.
+ order: Vec,
+ parts: HashMap,
+}
+
+impl PartsAccumulator {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Feed one `UIMessageChunk`-shaped chunk (a [`crate::run::RunEvent::Chunk`]
+ /// payload). Unrecognized `type`s (`start`, `finish`, `finish-step`,
+ /// any future chunk type this accumulator hasn't been taught about)
+ /// are ignored — they don't contribute a `parts` entry, matching how
+ /// AI SDK's own `UIMessage.parts` never includes lifecycle-only
+ /// chunks either.
+ pub fn feed(&mut self, chunk: &Value) {
+ match chunk.get("type").and_then(Value::as_str) {
+ Some("text-start") => self.open_text_like(chunk, false),
+ Some("text-delta") => self.append_text(chunk, false),
+ Some("reasoning-start") => self.open_text_like(chunk, true),
+ Some("reasoning-delta") => self.append_text(chunk, true),
+ Some("tool-input-available") => self.set_tool_input(chunk),
+ Some("tool-input-error") => self.set_tool_input_error(chunk),
+ Some("tool-output-available") => self.set_tool_output(chunk, false),
+ Some("tool-output-error") => self.set_tool_output(chunk, true),
+ _ => {}
+ }
+ }
+
+ fn open_text_like(&mut self, chunk: &Value, reasoning: bool) {
+ let Some(id) = chunk.get("id").and_then(Value::as_str) else {
+ return;
+ };
+ if !self.parts.contains_key(id) {
+ self.order.push(id.to_string());
+ self.parts.insert(
+ id.to_string(),
+ if reasoning {
+ OpenPart::Reasoning(String::new())
+ } else {
+ OpenPart::Text(String::new())
+ },
+ );
+ }
+ }
+
+ fn append_text(&mut self, chunk: &Value, reasoning: bool) {
+ let Some(id) = chunk.get("id").and_then(Value::as_str) else {
+ return;
+ };
+ let delta = chunk.get("delta").and_then(Value::as_str).unwrap_or("");
+ if !self.parts.contains_key(id) {
+ // A delta with no prior `-start` (shouldn't happen from
+ // either mapper, but never lose data) — synthesize the slot.
+ self.order.push(id.to_string());
+ self.parts.insert(
+ id.to_string(),
+ if reasoning {
+ OpenPart::Reasoning(String::new())
+ } else {
+ OpenPart::Text(String::new())
+ },
+ );
+ }
+ match self.parts.get_mut(id) {
+ Some(OpenPart::Text(s)) if !reasoning => s.push_str(delta),
+ Some(OpenPart::Reasoning(s)) if reasoning => s.push_str(delta),
+ // A delta arrived for an id that's open as the OTHER kind
+ // (text vs reasoning) — shouldn't happen (ids are namespaced
+ // by content-block index per mapper), dropped defensively.
+ _ => {}
+ }
+ }
+
+ fn set_tool_input(&mut self, chunk: &Value) {
+ let Some(id) = chunk.get("toolCallId").and_then(Value::as_str) else {
+ return;
+ };
+ let tool_name = chunk
+ .get("toolName")
+ .and_then(Value::as_str)
+ .unwrap_or("")
+ .to_string();
+ let input = chunk.get("input").cloned().unwrap_or(Value::Null);
+ if !self.parts.contains_key(id) {
+ self.order.push(id.to_string());
+ }
+ self.parts.insert(
+ id.to_string(),
+ OpenPart::Tool {
+ tool_name,
+ input: Some(input),
+ raw_input: None,
+ output: None,
+ error_text: None,
+ state: "input-available",
+ },
+ );
+ }
+
+ fn set_tool_input_error(&mut self, chunk: &Value) {
+ let Some(id) = chunk.get("toolCallId").and_then(Value::as_str) else {
+ return;
+ };
+ let tool_name = chunk
+ .get("toolName")
+ .and_then(Value::as_str)
+ .unwrap_or("")
+ .to_string();
+ let raw_input = chunk.get("input").cloned().unwrap_or(Value::Null);
+ let error_text = chunk
+ .get("errorText")
+ .and_then(Value::as_str)
+ .unwrap_or("")
+ .to_string();
+ if !self.parts.contains_key(id) {
+ self.order.push(id.to_string());
+ }
+ self.parts.insert(
+ id.to_string(),
+ OpenPart::Tool {
+ tool_name,
+ // AI SDK v6 represents a static tool-input parse failure
+ // with no typed `input` and preserves the rejected value in
+ // `rawInput`. Keeping the same shape makes a reloaded message
+ // indistinguishable from its live streamed state.
+ input: None,
+ raw_input: Some(raw_input),
+ output: None,
+ error_text: Some(error_text),
+ state: "output-error",
+ },
+ );
+ }
+
+ fn set_tool_output(&mut self, chunk: &Value, is_error: bool) {
+ let Some(id) = chunk.get("toolCallId").and_then(Value::as_str) else {
+ return;
+ };
+ let Some(OpenPart::Tool {
+ output,
+ error_text,
+ state,
+ ..
+ }) = self.parts.get_mut(id)
+ else {
+ // An output for a tool call we never saw the input for —
+ // shouldn't happen (both mappers always emit input-available
+ // before any output), dropped defensively rather than
+ // panicking or fabricating a tool part with no `toolName`.
+ return;
+ };
+ if is_error {
+ *error_text = chunk
+ .get("errorText")
+ .and_then(Value::as_str)
+ .map(str::to_string);
+ *state = "output-error";
+ } else {
+ *output = chunk.get("output").cloned();
+ *state = "output-available";
+ }
+ }
+
+ /// The final reconstructed parts array, in first-seen order.
+ /// `session`, when `Some((harness_id, session_id))`, appends the
+ /// resume-token pseudo-part described in the module doc.
+ pub fn finish(self, session: Option<(&str, &str)>) -> Vec {
+ let PartsAccumulator { order, parts } = self;
+ let mut out: Vec = order
+ .into_iter()
+ .filter_map(|id| parts.get(&id).map(|p| part_to_json(&id, p)))
+ .collect();
+ if let Some((harness_id, session_id)) = session {
+ out.push(json!({
+ "type": "data-harness-session",
+ "harnessId": harness_id,
+ "sessionId": session_id,
+ }));
+ }
+ out
+ }
+}
+
+fn part_to_json(id: &str, part: &OpenPart) -> Value {
+ match part {
+ OpenPart::Text(s) => json!({"type": "text", "text": s}),
+ OpenPart::Reasoning(s) => json!({"type": "reasoning", "text": s}),
+ OpenPart::Tool {
+ tool_name,
+ input,
+ raw_input,
+ output,
+ error_text,
+ state,
+ } => {
+ let mut v = json!({
+ "type": format!("tool-{tool_name}"),
+ "toolCallId": id,
+ "state": state,
+ });
+ if let Some(input) = input {
+ v["input"] = input.clone();
+ }
+ if let Some(raw_input) = raw_input {
+ v["rawInput"] = raw_input.clone();
+ }
+ if let Some(o) = output {
+ v["output"] = o.clone();
+ }
+ if let Some(e) = error_text {
+ v["errorText"] = json!(e);
+ }
+ v
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn accumulates_a_text_part_across_deltas() {
+ let mut acc = PartsAccumulator::new();
+ acc.feed(&json!({"type": "text-start", "id": "0"}));
+ acc.feed(&json!({"type": "text-delta", "id": "0", "delta": "hel"}));
+ acc.feed(&json!({"type": "text-delta", "id": "0", "delta": "lo"}));
+ acc.feed(&json!({"type": "text-end", "id": "0"}));
+ let parts = acc.finish(None);
+ assert_eq!(parts, vec![json!({"type": "text", "text": "hello"})]);
+ }
+
+ #[test]
+ fn accumulates_a_reasoning_part_separately_from_text() {
+ let mut acc = PartsAccumulator::new();
+ acc.feed(&json!({"type": "reasoning-start", "id": "0"}));
+ acc.feed(&json!({"type": "reasoning-delta", "id": "0", "delta": "thinking..."}));
+ acc.feed(&json!({"type": "text-start", "id": "1"}));
+ acc.feed(&json!({"type": "text-delta", "id": "1", "delta": "answer"}));
+ let parts = acc.finish(None);
+ assert_eq!(
+ parts,
+ vec![
+ json!({"type": "reasoning", "text": "thinking..."}),
+ json!({"type": "text", "text": "answer"}),
+ ]
+ );
+ }
+
+ #[test]
+ fn tool_call_transitions_from_input_available_to_output_available() {
+ let mut acc = PartsAccumulator::new();
+ acc.feed(&json!({
+ "type": "tool-input-available",
+ "toolCallId": "t1",
+ "toolName": "Bash",
+ "input": {"command": "echo hi"},
+ }));
+ acc.feed(&json!({
+ "type": "tool-output-available",
+ "toolCallId": "t1",
+ "output": "hi\n",
+ }));
+ let parts = acc.finish(None);
+ assert_eq!(
+ parts,
+ vec![json!({
+ "type": "tool-Bash",
+ "toolCallId": "t1",
+ "state": "output-available",
+ "input": {"command": "echo hi"},
+ "output": "hi\n",
+ })]
+ );
+ }
+
+ #[test]
+ fn tool_call_output_error_sets_error_state_and_text() {
+ let mut acc = PartsAccumulator::new();
+ acc.feed(&json!({
+ "type": "tool-input-available",
+ "toolCallId": "t1",
+ "toolName": "Bash",
+ "input": {},
+ }));
+ acc.feed(&json!({
+ "type": "tool-output-error",
+ "toolCallId": "t1",
+ "errorText": "boom",
+ }));
+ let parts = acc.finish(None);
+ assert_eq!(parts[0]["state"], "output-error");
+ assert_eq!(parts[0]["errorText"], "boom");
+ }
+
+ #[test]
+ fn tool_input_error_updates_an_existing_tool_to_a_terminal_error() {
+ let mut acc = PartsAccumulator::new();
+ acc.feed(&json!({
+ "type": "tool-input-available",
+ "toolCallId": "t1",
+ "toolName": "Bash",
+ "input": {"command": "echo hi"},
+ }));
+ acc.feed(&json!({
+ "type": "tool-input-error",
+ "toolCallId": "t1",
+ "toolName": "Bash",
+ "input": "{\"command\":",
+ "errorText": "input was truncated",
+ }));
+
+ assert_eq!(
+ acc.finish(None),
+ vec![json!({
+ "type": "tool-Bash",
+ "toolCallId": "t1",
+ "state": "output-error",
+ "rawInput": "{\"command\":",
+ "errorText": "input was truncated",
+ })]
+ );
+ }
+
+ #[test]
+ fn direct_tool_input_error_creates_a_persistable_tool_part() {
+ let mut acc = PartsAccumulator::new();
+ acc.feed(&json!({
+ "type": "tool-input-error",
+ "toolCallId": "t-direct",
+ "toolName": "Read",
+ "input": {"file_path": "/partial"},
+ "errorText": "stream ended",
+ }));
+
+ assert_eq!(
+ acc.finish(None),
+ vec![json!({
+ "type": "tool-Read",
+ "toolCallId": "t-direct",
+ "state": "output-error",
+ "rawInput": {"file_path": "/partial"},
+ "errorText": "stream ended",
+ })]
+ );
+ }
+
+ #[test]
+ fn output_for_an_unseen_tool_call_is_dropped_not_fabricated() {
+ let mut acc = PartsAccumulator::new();
+ acc.feed(&json!({"type": "tool-output-available", "toolCallId": "ghost", "output": 1}));
+ assert_eq!(acc.finish(None), Vec::::new());
+ }
+
+ #[test]
+ fn unknown_chunk_types_do_not_produce_a_part() {
+ let mut acc = PartsAccumulator::new();
+ acc.feed(&json!({"type": "start"}));
+ acc.feed(&json!({"type": "finish", "finishReason": "stop"}));
+ assert_eq!(acc.finish(None), Vec::::new());
+ }
+
+ #[test]
+ fn finish_appends_the_session_pseudo_part_when_present() {
+ let mut acc = PartsAccumulator::new();
+ acc.feed(&json!({"type": "text-start", "id": "0"}));
+ acc.feed(&json!({"type": "text-delta", "id": "0", "delta": "hi"}));
+ let parts = acc.finish(Some(("claude-code", "sess-1")));
+ assert_eq!(parts.len(), 2);
+ assert_eq!(
+ parts[1],
+ json!({"type": "data-harness-session", "harnessId": "claude-code", "sessionId": "sess-1"})
+ );
+ }
+
+ #[test]
+ fn finish_omits_the_session_part_when_none() {
+ let acc = PartsAccumulator::new();
+ assert_eq!(acc.finish(None), Vec::::new());
+ }
+
+ #[test]
+ fn order_is_first_seen_not_hashmap_order() {
+ let mut acc = PartsAccumulator::new();
+ acc.feed(&json!({"type": "text-start", "id": "z"}));
+ acc.feed(&json!({"type": "text-delta", "id": "z", "delta": "Z"}));
+ acc.feed(&json!({"type": "text-start", "id": "a"}));
+ acc.feed(&json!({"type": "text-delta", "id": "a", "delta": "A"}));
+ let parts = acc.finish(None);
+ assert_eq!(parts[0]["text"], "Z");
+ assert_eq!(parts[1]["text"], "A");
+ }
+}
diff --git a/apps/native/crates/harness/src/resolve.rs b/apps/native/crates/harness/src/resolve.rs
new file mode 100644
index 0000000000..3b1a67d25c
--- /dev/null
+++ b/apps/native/crates/harness/src/resolve.rs
@@ -0,0 +1,329 @@
+//! Binary resolution — the SHARED CONTRACT between the two Phase 2 agents
+//! working this branch: `local-api` resolves harness binaries via env
+//! overrides `LOCAL_API_CLAUDE_BIN` / `LOCAL_API_CODEX_BIN` (an absolute
+//! path, a bare PATH-relative name, or a JSON array of argv strings —
+//! `'["node", "/path/to/stub-harness.mjs"]'`), falling back to a PATH
+//! lookup of `claude`/`codex` when unset. This module is that
+//! implementation; the differential test suite's stub-harness binary is
+//! the other side of the contract, wired in via these exact env vars.
+//!
+//! The JSON-array-or-plain-string parsing mirrors the convention already
+//! used twice elsewhere in this migration —
+//! `apps/native/e2e/helpers.ts::resolveLocalApiCmd` and
+//! `packages/sandbox/daemon/daemon.e2e.helpers.ts::resolveDaemonCmd` — so a
+//! test author who already knows one of those knows this one.
+
+use std::path::{Path, PathBuf};
+
+/// The two harness CLIs desktop v1 supports (the desktop migration contract decision 1: no
+/// `"decopilot"` in the desktop app).
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum HarnessId {
+ ClaudeCode,
+ Codex,
+}
+
+impl HarnessId {
+ /// Stable order pinned by the contract doc's `GET /models` example:
+ /// `claude-code` then `codex`.
+ pub const ALL: [HarnessId; 2] = [HarnessId::ClaudeCode, HarnessId::Codex];
+
+ /// The wire id used in `harnessId` (dispatch) and `harness` (models).
+ pub fn wire_id(self) -> &'static str {
+ match self {
+ HarnessId::ClaudeCode => "claude-code",
+ HarnessId::Codex => "codex",
+ }
+ }
+
+ pub fn from_wire_id(id: &str) -> Option {
+ match id {
+ "claude-code" => Some(HarnessId::ClaudeCode),
+ "codex" => Some(HarnessId::Codex),
+ _ => None,
+ }
+ }
+
+ /// SHARED CONTRACT env var name.
+ pub fn env_override_var(self) -> &'static str {
+ match self {
+ HarnessId::ClaudeCode => "LOCAL_API_CLAUDE_BIN",
+ HarnessId::Codex => "LOCAL_API_CODEX_BIN",
+ }
+ }
+
+ /// PATH-lookup fallback name when no override is set.
+ pub fn default_binary_name(self) -> &'static str {
+ match self {
+ HarnessId::ClaudeCode => "claude",
+ HarnessId::Codex => "codex",
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ResolveError {
+ /// The env override was set (non-empty) but couldn't be parsed as
+ /// either a JSON string array or used as a plain string.
+ InvalidOverride(String),
+ /// A caller supplied a resume token containing no non-whitespace
+ /// characters. Passing it to either CLI would look like a resume request
+ /// while actually selecting no durable conversation.
+ InvalidSessionId,
+ /// Resolved argv[0] is neither an executable path nor found on PATH.
+ NotFound(String),
+}
+
+impl std::fmt::Display for ResolveError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ ResolveError::InvalidOverride(msg) => write!(f, "{msg}"),
+ ResolveError::InvalidSessionId => {
+ write!(f, "harness session id must not be empty")
+ }
+ ResolveError::NotFound(msg) => write!(f, "{msg}"),
+ }
+ }
+}
+
+impl std::error::Error for ResolveError {}
+
+/// Resolve the argv PREFIX to spawn for `harness`, reading the SHARED
+/// CONTRACT env override from the process environment. Never returns an
+/// empty vec.
+pub fn resolve_argv(harness: HarnessId) -> Result, ResolveError> {
+ let raw = std::env::var(harness.env_override_var()).ok();
+ resolve_argv_from(harness, raw.as_deref())
+}
+
+/// Pure variant of [`resolve_argv`] taking the override value explicitly
+/// instead of reading the environment — this is the one every unit test in
+/// this module calls, so tests never race each other over a shared
+/// process-global env var (`std::env::set_var` is famously not
+/// parallel-test-safe).
+pub fn resolve_argv_from(
+ harness: HarnessId,
+ override_raw: Option<&str>,
+) -> Result, ResolveError> {
+ if let Some(raw) = override_raw {
+ let trimmed = raw.trim();
+ if !trimmed.is_empty() {
+ return parse_override(harness.env_override_var(), trimmed);
+ }
+ }
+ Ok(vec![harness.default_binary_name().to_string()])
+}
+
+fn parse_override(var: &str, trimmed: &str) -> Result, ResolveError> {
+ if trimmed.starts_with('[') {
+ let parsed: Vec = serde_json::from_str(trimmed).map_err(|e| {
+ ResolveError::InvalidOverride(format!("{var} is not a valid JSON string array: {e}"))
+ })?;
+ if parsed.is_empty() {
+ return Err(ResolveError::InvalidOverride(format!(
+ "{var} is an empty argv array"
+ )));
+ }
+ return Ok(parsed);
+ }
+ Ok(vec![trimmed.to_string()])
+}
+
+/// Resolve AND verify argv[0] actually exists/is executable — a fast,
+/// synchronous, no-subprocess check suitable for the dispatch route's
+/// pre-stream "unknown_harness" gate (contract order #7: `lookupHarness`
+/// throws → 400 before any process is spawned). A bare name (no path
+/// separator) is searched against `PATH` by hand, mirroring POSIX
+/// `execvp` search semantics, so a missing CLI surfaces as this crate's
+/// own [`ResolveError::NotFound`] instead of an opaque spawn-time I/O
+/// error the caller would otherwise have to sniff.
+pub fn resolve_checked(harness: HarnessId) -> Result, ResolveError> {
+ let raw = std::env::var(harness.env_override_var()).ok();
+ resolve_checked_from(harness, raw.as_deref())
+}
+
+/// Pure variant of [`resolve_checked`] — see [`resolve_argv_from`] for why.
+pub fn resolve_checked_from(
+ harness: HarnessId,
+ override_raw: Option<&str>,
+) -> Result, ResolveError> {
+ let argv = resolve_argv_from(harness, override_raw)?;
+ let program = Path::new(&argv[0]);
+ if is_executable_path(program) {
+ return Ok(argv);
+ }
+ if !argv[0].contains(std::path::MAIN_SEPARATOR) && which(&argv[0]).is_some() {
+ return Ok(argv);
+ }
+ Err(ResolveError::NotFound(format!(
+ "{} CLI not found: {:?} is neither an executable path nor on PATH",
+ harness.wire_id(),
+ argv[0],
+ )))
+}
+
+fn is_executable_path(path: &Path) -> bool {
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ std::fs::metadata(path)
+ .map(|m| m.is_file() && (m.permissions().mode() & 0o111 != 0))
+ .unwrap_or(false)
+ }
+ #[cfg(not(unix))]
+ {
+ path.is_file()
+ }
+}
+
+/// Manual `PATH` search — POSIX `execvp`-style: for each `PATH` entry,
+/// check `/` is an executable file. Returns the first hit.
+/// Module-public (not just crate-private) since `detect.rs` and tests both
+/// use it directly.
+pub fn which(name: &str) -> Option {
+ let path_var = std::env::var_os("PATH")?;
+ for dir in std::env::split_paths(&path_var) {
+ let candidate = dir.join(name);
+ if is_executable_path(&candidate) {
+ return Some(candidate);
+ }
+ }
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::io::Write;
+
+ fn make_executable(path: &Path) {
+ std::fs::File::create(path)
+ .unwrap()
+ .write_all(b"#!/bin/sh\nexit 0\n")
+ .unwrap();
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
+ }
+ }
+
+ #[test]
+ fn no_override_falls_back_to_default_binary_name() {
+ assert_eq!(
+ resolve_argv_from(HarnessId::ClaudeCode, None).unwrap(),
+ vec!["claude".to_string()]
+ );
+ assert_eq!(
+ resolve_argv_from(HarnessId::Codex, None).unwrap(),
+ vec!["codex".to_string()]
+ );
+ }
+
+ #[test]
+ fn empty_override_falls_back_to_default_binary_name() {
+ assert_eq!(
+ resolve_argv_from(HarnessId::ClaudeCode, Some(" ")).unwrap(),
+ vec!["claude".to_string()]
+ );
+ }
+
+ #[test]
+ fn absolute_path_override_is_used_verbatim() {
+ assert_eq!(
+ resolve_argv_from(HarnessId::ClaudeCode, Some("/opt/claude/bin/claude")).unwrap(),
+ vec!["/opt/claude/bin/claude".to_string()]
+ );
+ }
+
+ #[test]
+ fn json_array_override_becomes_full_argv() {
+ assert_eq!(
+ resolve_argv_from(
+ HarnessId::Codex,
+ Some(r#"["node", "/path/to/stub-harness.mjs", "--codex"]"#)
+ )
+ .unwrap(),
+ vec![
+ "node".to_string(),
+ "/path/to/stub-harness.mjs".to_string(),
+ "--codex".to_string()
+ ]
+ );
+ }
+
+ #[test]
+ fn invalid_json_array_override_errors() {
+ let err = resolve_argv_from(HarnessId::ClaudeCode, Some("[not json")).unwrap_err();
+ assert!(matches!(err, ResolveError::InvalidOverride(_)));
+ }
+
+ #[test]
+ fn empty_json_array_override_errors() {
+ let err = resolve_argv_from(HarnessId::ClaudeCode, Some("[]")).unwrap_err();
+ assert!(matches!(err, ResolveError::InvalidOverride(_)));
+ }
+
+ #[test]
+ fn resolve_checked_accepts_an_existing_executable_override() {
+ let dir = tempfile::tempdir().unwrap();
+ let bin = dir.path().join("fake-claude");
+ make_executable(&bin);
+ let argv =
+ resolve_checked_from(HarnessId::ClaudeCode, Some(bin.to_str().unwrap())).unwrap();
+ assert_eq!(argv, vec![bin.to_str().unwrap().to_string()]);
+ }
+
+ #[test]
+ fn resolve_checked_rejects_a_nonexistent_override_path() {
+ let err = resolve_checked_from(
+ HarnessId::ClaudeCode,
+ Some("/definitely/not/a/real/path/claude"),
+ )
+ .unwrap_err();
+ assert!(matches!(err, ResolveError::NotFound(_)));
+ }
+
+ #[test]
+ fn resolve_checked_rejects_a_non_executable_file() {
+ let dir = tempfile::tempdir().unwrap();
+ let bin = dir.path().join("not-executable");
+ std::fs::write(&bin, b"nope").unwrap();
+ let err =
+ resolve_checked_from(HarnessId::ClaudeCode, Some(bin.to_str().unwrap())).unwrap_err();
+ assert!(matches!(err, ResolveError::NotFound(_)));
+ }
+
+ #[test]
+ fn which_finds_a_binary_placed_on_a_synthetic_path() {
+ let dir = tempfile::tempdir().unwrap();
+ let bin = dir.path().join("totally-fake-cli-12345");
+ make_executable(&bin);
+ let joined = std::env::join_paths([dir.path()]).unwrap();
+ let found = which_with_path("totally-fake-cli-12345", &joined);
+ assert_eq!(found, Some(bin));
+ }
+
+ #[test]
+ fn which_returns_none_for_a_name_absent_from_path() {
+ let dir = tempfile::tempdir().unwrap();
+ let joined = std::env::join_paths([dir.path()]).unwrap();
+ assert_eq!(
+ which_with_path("nonexistent-cli-98765", &joined),
+ None::
+ );
+ }
+
+ /// Test-only helper: `which()`'s search logic against an explicit
+ /// `PATH` value rather than the process environment (see the module
+ /// doc's parallel-test-safety rationale).
+ fn which_with_path(name: &str, path_var: &std::ffi::OsStr) -> Option {
+ for dir in std::env::split_paths(path_var) {
+ let candidate = dir.join(name);
+ if is_executable_path(&candidate) {
+ return Some(candidate);
+ }
+ }
+ None
+ }
+}
diff --git a/apps/native/crates/harness/src/run.rs b/apps/native/crates/harness/src/run.rs
new file mode 100644
index 0000000000..25a6c6bc3f
--- /dev/null
+++ b/apps/native/crates/harness/src/run.rs
@@ -0,0 +1,1981 @@
+//! Ties [`crate::resolve`] + [`crate::tiers`] + [`crate::spawn`] +
+//! [`crate::events`] together into the one thing `local-api`'s dispatch
+//! route actually drives: resolve → build argv → spawn (always plain
+//! pipes — see `spawn.rs`'s module doc) → parse ndjson line-by-line →
+//! yield [`RunEvent`]s, with cancellation support.
+//!
+//! ## Two-phase API (mirrors the dispatch contract's pre-stream gate)
+//!
+//! the native local-API contract's Dispatch Lifecycle pins
+//! `lookupHarness(harnessId) throws` as a PRE-stream gate (#7): a 400
+//! `unknown_harness` response BEFORE the SSE response even starts, so it
+//! must be resolved BEFORE any `200`/streaming headers are written. Once
+//! that gate passes, the caller is committed to a `200` SSE stream — any
+//! LATER failure (the resolved binary vanishes between the gate and the
+//! actual spawn, a permission error, the CLI itself crashing) has to
+//! surface as an in-stream `error` event, not a different HTTP status.
+//!
+//! [`build_argv`] is that gate: pure, synchronous (no process spawned),
+//! calls [`resolve::resolve_checked`] and fails exactly the way an
+//! `unknown_harness` gate should. [`start`] takes the now-resolved argv
+//! and is effectively infallible from the CALLER's point of view — any
+//! spawn-time failure after this point becomes the first (and only)
+//! [`RunEvent::FatalError`] on the returned handle's stream instead of a
+//! `Result::Err`.
+//!
+//! ## Stdin is always closed
+//!
+//! Every harness spawn closes stdin (`Stdio::null()`, set in
+//! `spawn::plain::spawn_plain`). This is not incidental: `codex exec`
+//! reads stdin as additional prompt content when it's a pipe and hangs
+//! waiting for EOF if left open — observed empirically on this machine
+//! (`codex exec --json ""` printed a `"Reading additional input
+//! from stdin..."` banner and read after the prompt argument was already
+//! provided, purely because stdin wasn't closed). Neither CLI needs stdin
+//! for the single-turn, non-interactive invocations this crate makes.
+
+use std::path::PathBuf;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
+use std::time::Instant;
+
+use serde_json::Value;
+use tokio::sync::{mpsc, Mutex};
+
+use crate::events::claude::ClaudeEventMapper;
+use crate::events::codex::CodexEventMapper;
+use crate::events::{EventMapper, FlushReason, MappedEvent};
+use crate::resolve::{self, HarnessId, ResolveError};
+use crate::spawn::{self, ExitInfo, ProcessLease, SpawnRequest};
+use crate::tiers;
+
+/// Mirrors `packages/harness/src/types.ts::ToolApprovalLevel`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ToolApproval {
+ Auto,
+ Readonly,
+}
+
+impl ToolApproval {
+ /// `"auto"` / `"readonly"` — the two values dispatch's pre-stream gate
+ /// (`validate_harness_input`, `routes/dispatch.rs`) already restricts
+ /// `input.toolApprovalLevel` to.
+ pub fn from_wire(level: &str) -> Self {
+ if level == "readonly" {
+ ToolApproval::Readonly
+ } else {
+ ToolApproval::Auto
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct RunSpec {
+ pub harness: HarnessId,
+ pub cwd: Option,
+ /// Extracted user-visible text (see [`extract_user_text`]) — the
+ /// CLI's positional prompt argument.
+ pub prompt: String,
+ /// `input.harness.sessionId` from a prior turn, if resuming.
+ pub resume_session_id: Option,
+ /// Composite model id (`input.models.thinking.id`, e.g.
+ /// `"claude-code:sonnet"`).
+ pub model_id: String,
+ pub tool_approval: ToolApproval,
+ /// `input.mode == "plan"` — same tool restrictions as `readonly` in
+ /// headless CLI mode, mirrors `isPlanMode` in
+ /// `packages/harness/src/claude-code/index.ts`.
+ pub plan_mode: bool,
+ /// Extra system-prompt text to APPEND to whatever the CLI already uses —
+ /// today the organization-filesystem block (see
+ /// `local-api::sandbox::org_prompt`).
+ ///
+ /// The two CLIs take this differently and the difference is load-bearing:
+ /// `claude` has a dedicated `--append-system-prompt` that adds to its own
+ /// default, while `codex` has no system-prompt flag at all and instead
+ /// takes `-c developer_instructions=…`, which SETS that layer rather than
+ /// appending. Nothing else populates codex's developer instructions today,
+ /// so this is additive in practice — but a second block would have to be
+ /// composed into one string rather than passed separately.
+ pub append_system_prompt: Option,
+ /// The agent's own MCP endpoint, so the CLI can call the agent's tools.
+ ///
+ /// Cluster runs get a per-run minted key; the desktop instead points the
+ /// CLI at THIS process, which proxies `/mcp/*` upstream with the
+ /// Keychain-backed token. That means the credential is local-api's own
+ /// control cookie, and local-api's embedded guard also demands the exact
+ /// `Origin` — hence both travel together.
+ pub mcp: Option,
+}
+
+/// Where the agent's MCP lives and what it takes to be let in.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct McpEndpoint {
+ pub url: String,
+ pub cookie: String,
+ pub origin: String,
+ /// Root certificate `url`'s TLS chains to, when it is not a public one.
+ /// claude is a Bun/BoringSSL binary that consults neither the macOS
+ /// keychain (where this CA is trusted) nor anything beyond its bundled
+ /// Mozilla roots — without being handed this file it cannot open `url`
+ /// at all and the agent silently loses every MCP tool.
+ pub ca_cert: Option,
+}
+
+/// Env var names codex is pointed at, so no secret appears in its argv.
+const MCP_URL_ENV: &str = "DECOCMS_MCP_URL";
+const MCP_COOKIE_ENV: &str = "DECOCMS_MCP_COOKIE";
+const MCP_ORIGIN_ENV: &str = "DECOCMS_MCP_ORIGIN";
+
+/// How the CA in [`McpEndpoint::ca_cert`] reaches the claude child. Node's
+/// (and Bun's) documented "additional CAs" variable — it EXTENDS the runtime's
+/// bundled roots, so the CLI's own `api.anthropic.com` connection is
+/// untouched. Never `SSL_CERT_FILE`, which REPLACES the root store for
+/// OpenSSL/rustls-native-certs consumers and would cut a child off from the
+/// public internet. codex needs neither: rustls-platform-verifier consults
+/// the keychain, where this CA is already trusted.
+const NODE_EXTRA_CA_CERTS_ENV: &str = "NODE_EXTRA_CA_CERTS";
+
+/// The environment the CLI child runs with. Secrets reach it here (codex) or
+/// via a 0600 file (claude), never argv — `ps` shows argv to every local
+/// process. Merged over the inherited environment by the spawn layer.
+fn mcp_child_env(spec: &RunSpec) -> Vec<(String, String)> {
+ let mut env = Vec::new();
+ if let Some(mcp) = spec.mcp.as_ref() {
+ env.push((MCP_URL_ENV.to_string(), mcp.url.clone()));
+ env.push((MCP_COOKIE_ENV.to_string(), mcp.cookie.clone()));
+ env.push((MCP_ORIGIN_ENV.to_string(), mcp.origin.clone()));
+ if let Some(ca_cert) = mcp.ca_cert.as_ref() {
+ env.push((
+ NODE_EXTRA_CA_CERTS_ENV.to_string(),
+ ca_cert.display().to_string(),
+ ));
+ }
+ }
+ env
+}
+
+/// The one MCP server name both CLIs see. Matches the cluster harness's
+/// `cms` key, so a prompt that refers to it means the same thing either side.
+const MCP_SERVER_NAME: &str = "cms";
+
+/// claude's `--mcp-config` document.
+///
+/// Every value is a `${VAR}` reference, so this string is safe to pass on the
+/// command line: the environment supplies the url, cookie and origin.
+pub fn claude_mcp_config() -> String {
+ serde_json::json!({
+ "mcpServers": {
+ MCP_SERVER_NAME: {
+ "type": "http",
+ "url": format!("${{{MCP_URL_ENV}}}"),
+ "headers": {
+ "Cookie": format!("${{{MCP_COOKIE_ENV}}}"),
+ "Origin": format!("${{{MCP_ORIGIN_ENV}}}"),
+ },
+ }
+ }
+ })
+ .to_string()
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum RunEvent {
+ /// Internal continuation checkpoint. Consumers must persist this before
+ /// acknowledging any later output from the same process; it is not an
+ /// AI-SDK wire chunk and must never be forwarded to the webview.
+ SessionId { session_id: String },
+ /// → dispatch's `{"type":"ui-message-chunk","chunk":}`.
+ Chunk(Value),
+ /// → dispatch's `{"type":"error","code":"harness_crashed",...}`. At
+ /// most one per run, always the LAST event before the channel closes.
+ FatalError { message: String },
+}
+
+/// Extracts the visible text from `input.userMessage` — a UIMessage-shaped
+/// `{ parts: [...] }` object opaque to the dispatch route (see the
+/// contract doc's Threads-store section: "the same 'UIMessage parts'
+/// shape the mesh chat wire already uses"). Joins every
+/// `{"type":"text","text":...}` part with newlines; non-text parts
+/// (images, tool calls) are ignored. Mirrors
+/// `packages/harness/src/cli-message-prep.ts::extractUserText`'s
+/// text-only extraction, simplified for dispatch's single-message (not
+/// full-history) input shape.
+pub fn extract_user_text(user_message: &Value) -> String {
+ let Some(parts) = user_message.get("parts").and_then(Value::as_array) else {
+ // Tolerate a bare string as the whole message too — defensive,
+ // since `userMessage` is opaque to the dispatch route's own
+ // schema (`chatMessageSchema = z.record(...)` on the TS side).
+ return user_message.as_str().unwrap_or_default().to_string();
+ };
+ parts
+ .iter()
+ .filter(|p| p.get("type").and_then(Value::as_str) == Some("text"))
+ .filter_map(|p| p.get("text").and_then(Value::as_str))
+ .collect::>()
+ .join("\n")
+}
+
+/// The pre-stream gate: resolves `spec.harness`'s binary (SHARED CONTRACT
+/// env overrides, PATH fallback) and builds the full argv, WITHOUT
+/// spawning anything. `Err` here is dispatch's `unknown_harness` 400 gate
+/// (contract order #7) — an unresolvable/missing CLI is treated the same
+/// as an unrecognized `harnessId`, since from the caller's point of view
+/// both mean "this run cannot start."
+pub fn build_argv(spec: &RunSpec) -> Result, ResolveError> {
+ let resume_session_id = normalized_resume_session_id(spec)?;
+ let mut argv = resolve::resolve_checked(spec.harness)?;
+ match spec.harness {
+ HarnessId::ClaudeCode => append_claude_args(&mut argv, spec, resume_session_id),
+ HarnessId::Codex => append_codex_args(&mut argv, spec, resume_session_id),
+ }
+ Ok(argv)
+}
+
+fn normalized_resume_session_id(spec: &RunSpec) -> Result