diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml
index da7b72c2..a5b7b1ec 100644
--- a/.github/workflows/claude-review.yml
+++ b/.github/workflows/claude-review.yml
@@ -172,7 +172,7 @@ jobs:
"dream-server/installers/"
"dream-server/dream-cli"
"dream-server/config/"
- "dream-server/extensions/services/dashboard-api/security.py"
+ "dream-server/extensions/services/dashboard-api/crates/dashboard-api/src/middleware.rs"
".github/workflows/"
".env"
"docker-compose"
diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml
index 2f8f65c4..11db78bd 100644
--- a/.github/workflows/dashboard.yml
+++ b/.github/workflows/dashboard.yml
@@ -43,18 +43,21 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- - name: Setup Python
- uses: actions/setup-python@v5
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo registry and build
+ uses: actions/cache@v4
with:
- python-version: "3.11"
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ dream-server/extensions/services/dashboard-api/target
+ key: ${{ runner.os }}-cargo-${{ hashFiles('dream-server/extensions/services/dashboard-api/Cargo.lock') }}
+ restore-keys: ${{ runner.os }}-cargo-
- name: API Syntax Check
- run: python -m py_compile main.py agent_monitor.py
-
- - name: Install Dependencies
- run: |
- pip install -r requirements.txt
- pip install -r tests/requirements-test.txt
+ run: cargo check --workspace
- name: Run Unit Tests
- run: pytest tests/ -v --cov=. --cov-report=term --cov-report=lcov:coverage.lcov
+ run: cargo test --workspace -- --test-threads=1
diff --git a/.gitignore b/.gitignore
index bc86d901..1cbb3115 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,6 +53,7 @@ dream-server/models/
dream-server/config/openclaw/workspace/
dream-server/**/node_modules/
dream-server/**/dist/
+dream-server/**/target/
dream-server/**/.coverage
dream-server/preflight-*.log
dream-server/.current-mode
diff --git a/dream-server/Makefile b/dream-server/Makefile
index 713a32e5..01656668 100644
--- a/dream-server/Makefile
+++ b/dream-server/Makefile
@@ -12,8 +12,8 @@ help: ## Show this help
lint: ## Syntax check all shell scripts + Python compile check
@echo "=== Shell syntax ==="
@fail=0; for f in $(SHELL_FILES); do bash -n "$$f" || fail=1; done; [ $$fail -eq 0 ]
- @echo "=== Python compile ==="
- @python3 -m py_compile extensions/services/dashboard-api/main.py extensions/services/dashboard-api/agent_monitor.py
+ @echo "=== Dashboard API (Rust) ==="
+ @cd extensions/services/dashboard-api && cargo check --workspace 2>&1 | tail -1
@echo "All lint checks passed."
test: ## Run unit and contract tests
diff --git a/dream-server/docs/COMPOSABILITY-EXECUTION-BOARD.md b/dream-server/docs/COMPOSABILITY-EXECUTION-BOARD.md
index edeb210c..aab271dc 100644
--- a/dream-server/docs/COMPOSABILITY-EXECUTION-BOARD.md
+++ b/dream-server/docs/COMPOSABILITY-EXECUTION-BOARD.md
@@ -134,14 +134,14 @@ Effort: 3-5 days
Files:
- `extensions/schema/service-manifest.v1.json` (new)
- `extensions/services/*.yaml` (new examples)
-- [`dashboard-api/main.py`](../extensions/services/dashboard-api/main.py)
+- [`dashboard-api/crates/dashboard-api/src/config.rs`](../extensions/services/dashboard-api/crates/dashboard-api/src/config.rs)
Acceptance:
- API can load service definitions from manifests.
- Health checks and feature cards reference manifest data, not hardcoded lists.
Progress notes:
- Added `extensions/schema/service-manifest.v1.json`.
- Added example manifests in `extensions/services/` for inference, voice, workflows, vector DB, and image generation services.
-- `dashboard-api/main.py` now loads and merges service/feature definitions from manifests with safe fallback defaults.
+- `dashboard-api` (Rust/Axum rewrite) loads and merges service/feature definitions from manifests with safe fallback defaults.
Milestone W4-M2 (PR-7): Environment schema and validation
Status: `IN_PROGRESS`
@@ -189,7 +189,7 @@ Owner: Frontend + API
Effort: 2-3 days
Files:
- [`dashboard/src/pages/Dashboard.jsx`](../extensions/services/dashboard/src/pages/Dashboard.jsx)
-- [`dashboard-api/main.py`](../extensions/services/dashboard-api/main.py)
+- [`dashboard-api/crates/dashboard-api/src/main.rs`](../extensions/services/dashboard-api/crates/dashboard-api/src/main.rs)
Acceptance:
- Feature tiles derive from API metadata.
- Ports/URLs are not hardcoded in JSX.
@@ -207,13 +207,13 @@ Owner: API + Docs
Effort: 1-2 days
Files:
- `config/n8n/catalog.json` (planned; not yet created)
-- [`dashboard-api/main.py`](../extensions/services/dashboard-api/main.py)
+- [`dashboard-api/crates/dashboard-api/src/routes/workflows.rs`](../extensions/services/dashboard-api/crates/dashboard-api/src/routes/workflows.rs)
- [INTEGRATION-GUIDE.md](INTEGRATION-GUIDE.md)
Acceptance:
- One canonical workflow path in code/docs.
- Catalog supports both templates and metadata cleanly.
Progress notes:
-- `dashboard-api/main.py` now resolves workflows from canonical `config/n8n` with legacy `workflows/` fallback.
+- `dashboard-api` (Rust) resolves workflows from canonical `config/n8n` with legacy `workflows/` fallback.
- Workflow catalog loading now validates structure and returns normalized fallback data on malformed input.
- `docs/INTEGRATION-GUIDE.md` updated to reference `config/n8n/*.json` and `config/n8n/catalog.json`.
@@ -285,7 +285,7 @@ Owner: Release
Effort: 2-3 days
Files:
- `manifest.json` (new)
-- [`dashboard-api/main.py`](../extensions/services/dashboard-api/main.py)
+- [`dashboard-api/crates/dashboard-api/src/main.rs`](../extensions/services/dashboard-api/crates/dashboard-api/src/main.rs)
- [`dream-update.sh`](../dream-update.sh)
Acceptance:
- Update path validates version compatibility and rollback point.
diff --git a/dream-server/docs/EXTENSIONS.md b/dream-server/docs/EXTENSIONS.md
index 3b55a33a..4251a2ce 100644
--- a/dream-server/docs/EXTENSIONS.md
+++ b/dream-server/docs/EXTENSIONS.md
@@ -349,7 +349,7 @@ AMD ROCm requires additional container configuration compared to NVIDIA:
## Testing Checklist (PR Gate)
- `bash -n` on changed shell files
-- `python3 -m py_compile dashboard-api/main.py`
+- `cargo check --workspace` (in `extensions/services/dashboard-api/`)
- `bash tests/integration-test.sh`
- relevant smoke scripts in `tests/smoke/`
- if dashboard code changed and Node is available:
diff --git a/dream-server/docs/HOST-AGENT-API.md b/dream-server/docs/HOST-AGENT-API.md
index 804d4992..aa6e9966 100644
--- a/dream-server/docs/HOST-AGENT-API.md
+++ b/dream-server/docs/HOST-AGENT-API.md
@@ -148,6 +148,6 @@ Protections in place:
## How the Dashboard API Calls It
-The Dashboard API (`extensions/services/dashboard-api/routers/extensions.py`) communicates with the host agent via the `AGENT_URL` environment variable (constructed from `DREAM_AGENT_HOST` and `DREAM_AGENT_PORT` in `config.py`). It uses `DREAM_AGENT_KEY` for authentication. The connection flows through Docker's `host.docker.internal` DNS name by default, allowing the containerized API to reach the host-bound agent.
+The Dashboard API (Rust binary at `extensions/services/dashboard-api/`) communicates with the host agent via the `DREAM_AGENT_URL` environment variable (constructed from `DREAM_AGENT_HOST` and `DREAM_AGENT_PORT`). It uses `DREAM_AGENT_KEY` for authentication. The connection flows through Docker's `host.docker.internal` DNS name by default, allowing the containerized API to reach the host-bound agent.
If the host agent is unreachable, mutation operations (install, enable, disable) still succeed at the file level but return `"restart_required": true` to signal that `dream restart` is needed.
diff --git a/dream-server/extensions/services/dashboard-api/.dockerignore b/dream-server/extensions/services/dashboard-api/.dockerignore
index b8e3bba6..626eec96 100644
--- a/dream-server/extensions/services/dashboard-api/.dockerignore
+++ b/dream-server/extensions/services/dashboard-api/.dockerignore
@@ -1,3 +1,6 @@
+# Rust
+target/
+
# Python
__pycache__/
*.py[cod]
diff --git a/dream-server/extensions/services/dashboard-api/Cargo.lock b/dream-server/extensions/services/dashboard-api/Cargo.lock
new file mode 100644
index 00000000..7f3fc905
--- /dev/null
+++ b/dream-server/extensions/services/dashboard-api/Cargo.lock
@@ -0,0 +1,2603 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[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 = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "assert-json-diff"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[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 = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "axum"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
+dependencies = [
+ "axum-core",
+ "bytes",
+ "form_urlencoded",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde_core",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "cc"
+version = "1.2.59"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[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 = "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 = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "dashboard-api"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "axum",
+ "base64",
+ "chrono",
+ "dirs",
+ "dream-common",
+ "futures",
+ "hostname",
+ "http",
+ "http-body-util",
+ "hyper",
+ "libc",
+ "moka",
+ "rand",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "shellexpand",
+ "tempfile",
+ "thiserror",
+ "tokio",
+ "tower",
+ "tower-http",
+ "tracing",
+ "tracing-subscriber",
+ "wiremock",
+]
+
+[[package]]
+name = "deadpool"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b"
+dependencies = [
+ "deadpool-runtime",
+ "lazy_static",
+ "num_cpus",
+ "tokio",
+]
+
+[[package]]
+name = "deadpool-runtime"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
+
+[[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 = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dream-common"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "axum",
+ "http-body-util",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "thiserror",
+ "tokio",
+]
+
+[[package]]
+name = "dream-scripts"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "base64",
+ "clap",
+ "dream-common",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "shellexpand",
+ "tempfile",
+ "tokio",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[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 = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[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 = "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-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[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 = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[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 = "hostname"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "windows-link",
+]
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[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.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
+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.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "system-configuration",
+ "tokio",
+ "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",
+]
+
+[[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 = "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 = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "iri-string"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.184"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
+
+[[package]]
+name = "libredox"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
+dependencies = [
+ "libc",
+]
+
+[[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 = "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.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[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.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mio"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "moka"
+version = "0.12.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046"
+dependencies = [
+ "async-lock",
+ "crossbeam-channel",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "equivalent",
+ "event-listener",
+ "futures-util",
+ "parking_lot",
+ "portable-atomic",
+ "smallvec",
+ "tagptr",
+ "uuid",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[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-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "openssl"
+version = "0.10.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.112"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[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",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[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 = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+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.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[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",
+]
+
+[[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 = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[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",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "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 = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags",
+ "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 = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[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_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",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+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_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_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[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 = "shellexpand"
+version = "3.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8"
+dependencies = [
+ "dirs",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[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 = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[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 = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+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",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
+dependencies = [
+ "bitflags",
+ "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 = "tagptr"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.2",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[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",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tokio"
+version = "1.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[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-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 = "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.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "iri-string",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[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",
+]
+
+[[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 = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[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",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
+dependencies = [
+ "getrandom 0.4.2",
+ "js-sys",
+ "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 = "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.2+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.67"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[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",
+ "windows-result",
+ "windows-strings",
+]
+
+[[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",
+]
+
+[[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",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-registry"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
+dependencies = [
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[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.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[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.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[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.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "wiremock"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
+dependencies = [
+ "assert-json-diff",
+ "base64",
+ "deadpool",
+ "futures",
+ "http",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "log",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_json",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "yoke"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
+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",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
+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",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[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",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/dream-server/extensions/services/dashboard-api/Cargo.toml b/dream-server/extensions/services/dashboard-api/Cargo.toml
new file mode 100644
index 00000000..d93a6cf7
--- /dev/null
+++ b/dream-server/extensions/services/dashboard-api/Cargo.toml
@@ -0,0 +1,7 @@
+[workspace]
+resolver = "2"
+members = [
+ "crates/dashboard-api",
+ "crates/dream-common",
+ "crates/dream-scripts",
+]
diff --git a/dream-server/extensions/services/dashboard-api/Dockerfile b/dream-server/extensions/services/dashboard-api/Dockerfile
index fddefe66..d8208f53 100644
--- a/dream-server/extensions/services/dashboard-api/Dockerfile
+++ b/dream-server/extensions/services/dashboard-api/Dockerfile
@@ -1,40 +1,67 @@
-# Dream Server Dashboard API
+# Dream Server Dashboard API — Rust/Axum
# Lightweight system status backend for the Dashboard UI
+#
+# Multi-stage build: compile in a full Rust image, copy the binary into a
+# minimal runtime image. Final image is ~30 MB (vs ~350 MB for Python).
-FROM python:3.11-slim
+# ── Stage 1: Build ────────────────────────────────────────────────────────────
+FROM rust:1.86-slim-bookworm AS builder
-LABEL org.opencontainers.image.source="https://github.com/Light-Heart-Labs/DreamServer"
-LABEL org.opencontainers.image.description="Dream Server Dashboard API"
-
-WORKDIR /app
-
-# Install system deps for GPU metrics access
RUN apt-get update && apt-get install -y --no-install-recommends \
- curl \
+ pkg-config libssl-dev \
&& rm -rf /var/lib/apt/lists/*
-# Install Python dependencies
-COPY requirements.txt .
-RUN pip install --no-cache-dir -r requirements.txt
+WORKDIR /build
-# Copy application
-COPY main.py config.py models.py security.py gpu.py helpers.py agent_monitor.py ./
-COPY routers/ routers/
+# Cache dependency builds: copy manifests first, create stub sources, build deps
+COPY Cargo.toml Cargo.lock ./
+COPY crates/dream-common/Cargo.toml crates/dream-common/Cargo.toml
+COPY crates/dashboard-api/Cargo.toml crates/dashboard-api/Cargo.toml
+COPY crates/dream-scripts/Cargo.toml crates/dream-scripts/Cargo.toml
-# Non-root user
+RUN mkdir -p crates/dream-common/src crates/dashboard-api/src crates/dream-scripts/src \
+ && echo "pub mod error; pub mod manifest; pub mod models;" > crates/dream-common/src/lib.rs \
+ && echo "fn main() {}" > crates/dashboard-api/src/main.rs \
+ && echo "fn main() {}" > crates/dream-scripts/src/main.rs \
+ && echo "" > crates/dream-scripts/src/lib.rs \
+ && cargo build --release --workspace 2>/dev/null || true
+
+# Copy actual source and build for real
+COPY crates/ crates/
+# Touch sources so cargo sees them as newer than the stubs
+RUN find crates -name "*.rs" -exec touch {} + \
+ && cargo build --release --workspace
+
+# ── Stage 2: Runtime ──────────────────────────────────────────────────────────
+FROM debian:bookworm-slim
+
+LABEL org.opencontainers.image.source="https://github.com/Light-Heart-Labs/DreamServer"
+LABEL org.opencontainers.image.description="Dream Server Dashboard API (Rust)"
+
+# Runtime deps: curl for healthcheck, ca-certificates for HTTPS
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ curl ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
+
+# Non-root user (matches Python image)
RUN useradd -m -u 1000 dreamer
-# B1 fix: Create /data directory and make it writable for API key generation
+# Create /data directory for API key generation (B1 fix parity)
RUN mkdir -p /data && chown dreamer:dreamer /data
+WORKDIR /app
+
+# Copy binaries from builder
+COPY --from=builder /build/target/release/dashboard-api /app/dashboard-api
+COPY --from=builder /build/target/release/dream-scripts /app/dream-scripts
+
USER dreamer
ENV DASHBOARD_API_PORT=3002
-# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD curl -f http://localhost:${DASHBOARD_API_PORT}/health || exit 1
EXPOSE ${DASHBOARD_API_PORT}
-CMD uvicorn main:app --host 0.0.0.0 --port ${DASHBOARD_API_PORT}
+CMD ["/app/dashboard-api"]
diff --git a/dream-server/extensions/services/dashboard-api/README.md b/dream-server/extensions/services/dashboard-api/README.md
index ed1ce23b..70e6b40f 100644
--- a/dream-server/extensions/services/dashboard-api/README.md
+++ b/dream-server/extensions/services/dashboard-api/README.md
@@ -1,10 +1,10 @@
# dashboard-api
-FastAPI backend providing system status, metrics, and management for Dream Server
+Rust/Axum backend providing system status, metrics, and management for Dream Server
## Overview
-The Dashboard API is a Python FastAPI service that powers the Dream Server Dashboard UI. It exposes endpoints for GPU metrics, service health monitoring, LLM inference stats, workflow management, agent monitoring, setup wizard, version checking, and Privacy Shield control.
+The Dashboard API is a Rust service built with Axum that powers the Dream Server Dashboard UI. It exposes endpoints for GPU metrics, service health monitoring, LLM inference stats, workflow management, agent monitoring, setup wizard, version checking, and Privacy Shield control.
It runs at `http://localhost:3002` and is the single backend used by the React dashboard frontend.
@@ -21,6 +21,7 @@ It runs at `http://localhost:3002` and is the single backend used by the React d
- **Privacy Shield control**: Enable/disable container, fetch PII scrubbing statistics
- **Version checking**: GitHub releases integration for update notifications
- **Storage reporting**: Breakdown of disk usage by models, vector DB, and total data
+- **Extension management**: Install, enable, disable, uninstall extensions from the portal
## Configuration
@@ -30,6 +31,7 @@ Environment variables (set in `.env`):
|----------|---------|-------------|
| `DASHBOARD_API_PORT` | `3002` | External + internal port |
| `DASHBOARD_API_KEY` | *(auto-generated)* | API key for all protected endpoints. If unset, a random key is generated and written to `/data/dashboard-api-key.txt` |
+| `DREAM_VERSION` | *(from Cargo.toml)* | Dream Server version reported by `/api/version` |
| `GPU_BACKEND` | `nvidia` | GPU backend: `nvidia` or `amd` |
| `OLLAMA_URL` | `http://llama-server:8080` | LLM backend URL |
| `LLM_MODEL` | `qwen3:30b-a3b` | Active model name shown in dashboard |
@@ -75,15 +77,20 @@ Environment variables (set in `.env`):
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/api/workflows` | Yes | Workflow catalog with install status |
+| `GET` | `/api/workflows/{id}` | Yes | Get a specific workflow template |
| `POST` | `/api/workflows/{id}/enable` | Yes | Import and activate a workflow in n8n |
-| `DELETE` | `/api/workflows/{id}` | Yes | Remove a workflow from n8n |
+| `POST` | `/api/workflows/{id}/disable` | Yes | Remove a workflow from n8n |
+| `DELETE` | `/api/workflows/{id}` | Yes | Remove a workflow from n8n (alias) |
| `GET` | `/api/workflows/{id}/executions` | Yes | Recent execution history |
+| `GET` | `/api/workflows/categories` | Yes | Workflow categories from catalog |
+| `GET` | `/api/workflows/n8n/status` | Yes | n8n availability check |
### Features
| Method | Path | Auth | Description |
|--------|------|------|-------------|
-| `GET` | `/api/features` | Yes | Feature status with hardware recommendations |
+| `GET` | `/api/features` | Yes | Feature list from manifests |
+| `GET` | `/api/features/status` | Yes | Feature status with service health |
| `GET` | `/api/features/{id}/enable` | Yes | Enable instructions for a feature |
### Setup Wizard
@@ -103,9 +110,10 @@ Environment variables (set in `.env`):
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/api/agents/metrics` | Yes | Full agent metrics (sessions, tokens, cost) |
-| `GET` | `/api/agents/metrics.html` | Yes | Agent metrics as HTML fragment (htmx) |
| `GET` | `/api/agents/cluster` | Yes | Cluster health and GPU node status |
| `GET` | `/api/agents/throughput` | Yes | Throughput stats (tokens/sec) |
+| `GET` | `/api/agents/sessions` | Yes | Active agent sessions |
+| `POST` | `/api/agents/chat` | Yes | Agent chat endpoint |
### Privacy Shield
@@ -120,22 +128,23 @@ Environment variables (set in `.env`):
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/api/version` | Yes | Current version + GitHub update check |
-| `GET` | `/api/releases/manifest` | No | Recent release history from GitHub |
+| `GET` | `/api/releases/manifest` | Yes | Recent release history from GitHub |
+| `GET` | `/api/update/dry-run` | Yes | Preview update actions |
| `POST` | `/api/update` | Yes | Trigger update actions (`check`, `backup`, `update`) |
### Extensions
| Method | Path | Auth | Description |
|--------|------|------|-------------|
-| `GET` | `/api/extensions/catalog` | Yes | Browse extension catalog with status, filterable by `category` and `gpu_compatible` query params |
-| `GET` | `/api/extensions/{service_id}` | Yes | Detailed info for a single extension (manifest, features, env vars, setup instructions) |
-| `POST` | `/api/extensions/{service_id}/install` | Yes | Install an extension from the extensions library into user-extensions |
-| `POST` | `/api/extensions/{service_id}/enable` | Yes | Enable a disabled extension (renames `compose.yaml.disabled` to `compose.yaml`, starts container via host agent) |
-| `POST` | `/api/extensions/{service_id}/disable` | Yes | Disable an enabled extension (stops container via host agent, renames `compose.yaml` to `compose.yaml.disabled`) |
-| `DELETE` | `/api/extensions/{service_id}` | Yes | Uninstall a disabled extension (removes its directory from user-extensions) |
-| `POST` | `/api/extensions/{service_id}/logs` | Yes | Fetch container logs via the host agent (last 100 lines) |
+| `GET` | `/api/extensions/catalog` | Yes | Browse extension catalog with status |
+| `GET` | `/api/extensions/{service_id}` | Yes | Detailed info for a single extension |
+| `POST` | `/api/extensions/{service_id}/install` | Yes | Install an extension from the library |
+| `POST` | `/api/extensions/{service_id}/enable` | Yes | Enable a disabled extension |
+| `POST` | `/api/extensions/{service_id}/disable` | Yes | Disable an enabled extension |
+| `DELETE` | `/api/extensions/{service_id}` | Yes | Uninstall a disabled extension |
+| `POST` | `/api/extensions/{service_id}/logs` | Yes | Fetch container logs via the host agent |
-Core services cannot be installed, enabled, disabled, or uninstalled via these endpoints (returns 403). The catalog endpoint also reports whether the [host agent](../../docs/HOST-AGENT-API.md) is available (`agent_available` field).
+Core services cannot be installed, enabled, disabled, or uninstalled via these endpoints. The catalog endpoint also reports whether the [host agent](../../docs/HOST-AGENT-API.md) is available.
## Authentication
@@ -152,34 +161,58 @@ When `DASHBOARD_API_KEY` is empty (default), all endpoints are accessible withou
```
Dashboard UI (:3001)
- │
- ▼
-Dashboard API (:3002)
- ├── gpu.py ──────────────── nvidia-smi / sysfs AMD
- ├── helpers.py ──────────── Docker-network health checks
- ├── agent_monitor.py ─────── Background metrics collection
- └── routers/
- ├── workflows.py ────── n8n API integration
- ├── features.py ─────── Hardware-aware feature discovery
- ├── setup.py ─────────── Setup wizard + persona system
- ├── updates.py ──────── GitHub releases + dream-update.sh
- ├── agents.py ───────── Agent session + throughput metrics
- ├── privacy.py ──────── Privacy Shield container control
- └── extensions.py ───── Extension catalog, install, enable/disable, logs
+ |
+ v
+Dashboard API (:3002) [Rust/Axum binary]
+ |-- gpu.rs --------------- nvidia-smi / sysfs AMD
+ |-- helpers.rs ----------- Docker-network health checks
+ |-- agent_monitor.rs ----- Background metrics collection
+ |-- middleware.rs -------- API key authentication
+ |-- config.rs ------------ Manifest loading + env config
+ +-- routes/
+ |-- workflows.rs ---- n8n API integration
+ |-- features.rs ----- Hardware-aware feature discovery
+ |-- setup.rs -------- Setup wizard + persona system
+ |-- updates.rs ------ GitHub releases + dream-update.sh
+ |-- agents.rs ------- Agent session + throughput metrics
+ |-- privacy.rs ------ Privacy Shield container control
+ |-- extensions.rs --- Extension catalog, install, enable/disable, uninstall
+ |-- services.rs ----- Core service endpoints (gpu, disk, model, status)
+ |-- settings.rs ----- Service tokens, external links, storage
+ |-- preflight.rs ---- Docker/GPU/port/disk preflight checks
+ |-- status.rs ------- Aggregated dashboard status
+ +-- health.rs ------- Health check endpoint
```
-## Files
-
-- `main.py` — FastAPI application, core endpoints, startup
-- `config.py` — Shared configuration and manifest loading
-- `models.py` — Pydantic response schemas
-- `security.py` — API key authentication
-- `gpu.py` — GPU detection for NVIDIA and AMD
-- `helpers.py` — Service health checks, LLM metrics, system metrics
-- `agent_monitor.py` — Background agent metrics collection
-- `routers/` — Endpoint modules (workflows, features, setup, updates, agents, privacy, extensions)
-- `Dockerfile` — Container definition
-- `requirements.txt` — Python dependencies
+## Workspace Structure
+
+The API is a Cargo workspace with three crates:
+
+- `crates/dashboard-api/` — Main binary and library (Axum routes, state, middleware)
+- `crates/dream-common/` — Shared types (manifest structs, service config, models)
+- `crates/dream-scripts/` — CLI scripts and utilities
+
+## Building
+
+```bash
+# Development build
+cargo build --workspace
+
+# Release build (used in Docker)
+cargo build --release --workspace
+
+# Run tests
+cargo test --workspace -- --test-threads=1
+
+# Check without building
+cargo check --workspace
+```
+
+## Dockerfile
+
+Multi-stage Rust build producing a minimal (~25 MB) distroless image:
+1. Builder stage: `rust:1-slim` with cargo build --release
+2. Runtime stage: `gcr.io/distroless/cc-debian12` with non-root user
## Troubleshooting
diff --git a/dream-server/extensions/services/dashboard-api/agent_monitor.py b/dream-server/extensions/services/dashboard-api/agent_monitor.py
deleted file mode 100644
index ad38751f..00000000
--- a/dream-server/extensions/services/dashboard-api/agent_monitor.py
+++ /dev/null
@@ -1,196 +0,0 @@
-"""
-Agent Monitoring Module for Dashboard API
-Collects real-time metrics on agent swarms, sessions, and throughput.
-"""
-
-import asyncio
-import json
-import logging
-from datetime import datetime, timedelta
-from typing import List
-import os
-
-import aiohttp
-
-logger = logging.getLogger(__name__)
-
-TOKEN_SPY_URL = os.environ.get("TOKEN_SPY_URL", "http://token-spy:8080")
-TOKEN_SPY_API_KEY = os.environ.get("TOKEN_SPY_API_KEY", "")
-
-
-class AgentMetrics:
- """Real-time agent monitoring metrics"""
-
- def __init__(self):
- self.last_update = datetime.now()
- self.session_count = 0
- self.tokens_per_second = 0.0 # no data source located; use throughput.get_stats() for live rate
- self.error_rate_1h = 0.0
- self.queue_depth = 0 # no data source located; llama-server /health does not expose queued requests
-
- def to_dict(self) -> dict:
- return {
- "session_count": self.session_count,
- "tokens_per_second": round(self.tokens_per_second, 2),
- "error_rate_1h": round(self.error_rate_1h, 2),
- "queue_depth": self.queue_depth,
- "last_update": self.last_update.isoformat()
- }
-
-
-class ClusterStatus:
- """Cluster health and node status"""
-
- def __init__(self):
- self.nodes: List[dict] = []
- self.failover_ready = False
- self.total_gpus = 0
- self.active_gpus = 0
-
- async def refresh(self):
- """Query cluster status from smart proxy"""
- logger.debug("Refreshing cluster status from proxy")
- try:
- proc = await asyncio.create_subprocess_exec(
- "curl", "-s", "--max-time", "4", f"http://localhost:{os.environ.get('CLUSTER_PROXY_PORT', '9199')}/status",
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.PIPE
- )
- stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
-
- if proc.returncode == 0:
- data = json.loads(stdout.decode())
- self.nodes = data.get("nodes", [])
- self.total_gpus = len(self.nodes)
- self.active_gpus = sum(1 for n in self.nodes if n.get("healthy", False))
- self.failover_ready = self.active_gpus > 1
- logger.debug("Cluster status: %d/%d GPUs active, failover_ready=%s",
- self.active_gpus, self.total_gpus, self.failover_ready)
- except FileNotFoundError:
- logger.debug("Cluster proxy not available: curl command not found")
- except asyncio.TimeoutError:
- proc.kill()
- await proc.wait()
- logger.debug("Cluster proxy health check timed out after 5s")
- except OSError as e:
- logger.debug("Cluster proxy connection failed: %s", e)
- except json.JSONDecodeError as e:
- logger.warning("Cluster proxy returned invalid JSON: %s", e)
-
- def to_dict(self) -> dict:
- return {
- "nodes": self.nodes,
- "total_gpus": self.total_gpus,
- "active_gpus": self.active_gpus,
- "failover_ready": self.failover_ready
- }
-
-
-class ThroughputMetrics:
- """Real-time throughput tracking"""
-
- def __init__(self, history_minutes: int = 15):
- self.history_minutes = history_minutes
- self.data_points: List[dict] = []
-
- def add_sample(self, tokens_per_sec: float):
- """Add a new throughput sample"""
- self.data_points.append({
- "timestamp": datetime.now().isoformat(),
- "tokens_per_sec": tokens_per_sec
- })
-
- # Prune old data
- cutoff = datetime.now() - timedelta(minutes=self.history_minutes)
- self.data_points = [
- p for p in self.data_points
- if datetime.fromisoformat(p["timestamp"]) > cutoff
- ]
-
- def get_stats(self) -> dict:
- """Get throughput statistics"""
- if not self.data_points:
- return {"current": 0, "average": 0, "peak": 0, "history": []}
-
- values = [p["tokens_per_sec"] for p in self.data_points]
- return {
- "current": values[-1] if values else 0,
- "average": sum(values) / len(values),
- "peak": max(values) if values else 0,
- "history": self.data_points[-30:] # Last 30 points
- }
-
-
-# Global metrics instances
-agent_metrics = AgentMetrics()
-cluster_status = ClusterStatus()
-throughput = ThroughputMetrics()
-
-
-async def _fetch_token_spy_metrics() -> None:
- """Pull per-agent session count and throughput from Token Spy /api/summary."""
- if not TOKEN_SPY_URL:
- logger.debug("Token Spy URL not configured, skipping metrics fetch")
- return
- logger.debug("Fetching metrics from Token Spy at %s", TOKEN_SPY_URL)
- try:
- headers = {}
- if TOKEN_SPY_API_KEY:
- headers["Authorization"] = f"Bearer {TOKEN_SPY_API_KEY}"
- timeout = aiohttp.ClientTimeout(total=5)
- async with aiohttp.ClientSession(timeout=timeout) as http:
- async with http.get(
- f"{TOKEN_SPY_URL}/api/summary",
- headers=headers,
- ) as resp:
- if resp.status == 200:
- data = await resp.json()
- agent_metrics.session_count = len(data)
- # total_output_tokens is a 24 h aggregate; dividing by 3600 gives
- # an average tokens/sec over the last hour (approximation)
- total_out = sum(r.get("total_output_tokens", 0) or 0 for r in data)
- throughput.add_sample(total_out / 3600.0)
- logger.debug("Token Spy metrics: %d sessions, %d total output tokens",
- len(data), total_out)
- else:
- logger.debug("Token Spy returned status %d", resp.status)
- except aiohttp.ClientError as e:
- logger.debug("Token Spy unavailable: %s", e)
- except asyncio.TimeoutError:
- logger.debug("Token Spy request timed out after 5s")
- except aiohttp.ContentTypeError as e:
- logger.warning("Token Spy returned unexpected content type: %s", e)
-
-
-async def collect_metrics():
- """Background task to collect metrics periodically"""
- while True:
- try:
- # Update cluster status
- await cluster_status.refresh()
-
- # Update agent session count and throughput from Token Spy
- await _fetch_token_spy_metrics()
-
- agent_metrics.last_update = datetime.now()
-
- except FileNotFoundError as e:
- logger.debug("Metrics collection failed: command not found - %s", e)
- except asyncio.TimeoutError:
- logger.debug("Metrics collection timed out")
- except OSError as e:
- logger.debug("Metrics collection OS error: %s", e)
- except json.JSONDecodeError as e:
- logger.warning("Metrics collection JSON decode error: %s", e)
-
- await asyncio.sleep(5) # Update every 5 seconds
-
-
-def get_full_agent_metrics() -> dict:
- """Get all agent monitoring metrics as a dict"""
- return {
- "timestamp": datetime.now().isoformat(),
- "agent": agent_metrics.to_dict(),
- "cluster": cluster_status.to_dict(),
- "throughput": throughput.get_stats()
- }
diff --git a/dream-server/extensions/services/dashboard-api/config.py b/dream-server/extensions/services/dashboard-api/config.py
deleted file mode 100644
index a1b2ef0b..00000000
--- a/dream-server/extensions/services/dashboard-api/config.py
+++ /dev/null
@@ -1,297 +0,0 @@
-"""Shared configuration and manifest loading for Dream Server Dashboard API."""
-
-import json
-import logging
-import os
-from pathlib import Path
-from typing import Any
-
-import yaml
-
-logger = logging.getLogger(__name__)
-
-# --- Paths ---
-
-INSTALL_DIR = os.environ.get("DREAM_INSTALL_DIR", os.path.expanduser("~/dream-server"))
-DATA_DIR = os.environ.get("DREAM_DATA_DIR", os.path.expanduser("~/.dream-server"))
-EXTENSIONS_DIR = Path(
- os.environ.get(
- "DREAM_EXTENSIONS_DIR",
- str(Path(INSTALL_DIR) / "extensions" / "services")
- )
-)
-
-DEFAULT_SERVICE_HOST = os.environ.get("SERVICE_HOST", "host.docker.internal")
-GPU_BACKEND = os.environ.get("GPU_BACKEND", "nvidia")
-
-
-def _read_env_from_file(key: str) -> str:
- """Read a variable from the .env file when not available in process environment."""
- env_path = Path(INSTALL_DIR) / ".env"
- try:
- for line in env_path.read_text().splitlines():
- if line.startswith(f"{key}="):
- return line.split("=", 1)[1].strip().strip("\"'")
- except OSError:
- pass
- return ""
-
-
-# --- Manifest Loading ---
-
-
-def _read_manifest_file(path: Path) -> dict[str, Any]:
- """Load a JSON or YAML extension manifest file."""
- text = path.read_text()
- if path.suffix.lower() == ".json":
- data = json.loads(text)
- else:
- data = yaml.safe_load(text)
- if not isinstance(data, dict):
- raise ValueError("Manifest root must be an object")
- return data
-
-
-def load_extension_manifests(
- manifest_dir: Path, gpu_backend: str,
-) -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]], list[dict[str, str]]]:
- """Load service and feature definitions from extension manifests.
-
- Returns a 3-tuple: (services, features, errors) where *errors* is a list
- of ``{"file": ..., "error": ...}`` dicts for manifests that failed to load.
- """
- services: dict[str, dict[str, Any]] = {}
- features: list[dict[str, Any]] = []
- errors: list[dict[str, str]] = []
- loaded = 0
-
- if not manifest_dir.exists():
- logger.info("Extension manifest directory not found: %s", manifest_dir)
- return services, features, errors
-
- manifest_files: list[Path] = []
- for item in sorted(manifest_dir.iterdir()):
- if item.is_dir():
- for name in ("manifest.yaml", "manifest.yml", "manifest.json"):
- candidate = item / name
- if candidate.exists():
- manifest_files.append(candidate)
- break
- elif item.suffix.lower() in (".yaml", ".yml", ".json"):
- manifest_files.append(item)
-
- for path in manifest_files:
- try:
- # Skip disabled extensions (compose.yaml.disabled convention)
- ext_dir = path.parent
- if (ext_dir / "compose.yaml.disabled").exists() or (ext_dir / "compose.yml.disabled").exists():
- logger.debug("Skipping disabled extension: %s", ext_dir.name)
- continue
-
- manifest = _read_manifest_file(path)
- if manifest.get("schema_version") != "dream.services.v1":
- logger.warning("Skipping manifest with unsupported schema_version: %s", path)
- errors.append({"file": str(path), "error": "Unsupported schema_version"})
- continue
-
- service = manifest.get("service")
- if isinstance(service, dict):
- service_id = service.get("id")
- if not service_id:
- raise ValueError("service.id is required")
- supported = service.get("gpu_backends", ["amd", "nvidia", "apple"])
- if gpu_backend == "apple":
- if service.get("type") == "host-systemd":
- continue # Linux-only service, not available on macOS
- # All docker services run on macOS regardless of gpu_backends declaration
- elif gpu_backend not in supported and "all" not in supported:
- continue
-
- host_env = service.get("host_env")
- default_host = service.get("default_host", "localhost")
- host = os.environ.get(host_env, default_host) if host_env else default_host
-
- ext_port_env = service.get("external_port_env")
- ext_port_default = service.get("external_port_default", service.get("port", 0))
- if ext_port_env:
- val = os.environ.get(ext_port_env) or _read_env_from_file(ext_port_env)
- external_port = int(val) if val else int(ext_port_default)
- else:
- external_port = int(ext_port_default)
-
- services[service_id] = {
- "host": host,
- "port": int(service.get("port", 0)),
- "external_port": external_port,
- "health": service.get("health", "/health"),
- "name": service.get("name", service_id),
- "ui_path": service.get("ui_path", "/"),
- **({"type": service["type"]} if "type" in service else {}),
- **({"health_port": int(service["health_port"])} if "health_port" in service else {}),
- }
-
- manifest_features = manifest.get("features", [])
- if isinstance(manifest_features, list):
- for feature in manifest_features:
- if not isinstance(feature, dict):
- continue
- supported = feature.get("gpu_backends", ["amd", "nvidia", "apple"])
- if gpu_backend != "apple" and gpu_backend not in supported and "all" not in supported:
- continue
- if feature.get("id") and feature.get("name"):
- missing = [f for f in ("description", "icon", "category", "setup_time", "priority") if f not in feature]
- if missing:
- logger.warning("Feature '%s' in %s missing optional fields: %s", feature["id"], path, ", ".join(missing))
- features.append(feature)
-
- loaded += 1
- except (yaml.YAMLError, json.JSONDecodeError, OSError, KeyError, TypeError, ValueError) as e:
- logger.warning("Failed loading manifest %s: %s", path, e)
- errors.append({"file": str(path), "error": str(e)})
-
- logger.info("Loaded %d extension manifests (%d services, %d features)", loaded, len(services), len(features))
- return services, features, errors
-
-
-# --- Service Registry ---
-
-MANIFEST_SERVICES, MANIFEST_FEATURES, MANIFEST_ERRORS = load_extension_manifests(EXTENSIONS_DIR, GPU_BACKEND)
-SERVICES = MANIFEST_SERVICES
-if not SERVICES:
- logger.error("No services loaded from manifests in %s — dashboard will have no services", EXTENSIONS_DIR)
-
-# Lemonade serves at /api/v1 instead of llama.cpp's /v1. Override the
-# health path so the dashboard poll loop hits the correct endpoint.
-LLM_BACKEND = os.environ.get("LLM_BACKEND", "")
-if LLM_BACKEND == "lemonade" and "llama-server" in SERVICES:
- SERVICES["llama-server"]["health"] = "/api/v1/health"
- logger.info("Lemonade backend detected — overriding llama-server health to /api/v1/health")
-
-# --- Features ---
-
-FEATURES = MANIFEST_FEATURES
-if not FEATURES:
- logger.warning("No features loaded from manifests — check %s", EXTENSIONS_DIR)
-
-# --- Workflow Config ---
-
-
-def resolve_workflow_dir() -> Path:
- """Resolve canonical workflow directory with legacy fallback."""
- env_dir = os.environ.get("WORKFLOW_DIR")
- if env_dir:
- return Path(env_dir)
- canonical = Path(INSTALL_DIR) / "config" / "n8n"
- if canonical.exists():
- return canonical
- return Path(INSTALL_DIR) / "workflows"
-
-
-WORKFLOW_DIR = resolve_workflow_dir()
-WORKFLOW_CATALOG_FILE = WORKFLOW_DIR / "catalog.json"
-DEFAULT_WORKFLOW_CATALOG = {"workflows": [], "categories": {}}
-
-def _default_n8n_url() -> str:
- cfg = SERVICES.get("n8n", {})
- host = cfg.get("host", "n8n")
- port = cfg.get("port", 5678)
- return f"http://{host}:{port}"
-
-N8N_URL = os.environ.get("N8N_URL", _default_n8n_url())
-N8N_API_KEY = os.environ.get("N8N_API_KEY", "")
-
-# --- Setup / Personas ---
-
-SETUP_CONFIG_DIR = Path(DATA_DIR) / "config"
-
-PERSONAS = {
- "general": {
- "name": "General Helper",
- "system_prompt": "You are a friendly and helpful AI assistant. You're knowledgeable, patient, and aim to be genuinely useful. Keep responses clear and conversational.",
- "icon": "\U0001f4ac"
- },
- "coding": {
- "name": "Coding Buddy",
- "system_prompt": "You are a skilled programmer and technical assistant. You write clean, well-documented code and explain technical concepts clearly. You're precise, thorough, and love solving problems.",
- "icon": "\U0001f4bb"
- },
- "creative": {
- "name": "Creative Writer",
- "system_prompt": "You are an imaginative creative writer and storyteller. You craft vivid descriptions, engaging narratives, and think outside the box. You're expressive and enjoy wordplay.",
- "icon": "\U0001f3a8"
- }
-}
-
-# --- Sidebar Icons ---
-
-SIDEBAR_ICONS = {
- "open-webui": "MessageSquare",
- "n8n": "Network",
- "openclaw": "Bot",
- "opencode": "Code",
- "perplexica": "Search",
- "comfyui": "Image",
- "token-spy": "Terminal",
- "langfuse": "BarChart2",
-}
-
-# --- Extensions Portal ---
-
-CATALOG_PATH = Path(os.environ.get(
- "DREAM_EXTENSIONS_CATALOG",
- str(Path(INSTALL_DIR) / "config" / "extensions-catalog.json")
-))
-
-EXTENSIONS_LIBRARY_DIR = Path(os.environ.get(
- "DREAM_EXTENSIONS_LIBRARY_DIR",
- str(Path(DATA_DIR) / "extensions-library")
-))
-
-USER_EXTENSIONS_DIR = Path(os.environ.get(
- "DREAM_USER_EXTENSIONS_DIR",
- str(Path(DATA_DIR) / "user-extensions")
-))
-
-def _load_core_service_ids() -> frozenset:
- core_ids_path = Path(INSTALL_DIR) / "config" / "core-service-ids.json"
- if core_ids_path.exists():
- try:
- return frozenset(json.loads(core_ids_path.read_text(encoding="utf-8")))
- except (json.JSONDecodeError, OSError):
- pass
- # Fallback to hardcoded list
- return frozenset({
- "dashboard-api", "dashboard", "llama-server", "open-webui",
- "litellm", "langfuse", "n8n", "openclaw", "opencode",
- "perplexica", "searxng", "qdrant", "tts", "whisper",
- "embeddings", "token-spy", "comfyui", "ape", "privacy-shield",
- })
-
-
-CORE_SERVICE_IDS = _load_core_service_ids()
-
-
-def load_extension_catalog() -> list[dict]:
- """Load the static extensions catalog JSON. Returns empty list on failure."""
- if not CATALOG_PATH.exists():
- logger.info("Extensions catalog not found at %s", CATALOG_PATH)
- return []
- try:
- data = json.loads(CATALOG_PATH.read_text(encoding="utf-8"))
- return data.get("extensions", [])
- except (json.JSONDecodeError, OSError) as e:
- logger.warning("Failed to load extensions catalog: %s", e)
- return []
-
-
-EXTENSION_CATALOG = load_extension_catalog()
-
-# --- Host Agent ---
-
-AGENT_HOST = os.environ.get("DREAM_AGENT_HOST", "host.docker.internal")
-AGENT_PORT = int(os.environ.get("DREAM_AGENT_PORT", "7710"))
-AGENT_URL = f"http://{AGENT_HOST}:{AGENT_PORT}"
-DASHBOARD_API_KEY = os.environ.get("DASHBOARD_API_KEY", "")
-# Prefer dedicated DREAM_AGENT_KEY; fall back to DASHBOARD_API_KEY for
-# existing installs that haven't generated a separate key yet.
-DREAM_AGENT_KEY = os.environ.get("DREAM_AGENT_KEY", "") or DASHBOARD_API_KEY
diff --git a/dream-server/extensions/services/dashboard-api/crates/dashboard-api/Cargo.toml b/dream-server/extensions/services/dashboard-api/crates/dashboard-api/Cargo.toml
new file mode 100644
index 00000000..0ef27ddb
--- /dev/null
+++ b/dream-server/extensions/services/dashboard-api/crates/dashboard-api/Cargo.toml
@@ -0,0 +1,44 @@
+[package]
+name = "dashboard-api"
+version = "0.1.0"
+edition = "2021"
+
+[[bin]]
+name = "dashboard-api"
+path = "src/main.rs"
+
+[lib]
+name = "dashboard_api"
+path = "src/lib.rs"
+
+[dependencies]
+dream-common = { path = "../dream-common" }
+axum = "0.8"
+tokio = { version = "1", features = ["full"] }
+tower = "0.5"
+tower-http = { version = "0.6", features = ["cors", "trace"] }
+reqwest = { version = "0.12", features = ["json"] }
+moka = { version = "0.12", features = ["future"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+serde_yaml = "0.9"
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+thiserror = "2"
+anyhow = "1"
+chrono = { version = "0.4", features = ["serde"] }
+rand = "0.9"
+base64 = "0.22"
+shellexpand = "3"
+dirs = "6"
+hostname = "0.4"
+futures = "0.3"
+http = "1"
+libc = "0.2"
+
+[dev-dependencies]
+tower = { version = "0.5", features = ["util"] }
+http-body-util = "0.1"
+hyper = "1"
+tempfile = "3"
+wiremock = "0.6"
diff --git a/dream-server/extensions/services/dashboard-api/crates/dashboard-api/src/agent_monitor.rs b/dream-server/extensions/services/dashboard-api/crates/dashboard-api/src/agent_monitor.rs
new file mode 100644
index 00000000..1d4acead
--- /dev/null
+++ b/dream-server/extensions/services/dashboard-api/crates/dashboard-api/src/agent_monitor.rs
@@ -0,0 +1,413 @@
+//! Agent monitoring module — collects real-time metrics on agent swarms,
+//! sessions, and throughput. Mirrors `agent_monitor.py`.
+
+use serde_json::json;
+use std::sync::Mutex;
+use tracing::debug;
+
+// ---------------------------------------------------------------------------
+// Global metrics (module-level singletons matching the Python globals)
+// ---------------------------------------------------------------------------
+
+static AGENT_METRICS: Mutex