diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index afbf7e8..73bb157 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -80,6 +80,12 @@ jobs: uv run --package intentframe-integrations-cli python tests/intentframe_integrations/test_scoped_governance_yaml.py uv run --package intentframe-integrations-cli python tests/intentframe_integrations/test_actions_manifest.py + - name: Control plane unit tests + run: | + uv run --package intentframe-control-plane python tests/intentframe_control_plane/test_lifecycle.py + uv run --package intentframe-control-plane python tests/intentframe_control_plane/test_read_models.py + uv run --package intentframe-control-plane python tests/intentframe_control_plane/test_server.py + - name: Hermes gateway unit tests run: | uv run --package intentframe-integrations-cli python tests/hermes_gateway/test_isolation.py @@ -109,6 +115,8 @@ jobs: npm ci npm run build test -f if-integration-clients/typescript/dist/index.js + cd intentframe-control-plane/web && npm ci && npm run build + test -f ../src/intentframe_control_plane/static/index.html docker-install: name: Docker install (curl | bash) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c161f1..d91e6ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,10 +25,22 @@ git clone https://github.com/intentframe/agent-integrations.git cd agent-integrations uv sync --all-packages npm ci && npm run build +cd intentframe-control-plane/web && npm ci && npm run build ``` The dev launcher is `./bin/intentframe-integrations`. +### Control plane frontend + +When you change `intentframe-control-plane/web/`, rebuild and **commit** the output: + +```bash +cd intentframe-control-plane/web && npm run build +git add ../src/intentframe_control_plane/static/ +``` + +Static assets under `src/intentframe_control_plane/static/` are git-tracked so installs and Docker work without Node.js. CI verifies `static/index.html` exists after build. + ## Running tests ```bash @@ -49,6 +61,7 @@ Install regression tests: bash tests/install/test_ref_resolution.sh bash tests/install/test_installer_bootstrap_docker.sh bash tests/install/test_installer_curl_docker.sh +bash tests/docker/test_control_plane_smoke.sh # local throwaway CP lifecycle smoke ``` ## Project structure @@ -56,10 +69,13 @@ bash tests/install/test_installer_curl_docker.sh | Path | Purpose | |------|---------| | `intentframe-integrations-cli/` | User-facing CLI | +| `intentframe-control-plane/` | Operator UI (React) + FastAPI server on :9720 | | `if-integration-backend/` | Validate-only IntentFrame runtime supervisor | | `if-integration-clients/` | Bridge clients (Python + TypeScript) | | `integrations/hermes/` | Hermes plugin, adapter, governance templates | | `integrations/_template/` | Scaffold for new agent integrations | +| `tests/intentframe_control_plane/` | Control plane lifecycle + API unit tests | +| `tests/docker/` | Production-like Docker user journey (CP :9720 + chat :9119) | | `tests/` | Unit, install, Docker, and gateway E2E tests | Use `uv sync --all-packages` from the repo root. Do not install workspace members diff --git a/README.md b/README.md index 05f6190..6ffb161 100644 --- a/README.md +++ b/README.md @@ -68,22 +68,24 @@ No git clone required: curl -fsSL https://github.com/intentframe/agent-integrations/raw/main/scripts/install-hermes-plugin.sh | bash ``` -Then run Hermes with IntentFrame: +Then open the IntentFrame Control Plane (started by the installer): + +```text +http://127.0.0.1:9720 +``` + +Use it to configure keys, start the enforcement stack, manage governed tools, and load policy. + +Hermes chat (separate, after the stack is up): ```bash -export OPENAI_API_KEY=sk-... -intentframe-integrations up hermes hermes dashboard ``` -Open: - ```text -http://localhost:9119/chat +http://127.0.0.1:9119/chat ``` -Ask Hermes to run a terminal command. If the tool is governed, IntentFrame validates it before Hermes executes it. - --- ## See It Work @@ -248,6 +250,12 @@ curl -fsSL https://github.com/intentframe/agent-integrations/raw/main/scripts/in curl -fsSL https://github.com/intentframe/agent-integrations/raw/main/scripts/install-hermes-plugin.sh | bash -s -- --headless ``` +**Skip control plane during install** (Docker/CI — entrypoint starts it separately): + +```bash +curl -fsSL .../install-hermes-plugin.sh | bash -s -- --headless --no-control-plane +``` + From a git clone (same flags): ```bash @@ -257,7 +265,7 @@ bash scripts/install-hermes-plugin.sh --headless **Pinned release** (script URL and pack ref should match): ```bash -curl -fsSL https://github.com/intentframe/agent-integrations/raw/v0.2.0/scripts/install-hermes-plugin.sh | bash -s -- --ref v0.2.0 +curl -fsSL https://github.com/intentframe/agent-integrations/raw/v0.2.1/scripts/install-hermes-plugin.sh | bash -s -- --ref v0.2.1 ``` After headless install, set `OPENAI_API_KEY` (and run `hermes setup` if chat returns 401). Then the same [three commands](#run-three-commands) as below. @@ -310,16 +318,18 @@ Full tables (what is / is not removed): [docs/hermes-cli.md#uninstall](docs/herm ## Status and Resources -**Current release:** [v0.2.0](https://github.com/intentframe/agent-integrations/releases/tag/v0.2.0) +**Current release:** [v0.2.1](https://github.com/intentframe/agent-integrations/releases/tag/v0.2.1) **Integration maturity:** Hermes plugin + adapter + CLI; Docker E2E; known uninstall caveats on root/FHS — [limitations](docs/hermes-known-limitations.md). ### Documentation | Doc | Audience | |-----|----------| +| [docs/intentframe-control-plane.md](docs/intentframe-control-plane.md) | Operator UI — ports, frontend, health checks, Docker | | [docs/hermes-cli.md](docs/hermes-cli.md) | CLI commands — governance, policy, gateway, env vars | | [docs/hermes-known-limitations.md](docs/hermes-known-limitations.md) | Install/uninstall caveats and roadmap | | [docs/hermes-intentframe-integration-guide.md](docs/hermes-intentframe-integration-guide.md) | Architecture, adding tools, troubleshooting | +| [tests/docker/README.md](tests/docker/README.md) | Docker user journey (:9720 control plane + :9119 chat) | | [tests/docker/logs/](tests/docker/logs/README.md) | Captured Docker chat + gating audit sessions (example probes) | | [integrations/hermes/README.md](integrations/hermes/README.md) | Monorepo dev reference | | [IntentFrame](https://github.com/intentframe/intentframe) | Core runtime — threat model, principles, Actor SDK | @@ -342,10 +352,12 @@ uv sync --all-packages | Path | Purpose | |------|---------| | `intentframe-integrations-cli/` | `intentframe-integrations` CLI | +| `intentframe-control-plane/` | Operator UI (React + FastAPI on :9720) | | `integrations/hermes/` | Plugin, adapter, governance templates | | `integrations/_template/` | Scaffold for adding a new agent integration | | `if-integration-backend/` | IntentFrame runtime supervisor | | `if-integration-clients/` | Bridge clients (Python + TypeScript) | +| `tests/intentframe_control_plane/` | Control plane unit tests | | `tests/hermes_gateway/` | Opt-in gateway E2E (isolated sandbox) | | `tests/docker/` | Production-like Docker user journey | | `tests/docker/logs/` | Captured manual gating sessions (chat + audit trail) | diff --git a/RELEASE.json b/RELEASE.json index 5509955..3d849d7 100644 --- a/RELEASE.json +++ b/RELEASE.json @@ -1,6 +1,6 @@ { - "version": "0.2.0", - "tag": "v0.2.0", + "version": "0.2.1", + "tag": "v0.2.1", "product": "agent-integrations", - "description": "IntentFrame Hermes integration pack release metadata. Tag v0.2.0 should match this file." + "description": "IntentFrame Hermes integration pack release metadata. Tag v0.2.1 should match this file." } diff --git a/docs/hermes-cli.md b/docs/hermes-cli.md index 32d4379..29e969e 100644 --- a/docs/hermes-cli.md +++ b/docs/hermes-cli.md @@ -29,6 +29,8 @@ Skips Hermes setup wizard and browser engine. You must set `OPENAI_API_KEY` your curl -fsSL https://github.com/intentframe/agent-integrations/raw/main/scripts/install-hermes-plugin.sh | bash -s -- --headless ``` +Docker test harness also passes `--no-control-plane` so the entrypoint can bind `0.0.0.0:9720` before starting the UI. See [tests/docker/README.md](../tests/docker/README.md). + From a git clone: ```bash @@ -44,7 +46,7 @@ Use the **same ref** in the script URL and `--ref` (or `REF=`). `VERSION=` is a |------|---------| | Latest | `curl -fsSL …/raw/main/scripts/install-hermes-plugin.sh \| bash` | | Branch (pre-merge) | `curl -fsSL …/raw/my-branch/… \| bash -s -- --ref my-branch --headless` | -| Release tag | `curl -fsSL …/raw/v0.2.0/… \| bash -s -- --ref v0.2.0` | +| Release tag | `curl -fsSL …/raw/v0.2.1/… \| bash -s -- --ref v0.2.1` | | Commit SHA | `curl -fsSL …/raw//… \| bash -s -- --ref ` | After install, `intentframe-integrations doctor hermes` shows the pinned ref from `~/.intentframe/agent-integrations/.install-manifest.json`. @@ -90,13 +92,23 @@ command -v hermes ## Happy path +After [install](hermes-cli.md#install), the installer starts the **IntentFrame Control Plane** at `http://127.0.0.1:9720`. + ```bash +# Control plane (operator UI — started by installer) +open http://127.0.0.1:9720 + +# From Control Plane or CLI: start enforcement stack export OPENAI_API_KEY=sk-... intentframe-integrations up hermes # backend + adapter + gateway -hermes dashboard # http://localhost:9119/chat -intentframe-integrations stop # tear down + +# Hermes chat (separate from control plane) +hermes dashboard # http://127.0.0.1:9119/chat +intentframe-integrations stop # enforcement stack only (not control plane) ``` +See [intentframe-control-plane.md](intentframe-control-plane.md) for port registry and lifecycle. + ## Command overview ### Install and integrate @@ -262,6 +274,29 @@ Runtime file: `~/.intentframe/integrations/hermes/policy.yaml` Policy changes apply immediately — no gateway restart. +### Control plane (operator UI) + +Separate from Hermes dashboard. Default: `http://127.0.0.1:9720`. One uvicorn process serves the React UI and `/api/*`. + +```bash +intentframe-integrations control-plane start [--host HOST] [--port PORT] +intentframe-integrations control-plane stop # UI only — does not stop enforcement stack +intentframe-integrations control-plane status +intentframe-integrations control-plane serve # foreground dev (no PID file) +``` + +`intentframe-integrations stop` stops the **enforcement stack only** — not the control plane. + +Machine-readable output (scripting): + +```bash +intentframe-integrations status --json +intentframe-integrations governance list hermes --json +intentframe-integrations policy show hermes --json +``` + +See [intentframe-control-plane.md](intentframe-control-plane.md) for frontend build, health checks, and Docker. + ### Other ```bash @@ -286,6 +321,15 @@ Written to `~/.hermes/.env` on install (plugin paths): | `HERMES_BIN` | Override Hermes binary | | `HERMES_HOME` | Hermes config dir (default `~/.hermes`) | +Written to `~/.intentframe/.env` on install (control plane): + +| Variable | Purpose | +|----------|---------| +| `INTENTFRAME_CONTROL_PLANE_HOST` | Bind host (default `127.0.0.1`; Docker uses `0.0.0.0`) | +| `INTENTFRAME_CONTROL_PLANE_PORT` | UI port (default `9720`) | +| `INTENTFRAME_CONTROL_PLANE_TOKEN` | Optional bearer token for `/api/*` | +| `INTENTFRAME_CONTROL_PLANE_ALLOW_REMOTE` | Set `1` to allow binding `0.0.0.0` / non-loopback (Docker) | + Shell exports win over `agent.json` defaults (`setdefault` in `load_and_activate_pack`). `gateway start hermes` logs the effective governance path on stderr: diff --git a/docs/hermes-known-limitations.md b/docs/hermes-known-limitations.md index 4c2d7a5..c2eff8f 100644 --- a/docs/hermes-known-limitations.md +++ b/docs/hermes-known-limitations.md @@ -46,7 +46,7 @@ The installer supports git **branches**, **tags**, and **commit SHAs** via `--re |------|---------| | Latest (default) | `curl …/raw/main/scripts/install-hermes-plugin.sh \| bash` | | Pre-merge / branch | `curl …/raw/my-branch/… \| bash -s -- --ref my-branch --headless` | -| Stable release | `curl …/raw/v0.2.0/… \| bash -s -- --ref v0.2.0` | +| Stable release | `curl …/raw/v0.2.1/… \| bash -s -- --ref v0.2.1` | | Locked / reproducible | `curl …/raw//… \| bash -s -- --ref ` | Install provenance is written to `~/.intentframe/agent-integrations/.install-manifest.json` and shown by `intentframe-integrations doctor hermes`. diff --git a/docs/intentframe-control-plane.md b/docs/intentframe-control-plane.md new file mode 100644 index 0000000..9fd3d1e --- /dev/null +++ b/docs/intentframe-control-plane.md @@ -0,0 +1,139 @@ +# IntentFrame Control Plane + +Operator admin UI for IntentFrame agent integrations. Runs **outside Hermes** on port **9720**. + +Hermes chat (`http://127.0.0.1:9119/chat`) and the control plane are **separate processes and ports**. The control plane manages governance, policy, stack lifecycle, and audit logs; Hermes is where you chat with the agent. + +## Port registry + +| Surface | Owner | Default | +|---------|-------|---------| +| IntentFrame Control Plane | IntentFrame | `http://127.0.0.1:9720` | +| IntentFrame enforcement | IntentFrame | UDS (`~/.intentframe/backend/bridge.sock`, adapter socket) | +| Hermes dashboard | Hermes | `http://127.0.0.1:9119/chat` | +| Hermes API server | Hermes | `8642` | + +## Lifecycle (separate from enforcement) + +```bash +intentframe-integrations control-plane start +intentframe-integrations control-plane stop +intentframe-integrations control-plane status +intentframe-integrations control-plane serve # foreground dev (no PID file) +``` + +- `up hermes` / `stop` manage the **enforcement stack only** (backend, adapter, gateway). +- `control-plane start|stop` manage the **operator UI only**. +- `control-plane stop` does **not** stop Hermes or the enforcement stack. + +Configuration: `~/.intentframe/.env` + +```bash +INTENTFRAME_CONTROL_PLANE_HOST=127.0.0.1 +INTENTFRAME_CONTROL_PLANE_PORT=9720 +# optional: +INTENTFRAME_CONTROL_PLANE_TOKEN=your-local-token +# Docker / LAN bind (requires explicit opt-in): +INTENTFRAME_CONTROL_PLANE_ALLOW_REMOTE=1 +``` + +## How the frontend is served + +There is no separate nginx or Vite server in production. One **uvicorn** process serves both the React UI and the JSON API on the same port. + +```text +Browser → :9720 → uvicorn (FastAPI) + ├── GET / → static/index.html (React SPA) + ├── GET /assets/* → built JS/CSS + ├── GET /governance → index.html (SPA fallback for deep links) + └── GET /api/* → JSON backend +``` + +The React app calls same-origin APIs (`fetch("/api/status")`, etc.) — no CORS, no second port. + +### Static build (git-tracked) + +Vite builds `intentframe-control-plane/web/` into: + +```text +intentframe-control-plane/src/intentframe_control_plane/static/ +``` + +That directory is **committed to git** so installs and Docker work without Node.js. The [install script](../scripts/install-hermes-plugin.sh) runs `npm ci && npm run build` only when `static/index.html` is missing. + +Contributors: after changing `web/`, run `npm run build` and commit the updated `static/` files (see [CONTRIBUTING.md](../CONTRIBUTING.md)). + +### UI pages + +| Route | Purpose | +|-------|---------| +| `/` (Overview) | Enforcement stack status, control plane health, start/stop stack | +| `/governance` | Enable/disable governed Hermes tools | +| `/policy` | View, upload, reload, reset policy | +| `/audit` | Tail IntentFrame server log | + +Mutations subprocess to `intentframe-integrations` CLI commands — governance, policy, and stack logic is not duplicated in the control plane. + +## Health checks + +Health is checked differently depending on **who** is asking: + +| Caller | Mechanism | +|--------|-----------| +| CLI / Docker startup (`control-plane start`, `control-plane status`) | External HTTP probe to `GET /api/health` | +| Overview UI (`GET /api/status` from inside uvicorn) | In-process: if PID file matches `os.getpid()`, healthy without HTTP | + +This avoids a single-worker uvicorn deadlock where a request handler blocks waiting for a second request to the same worker. + +Implementation notes: + +- `/api/health` is lightweight (no nested status calls). +- External probes map bind-all hosts (`0.0.0.0`, `::`) to `127.0.0.1` for the HTTP check. +- `control-plane serve` (foreground) does not write a PID file; Overview may show control plane as not running in that dev mode. + +## Install + +The [install script](../scripts/install-hermes-plugin.sh) seeds IntentFrame env, uses pre-built static assets (or builds if missing), starts the control plane, and opens `http://127.0.0.1:9720`. + +Flags: + +- `--no-control-plane` — skip starting the UI during install (CI/Docker entrypoints start it separately) +- `--no-open` — do not open a browser (`--headless` implies this) + +Docker test harness: installer uses `--no-control-plane`; [entrypoint.sh](../tests/docker/entrypoint.sh) seeds `0.0.0.0:9720` config and starts the control plane after install. See [tests/docker/README.md](../tests/docker/README.md). + +## Development + +```bash +uv sync --all-packages +cd intentframe-control-plane/web && npm ci && npm run build +bin/intentframe-integrations control-plane serve +# or: npm run dev (Vite :5173, proxies /api → :9720) +``` + +Unit tests: + +```bash +uv run --package intentframe-control-plane python tests/intentframe_control_plane/test_lifecycle.py +uv run --package intentframe-control-plane python tests/intentframe_control_plane/test_read_models.py +uv run --package intentframe-control-plane python tests/intentframe_control_plane/test_server.py +``` + +Local Docker smoke (no full Hermes install): + +```bash +bash tests/docker/test_control_plane_smoke.sh +``` + +## Security + +- Binds to `127.0.0.1` by default. +- Optional bearer token on `/api/*` when `INTENTFRAME_CONTROL_PLANE_TOKEN` is set (`/api/health` and `/api/config` are exempt). +- Destructive API actions require `X-Confirm: true`. +- Do not expose on `0.0.0.0` without `INTENTFRAME_CONTROL_PLANE_ALLOW_REMOTE=1` and additional auth. + +## Architecture + +The React UI calls a local FastAPI server. Read endpoints use direct file/PID reads ([`read_models.py`](../intentframe-control-plane/src/intentframe_control_plane/read_models.py)) to avoid subprocess side effects. Write endpoints delegate to `intentframe-integrations` via [`cli_runner.py`](../intentframe-control-plane/src/intentframe_control_plane/cli_runner.py). + +State lives under `~/.intentframe/` (never `~/.hermes/`). diff --git a/if-integration-backend/pyproject.toml b/if-integration-backend/pyproject.toml index 9fe017f..adc7efd 100644 --- a/if-integration-backend/pyproject.toml +++ b/if-integration-backend/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "if-integration-backend" -version = "0.2.0" +version = "0.2.1" description = "Generic IntentFrame validate-only backend: runtime, bridge, noop executor" readme = "README.md" requires-python = ">=3.14" diff --git a/if-integration-backend/src/if_security_backend/__init__.py b/if-integration-backend/src/if_security_backend/__init__.py index 8e107f3..9ee2d06 100644 --- a/if-integration-backend/src/if_security_backend/__init__.py +++ b/if-integration-backend/src/if_security_backend/__init__.py @@ -1,3 +1,3 @@ """Generic IntentFrame validate-only backend (runtime + UDS bridge).""" -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/if-integration-backend/uv.lock b/if-integration-backend/uv.lock index 0dfc25a..4ebadaf 100644 --- a/if-integration-backend/uv.lock +++ b/if-integration-backend/uv.lock @@ -340,7 +340,7 @@ wheels = [ [[package]] name = "if-integration-backend" -version = "0.2.0" +version = "0.2.1" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/if-integration-clients/python/pyproject.toml b/if-integration-clients/python/pyproject.toml index 7af9da7..3664f81 100644 --- a/if-integration-clients/python/pyproject.toml +++ b/if-integration-clients/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "if-integration-bridge-client" -version = "0.2.0" +version = "0.2.1" description = "HTTP-over-UDS client for the IntentFrame validate bridge (handshake + validate)" readme = "README.md" requires-python = ">=3.11" diff --git a/if-integration-clients/typescript/package.json b/if-integration-clients/typescript/package.json index d4fd016..d0a0608 100644 --- a/if-integration-clients/typescript/package.json +++ b/if-integration-clients/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@if-integration/bridge-client", - "version": "0.2.0", + "version": "0.2.1", "description": "HTTP-over-UDS client for the IntentFrame validate bridge", "type": "module", "main": "./dist/index.js", diff --git a/integrations/hermes/adapter/pyproject.toml b/integrations/hermes/adapter/pyproject.toml index f226992..ee6766d 100644 --- a/integrations/hermes/adapter/pyproject.toml +++ b/integrations/hermes/adapter/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "hermes-adapter" -version = "0.2.0" +version = "0.2.1" description = "Hermes agent adapter sidecar — maps Hermes tools to IntentFrame validate bridge" readme = "README.md" requires-python = ">=3.11" diff --git a/integrations/hermes/adapter/src/hermes_adapter/__init__.py b/integrations/hermes/adapter/src/hermes_adapter/__init__.py index 210dc02..5677ba6 100644 --- a/integrations/hermes/adapter/src/hermes_adapter/__init__.py +++ b/integrations/hermes/adapter/src/hermes_adapter/__init__.py @@ -1,3 +1,3 @@ """Hermes agent adapter — tool mapping and IntentFrame bridge client.""" -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/integrations/hermes/plugin/intentframe-gate/plugin.yaml b/integrations/hermes/plugin/intentframe-gate/plugin.yaml index b350a02..0c1c4b3 100644 --- a/integrations/hermes/plugin/intentframe-gate/plugin.yaml +++ b/integrations/hermes/plugin/intentframe-gate/plugin.yaml @@ -1,5 +1,5 @@ name: intentframe-gate -version: 0.2.0 +version: 0.2.1 description: "IntentFrame validate-only gate for governed Hermes tools — requires reason, calls adapter before side effects." author: "IntentFrame Integrations" requires_env: diff --git a/integrations/hermes/plugin/intentframe-gate/pyproject.toml b/integrations/hermes/plugin/intentframe-gate/pyproject.toml index b0b58db..abbcb96 100644 --- a/integrations/hermes/plugin/intentframe-gate/pyproject.toml +++ b/integrations/hermes/plugin/intentframe-gate/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "intentframe-hermes-gate-plugin" -version = "0.2.0" +version = "0.2.1" description = "Hermes plugin: IntentFrame validate-only gate for governed tools with required reason" readme = "README.md" requires-python = ">=3.11" diff --git a/integrations/hermes/shared/pyproject.toml b/integrations/hermes/shared/pyproject.toml index c3c24b1..9a75541 100644 --- a/integrations/hermes/shared/pyproject.toml +++ b/integrations/hermes/shared/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "hermes-governance" -version = "0.2.0" +version = "0.2.1" description = "Shared governed-tool contract for Hermes IntentFrame integration" requires-python = ">=3.11" license = "Apache-2.0" diff --git a/intentframe-control-plane/README.md b/intentframe-control-plane/README.md new file mode 100644 index 0000000..a632e7e --- /dev/null +++ b/intentframe-control-plane/README.md @@ -0,0 +1,47 @@ +# IntentFrame Control Plane + +Operator admin UI for IntentFrame agent integrations. Runs separately from Hermes on port **9720**. + +Full docs: [docs/intentframe-control-plane.md](../docs/intentframe-control-plane.md). + +## Quick start + +```bash +intentframe-integrations control-plane start +# http://127.0.0.1:9720 +``` + +Hermes chat is separate: `hermes dashboard` → `http://127.0.0.1:9119/chat`. + +## Frontend build + +Source: `web/` (React + Vite + Tailwind). + +Build output (git-tracked, shipped with the Python package): + +```bash +cd web && npm ci && npm run build +# writes to ../src/intentframe_control_plane/static/ +``` + +Commit updated `static/` when you change `web/`. End-user installs skip npm when `static/index.html` already exists in the integration pack. + +uvicorn serves `static/` and `/api/*` from the same process — no separate frontend server in production. + +## Layout + +| Path | Role | +|------|------| +| `web/` | React source (dev: `npm run dev` proxies API to :9720) | +| `src/intentframe_control_plane/static/` | Vite build output (committed) | +| `src/intentframe_control_plane/server.py` | FastAPI app + SPA fallback | +| `src/intentframe_control_plane/lifecycle.py` | start/stop/status, health probes | +| `src/intentframe_control_plane/read_models.py` | Read-only status from PID files / YAML | + +## Dev + +```bash +uv sync --all-packages +cd web && npm ci && npm run build +bin/intentframe-integrations control-plane serve +``` diff --git a/intentframe-control-plane/pyproject.toml b/intentframe-control-plane/pyproject.toml new file mode 100644 index 0000000..beadbc2 --- /dev/null +++ b/intentframe-control-plane/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "intentframe-control-plane" +version = "0.2.1" +description = "IntentFrame operator control plane — external admin UI for agent integrations" +readme = "README.md" +requires-python = ">=3.14" +license = "Apache-2.0" +dependencies = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.32.0", + "python-multipart>=0.0.18", + "pyyaml>=6.0.2", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/intentframe_control_plane"] + +[tool.hatch.build.targets.wheel.force-include] +"src/intentframe_control_plane/static" = "intentframe_control_plane/static" diff --git a/intentframe-control-plane/src/intentframe_control_plane/__init__.py b/intentframe-control-plane/src/intentframe_control_plane/__init__.py new file mode 100644 index 0000000..93ff3d2 --- /dev/null +++ b/intentframe-control-plane/src/intentframe_control_plane/__init__.py @@ -0,0 +1,3 @@ +"""IntentFrame operator control plane (UI on :9720, separate from Hermes chat).""" + +__version__ = "0.2.1" diff --git a/intentframe-control-plane/src/intentframe_control_plane/auth.py b/intentframe-control-plane/src/intentframe_control_plane/auth.py new file mode 100644 index 0000000..edd7202 --- /dev/null +++ b/intentframe-control-plane/src/intentframe_control_plane/auth.py @@ -0,0 +1,25 @@ +"""Optional bearer token auth for control plane API routes.""" + +from __future__ import annotations + +from fastapi import HTTPException, Request + +from intentframe_control_plane.config import ControlPlaneSettings + + +def require_auth(request: Request, settings: ControlPlaneSettings) -> None: + if not settings.token: + return + auth = request.headers.get("Authorization", "") + if auth == f"Bearer {settings.token}": + return + raise HTTPException(status_code=401, detail="Unauthorized") + + +def require_confirm(request: Request) -> None: + if request.headers.get("X-Confirm", "").lower() in {"1", "true", "yes"}: + return + raise HTTPException( + status_code=400, + detail="Destructive action requires X-Confirm: true header", + ) diff --git a/intentframe-control-plane/src/intentframe_control_plane/cli_runner.py b/intentframe-control-plane/src/intentframe_control_plane/cli_runner.py new file mode 100644 index 0000000..f662fd6 --- /dev/null +++ b/intentframe-control-plane/src/intentframe_control_plane/cli_runner.py @@ -0,0 +1,52 @@ +"""Subprocess wrapper for intentframe-integrations CLI.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class CliResult: + argv: list[str] + returncode: int + stdout: str + stderr: str + + +def resolve_cli_bin() -> str: + override = os.environ.get("INTENTFRAME_INTEGRATIONS_BIN") + if override: + return override + found = shutil.which("intentframe-integrations") + if found: + return found + venv_candidate = Path(sys.executable).resolve().parent / "intentframe-integrations" + if venv_candidate.is_file(): + return str(venv_candidate) + raise RuntimeError( + "intentframe-integrations not found on PATH. " + "Install the CLI or set INTENTFRAME_INTEGRATIONS_BIN." + ) + + +def run_cli(args: list[str], *, timeout: float | None = 300.0) -> CliResult: + bin_path = resolve_cli_bin() + argv = [bin_path, *args] + proc = subprocess.run( + argv, + capture_output=True, + text=True, + timeout=timeout, + env=os.environ.copy(), + ) + return CliResult( + argv=argv, + returncode=proc.returncode, + stdout=proc.stdout, + stderr=proc.stderr, + ) diff --git a/intentframe-control-plane/src/intentframe_control_plane/config.py b/intentframe-control-plane/src/intentframe_control_plane/config.py new file mode 100644 index 0000000..762e5e2 --- /dev/null +++ b/intentframe-control-plane/src/intentframe_control_plane/config.py @@ -0,0 +1,77 @@ +"""Control plane configuration from environment and ~/.intentframe/.env.""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from pathlib import Path + +DEFAULT_HOST = "127.0.0.1" +DEFAULT_PORT = 9720 + +INTENTFRAME_HOME = Path.home() / ".intentframe" +ENV_FILE = INTENTFRAME_HOME / ".env" +PID_FILE = INTENTFRAME_HOME / "control-plane.pid" # uvicorn PID from control-plane start +LOG_FILE = INTENTFRAME_HOME / "logs" / "control-plane.log" +SERVER_LOG = INTENTFRAME_HOME / "logs" / "intentframe-server.log" +POLICY_RUNTIME = INTENTFRAME_HOME / "integrations" / "hermes" / "policy.yaml" +BRIDGE_SOCKET = INTENTFRAME_HOME / "backend" / "bridge.sock" + + +def _parse_env_file(path: Path) -> dict[str, str]: + if not path.is_file(): + return {} + values: dict[str, str] = {} + for line in path.read_text(encoding="utf-8").splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("#") or "=" not in stripped: + continue + key, value = stripped.split("=", 1) + values[key.strip()] = value.strip() + return values + + +def load_dotenv() -> None: + for key, value in _parse_env_file(ENV_FILE).items(): + os.environ.setdefault(key, value) + + +@dataclass(frozen=True) +class ControlPlaneSettings: + host: str + port: int + token: str | None + allow_remote: bool + + @property + def url(self) -> str: + return f"http://{self.host}:{self.port}" + + @classmethod + def from_env(cls) -> ControlPlaneSettings: + load_dotenv() + host = os.environ.get("INTENTFRAME_CONTROL_PLANE_HOST", DEFAULT_HOST) + port_raw = os.environ.get("INTENTFRAME_CONTROL_PLANE_PORT", str(DEFAULT_PORT)) + token = os.environ.get("INTENTFRAME_CONTROL_PLANE_TOKEN") or None + allow_remote = os.environ.get("INTENTFRAME_CONTROL_PLANE_ALLOW_REMOTE", "").lower() in { + "1", + "true", + "yes", + } + return cls( + host=host, + port=int(port_raw), + token=token, + allow_remote=allow_remote, + ) + + +def validate_bind_host(host: str, *, allow_remote: bool) -> None: + if host in ("127.0.0.1", "localhost", "::1"): + return + if allow_remote: + return + raise ValueError( + f"Refusing to bind control plane to {host!r}. " + "Use 127.0.0.1 or set INTENTFRAME_CONTROL_PLANE_ALLOW_REMOTE=1." + ) diff --git a/intentframe-control-plane/src/intentframe_control_plane/lifecycle.py b/intentframe-control-plane/src/intentframe_control_plane/lifecycle.py new file mode 100644 index 0000000..b2a30bd --- /dev/null +++ b/intentframe-control-plane/src/intentframe_control_plane/lifecycle.py @@ -0,0 +1,305 @@ +"""Start/stop/status for the IntentFrame control plane HTTP server. + +The control plane runs as a background uvicorn process (``control-plane start``) or +foreground (``control-plane serve``). Health is determined differently by caller: + +- **External** (CLI, startup loop, ``control-plane status``): HTTP GET ``/api/health``. +- **In-process** (``GET /api/status`` inside uvicorn): skip HTTP when PID file matches + ``os.getpid()`` to avoid single-worker self-deadlock. + +External probes map bind-all hosts (``0.0.0.0``, ``::``) to ``127.0.0.1`` via +``_health_host()``. +""" + +from __future__ import annotations + +import os +import signal +import subprocess +import sys +import time +import urllib.error +import urllib.request +from dataclasses import dataclass +from pathlib import Path + +from intentframe_control_plane.config import ( + LOG_FILE, + PID_FILE, + ControlPlaneSettings, + validate_bind_host, +) + + +class ControlPlaneError(Exception): + pass + + +@dataclass(frozen=True) +class ControlPlaneStatus: + running: bool + pid: int | None + host: str + port: int + url: str + healthy: bool + + +def _read_pid(path: Path) -> int | None: + if not path.is_file(): + return None + try: + return int(path.read_text(encoding="utf-8").strip()) + except (OSError, ValueError): + return None + + +def _pid_alive(pid: int) -> bool: + try: + os.kill(pid, 0) + return True + except OSError: + return False + + +def _process_group_kwargs() -> dict[str, object]: + if os.name == "nt": + return {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP} + return {"start_new_session": True} + + +def _terminate_pid(pid: int) -> None: + if os.name == "nt": + try: + os.kill(pid, signal.CTRL_BREAK_EVENT) + return + except OSError: + pass + os.kill(pid, signal.SIGTERM) + + +def _kill_pid(pid: int) -> None: + if os.name == "nt": + os.kill(pid, signal.SIGTERM) + else: + os.kill(pid, signal.SIGKILL) + + +def is_control_plane_running() -> bool: + pid = _read_pid(PID_FILE) + if pid is None: + return False + if not _pid_alive(pid): + PID_FILE.unlink(missing_ok=True) + return False + return True + + +def _health_host(bind_host: str) -> str: + """Map bind-all addresses to a loopback host for HTTP health probes.""" + if bind_host in {"0.0.0.0", "::"}: + return "127.0.0.1" + return bind_host + + +def _health_check(host: str, port: int, *, timeout: float = 2.0) -> bool: + """External liveness probe — used by CLI and startup, not in-process /api/status.""" + url = f"http://{host}:{port}/api/health" + try: + with urllib.request.urlopen(url, timeout=timeout) as resp: + return resp.status == 200 + except (urllib.error.URLError, TimeoutError, OSError): + return False + + +def control_plane_status(settings: ControlPlaneSettings | None = None) -> ControlPlaneStatus: + """Return running/healthy state from PID file and optional HTTP probe. + + When called from inside the running server (PID file matches current process), + ``healthy`` is True without HTTP — serving this request proves the server is up. + """ + cfg = settings or ControlPlaneSettings.from_env() + pid = _read_pid(PID_FILE) + running = pid is not None and _pid_alive(pid) + if not running: + pid = None + healthy = False + elif pid == os.getpid(): + # In-process callers (e.g. /api/status) must not HTTP-probe this server: + # a single-worker uvicorn would deadlock waiting on itself. + healthy = True + else: + healthy = _health_check(_health_host(cfg.host), cfg.port) + return ControlPlaneStatus( + running=running, + pid=pid, + host=cfg.host, + port=cfg.port, + url=cfg.url, + healthy=healthy, + ) + + +def format_status_line(status: ControlPlaneStatus) -> str: + state = "running" if status.running else "stopped" + health = "healthy" if status.healthy else "unhealthy" if status.running else "n/a" + return ( + f"control-plane: {state} (pid={status.pid or 'none'}, " + f"url={status.url}, health={health})" + ) + + +def start_control_plane( + *, + host: str | None = None, + port: int | None = None, + quiet: bool = False, +) -> ControlPlaneStatus: + settings = ControlPlaneSettings.from_env() + bind_host = host or settings.host + bind_port = port if port is not None else settings.port + validate_bind_host(bind_host, allow_remote=settings.allow_remote) + + existing = control_plane_status( + ControlPlaneSettings(host=bind_host, port=bind_port, token=settings.token, allow_remote=settings.allow_remote) + ) + if existing.running and existing.healthy: + if not quiet: + print(f"Control plane already running at {existing.url}", file=sys.stderr) + return existing + + if existing.running: + stop_control_plane(quiet=True) + + LOG_FILE.parent.mkdir(parents=True, exist_ok=True) + PID_FILE.parent.mkdir(parents=True, exist_ok=True) + + env = os.environ.copy() + env["INTENTFRAME_CONTROL_PLANE_HOST"] = bind_host + env["INTENTFRAME_CONTROL_PLANE_PORT"] = str(bind_port) + if settings.token: + env.setdefault("INTENTFRAME_CONTROL_PLANE_TOKEN", settings.token) + + try: + from intentframe_control_plane.cli_runner import resolve_cli_bin + + env["INTENTFRAME_INTEGRATIONS_BIN"] = resolve_cli_bin() + except RuntimeError: + pass + + log_fh = open(LOG_FILE, "a", encoding="utf-8") + cmd = [ + sys.executable, + "-m", + "uvicorn", + "intentframe_control_plane.server:app", + "--host", + bind_host, + "--port", + str(bind_port), + "--log-level", + "info", + ] + if not quiet: + print(f"Starting control plane at http://{bind_host}:{bind_port}...", file=sys.stderr) + + proc = subprocess.Popen( + cmd, + env=env, + stdout=log_fh, + stderr=subprocess.STDOUT, + **_process_group_kwargs(), + ) + proc_pid = proc.pid + PID_FILE.write_text(str(proc_pid), encoding="utf-8") + log_fh.close() + + health_host = _health_host(bind_host) + + deadline = time.monotonic() + 30.0 + while time.monotonic() < deadline: + if _health_check(health_host, bind_port, timeout=1.0): + status = control_plane_status( + ControlPlaneSettings( + host=bind_host, + port=bind_port, + token=settings.token, + allow_remote=settings.allow_remote, + ) + ) + if not quiet: + print(f"Control plane ready at {status.url}") + return status + if not _pid_alive(proc_pid): + tail = "" + try: + tail = LOG_FILE.read_text(encoding="utf-8")[-2000:] + except OSError: + pass + PID_FILE.unlink(missing_ok=True) + raise ControlPlaneError(f"Control plane exited during startup.\n{tail}") + time.sleep(0.1) + + if _pid_alive(proc_pid): + _terminate_pid(proc_pid) + deadline = time.monotonic() + 5.0 + while time.monotonic() < deadline and _pid_alive(proc_pid): + time.sleep(0.1) + if _pid_alive(proc_pid): + _kill_pid(proc_pid) + PID_FILE.unlink(missing_ok=True) + tail = "" + try: + tail = LOG_FILE.read_text(encoding="utf-8")[-2000:] + except OSError: + pass + raise ControlPlaneError( + f"Control plane did not become healthy within 30s (log: {LOG_FILE})\n{tail}" + ) + + +def stop_control_plane(*, timeout: float = 15.0, quiet: bool = False) -> None: + pid = _read_pid(PID_FILE) + if pid is None or not _pid_alive(pid): + PID_FILE.unlink(missing_ok=True) + if not quiet: + print("Control plane is not running.", file=sys.stderr) + return + + if not quiet: + print(f"Stopping control plane (pid {pid})...", file=sys.stderr) + _terminate_pid(pid) + + deadline = time.monotonic() + timeout + while time.monotonic() < deadline and _pid_alive(pid): + time.sleep(0.1) + if _pid_alive(pid): + _kill_pid(pid) + + PID_FILE.unlink(missing_ok=True) + if not quiet: + print("Control plane stopped.") + + +def serve_control_plane( + *, + host: str | None = None, + port: int | None = None, +) -> None: + """Run control plane in foreground (no PID file).""" + import uvicorn + + settings = ControlPlaneSettings.from_env() + bind_host = host or settings.host + bind_port = port if port is not None else settings.port + validate_bind_host(bind_host, allow_remote=settings.allow_remote) + + os.environ["INTENTFRAME_CONTROL_PLANE_HOST"] = bind_host + os.environ["INTENTFRAME_CONTROL_PLANE_PORT"] = str(bind_port) + + uvicorn.run( + "intentframe_control_plane.server:app", + host=bind_host, + port=bind_port, + log_level="info", + ) diff --git a/intentframe-control-plane/src/intentframe_control_plane/main.py b/intentframe-control-plane/src/intentframe_control_plane/main.py new file mode 100644 index 0000000..b785432 --- /dev/null +++ b/intentframe-control-plane/src/intentframe_control_plane/main.py @@ -0,0 +1,65 @@ +"""CLI entrypoints for control plane lifecycle.""" + +from __future__ import annotations + +import argparse +import sys + +from intentframe_control_plane.lifecycle import ( + ControlPlaneError, + format_status_line, + control_plane_status, + serve_control_plane, + start_control_plane, + stop_control_plane, +) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="intentframe-control-plane", + description="IntentFrame operator control plane (http://127.0.0.1:9720)", + ) + sub = parser.add_subparsers(dest="command", required=True) + + p_start = sub.add_parser("start", help="Start control plane in background") + p_start.add_argument("--host", default=None) + p_start.add_argument("--port", type=int, default=None) + + sub.add_parser("stop", help="Stop control plane") + sub.add_parser("status", help="Show control plane status") + + p_serve = sub.add_parser("serve", help="Run control plane in foreground") + p_serve.add_argument("--host", default=None) + p_serve.add_argument("--port", type=int, default=None) + + args = parser.parse_args(argv) + + try: + match args.command: + case "start": + status = start_control_plane(host=args.host, port=args.port) + print(status.url) + return 0 + case "stop": + stop_control_plane() + return 0 + case "status": + print(format_status_line(control_plane_status())) + return 0 + case "serve": + serve_control_plane(host=args.host, port=args.port) + return 0 + case _: + parser.error(f"Unknown command: {args.command}") + return 2 + except ControlPlaneError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + except ValueError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/intentframe-control-plane/src/intentframe_control_plane/read_models.py b/intentframe-control-plane/src/intentframe_control_plane/read_models.py new file mode 100644 index 0000000..70c64d4 --- /dev/null +++ b/intentframe-control-plane/src/intentframe_control_plane/read_models.py @@ -0,0 +1,172 @@ +"""Read-only views of runtime state without subprocess side effects. + +Used by ``GET /api/status``, ``/api/governance``, and ``/api/policy`` so page loads +do not spawn CLI subprocesses or mutate runtime state. +""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +import yaml + +from intentframe_control_plane.config import ( + BRIDGE_SOCKET, + INTENTFRAME_HOME, + POLICY_RUNTIME, +) + +HERMES_AGENT_ID = "hermes" +INTEGRATION_DIR = INTENTFRAME_HOME / "integrations" / HERMES_AGENT_ID +GOVERNANCE_YAML = INTEGRATION_DIR / "governance" / "tools.yaml" +GATEWAY_PID_FILE = INTEGRATION_DIR / "gateway.pid" +ADAPTER_PID_FILE = INTEGRATION_DIR / "adapter.pid" +ADAPTER_SOCKET = INTEGRATION_DIR / "adapter.sock" +POLICY_REGISTRY_SOCKET = INTENTFRAME_HOME / "run" / "policy-registry.sock" + + +def _read_pid(path: Path) -> int | None: + if not path.is_file(): + return None + try: + return int(path.read_text(encoding="utf-8").strip()) + except (OSError, ValueError): + return None + + +def _pid_alive(pid: int) -> bool: + try: + os.kill(pid, 0) + return True + except OSError: + return False + + +def _gateway_running() -> bool: + pid = _read_pid(GATEWAY_PID_FILE) + return pid is not None and _pid_alive(pid) + + +def _adapter_running() -> bool: + pid = _read_pid(ADAPTER_PID_FILE) + return pid is not None and _pid_alive(pid) and ADAPTER_SOCKET.exists() + + +def collect_status_dict() -> dict[str, Any]: + gw_running = _gateway_running() + gw_pid = _read_pid(GATEWAY_PID_FILE) if gw_running else None + adapter_running = _adapter_running() + return { + "bridge_socket": str(BRIDGE_SOCKET), + "bridge_present": BRIDGE_SOCKET.exists(), + "gateway_running": gw_running, + "gateway_pid": gw_pid, + "adapters": [ + { + "agent_id": HERMES_AGENT_ID, + "running": adapter_running, + "pid": _read_pid(ADAPTER_PID_FILE) if adapter_running else None, + "socket": str(ADAPTER_SOCKET), + } + ], + } + + +def load_governance_dict() -> dict[str, Any]: + if not GOVERNANCE_YAML.is_file(): + return {"agent": HERMES_AGENT_ID, "tools": [], "runtime_governed": []} + + raw = yaml.safe_load(GOVERNANCE_YAML.read_text(encoding="utf-8")) or {} + tools_map = raw.get("tools") if isinstance(raw, dict) else None + if not isinstance(tools_map, dict): + return {"agent": HERMES_AGENT_ID, "tools": [], "runtime_governed": []} + + tools: list[dict[str, Any]] = [] + runtime_governed: list[str] = [] + for name, spec in tools_map.items(): + if not isinstance(name, str) or not isinstance(spec, dict): + continue + enabled = spec.get("enabled", True) + if not isinstance(enabled, bool): + enabled = True + tools.append({"name": name, "enabled": enabled}) + if enabled: + runtime_governed.append(name) + + tools.sort(key=lambda item: item["name"]) + runtime_governed.sort() + return { + "agent": HERMES_AGENT_ID, + "tools": tools, + "runtime_governed": runtime_governed, + } + + +def _registry_message() -> tuple[bool, int | None, str]: + if not POLICY_REGISTRY_SOCKET.exists(): + return False, None, "policy-registry not running (start the enforcement stack first)" + return False, None, "registry socket present (reload policy after stack start to verify load)" + + +def load_policy_dict() -> dict[str, Any]: + runtime_exists = POLICY_RUNTIME.is_file() + yaml_text = "" + if runtime_exists: + yaml_text = POLICY_RUNTIME.read_text(encoding="utf-8") + + agent_id = HERMES_AGENT_ID + user_id = "default" + if runtime_exists: + try: + raw = yaml.safe_load(yaml_text) or {} + if isinstance(raw, dict): + if isinstance(raw.get("agent_id"), str): + agent_id = raw["agent_id"] + if isinstance(raw.get("user_id"), str): + user_id = raw["user_id"] + except yaml.YAMLError: + pass + + loaded, action_count, message = _registry_message() + return { + "meta": { + "agent_id": agent_id, + "user_id": user_id, + "runtime_path": str(POLICY_RUNTIME), + "runtime_exists": runtime_exists, + "shipped_template": "", + "registry_loaded": loaded, + "registry_action_count": action_count, + "registry_message": message, + }, + "yaml": yaml_text, + } + + +def tail_log_lines(path: Path, *, max_lines: int) -> list[str]: + """Return the last max_lines from a text file without loading the whole file.""" + if not path.is_file(): + return [] + + chunk_size = 8192 + data = b"" + with path.open("rb") as fh: + fh.seek(0, os.SEEK_END) + position = fh.tell() + while position > 0 and data.count(b"\n") <= max_lines: + read_size = min(chunk_size, position) + position -= read_size + fh.seek(position) + data = fh.read(read_size) + data + + text = data.decode("utf-8", errors="replace") + lines = text.splitlines() + return lines[-max_lines:] + + +def public_config_dict() -> dict[str, str]: + host = os.environ.get("HERMES_DASHBOARD_HOST", "127.0.0.1") + port = os.environ.get("HERMES_DASHBOARD_PORT", "9119") + return {"hermes_chat_url": f"http://{host}:{port}/chat"} diff --git a/intentframe-control-plane/src/intentframe_control_plane/server.py b/intentframe-control-plane/src/intentframe_control_plane/server.py new file mode 100644 index 0000000..6646b5b --- /dev/null +++ b/intentframe-control-plane/src/intentframe_control_plane/server.py @@ -0,0 +1,300 @@ +"""FastAPI control plane server and JSON API. + +Serves the built React SPA from ``static/`` (same port as ``/api/*``). Vite output is +git-tracked under ``static/``; uvicorn is the only production frontend server. + +Read endpoints use ``read_models`` (direct file/PID reads). Mutations subprocess to +``intentframe-integrations`` via ``cli_runner``. +""" + +from __future__ import annotations + +import os +import tempfile +from pathlib import Path +from typing import Any + +from fastapi import FastAPI, File, HTTPException, Request, UploadFile +from fastapi.responses import FileResponse, JSONResponse + +from intentframe_control_plane.auth import require_auth, require_confirm +from intentframe_control_plane.cli_runner import CliResult, run_cli +from intentframe_control_plane.config import ( + SERVER_LOG, + ControlPlaneSettings, + load_dotenv, +) +from intentframe_control_plane.lifecycle import control_plane_status +from intentframe_control_plane.read_models import ( + collect_status_dict, + load_governance_dict, + load_policy_dict, + public_config_dict, + tail_log_lines, +) + +load_dotenv() + +STATIC_DIR = Path(__file__).resolve().parent / "static" +INDEX_HTML = STATIC_DIR / "index.html" +ASSETS_DIR = STATIC_DIR / "assets" + + +def _ok(data: Any = None, **extra: Any) -> JSONResponse: + payload: dict[str, Any] = {"ok": True} + if data is not None: + payload["data"] = data + payload.update(extra) + return JSONResponse(payload) + + +def _err(message: str, status: int = 400, **extra: Any) -> JSONResponse: + payload: dict[str, Any] = {"ok": False, "error": message} + payload.update(extra) + return JSONResponse(payload, status_code=status) + + +def _cli_payload(result: CliResult) -> dict[str, Any]: + return { + "returncode": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + "argv": result.argv, + } + + +def _run_or_error(args: list[str], *, timeout: float | None = 300.0) -> tuple[CliResult | None, JSONResponse | None]: + try: + result = run_cli(args, timeout=timeout) + except RuntimeError as exc: + return None, _err(str(exc), status=500) + if result.returncode != 0: + return result, _err( + result.stderr.strip() or result.stdout.strip() or "CLI command failed", + status=500, + cli=_cli_payload(result), + ) + return result, None + + +def _require_cli_result( + result: CliResult | None, + err: JSONResponse | None, +) -> tuple[CliResult | None, JSONResponse | None]: + if err is not None: + return None, err + if result is None: + return None, _err("CLI command returned no result", status=500) + return result, None + + +app = FastAPI(title="IntentFrame Control Plane", version="0.2.1") + + +@app.exception_handler(HTTPException) +async def http_exception_handler(_request: Request, exc: HTTPException) -> JSONResponse: + detail = exc.detail if isinstance(exc.detail, str) else str(exc.detail) + return JSONResponse({"ok": False, "error": detail}, status_code=exc.status_code) + + +@app.middleware("http") +async def auth_middleware(request: Request, call_next): + if request.url.path.startswith("/api/") and request.url.path not in {"/api/health", "/api/config"}: + settings = ControlPlaneSettings.from_env() + require_auth(request, settings) + return await call_next(request) + + +@app.get("/api/health") +async def health() -> JSONResponse: + """Lightweight liveness for external probes (CLI startup, ``control-plane status``). + + Must not call ``control_plane_status()`` — that would HTTP-probe back into this + process and deadlock a single-worker uvicorn. + """ + settings = ControlPlaneSettings.from_env() + return _ok( + { + "service": "intentframe-control-plane", + "url": settings.url, + "status": "ok", + } + ) + + +@app.get("/api/config") +async def api_config() -> JSONResponse: + return _ok(public_config_dict()) + + +@app.get("/api/status") +async def api_status() -> JSONResponse: + """Enforcement stack snapshot + control plane row for the Overview page.""" + data = collect_status_dict() + cp = control_plane_status() + data["control_plane"] = { + "running": cp.running, + "healthy": cp.healthy, + "pid": cp.pid, + "url": cp.url, + } + data["openai_api_key_set"] = bool(os.environ.get("OPENAI_API_KEY")) + return _ok(data) + + +@app.get("/api/doctor") +async def api_doctor() -> JSONResponse: + result, err = _run_or_error(["doctor", "hermes"]) + result, err = _require_cli_result(result, err) + if err is not None: + return err + return _ok( + { + "returncode": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + "ok": result.returncode == 0, + } + ) + + +@app.get("/api/governance") +async def api_governance_list() -> JSONResponse: + return _ok(load_governance_dict()) + + +@app.post("/api/governance/{tool}/enable") +async def api_governance_enable(tool: str) -> JSONResponse: + result, err = _run_or_error(["governance", "enable", "hermes", tool]) + result, err = _require_cli_result(result, err) + if err is not None: + return err + return _ok({"message": result.stdout.strip()}) + + +@app.post("/api/governance/{tool}/disable") +async def api_governance_disable(tool: str) -> JSONResponse: + result, err = _run_or_error(["governance", "disable", "hermes", tool]) + result, err = _require_cli_result(result, err) + if err is not None: + return err + return _ok({"message": result.stdout.strip()}) + + +@app.post("/api/governance/apply") +async def api_governance_apply() -> JSONResponse: + stop_result, stop_err = _run_or_error(["gateway", "stop", "hermes"]) + stop_result, stop_err = _require_cli_result(stop_result, stop_err) + if stop_err is not None: + return stop_err + start_result, start_err = _run_or_error(["gateway", "start", "hermes"]) + start_result, start_err = _require_cli_result(start_result, start_err) + if start_err is not None: + return start_err + return _ok( + { + "stop": stop_result.stdout.strip(), + "start": start_result.stdout.strip(), + } + ) + + +@app.get("/api/policy") +async def api_policy_get() -> JSONResponse: + return _ok(load_policy_dict()) + + +@app.post("/api/policy/reload") +async def api_policy_reload() -> JSONResponse: + result, err = _run_or_error(["policy", "reload", "hermes"]) + result, err = _require_cli_result(result, err) + if err is not None: + return err + return _ok({"message": result.stdout.strip()}) + + +@app.post("/api/policy/apply") +async def api_policy_apply(file: UploadFile = File(...)) -> JSONResponse: + suffix = ".yaml" + if file.filename and file.filename.endswith((".yml", ".yaml")): + suffix = Path(file.filename).suffix + content = await file.read() + with tempfile.NamedTemporaryFile(mode="wb", suffix=suffix, delete=False) as tmp: + tmp.write(content) + tmp_path = tmp.name + try: + result, err = _run_or_error(["policy", "set", "hermes", tmp_path]) + result, err = _require_cli_result(result, err) + if err is not None: + return err + return _ok({"message": result.stdout.strip()}) + finally: + Path(tmp_path).unlink(missing_ok=True) + + +@app.post("/api/policy/reset") +async def api_policy_reset(request: Request) -> JSONResponse: + require_confirm(request) + result, err = _run_or_error(["policy", "reset", "hermes"]) + result, err = _require_cli_result(result, err) + if err is not None: + return err + return _ok({"message": result.stdout.strip()}) + + +@app.post("/api/stack/up") +async def api_stack_up() -> JSONResponse: + if not os.environ.get("OPENAI_API_KEY"): + return _err("OPENAI_API_KEY is not set", status=400) + result, err = _run_or_error(["up", "hermes"], timeout=120.0) + result, err = _require_cli_result(result, err) + if err is not None: + return err + return _ok({"message": result.stdout.strip()}) + + +@app.post("/api/stack/stop") +async def api_stack_stop(request: Request) -> JSONResponse: + require_confirm(request) + result, err = _run_or_error(["stop"]) + result, err = _require_cli_result(result, err) + if err is not None: + return err + return _ok({"message": "Enforcement stack stopped (control plane still running)"}) + + +@app.get("/api/audit/log") +async def api_audit_log(tail: int = 200) -> JSONResponse: + tail = max(1, min(tail, 2000)) + lines = tail_log_lines(SERVER_LOG, max_lines=tail) + return _ok({"lines": lines, "path": str(SERVER_LOG)}) + + +# Static assets + SPA fallback (BrowserRouter deep links like /governance). +# Built by Vite into static/; committed so installs work without Node.js. +if INDEX_HTML.is_file(): + + @app.get("/assets/{asset_path:path}") + async def static_assets(asset_path: str) -> FileResponse: + target = ASSETS_DIR / asset_path + if not target.is_file(): + raise HTTPException(status_code=404, detail="Asset not found") + return FileResponse(target) + + @app.get("/{full_path:path}") + async def spa_fallback(full_path: str) -> FileResponse: + if full_path.startswith("api/"): + raise HTTPException(status_code=404, detail="Not found") + if full_path: + candidate = STATIC_DIR / full_path + if candidate.is_file(): + return FileResponse(candidate) + return FileResponse(INDEX_HTML) +else: + + @app.get("/") + async def missing_frontend() -> JSONResponse: + return _err( + "Control plane frontend not built. Run: cd intentframe-control-plane/web && npm ci && npm run build", + status=503, + ) diff --git a/intentframe-control-plane/src/intentframe_control_plane/static/assets/index-Bw5rNWG0.js b/intentframe-control-plane/src/intentframe_control_plane/static/assets/index-Bw5rNWG0.js new file mode 100644 index 0000000..526edba --- /dev/null +++ b/intentframe-control-plane/src/intentframe_control_plane/static/assets/index-Bw5rNWG0.js @@ -0,0 +1,91 @@ +var $m=u=>{throw TypeError(u)};var cf=(u,i,r)=>i.has(u)||$m("Cannot "+r);var p=(u,i,r)=>(cf(u,i,"read from private field"),r?r.call(u):i.get(u)),W=(u,i,r)=>i.has(u)?$m("Cannot add the same private member more than once"):i instanceof WeakSet?i.add(u):i.set(u,r),K=(u,i,r,s)=>(cf(u,i,"write to private field"),s?s.call(u,r):i.set(u,r),r),st=(u,i,r)=>(cf(u,i,"access private method"),r);var Cc=(u,i,r,s)=>({set _(o){K(u,i,o,r)},get _(){return p(u,i,s)}});(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))s(o);new MutationObserver(o=>{for(const d of o)if(d.type==="childList")for(const y of d.addedNodes)y.tagName==="LINK"&&y.rel==="modulepreload"&&s(y)}).observe(document,{childList:!0,subtree:!0});function r(o){const d={};return o.integrity&&(d.integrity=o.integrity),o.referrerPolicy&&(d.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?d.credentials="include":o.crossOrigin==="anonymous"?d.credentials="omit":d.credentials="same-origin",d}function s(o){if(o.ep)return;o.ep=!0;const d=r(o);fetch(o.href,d)}})();var sf={exports:{}},Ku={};/** + * @license React + * react-jsx-runtime.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Wm;function Gp(){if(Wm)return Ku;Wm=1;var u=Symbol.for("react.transitional.element"),i=Symbol.for("react.fragment");function r(s,o,d){var y=null;if(d!==void 0&&(y=""+d),o.key!==void 0&&(y=""+o.key),"key"in o){d={};for(var b in o)b!=="key"&&(d[b]=o[b])}else d=o;return o=d.ref,{$$typeof:u,type:s,key:y,ref:o!==void 0?o:null,props:d}}return Ku.Fragment=i,Ku.jsx=r,Ku.jsxs=r,Ku}var Pm;function Xp(){return Pm||(Pm=1,sf.exports=Gp()),sf.exports}var D=Xp(),rf={exports:{}},nt={};/** + * @license React + * react.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Im;function Zp(){if(Im)return nt;Im=1;var u=Symbol.for("react.transitional.element"),i=Symbol.for("react.portal"),r=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),o=Symbol.for("react.profiler"),d=Symbol.for("react.consumer"),y=Symbol.for("react.context"),b=Symbol.for("react.forward_ref"),g=Symbol.for("react.suspense"),m=Symbol.for("react.memo"),O=Symbol.for("react.lazy"),T=Symbol.for("react.activity"),N=Symbol.iterator;function Q(E){return E===null||typeof E!="object"?null:(E=N&&E[N]||E["@@iterator"],typeof E=="function"?E:null)}var Y={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},L=Object.assign,w={};function B(E,q,V){this.props=E,this.context=q,this.refs=w,this.updater=V||Y}B.prototype.isReactComponent={},B.prototype.setState=function(E,q){if(typeof E!="object"&&typeof E!="function"&&E!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,E,q,"setState")},B.prototype.forceUpdate=function(E){this.updater.enqueueForceUpdate(this,E,"forceUpdate")};function F(){}F.prototype=B.prototype;function X(E,q,V){this.props=E,this.context=q,this.refs=w,this.updater=V||Y}var G=X.prototype=new F;G.constructor=X,L(G,B.prototype),G.isPureReactComponent=!0;var et=Array.isArray;function rt(){}var I={H:null,A:null,T:null,S:null},lt=Object.prototype.hasOwnProperty;function yt(E,q,V){var k=V.ref;return{$$typeof:u,type:E,key:q,ref:k!==void 0?k:null,props:V}}function Dt(E,q){return yt(E.type,q,E.props)}function wt(E){return typeof E=="object"&&E!==null&&E.$$typeof===u}function Yt(E){var q={"=":"=0",":":"=2"};return"$"+E.replace(/[=:]/g,function(V){return q[V]})}var Ee=/\/+/g;function te(E,q){return typeof E=="object"&&E!==null&&E.key!=null?Yt(""+E.key):q.toString(36)}function Ut(E){switch(E.status){case"fulfilled":return E.value;case"rejected":throw E.reason;default:switch(typeof E.status=="string"?E.then(rt,rt):(E.status="pending",E.then(function(q){E.status==="pending"&&(E.status="fulfilled",E.value=q)},function(q){E.status==="pending"&&(E.status="rejected",E.reason=q)})),E.status){case"fulfilled":return E.value;case"rejected":throw E.reason}}throw E}function U(E,q,V,k,ut){var ft=typeof E;(ft==="undefined"||ft==="boolean")&&(E=null);var Et=!1;if(E===null)Et=!0;else switch(ft){case"bigint":case"string":case"number":Et=!0;break;case"object":switch(E.$$typeof){case u:case i:Et=!0;break;case O:return Et=E._init,U(Et(E._payload),q,V,k,ut)}}if(Et)return ut=ut(E),Et=k===""?"."+te(E,0):k,et(ut)?(V="",Et!=null&&(V=Et.replace(Ee,"$&/")+"/"),U(ut,q,V,"",function(Pn){return Pn})):ut!=null&&(wt(ut)&&(ut=Dt(ut,V+(ut.key==null||E&&E.key===ut.key?"":(""+ut.key).replace(Ee,"$&/")+"/")+Et)),q.push(ut)),1;Et=0;var se=k===""?".":k+":";if(et(E))for(var Gt=0;Gt>>1,Mt=U[xt];if(0>>1;xto(V,at))ko(ut,V)?(U[xt]=ut,U[k]=at,xt=k):(U[xt]=V,U[q]=at,xt=q);else if(ko(ut,at))U[xt]=ut,U[k]=at,xt=k;else break t}}return Z}function o(U,Z){var at=U.sortIndex-Z.sortIndex;return at!==0?at:U.id-Z.id}if(u.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var d=performance;u.unstable_now=function(){return d.now()}}else{var y=Date,b=y.now();u.unstable_now=function(){return y.now()-b}}var g=[],m=[],O=1,T=null,N=3,Q=!1,Y=!1,L=!1,w=!1,B=typeof setTimeout=="function"?setTimeout:null,F=typeof clearTimeout=="function"?clearTimeout:null,X=typeof setImmediate<"u"?setImmediate:null;function G(U){for(var Z=r(m);Z!==null;){if(Z.callback===null)s(m);else if(Z.startTime<=U)s(m),Z.sortIndex=Z.expirationTime,i(g,Z);else break;Z=r(m)}}function et(U){if(L=!1,G(U),!Y)if(r(g)!==null)Y=!0,rt||(rt=!0,Yt());else{var Z=r(m);Z!==null&&Ut(et,Z.startTime-U)}}var rt=!1,I=-1,lt=5,yt=-1;function Dt(){return w?!0:!(u.unstable_now()-ytU&&Dt());){var xt=T.callback;if(typeof xt=="function"){T.callback=null,N=T.priorityLevel;var Mt=xt(T.expirationTime<=U);if(U=u.unstable_now(),typeof Mt=="function"){T.callback=Mt,G(U),Z=!0;break e}T===r(g)&&s(g),G(U)}else s(g);T=r(g)}if(T!==null)Z=!0;else{var E=r(m);E!==null&&Ut(et,E.startTime-U),Z=!1}}break t}finally{T=null,N=at,Q=!1}Z=void 0}}finally{Z?Yt():rt=!1}}}var Yt;if(typeof X=="function")Yt=function(){X(wt)};else if(typeof MessageChannel<"u"){var Ee=new MessageChannel,te=Ee.port2;Ee.port1.onmessage=wt,Yt=function(){te.postMessage(null)}}else Yt=function(){B(wt,0)};function Ut(U,Z){I=B(function(){U(u.unstable_now())},Z)}u.unstable_IdlePriority=5,u.unstable_ImmediatePriority=1,u.unstable_LowPriority=4,u.unstable_NormalPriority=3,u.unstable_Profiling=null,u.unstable_UserBlockingPriority=2,u.unstable_cancelCallback=function(U){U.callback=null},u.unstable_forceFrameRate=function(U){0>U||125xt?(U.sortIndex=at,i(m,U),r(g)===null&&U===r(m)&&(L?(F(I),I=-1):L=!0,Ut(et,at-xt))):(U.sortIndex=Mt,i(g,U),Y||Q||(Y=!0,rt||(rt=!0,Yt()))),U},u.unstable_shouldYield=Dt,u.unstable_wrapCallback=function(U){var Z=N;return function(){var at=N;N=Z;try{return U.apply(this,arguments)}finally{N=at}}}})(hf)),hf}var ly;function Vp(){return ly||(ly=1,of.exports=Kp()),of.exports}var df={exports:{}},ie={};/** + * @license React + * react-dom.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var ay;function Jp(){if(ay)return ie;ay=1;var u=jf();function i(g){var m="https://react.dev/errors/"+g;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(u)}catch(i){console.error(i)}}return u(),df.exports=Jp(),df.exports}/** + * @license React + * react-dom-client.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var uy;function kp(){if(uy)return Vu;uy=1;var u=Vp(),i=jf(),r=Fp();function s(t){var e="https://react.dev/errors/"+t;if(1Mt||(t.current=xt[Mt],xt[Mt]=null,Mt--)}function V(t,e){Mt++,xt[Mt]=t.current,t.current=e}var k=E(null),ut=E(null),ft=E(null),Et=E(null);function se(t,e){switch(V(ft,e),V(ut,t),V(k,null),e.nodeType){case 9:case 11:t=(t=e.documentElement)&&(t=t.namespaceURI)?bm(t):0;break;default:if(t=e.tagName,e=e.namespaceURI)e=bm(e),t=Sm(e,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}q(k),V(k,t)}function Gt(){q(k),q(ut),q(ft)}function Pn(t){t.memoizedState!==null&&V(Et,t);var e=k.current,l=Sm(e,t.type);e!==l&&(V(ut,t),V(k,l))}function ri(t){ut.current===t&&(q(k),q(ut)),Et.current===t&&(q(Et),Yu._currentValue=at)}var Gc,Ff;function ga(t){if(Gc===void 0)try{throw Error()}catch(l){var e=l.stack.trim().match(/\n( *(at )?)/);Gc=e&&e[1]||"",Ff=-1)":-1n||v[a]!==A[n]){var _=` +`+v[a].replace(" at new "," at ");return t.displayName&&_.includes("")&&(_=_.replace("",t.displayName)),_}while(1<=a&&0<=n);break}}}finally{Xc=!1,Error.prepareStackTrace=l}return(l=t?t.displayName||t.name:"")?ga(l):""}function bv(t,e){switch(t.tag){case 26:case 27:case 5:return ga(t.type);case 16:return ga("Lazy");case 13:return t.child!==e&&e!==null?ga("Suspense Fallback"):ga("Suspense");case 19:return ga("SuspenseList");case 0:case 15:return Zc(t.type,!1);case 11:return Zc(t.type.render,!1);case 1:return Zc(t.type,!0);case 31:return ga("Activity");default:return""}}function kf(t){try{var e="",l=null;do e+=bv(t,l),l=t,t=t.return;while(t);return e}catch(a){return` +Error generating stack: `+a.message+` +`+a.stack}}var Kc=Object.prototype.hasOwnProperty,Vc=u.unstable_scheduleCallback,Jc=u.unstable_cancelCallback,Sv=u.unstable_shouldYield,Ev=u.unstable_requestPaint,Te=u.unstable_now,Tv=u.unstable_getCurrentPriorityLevel,$f=u.unstable_ImmediatePriority,Wf=u.unstable_UserBlockingPriority,fi=u.unstable_NormalPriority,Rv=u.unstable_LowPriority,Pf=u.unstable_IdlePriority,Ov=u.log,xv=u.unstable_setDisableYieldValue,In=null,Re=null;function ql(t){if(typeof Ov=="function"&&xv(t),Re&&typeof Re.setStrictMode=="function")try{Re.setStrictMode(In,t)}catch{}}var Oe=Math.clz32?Math.clz32:zv,Av=Math.log,Cv=Math.LN2;function zv(t){return t>>>=0,t===0?32:31-(Av(t)/Cv|0)|0}var oi=256,hi=262144,di=4194304;function ba(t){var e=t&42;if(e!==0)return e;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function mi(t,e,l){var a=t.pendingLanes;if(a===0)return 0;var n=0,c=t.suspendedLanes,f=t.pingedLanes;t=t.warmLanes;var h=a&134217727;return h!==0?(a=h&~c,a!==0?n=ba(a):(f&=h,f!==0?n=ba(f):l||(l=h&~t,l!==0&&(n=ba(l))))):(h=a&~c,h!==0?n=ba(h):f!==0?n=ba(f):l||(l=a&~t,l!==0&&(n=ba(l)))),n===0?0:e!==0&&e!==n&&(e&c)===0&&(c=n&-n,l=e&-e,c>=l||c===32&&(l&4194048)!==0)?e:n}function tu(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function Mv(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function If(){var t=di;return di<<=1,(di&62914560)===0&&(di=4194304),t}function Fc(t){for(var e=[],l=0;31>l;l++)e.push(t);return e}function eu(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function _v(t,e,l,a,n,c){var f=t.pendingLanes;t.pendingLanes=l,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=l,t.entangledLanes&=l,t.errorRecoveryDisabledLanes&=l,t.shellSuspendCounter=0;var h=t.entanglements,v=t.expirationTimes,A=t.hiddenUpdates;for(l=f&~l;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var qv=/[\n"\\]/g;function qe(t){return t.replace(qv,function(e){return"\\"+e.charCodeAt(0).toString(16)+" "})}function ts(t,e,l,a,n,c,f,h){t.name="",f!=null&&typeof f!="function"&&typeof f!="symbol"&&typeof f!="boolean"?t.type=f:t.removeAttribute("type"),e!=null?f==="number"?(e===0&&t.value===""||t.value!=e)&&(t.value=""+He(e)):t.value!==""+He(e)&&(t.value=""+He(e)):f!=="submit"&&f!=="reset"||t.removeAttribute("value"),e!=null?es(t,f,He(e)):l!=null?es(t,f,He(l)):a!=null&&t.removeAttribute("value"),n==null&&c!=null&&(t.defaultChecked=!!c),n!=null&&(t.checked=n&&typeof n!="function"&&typeof n!="symbol"),h!=null&&typeof h!="function"&&typeof h!="symbol"&&typeof h!="boolean"?t.name=""+He(h):t.removeAttribute("name")}function ho(t,e,l,a,n,c,f,h){if(c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"&&(t.type=c),e!=null||l!=null){if(!(c!=="submit"&&c!=="reset"||e!=null)){Ic(t);return}l=l!=null?""+He(l):"",e=e!=null?""+He(e):l,h||e===t.value||(t.value=e),t.defaultValue=e}a=a??n,a=typeof a!="function"&&typeof a!="symbol"&&!!a,t.checked=h?t.checked:!!a,t.defaultChecked=!!a,f!=null&&typeof f!="function"&&typeof f!="symbol"&&typeof f!="boolean"&&(t.name=f),Ic(t)}function es(t,e,l){e==="number"&&pi(t.ownerDocument)===t||t.defaultValue===""+l||(t.defaultValue=""+l)}function en(t,e,l,a){if(t=t.options,e){e={};for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),is=!1;if(fl)try{var uu={};Object.defineProperty(uu,"passive",{get:function(){is=!0}}),window.addEventListener("test",uu,uu),window.removeEventListener("test",uu,uu)}catch{is=!1}var Ql=null,cs=null,bi=null;function So(){if(bi)return bi;var t,e=cs,l=e.length,a,n="value"in Ql?Ql.value:Ql.textContent,c=n.length;for(t=0;t=su),Ao=" ",Co=!1;function zo(t,e){switch(t){case"keyup":return f0.indexOf(e.keyCode)!==-1;case"keydown":return e.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Mo(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var un=!1;function h0(t,e){switch(t){case"compositionend":return Mo(e);case"keypress":return e.which!==32?null:(Co=!0,Ao);case"textInput":return t=e.data,t===Ao&&Co?null:t;default:return null}}function d0(t,e){if(un)return t==="compositionend"||!hs&&zo(t,e)?(t=So(),bi=cs=Ql=null,un=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(e.ctrlKey||e.altKey||e.metaKey)||e.ctrlKey&&e.altKey){if(e.char&&1=e)return{node:l,offset:e-t};t=a}t:{for(;l;){if(l.nextSibling){l=l.nextSibling;break t}l=l.parentNode}l=void 0}l=Bo(l)}}function Lo(t,e){return t&&e?t===e?!0:t&&t.nodeType===3?!1:e&&e.nodeType===3?Lo(t,e.parentNode):"contains"in t?t.contains(e):t.compareDocumentPosition?!!(t.compareDocumentPosition(e)&16):!1:!1}function wo(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var e=pi(t.document);e instanceof t.HTMLIFrameElement;){try{var l=typeof e.contentWindow.location.href=="string"}catch{l=!1}if(l)t=e.contentWindow;else break;e=pi(t.document)}return e}function ys(t){var e=t&&t.nodeName&&t.nodeName.toLowerCase();return e&&(e==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||e==="textarea"||t.contentEditable==="true")}var E0=fl&&"documentMode"in document&&11>=document.documentMode,cn=null,vs=null,hu=null,ps=!1;function Yo(t,e,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;ps||cn==null||cn!==pi(a)||(a=cn,"selectionStart"in a&&ys(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),hu&&ou(hu,a)||(hu=a,a=hc(vs,"onSelect"),0>=f,n-=f,Ie=1<<32-Oe(e)+n|l<ct?(mt=$,$=null):mt=$.sibling;var bt=C(R,$,x[ct],j);if(bt===null){$===null&&($=mt);break}t&&$&&bt.alternate===null&&e(R,$),S=c(bt,S,ct),gt===null?P=bt:gt.sibling=bt,gt=bt,$=mt}if(ct===x.length)return l(R,$),vt&&hl(R,ct),P;if($===null){for(;ctct?(mt=$,$=null):mt=$.sibling;var ia=C(R,$,bt.value,j);if(ia===null){$===null&&($=mt);break}t&&$&&ia.alternate===null&&e(R,$),S=c(ia,S,ct),gt===null?P=ia:gt.sibling=ia,gt=ia,$=mt}if(bt.done)return l(R,$),vt&&hl(R,ct),P;if($===null){for(;!bt.done;ct++,bt=x.next())bt=H(R,bt.value,j),bt!==null&&(S=c(bt,S,ct),gt===null?P=bt:gt.sibling=bt,gt=bt);return vt&&hl(R,ct),P}for($=a($);!bt.done;ct++,bt=x.next())bt=M($,R,ct,bt.value,j),bt!==null&&(t&&bt.alternate!==null&&$.delete(bt.key===null?ct:bt.key),S=c(bt,S,ct),gt===null?P=bt:gt.sibling=bt,gt=bt);return t&&$.forEach(function(Yp){return e(R,Yp)}),vt&&hl(R,ct),P}function zt(R,S,x,j){if(typeof x=="object"&&x!==null&&x.type===L&&x.key===null&&(x=x.props.children),typeof x=="object"&&x!==null){switch(x.$$typeof){case Q:t:{for(var P=x.key;S!==null;){if(S.key===P){if(P=x.type,P===L){if(S.tag===7){l(R,S.sibling),j=n(S,x.props.children),j.return=R,R=j;break t}}else if(S.elementType===P||typeof P=="object"&&P!==null&&P.$$typeof===lt&&_a(P)===S.type){l(R,S.sibling),j=n(S,x.props),gu(j,x),j.return=R,R=j;break t}l(R,S);break}else e(R,S);S=S.sibling}x.type===L?(j=xa(x.props.children,R.mode,j,x.key),j.return=R,R=j):(j=Mi(x.type,x.key,x.props,null,R.mode,j),gu(j,x),j.return=R,R=j)}return f(R);case Y:t:{for(P=x.key;S!==null;){if(S.key===P)if(S.tag===4&&S.stateNode.containerInfo===x.containerInfo&&S.stateNode.implementation===x.implementation){l(R,S.sibling),j=n(S,x.children||[]),j.return=R,R=j;break t}else{l(R,S);break}else e(R,S);S=S.sibling}j=Os(x,R.mode,j),j.return=R,R=j}return f(R);case lt:return x=_a(x),zt(R,S,x,j)}if(Ut(x))return J(R,S,x,j);if(Yt(x)){if(P=Yt(x),typeof P!="function")throw Error(s(150));return x=P.call(x),tt(R,S,x,j)}if(typeof x.then=="function")return zt(R,S,qi(x),j);if(x.$$typeof===X)return zt(R,S,Ui(R,x),j);Bi(R,x)}return typeof x=="string"&&x!==""||typeof x=="number"||typeof x=="bigint"?(x=""+x,S!==null&&S.tag===6?(l(R,S.sibling),j=n(S,x),j.return=R,R=j):(l(R,S),j=Rs(x,R.mode,j),j.return=R,R=j),f(R)):l(R,S)}return function(R,S,x,j){try{pu=0;var P=zt(R,S,x,j);return gn=null,P}catch($){if($===pn||$===ji)throw $;var gt=Ae(29,$,null,R.mode);return gt.lanes=j,gt.return=R,gt}finally{}}}var Ua=fh(!0),oh=fh(!1),Xl=!1;function qs(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Bs(t,e){t=t.updateQueue,e.updateQueue===t&&(e.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function Zl(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function Kl(t,e,l){var a=t.updateQueue;if(a===null)return null;if(a=a.shared,(St&2)!==0){var n=a.pending;return n===null?e.next=e:(e.next=n.next,n.next=e),a.pending=e,e=zi(t),Fo(t,null,l),e}return Ci(t,a,e,l),zi(t)}function bu(t,e,l){if(e=e.updateQueue,e!==null&&(e=e.shared,(l&4194048)!==0)){var a=e.lanes;a&=t.pendingLanes,l|=a,e.lanes=l,eo(t,l)}}function Qs(t,e){var l=t.updateQueue,a=t.alternate;if(a!==null&&(a=a.updateQueue,l===a)){var n=null,c=null;if(l=l.firstBaseUpdate,l!==null){do{var f={lane:l.lane,tag:l.tag,payload:l.payload,callback:null,next:null};c===null?n=c=f:c=c.next=f,l=l.next}while(l!==null);c===null?n=c=e:c=c.next=e}else n=c=e;l={baseState:a.baseState,firstBaseUpdate:n,lastBaseUpdate:c,shared:a.shared,callbacks:a.callbacks},t.updateQueue=l;return}t=l.lastBaseUpdate,t===null?l.firstBaseUpdate=e:t.next=e,l.lastBaseUpdate=e}var Ls=!1;function Su(){if(Ls){var t=vn;if(t!==null)throw t}}function Eu(t,e,l,a){Ls=!1;var n=t.updateQueue;Xl=!1;var c=n.firstBaseUpdate,f=n.lastBaseUpdate,h=n.shared.pending;if(h!==null){n.shared.pending=null;var v=h,A=v.next;v.next=null,f===null?c=A:f.next=A,f=v;var _=t.alternate;_!==null&&(_=_.updateQueue,h=_.lastBaseUpdate,h!==f&&(h===null?_.firstBaseUpdate=A:h.next=A,_.lastBaseUpdate=v))}if(c!==null){var H=n.baseState;f=0,_=A=v=null,h=c;do{var C=h.lane&-536870913,M=C!==h.lane;if(M?(dt&C)===C:(a&C)===C){C!==0&&C===yn&&(Ls=!0),_!==null&&(_=_.next={lane:0,tag:h.tag,payload:h.payload,callback:null,next:null});t:{var J=t,tt=h;C=e;var zt=l;switch(tt.tag){case 1:if(J=tt.payload,typeof J=="function"){H=J.call(zt,H,C);break t}H=J;break t;case 3:J.flags=J.flags&-65537|128;case 0:if(J=tt.payload,C=typeof J=="function"?J.call(zt,H,C):J,C==null)break t;H=T({},H,C);break t;case 2:Xl=!0}}C=h.callback,C!==null&&(t.flags|=64,M&&(t.flags|=8192),M=n.callbacks,M===null?n.callbacks=[C]:M.push(C))}else M={lane:C,tag:h.tag,payload:h.payload,callback:h.callback,next:null},_===null?(A=_=M,v=H):_=_.next=M,f|=C;if(h=h.next,h===null){if(h=n.shared.pending,h===null)break;M=h,h=M.next,M.next=null,n.lastBaseUpdate=M,n.shared.pending=null}}while(!0);_===null&&(v=H),n.baseState=v,n.firstBaseUpdate=A,n.lastBaseUpdate=_,c===null&&(n.shared.lanes=0),$l|=f,t.lanes=f,t.memoizedState=H}}function hh(t,e){if(typeof t!="function")throw Error(s(191,t));t.call(e)}function dh(t,e){var l=t.callbacks;if(l!==null)for(t.callbacks=null,t=0;tc?c:8;var f=U.T,h={};U.T=h,nr(t,!1,e,l);try{var v=n(),A=U.S;if(A!==null&&A(h,v),v!==null&&typeof v=="object"&&typeof v.then=="function"){var _=_0(v,a);Ou(t,e,_,De(t))}else Ou(t,e,a,De(t))}catch(H){Ou(t,e,{then:function(){},status:"rejected",reason:H},De())}finally{Z.p=c,f!==null&&h.types!==null&&(f.types=h.types),U.T=f}}function q0(){}function lr(t,e,l,a){if(t.tag!==5)throw Error(s(476));var n=Kh(t).queue;Zh(t,n,e,at,l===null?q0:function(){return Vh(t),l(a)})}function Kh(t){var e=t.memoizedState;if(e!==null)return e;e={memoizedState:at,baseState:at,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:vl,lastRenderedState:at},next:null};var l={};return e.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:vl,lastRenderedState:l},next:null},t.memoizedState=e,t=t.alternate,t!==null&&(t.memoizedState=e),e}function Vh(t){var e=Kh(t);e.next===null&&(e=t.alternate.memoizedState),Ou(t,e.next.queue,{},De())}function ar(){return ae(Yu)}function Jh(){return Zt().memoizedState}function Fh(){return Zt().memoizedState}function B0(t){for(var e=t.return;e!==null;){switch(e.tag){case 24:case 3:var l=De();t=Zl(l);var a=Kl(e,t,l);a!==null&&(ge(a,e,l),bu(a,e,l)),e={cache:Us()},t.payload=e;return}e=e.return}}function Q0(t,e,l){var a=De();l={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},Ji(t)?$h(e,l):(l=Es(t,e,l,a),l!==null&&(ge(l,t,a),Wh(l,e,a)))}function kh(t,e,l){var a=De();Ou(t,e,l,a)}function Ou(t,e,l,a){var n={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null};if(Ji(t))$h(e,n);else{var c=t.alternate;if(t.lanes===0&&(c===null||c.lanes===0)&&(c=e.lastRenderedReducer,c!==null))try{var f=e.lastRenderedState,h=c(f,l);if(n.hasEagerState=!0,n.eagerState=h,xe(h,f))return Ci(t,e,n,0),_t===null&&Ai(),!1}catch{}finally{}if(l=Es(t,e,n,a),l!==null)return ge(l,t,a),Wh(l,e,a),!0}return!1}function nr(t,e,l,a){if(a={lane:2,revertLane:qr(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},Ji(t)){if(e)throw Error(s(479))}else e=Es(t,l,a,2),e!==null&&ge(e,t,2)}function Ji(t){var e=t.alternate;return t===it||e!==null&&e===it}function $h(t,e){Sn=wi=!0;var l=t.pending;l===null?e.next=e:(e.next=l.next,l.next=e),t.pending=e}function Wh(t,e,l){if((l&4194048)!==0){var a=e.lanes;a&=t.pendingLanes,l|=a,e.lanes=l,eo(t,l)}}var xu={readContext:ae,use:Xi,useCallback:Bt,useContext:Bt,useEffect:Bt,useImperativeHandle:Bt,useLayoutEffect:Bt,useInsertionEffect:Bt,useMemo:Bt,useReducer:Bt,useRef:Bt,useState:Bt,useDebugValue:Bt,useDeferredValue:Bt,useTransition:Bt,useSyncExternalStore:Bt,useId:Bt,useHostTransitionStatus:Bt,useFormState:Bt,useActionState:Bt,useOptimistic:Bt,useMemoCache:Bt,useCacheRefresh:Bt};xu.useEffectEvent=Bt;var Ph={readContext:ae,use:Xi,useCallback:function(t,e){return re().memoizedState=[t,e===void 0?null:e],t},useContext:ae,useEffect:Hh,useImperativeHandle:function(t,e,l){l=l!=null?l.concat([t]):null,Ki(4194308,4,Lh.bind(null,e,t),l)},useLayoutEffect:function(t,e){return Ki(4194308,4,t,e)},useInsertionEffect:function(t,e){Ki(4,2,t,e)},useMemo:function(t,e){var l=re();e=e===void 0?null:e;var a=t();if(Na){ql(!0);try{t()}finally{ql(!1)}}return l.memoizedState=[a,e],a},useReducer:function(t,e,l){var a=re();if(l!==void 0){var n=l(e);if(Na){ql(!0);try{l(e)}finally{ql(!1)}}}else n=e;return a.memoizedState=a.baseState=n,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:n},a.queue=t,t=t.dispatch=Q0.bind(null,it,t),[a.memoizedState,t]},useRef:function(t){var e=re();return t={current:t},e.memoizedState=t},useState:function(t){t=Ws(t);var e=t.queue,l=kh.bind(null,it,e);return e.dispatch=l,[t.memoizedState,l]},useDebugValue:tr,useDeferredValue:function(t,e){var l=re();return er(l,t,e)},useTransition:function(){var t=Ws(!1);return t=Zh.bind(null,it,t.queue,!0,!1),re().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,e,l){var a=it,n=re();if(vt){if(l===void 0)throw Error(s(407));l=l()}else{if(l=e(),_t===null)throw Error(s(349));(dt&127)!==0||bh(a,e,l)}n.memoizedState=l;var c={value:l,getSnapshot:e};return n.queue=c,Hh(Eh.bind(null,a,c,t),[t]),a.flags|=2048,Tn(9,{destroy:void 0},Sh.bind(null,a,c,l,e),null),l},useId:function(){var t=re(),e=_t.identifierPrefix;if(vt){var l=tl,a=Ie;l=(a&~(1<<32-Oe(a)-1)).toString(32)+l,e="_"+e+"R_"+l,l=Yi++,0<\/script>",c=c.removeChild(c.firstChild);break;case"select":c=typeof a.is=="string"?f.createElement("select",{is:a.is}):f.createElement("select"),a.multiple?c.multiple=!0:a.size&&(c.size=a.size);break;default:c=typeof a.is=="string"?f.createElement(n,{is:a.is}):f.createElement(n)}}c[ee]=e,c[he]=a;t:for(f=e.child;f!==null;){if(f.tag===5||f.tag===6)c.appendChild(f.stateNode);else if(f.tag!==4&&f.tag!==27&&f.child!==null){f.child.return=f,f=f.child;continue}if(f===e)break t;for(;f.sibling===null;){if(f.return===null||f.return===e)break t;f=f.return}f.sibling.return=f.return,f=f.sibling}e.stateNode=c;t:switch(ue(c,n,a),n){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break t;case"img":a=!0;break t;default:a=!1}a&&gl(e)}}return jt(e),gr(e,e.type,t===null?null:t.memoizedProps,e.pendingProps,l),null;case 6:if(t&&e.stateNode!=null)t.memoizedProps!==a&&gl(e);else{if(typeof a!="string"&&e.stateNode===null)throw Error(s(166));if(t=ft.current,dn(e)){if(t=e.stateNode,l=e.memoizedProps,a=null,n=le,n!==null)switch(n.tag){case 27:case 5:a=n.memoizedProps}t[ee]=e,t=!!(t.nodeValue===l||a!==null&&a.suppressHydrationWarning===!0||pm(t.nodeValue,l)),t||Yl(e,!0)}else t=dc(t).createTextNode(a),t[ee]=e,e.stateNode=t}return jt(e),null;case 31:if(l=e.memoizedState,t===null||t.memoizedState!==null){if(a=dn(e),l!==null){if(t===null){if(!a)throw Error(s(318));if(t=e.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(s(557));t[ee]=e}else Aa(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;jt(e),t=!1}else l=zs(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=l),t=!0;if(!t)return e.flags&256?(ze(e),e):(ze(e),null);if((e.flags&128)!==0)throw Error(s(558))}return jt(e),null;case 13:if(a=e.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(n=dn(e),a!==null&&a.dehydrated!==null){if(t===null){if(!n)throw Error(s(318));if(n=e.memoizedState,n=n!==null?n.dehydrated:null,!n)throw Error(s(317));n[ee]=e}else Aa(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;jt(e),n=!1}else n=zs(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=n),n=!0;if(!n)return e.flags&256?(ze(e),e):(ze(e),null)}return ze(e),(e.flags&128)!==0?(e.lanes=l,e):(l=a!==null,t=t!==null&&t.memoizedState!==null,l&&(a=e.child,n=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(n=a.alternate.memoizedState.cachePool.pool),c=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(c=a.memoizedState.cachePool.pool),c!==n&&(a.flags|=2048)),l!==t&&l&&(e.child.flags|=8192),Pi(e,e.updateQueue),jt(e),null);case 4:return Gt(),t===null&&wr(e.stateNode.containerInfo),jt(e),null;case 10:return ml(e.type),jt(e),null;case 19:if(q(Xt),a=e.memoizedState,a===null)return jt(e),null;if(n=(e.flags&128)!==0,c=a.rendering,c===null)if(n)Cu(a,!1);else{if(Qt!==0||t!==null&&(t.flags&128)!==0)for(t=e.child;t!==null;){if(c=Li(t),c!==null){for(e.flags|=128,Cu(a,!1),t=c.updateQueue,e.updateQueue=t,Pi(e,t),e.subtreeFlags=0,t=l,l=e.child;l!==null;)ko(l,t),l=l.sibling;return V(Xt,Xt.current&1|2),vt&&hl(e,a.treeForkCount),e.child}t=t.sibling}a.tail!==null&&Te()>ac&&(e.flags|=128,n=!0,Cu(a,!1),e.lanes=4194304)}else{if(!n)if(t=Li(c),t!==null){if(e.flags|=128,n=!0,t=t.updateQueue,e.updateQueue=t,Pi(e,t),Cu(a,!0),a.tail===null&&a.tailMode==="hidden"&&!c.alternate&&!vt)return jt(e),null}else 2*Te()-a.renderingStartTime>ac&&l!==536870912&&(e.flags|=128,n=!0,Cu(a,!1),e.lanes=4194304);a.isBackwards?(c.sibling=e.child,e.child=c):(t=a.last,t!==null?t.sibling=c:e.child=c,a.last=c)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=Te(),t.sibling=null,l=Xt.current,V(Xt,n?l&1|2:l&1),vt&&hl(e,a.treeForkCount),t):(jt(e),null);case 22:case 23:return ze(e),Ys(),a=e.memoizedState!==null,t!==null?t.memoizedState!==null!==a&&(e.flags|=8192):a&&(e.flags|=8192),a?(l&536870912)!==0&&(e.flags&128)===0&&(jt(e),e.subtreeFlags&6&&(e.flags|=8192)):jt(e),l=e.updateQueue,l!==null&&Pi(e,l.retryQueue),l=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(l=t.memoizedState.cachePool.pool),a=null,e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(a=e.memoizedState.cachePool.pool),a!==l&&(e.flags|=2048),t!==null&&q(Ma),null;case 24:return l=null,t!==null&&(l=t.memoizedState.cache),e.memoizedState.cache!==l&&(e.flags|=2048),ml(Kt),jt(e),null;case 25:return null;case 30:return null}throw Error(s(156,e.tag))}function X0(t,e){switch(As(e),e.tag){case 1:return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 3:return ml(Kt),Gt(),t=e.flags,(t&65536)!==0&&(t&128)===0?(e.flags=t&-65537|128,e):null;case 26:case 27:case 5:return ri(e),null;case 31:if(e.memoizedState!==null){if(ze(e),e.alternate===null)throw Error(s(340));Aa()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 13:if(ze(e),t=e.memoizedState,t!==null&&t.dehydrated!==null){if(e.alternate===null)throw Error(s(340));Aa()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 19:return q(Xt),null;case 4:return Gt(),null;case 10:return ml(e.type),null;case 22:case 23:return ze(e),Ys(),t!==null&&q(Ma),t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 24:return ml(Kt),null;case 25:return null;default:return null}}function Td(t,e){switch(As(e),e.tag){case 3:ml(Kt),Gt();break;case 26:case 27:case 5:ri(e);break;case 4:Gt();break;case 31:e.memoizedState!==null&&ze(e);break;case 13:ze(e);break;case 19:q(Xt);break;case 10:ml(e.type);break;case 22:case 23:ze(e),Ys(),t!==null&&q(Ma);break;case 24:ml(Kt)}}function zu(t,e){try{var l=e.updateQueue,a=l!==null?l.lastEffect:null;if(a!==null){var n=a.next;l=n;do{if((l.tag&t)===t){a=void 0;var c=l.create,f=l.inst;a=c(),f.destroy=a}l=l.next}while(l!==n)}}catch(h){Ot(e,e.return,h)}}function Fl(t,e,l){try{var a=e.updateQueue,n=a!==null?a.lastEffect:null;if(n!==null){var c=n.next;a=c;do{if((a.tag&t)===t){var f=a.inst,h=f.destroy;if(h!==void 0){f.destroy=void 0,n=e;var v=l,A=h;try{A()}catch(_){Ot(n,v,_)}}}a=a.next}while(a!==c)}}catch(_){Ot(e,e.return,_)}}function Rd(t){var e=t.updateQueue;if(e!==null){var l=t.stateNode;try{dh(e,l)}catch(a){Ot(t,t.return,a)}}}function Od(t,e,l){l.props=ja(t.type,t.memoizedProps),l.state=t.memoizedState;try{l.componentWillUnmount()}catch(a){Ot(t,e,a)}}function Mu(t,e){try{var l=t.ref;if(l!==null){switch(t.tag){case 26:case 27:case 5:var a=t.stateNode;break;case 30:a=t.stateNode;break;default:a=t.stateNode}typeof l=="function"?t.refCleanup=l(a):l.current=a}}catch(n){Ot(t,e,n)}}function el(t,e){var l=t.ref,a=t.refCleanup;if(l!==null)if(typeof a=="function")try{a()}catch(n){Ot(t,e,n)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof l=="function")try{l(null)}catch(n){Ot(t,e,n)}else l.current=null}function xd(t){var e=t.type,l=t.memoizedProps,a=t.stateNode;try{t:switch(e){case"button":case"input":case"select":case"textarea":l.autoFocus&&a.focus();break t;case"img":l.src?a.src=l.src:l.srcSet&&(a.srcset=l.srcSet)}}catch(n){Ot(t,t.return,n)}}function br(t,e,l){try{var a=t.stateNode;op(a,t.type,l,e),a[he]=e}catch(n){Ot(t,t.return,n)}}function Ad(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&ea(t.type)||t.tag===4}function Sr(t){t:for(;;){for(;t.sibling===null;){if(t.return===null||Ad(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&ea(t.type)||t.flags&2||t.child===null||t.tag===4)continue t;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function Er(t,e,l){var a=t.tag;if(a===5||a===6)t=t.stateNode,e?(l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l).insertBefore(t,e):(e=l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l,e.appendChild(t),l=l._reactRootContainer,l!=null||e.onclick!==null||(e.onclick=rl));else if(a!==4&&(a===27&&ea(t.type)&&(l=t.stateNode,e=null),t=t.child,t!==null))for(Er(t,e,l),t=t.sibling;t!==null;)Er(t,e,l),t=t.sibling}function Ii(t,e,l){var a=t.tag;if(a===5||a===6)t=t.stateNode,e?l.insertBefore(t,e):l.appendChild(t);else if(a!==4&&(a===27&&ea(t.type)&&(l=t.stateNode),t=t.child,t!==null))for(Ii(t,e,l),t=t.sibling;t!==null;)Ii(t,e,l),t=t.sibling}function Cd(t){var e=t.stateNode,l=t.memoizedProps;try{for(var a=t.type,n=e.attributes;n.length;)e.removeAttributeNode(n[0]);ue(e,a,l),e[ee]=t,e[he]=l}catch(c){Ot(t,t.return,c)}}var bl=!1,Ft=!1,Tr=!1,zd=typeof WeakSet=="function"?WeakSet:Set,Pt=null;function Z0(t,e){if(t=t.containerInfo,Xr=Sc,t=wo(t),ys(t)){if("selectionStart"in t)var l={start:t.selectionStart,end:t.selectionEnd};else t:{l=(l=t.ownerDocument)&&l.defaultView||window;var a=l.getSelection&&l.getSelection();if(a&&a.rangeCount!==0){l=a.anchorNode;var n=a.anchorOffset,c=a.focusNode;a=a.focusOffset;try{l.nodeType,c.nodeType}catch{l=null;break t}var f=0,h=-1,v=-1,A=0,_=0,H=t,C=null;e:for(;;){for(var M;H!==l||n!==0&&H.nodeType!==3||(h=f+n),H!==c||a!==0&&H.nodeType!==3||(v=f+a),H.nodeType===3&&(f+=H.nodeValue.length),(M=H.firstChild)!==null;)C=H,H=M;for(;;){if(H===t)break e;if(C===l&&++A===n&&(h=f),C===c&&++_===a&&(v=f),(M=H.nextSibling)!==null)break;H=C,C=H.parentNode}H=M}l=h===-1||v===-1?null:{start:h,end:v}}else l=null}l=l||{start:0,end:0}}else l=null;for(Zr={focusedElem:t,selectionRange:l},Sc=!1,Pt=e;Pt!==null;)if(e=Pt,t=e.child,(e.subtreeFlags&1028)!==0&&t!==null)t.return=e,Pt=t;else for(;Pt!==null;){switch(e=Pt,c=e.alternate,t=e.flags,e.tag){case 0:if((t&4)!==0&&(t=e.updateQueue,t=t!==null?t.events:null,t!==null))for(l=0;l title"))),ue(c,a,l),c[ee]=t,Wt(c),a=c;break t;case"link":var f=jm("link","href",n).get(a+(l.href||""));if(f){for(var h=0;hzt&&(f=zt,zt=tt,tt=f);var R=Qo(h,tt),S=Qo(h,zt);if(R&&S&&(M.rangeCount!==1||M.anchorNode!==R.node||M.anchorOffset!==R.offset||M.focusNode!==S.node||M.focusOffset!==S.offset)){var x=H.createRange();x.setStart(R.node,R.offset),M.removeAllRanges(),tt>zt?(M.addRange(x),M.extend(S.node,S.offset)):(x.setEnd(S.node,S.offset),M.addRange(x))}}}}for(H=[],M=h;M=M.parentNode;)M.nodeType===1&&H.push({element:M,left:M.scrollLeft,top:M.scrollTop});for(typeof h.focus=="function"&&h.focus(),h=0;hl?32:l,U.T=null,l=Mr,Mr=null;var c=Pl,f=Ol;if(kt=0,Cn=Pl=null,Ol=0,(St&6)!==0)throw Error(s(331));var h=St;if(St|=4,Ld(c.current),qd(c,c.current,f,l),St=h,Hu(0,!1),Re&&typeof Re.onPostCommitFiberRoot=="function")try{Re.onPostCommitFiberRoot(In,c)}catch{}return!0}finally{Z.p=n,U.T=a,am(t,e)}}function um(t,e,l){e=Qe(l,e),e=sr(t.stateNode,e,2),t=Kl(t,e,2),t!==null&&(eu(t,2),ll(t))}function Ot(t,e,l){if(t.tag===3)um(t,t,l);else for(;e!==null;){if(e.tag===3){um(e,t,l);break}else if(e.tag===1){var a=e.stateNode;if(typeof e.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(Wl===null||!Wl.has(a))){t=Qe(l,t),l=id(2),a=Kl(e,l,2),a!==null&&(cd(l,a,e,t),eu(a,2),ll(a));break}}e=e.return}}function Nr(t,e,l){var a=t.pingCache;if(a===null){a=t.pingCache=new J0;var n=new Set;a.set(e,n)}else n=a.get(e),n===void 0&&(n=new Set,a.set(e,n));n.has(l)||(xr=!0,n.add(l),t=P0.bind(null,t,e,l),e.then(t,t))}function P0(t,e,l){var a=t.pingCache;a!==null&&a.delete(e),t.pingedLanes|=t.suspendedLanes&l,t.warmLanes&=~l,_t===t&&(dt&l)===l&&(Qt===4||Qt===3&&(dt&62914560)===dt&&300>Te()-lc?(St&2)===0&&zn(t,0):Ar|=l,An===dt&&(An=0)),ll(t)}function im(t,e){e===0&&(e=If()),t=Oa(t,e),t!==null&&(eu(t,e),ll(t))}function I0(t){var e=t.memoizedState,l=0;e!==null&&(l=e.retryLane),im(t,l)}function tp(t,e){var l=0;switch(t.tag){case 31:case 13:var a=t.stateNode,n=t.memoizedState;n!==null&&(l=n.retryLane);break;case 19:a=t.stateNode;break;case 22:a=t.stateNode._retryCache;break;default:throw Error(s(314))}a!==null&&a.delete(e),im(t,l)}function ep(t,e){return Vc(t,e)}var rc=null,_n=null,jr=!1,fc=!1,Hr=!1,ta=0;function ll(t){t!==_n&&t.next===null&&(_n===null?rc=_n=t:_n=_n.next=t),fc=!0,jr||(jr=!0,ap())}function Hu(t,e){if(!Hr&&fc){Hr=!0;do for(var l=!1,a=rc;a!==null;){if(t!==0){var n=a.pendingLanes;if(n===0)var c=0;else{var f=a.suspendedLanes,h=a.pingedLanes;c=(1<<31-Oe(42|t)+1)-1,c&=n&~(f&~h),c=c&201326741?c&201326741|1:c?c|2:0}c!==0&&(l=!0,fm(a,c))}else c=dt,c=mi(a,a===_t?c:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(c&3)===0||tu(a,c)||(l=!0,fm(a,c));a=a.next}while(l);Hr=!1}}function lp(){cm()}function cm(){fc=jr=!1;var t=0;ta!==0&&dp()&&(t=ta);for(var e=Te(),l=null,a=rc;a!==null;){var n=a.next,c=sm(a,e);c===0?(a.next=null,l===null?rc=n:l.next=n,n===null&&(_n=l)):(l=a,(t!==0||(c&3)!==0)&&(fc=!0)),a=n}kt!==0&&kt!==5||Hu(t),ta!==0&&(ta=0)}function sm(t,e){for(var l=t.suspendedLanes,a=t.pingedLanes,n=t.expirationTimes,c=t.pendingLanes&-62914561;0h)break;var _=v.transferSize,H=v.initiatorType;_&&gm(H)&&(v=v.responseEnd,f+=_*(v"u"?null:document;function _m(t,e,l){var a=Dn;if(a&&typeof e=="string"&&e){var n=qe(e);n='link[rel="'+t+'"][href="'+n+'"]',typeof l=="string"&&(n+='[crossorigin="'+l+'"]'),Mm.has(n)||(Mm.add(n),t={rel:t,crossOrigin:l,href:e},a.querySelector(n)===null&&(e=a.createElement("link"),ue(e,"link",t),Wt(e),a.head.appendChild(e)))}}function Tp(t){xl.D(t),_m("dns-prefetch",t,null)}function Rp(t,e){xl.C(t,e),_m("preconnect",t,e)}function Op(t,e,l){xl.L(t,e,l);var a=Dn;if(a&&t&&e){var n='link[rel="preload"][as="'+qe(e)+'"]';e==="image"&&l&&l.imageSrcSet?(n+='[imagesrcset="'+qe(l.imageSrcSet)+'"]',typeof l.imageSizes=="string"&&(n+='[imagesizes="'+qe(l.imageSizes)+'"]')):n+='[href="'+qe(t)+'"]';var c=n;switch(e){case"style":c=Un(t);break;case"script":c=Nn(t)}Ze.has(c)||(t=T({rel:"preload",href:e==="image"&&l&&l.imageSrcSet?void 0:t,as:e},l),Ze.set(c,t),a.querySelector(n)!==null||e==="style"&&a.querySelector(Lu(c))||e==="script"&&a.querySelector(wu(c))||(e=a.createElement("link"),ue(e,"link",t),Wt(e),a.head.appendChild(e)))}}function xp(t,e){xl.m(t,e);var l=Dn;if(l&&t){var a=e&&typeof e.as=="string"?e.as:"script",n='link[rel="modulepreload"][as="'+qe(a)+'"][href="'+qe(t)+'"]',c=n;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":c=Nn(t)}if(!Ze.has(c)&&(t=T({rel:"modulepreload",href:t},e),Ze.set(c,t),l.querySelector(n)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(l.querySelector(wu(c)))return}a=l.createElement("link"),ue(a,"link",t),Wt(a),l.head.appendChild(a)}}}function Ap(t,e,l){xl.S(t,e,l);var a=Dn;if(a&&t){var n=Ia(a).hoistableStyles,c=Un(t);e=e||"default";var f=n.get(c);if(!f){var h={loading:0,preload:null};if(f=a.querySelector(Lu(c)))h.loading=5;else{t=T({rel:"stylesheet",href:t,"data-precedence":e},l),(l=Ze.get(c))&&Wr(t,l);var v=f=a.createElement("link");Wt(v),ue(v,"link",t),v._p=new Promise(function(A,_){v.onload=A,v.onerror=_}),v.addEventListener("load",function(){h.loading|=1}),v.addEventListener("error",function(){h.loading|=2}),h.loading|=4,yc(f,e,a)}f={type:"stylesheet",instance:f,count:1,state:h},n.set(c,f)}}}function Cp(t,e){xl.X(t,e);var l=Dn;if(l&&t){var a=Ia(l).hoistableScripts,n=Nn(t),c=a.get(n);c||(c=l.querySelector(wu(n)),c||(t=T({src:t,async:!0},e),(e=Ze.get(n))&&Pr(t,e),c=l.createElement("script"),Wt(c),ue(c,"link",t),l.head.appendChild(c)),c={type:"script",instance:c,count:1,state:null},a.set(n,c))}}function zp(t,e){xl.M(t,e);var l=Dn;if(l&&t){var a=Ia(l).hoistableScripts,n=Nn(t),c=a.get(n);c||(c=l.querySelector(wu(n)),c||(t=T({src:t,async:!0,type:"module"},e),(e=Ze.get(n))&&Pr(t,e),c=l.createElement("script"),Wt(c),ue(c,"link",t),l.head.appendChild(c)),c={type:"script",instance:c,count:1,state:null},a.set(n,c))}}function Dm(t,e,l,a){var n=(n=ft.current)?mc(n):null;if(!n)throw Error(s(446));switch(t){case"meta":case"title":return null;case"style":return typeof l.precedence=="string"&&typeof l.href=="string"?(e=Un(l.href),l=Ia(n).hoistableStyles,a=l.get(e),a||(a={type:"style",instance:null,count:0,state:null},l.set(e,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(l.rel==="stylesheet"&&typeof l.href=="string"&&typeof l.precedence=="string"){t=Un(l.href);var c=Ia(n).hoistableStyles,f=c.get(t);if(f||(n=n.ownerDocument||n,f={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},c.set(t,f),(c=n.querySelector(Lu(t)))&&!c._p&&(f.instance=c,f.state.loading=5),Ze.has(t)||(l={rel:"preload",as:"style",href:l.href,crossOrigin:l.crossOrigin,integrity:l.integrity,media:l.media,hrefLang:l.hrefLang,referrerPolicy:l.referrerPolicy},Ze.set(t,l),c||Mp(n,t,l,f.state))),e&&a===null)throw Error(s(528,""));return f}if(e&&a!==null)throw Error(s(529,""));return null;case"script":return e=l.async,l=l.src,typeof l=="string"&&e&&typeof e!="function"&&typeof e!="symbol"?(e=Nn(l),l=Ia(n).hoistableScripts,a=l.get(e),a||(a={type:"script",instance:null,count:0,state:null},l.set(e,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(s(444,t))}}function Un(t){return'href="'+qe(t)+'"'}function Lu(t){return'link[rel="stylesheet"]['+t+"]"}function Um(t){return T({},t,{"data-precedence":t.precedence,precedence:null})}function Mp(t,e,l,a){t.querySelector('link[rel="preload"][as="style"]['+e+"]")?a.loading=1:(e=t.createElement("link"),a.preload=e,e.addEventListener("load",function(){return a.loading|=1}),e.addEventListener("error",function(){return a.loading|=2}),ue(e,"link",l),Wt(e),t.head.appendChild(e))}function Nn(t){return'[src="'+qe(t)+'"]'}function wu(t){return"script[async]"+t}function Nm(t,e,l){if(e.count++,e.instance===null)switch(e.type){case"style":var a=t.querySelector('style[data-href~="'+qe(l.href)+'"]');if(a)return e.instance=a,Wt(a),a;var n=T({},l,{"data-href":l.href,"data-precedence":l.precedence,href:null,precedence:null});return a=(t.ownerDocument||t).createElement("style"),Wt(a),ue(a,"style",n),yc(a,l.precedence,t),e.instance=a;case"stylesheet":n=Un(l.href);var c=t.querySelector(Lu(n));if(c)return e.state.loading|=4,e.instance=c,Wt(c),c;a=Um(l),(n=Ze.get(n))&&Wr(a,n),c=(t.ownerDocument||t).createElement("link"),Wt(c);var f=c;return f._p=new Promise(function(h,v){f.onload=h,f.onerror=v}),ue(c,"link",a),e.state.loading|=4,yc(c,l.precedence,t),e.instance=c;case"script":return c=Nn(l.src),(n=t.querySelector(wu(c)))?(e.instance=n,Wt(n),n):(a=l,(n=Ze.get(c))&&(a=T({},l),Pr(a,n)),t=t.ownerDocument||t,n=t.createElement("script"),Wt(n),ue(n,"link",a),t.head.appendChild(n),e.instance=n);case"void":return null;default:throw Error(s(443,e.type))}else e.type==="stylesheet"&&(e.state.loading&4)===0&&(a=e.instance,e.state.loading|=4,yc(a,l.precedence,t));return e.instance}function yc(t,e,l){for(var a=l.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),n=a.length?a[a.length-1]:null,c=n,f=0;f title"):null)}function _p(t,e,l){if(l===1||e.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof e.precedence!="string"||typeof e.href!="string"||e.href==="")break;return!0;case"link":if(typeof e.rel!="string"||typeof e.href!="string"||e.href===""||e.onLoad||e.onError)break;switch(e.rel){case"stylesheet":return t=e.disabled,typeof e.precedence=="string"&&t==null;default:return!0}case"script":if(e.async&&typeof e.async!="function"&&typeof e.async!="symbol"&&!e.onLoad&&!e.onError&&e.src&&typeof e.src=="string")return!0}return!1}function qm(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function Dp(t,e,l,a){if(l.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(l.state.loading&4)===0){if(l.instance===null){var n=Un(a.href),c=e.querySelector(Lu(n));if(c){e=c._p,e!==null&&typeof e=="object"&&typeof e.then=="function"&&(t.count++,t=pc.bind(t),e.then(t,t)),l.state.loading|=4,l.instance=c,Wt(c);return}c=e.ownerDocument||e,a=Um(a),(n=Ze.get(n))&&Wr(a,n),c=c.createElement("link"),Wt(c);var f=c;f._p=new Promise(function(h,v){f.onload=h,f.onerror=v}),ue(c,"link",a),l.instance=c}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(l,e),(e=l.state.preload)&&(l.state.loading&3)===0&&(t.count++,l=pc.bind(t),e.addEventListener("load",l),e.addEventListener("error",l))}}var Ir=0;function Up(t,e){return t.stylesheets&&t.count===0&&bc(t,t.stylesheets),0Ir?50:800)+e);return t.unsuspend=l,function(){t.unsuspend=null,clearTimeout(a),clearTimeout(n)}}:null}function pc(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)bc(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var gc=null;function bc(t,e){t.stylesheets=null,t.unsuspend!==null&&(t.count++,gc=new Map,e.forEach(Np,t),gc=null,pc.call(t))}function Np(t,e){if(!(e.state.loading&4)){var l=gc.get(t);if(l)var a=l.get(null);else{l=new Map,gc.set(t,l);for(var n=t.querySelectorAll("link[data-precedence],style[data-precedence]"),c=0;c"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(u)}catch(i){console.error(i)}}return u(),ff.exports=kp(),ff.exports}var Wp=$p(),kn=class{constructor(){this.listeners=new Set,this.subscribe=this.subscribe.bind(this)}subscribe(u){return this.listeners.add(u),this.onSubscribe(),()=>{this.listeners.delete(u),this.onUnsubscribe()}}hasListeners(){return this.listeners.size>0}onSubscribe(){}onUnsubscribe(){}},Qa,sa,qn,Oy,Pp=(Oy=class extends kn{constructor(){super();W(this,Qa);W(this,sa);W(this,qn);K(this,qn,i=>{if(typeof window<"u"&&window.addEventListener){const r=()=>i();return window.addEventListener("visibilitychange",r,!1),()=>{window.removeEventListener("visibilitychange",r)}}})}onSubscribe(){p(this,sa)||this.setEventListener(p(this,qn))}onUnsubscribe(){var i;this.hasListeners()||((i=p(this,sa))==null||i.call(this),K(this,sa,void 0))}setEventListener(i){var r;K(this,qn,i),(r=p(this,sa))==null||r.call(this),K(this,sa,i(s=>{typeof s=="boolean"?this.setFocused(s):this.onFocus()}))}setFocused(i){p(this,Qa)!==i&&(K(this,Qa,i),this.onFocus())}onFocus(){const i=this.isFocused();this.listeners.forEach(r=>{r(i)})}isFocused(){var i;return typeof p(this,Qa)=="boolean"?p(this,Qa):((i=globalThis.document)==null?void 0:i.visibilityState)!=="hidden"}},Qa=new WeakMap,sa=new WeakMap,qn=new WeakMap,Oy),Hf=new Pp,Ip={setTimeout:(u,i)=>setTimeout(u,i),clearTimeout:u=>clearTimeout(u),setInterval:(u,i)=>setInterval(u,i),clearInterval:u=>clearInterval(u)},ra,Nf,xy,tg=(xy=class{constructor(){W(this,ra,Ip);W(this,Nf,!1)}setTimeoutProvider(u){K(this,ra,u)}setTimeout(u,i){return p(this,ra).setTimeout(u,i)}clearTimeout(u){p(this,ra).clearTimeout(u)}setInterval(u,i){return p(this,ra).setInterval(u,i)}clearInterval(u){p(this,ra).clearInterval(u)}},ra=new WeakMap,Nf=new WeakMap,xy),Ba=new tg;function eg(u){setTimeout(u,0)}var lg=typeof window>"u"||"Deno"in globalThis;function oe(){}function ag(u,i){return typeof u=="function"?u(i):u}function pf(u){return typeof u=="number"&&u>=0&&u!==1/0}function Hy(u,i){return Math.max(u+(i||0)-Date.now(),0)}function pa(u,i){return typeof u=="function"?u(i):u}function Ne(u,i){return typeof u=="function"?u(i):u}function cy(u,i){const{type:r="all",exact:s,fetchStatus:o,predicate:d,queryKey:y,stale:b}=u;if(y){if(s){if(i.queryHash!==qf(y,i.options))return!1}else if(!ku(i.queryKey,y))return!1}if(r!=="all"){const g=i.isActive();if(r==="active"&&!g||r==="inactive"&&g)return!1}return!(typeof b=="boolean"&&i.isStale()!==b||o&&o!==i.state.fetchStatus||d&&!d(i))}function sy(u,i){const{exact:r,status:s,predicate:o,mutationKey:d}=u;if(d){if(!i.options.mutationKey)return!1;if(r){if(ka(i.options.mutationKey)!==ka(d))return!1}else if(!ku(i.options.mutationKey,d))return!1}return!(s&&i.state.status!==s||o&&!o(i))}function qf(u,i){return((i==null?void 0:i.queryKeyHashFn)||ka)(u)}function ka(u){return JSON.stringify(u,(i,r)=>gf(r)?Object.keys(r).sort().reduce((s,o)=>(s[o]=r[o],s),{}):r)}function ku(u,i){return u===i?!0:typeof u!=typeof i?!1:u&&i&&typeof u=="object"&&typeof i=="object"?Object.keys(i).every(r=>ku(u[r],i[r])):!1}var ng=Object.prototype.hasOwnProperty;function qy(u,i,r=0){if(u===i)return u;if(r>500)return i;const s=ry(u)&&ry(i);if(!s&&!(gf(u)&&gf(i)))return i;const d=(s?u:Object.keys(u)).length,y=s?i:Object.keys(i),b=y.length,g=s?new Array(b):{};let m=0;for(let O=0;O{Ba.setTimeout(i,u)})}function bf(u,i,r){return typeof r.structuralSharing=="function"?r.structuralSharing(u,i):r.structuralSharing!==!1?qy(u,i):i}function ig(u,i,r=0){const s=[...u,i];return r&&s.length>r?s.slice(1):s}function cg(u,i,r=0){const s=[i,...u];return r&&s.length>r?s.slice(0,-1):s}var Bf=Symbol();function By(u,i){return!u.queryFn&&(i!=null&&i.initialPromise)?()=>i.initialPromise:!u.queryFn||u.queryFn===Bf?()=>Promise.reject(new Error(`Missing queryFn: '${u.queryHash}'`)):u.queryFn}function Qf(u,i){return typeof u=="function"?u(...i):!!u}function sg(u,i,r){let s=!1,o;return Object.defineProperty(u,"signal",{enumerable:!0,get:()=>(o??(o=i()),s||(s=!0,o.aborted?r():o.addEventListener("abort",r,{once:!0})),o)}),u}var $u=(()=>{let u=()=>lg;return{isServer(){return u()},setIsServer(i){u=i}}})();function Sf(){let u,i;const r=new Promise((o,d)=>{u=o,i=d});r.status="pending",r.catch(()=>{});function s(o){Object.assign(r,o),delete r.resolve,delete r.reject}return r.resolve=o=>{s({status:"fulfilled",value:o}),u(o)},r.reject=o=>{s({status:"rejected",reason:o}),i(o)},r}var rg=eg;function fg(){let u=[],i=0,r=b=>{b()},s=b=>{b()},o=rg;const d=b=>{i?u.push(b):o(()=>{r(b)})},y=()=>{const b=u;u=[],b.length&&o(()=>{s(()=>{b.forEach(g=>{r(g)})})})};return{batch:b=>{let g;i++;try{g=b()}finally{i--,i||y()}return g},batchCalls:b=>(...g)=>{d(()=>{b(...g)})},schedule:d,setNotifyFunction:b=>{r=b},setBatchNotifyFunction:b=>{s=b},setScheduler:b=>{o=b}}}var $t=fg(),Bn,fa,Qn,Ay,og=(Ay=class extends kn{constructor(){super();W(this,Bn,!0);W(this,fa);W(this,Qn);K(this,Qn,i=>{if(typeof window<"u"&&window.addEventListener){const r=()=>i(!0),s=()=>i(!1);return window.addEventListener("online",r,!1),window.addEventListener("offline",s,!1),()=>{window.removeEventListener("online",r),window.removeEventListener("offline",s)}}})}onSubscribe(){p(this,fa)||this.setEventListener(p(this,Qn))}onUnsubscribe(){var i;this.hasListeners()||((i=p(this,fa))==null||i.call(this),K(this,fa,void 0))}setEventListener(i){var r;K(this,Qn,i),(r=p(this,fa))==null||r.call(this),K(this,fa,i(this.setOnline.bind(this)))}setOnline(i){p(this,Bn)!==i&&(K(this,Bn,i),this.listeners.forEach(s=>{s(i)}))}isOnline(){return p(this,Bn)}},Bn=new WeakMap,fa=new WeakMap,Qn=new WeakMap,Ay),Hc=new og;function hg(u){return Math.min(1e3*2**u,3e4)}function Qy(u){return(u??"online")==="online"?Hc.isOnline():!0}var Ef=class extends Error{constructor(u){super("CancelledError"),this.revert=u==null?void 0:u.revert,this.silent=u==null?void 0:u.silent}};function Ly(u){let i=!1,r=0,s;const o=Sf(),d=()=>o.status!=="pending",y=L=>{var w;if(!d()){const B=new Ef(L);N(B),(w=u.onCancel)==null||w.call(u,B)}},b=()=>{i=!0},g=()=>{i=!1},m=()=>Hf.isFocused()&&(u.networkMode==="always"||Hc.isOnline())&&u.canRun(),O=()=>Qy(u.networkMode)&&u.canRun(),T=L=>{d()||(s==null||s(),o.resolve(L))},N=L=>{d()||(s==null||s(),o.reject(L))},Q=()=>new Promise(L=>{var w;s=B=>{(d()||m())&&L(B)},(w=u.onPause)==null||w.call(u)}).then(()=>{var L;s=void 0,d()||(L=u.onContinue)==null||L.call(u)}),Y=()=>{if(d())return;let L;const w=r===0?u.initialPromise:void 0;try{L=w??u.fn()}catch(B){L=Promise.reject(B)}Promise.resolve(L).then(T).catch(B=>{var rt;if(d())return;const F=u.retry??($u.isServer()?0:3),X=u.retryDelay??hg,G=typeof X=="function"?X(r,B):X,et=F===!0||typeof F=="number"&&rm()?void 0:Q()).then(()=>{i?N(B):Y()})})};return{promise:o,status:()=>o.status,cancel:y,continue:()=>(s==null||s(),o),cancelRetry:b,continueRetry:g,canStart:O,start:()=>(O()?Y():Q().then(Y),o)}}var La,Cy,wy=(Cy=class{constructor(){W(this,La)}destroy(){this.clearGcTimeout()}scheduleGc(){this.clearGcTimeout(),pf(this.gcTime)&&K(this,La,Ba.setTimeout(()=>{this.optionalRemove()},this.gcTime))}updateGcTime(u){this.gcTime=Math.max(this.gcTime||0,u??($u.isServer()?1/0:300*1e3))}clearGcTimeout(){p(this,La)!==void 0&&(Ba.clearTimeout(p(this,La)),K(this,La,void 0))}},La=new WeakMap,Cy);function dg(u){return{onFetch:(i,r)=>{var O,T,N,Q,Y;const s=i.options,o=(N=(T=(O=i.fetchOptions)==null?void 0:O.meta)==null?void 0:T.fetchMore)==null?void 0:N.direction,d=((Q=i.state.data)==null?void 0:Q.pages)||[],y=((Y=i.state.data)==null?void 0:Y.pageParams)||[];let b={pages:[],pageParams:[]},g=0;const m=async()=>{let L=!1;const w=X=>{sg(X,()=>i.signal,()=>L=!0)},B=By(i.options,i.fetchOptions),F=async(X,G,et)=>{if(L)return Promise.reject(i.signal.reason);if(G==null&&X.pages.length)return Promise.resolve(X);const I=(()=>{const wt={client:i.client,queryKey:i.queryKey,pageParam:G,direction:et?"backward":"forward",meta:i.options.meta};return w(wt),wt})(),lt=await B(I),{maxPages:yt}=i.options,Dt=et?cg:ig;return{pages:Dt(X.pages,lt,yt),pageParams:Dt(X.pageParams,G,yt)}};if(o&&d.length){const X=o==="backward",G=X?mg:oy,et={pages:d,pageParams:y},rt=G(s,et);b=await F(et,rt,X)}else{const X=u??d.length;do{const G=g===0?y[0]??s.initialPageParam:oy(s,b);if(g>0&&G==null)break;b=await F(b,G),g++}while(g{var L,w;return(w=(L=i.options).persister)==null?void 0:w.call(L,m,{client:i.client,queryKey:i.queryKey,meta:i.options.meta,signal:i.signal},r)}:i.fetchFn=m}}}function oy(u,{pages:i,pageParams:r}){const s=i.length-1;return i.length>0?u.getNextPageParam(i[s],i,r[s],r):void 0}function mg(u,{pages:i,pageParams:r}){var s;return i.length>0?(s=u.getPreviousPageParam)==null?void 0:s.call(u,i[0],i,r[0],r):void 0}var Ln,wa,wn,Ve,Ya,It,Pu,Ga,Ue,Yy,Al,zy,yg=(zy=class extends wy{constructor(i){super();W(this,Ue);W(this,Ln);W(this,wa);W(this,wn);W(this,Ve);W(this,Ya);W(this,It);W(this,Pu);W(this,Ga);K(this,Ga,!1),K(this,Pu,i.defaultOptions),this.setOptions(i.options),this.observers=[],K(this,Ya,i.client),K(this,Ve,p(this,Ya).getQueryCache()),this.queryKey=i.queryKey,this.queryHash=i.queryHash,K(this,wa,dy(this.options)),this.state=i.state??p(this,wa),this.scheduleGc()}get meta(){return this.options.meta}get queryType(){return p(this,Ln)}get promise(){var i;return(i=p(this,It))==null?void 0:i.promise}setOptions(i){if(this.options={...p(this,Pu),...i},i!=null&&i._type&&K(this,Ln,i._type),this.updateGcTime(this.options.gcTime),this.state&&this.state.data===void 0){const r=dy(this.options);r.data!==void 0&&(this.setState(hy(r.data,r.dataUpdatedAt)),K(this,wa,r))}}optionalRemove(){!this.observers.length&&this.state.fetchStatus==="idle"&&p(this,Ve).remove(this)}setData(i,r){const s=bf(this.state.data,i,this.options);return st(this,Ue,Al).call(this,{data:s,type:"success",dataUpdatedAt:r==null?void 0:r.updatedAt,manual:r==null?void 0:r.manual}),s}setState(i){st(this,Ue,Al).call(this,{type:"setState",state:i})}cancel(i){var s,o;const r=(s=p(this,It))==null?void 0:s.promise;return(o=p(this,It))==null||o.cancel(i),r?r.then(oe).catch(oe):Promise.resolve()}destroy(){super.destroy(),this.cancel({silent:!0})}get resetState(){return p(this,wa)}reset(){this.destroy(),this.setState(this.resetState)}isActive(){return this.observers.some(i=>Ne(i.options.enabled,this)!==!1)}isDisabled(){return this.getObserversCount()>0?!this.isActive():this.options.queryFn===Bf||!this.isFetched()}isFetched(){return this.state.dataUpdateCount+this.state.errorUpdateCount>0}isStatic(){return this.getObserversCount()>0?this.observers.some(i=>pa(i.options.staleTime,this)==="static"):!1}isStale(){return this.getObserversCount()>0?this.observers.some(i=>i.getCurrentResult().isStale):this.state.data===void 0||this.state.isInvalidated}isStaleByTime(i=0){return this.state.data===void 0?!0:i==="static"?!1:this.state.isInvalidated?!0:!Hy(this.state.dataUpdatedAt,i)}onFocus(){var r;const i=this.observers.find(s=>s.shouldFetchOnWindowFocus());i==null||i.refetch({cancelRefetch:!1}),(r=p(this,It))==null||r.continue()}onOnline(){var r;const i=this.observers.find(s=>s.shouldFetchOnReconnect());i==null||i.refetch({cancelRefetch:!1}),(r=p(this,It))==null||r.continue()}addObserver(i){this.observers.includes(i)||(this.observers.push(i),this.clearGcTimeout(),p(this,Ve).notify({type:"observerAdded",query:this,observer:i}))}removeObserver(i){this.observers.includes(i)&&(this.observers=this.observers.filter(r=>r!==i),this.observers.length||(p(this,It)&&(p(this,Ga)||st(this,Ue,Yy).call(this)?p(this,It).cancel({revert:!0}):p(this,It).cancelRetry()),this.scheduleGc()),p(this,Ve).notify({type:"observerRemoved",query:this,observer:i}))}getObserversCount(){return this.observers.length}invalidate(){this.state.isInvalidated||st(this,Ue,Al).call(this,{type:"invalidate"})}async fetch(i,r){var m,O,T,N,Q,Y,L,w,B,F,X;if(this.state.fetchStatus!=="idle"&&((m=p(this,It))==null?void 0:m.status())!=="rejected"){if(this.state.data!==void 0&&(r!=null&&r.cancelRefetch))this.cancel({silent:!0});else if(p(this,It))return p(this,It).continueRetry(),p(this,It).promise}if(i&&this.setOptions(i),!this.options.queryFn){const G=this.observers.find(et=>et.options.queryFn);G&&this.setOptions(G.options)}const s=new AbortController,o=G=>{Object.defineProperty(G,"signal",{enumerable:!0,get:()=>(K(this,Ga,!0),s.signal)})},d=()=>{const G=By(this.options,r),rt=(()=>{const I={client:p(this,Ya),queryKey:this.queryKey,meta:this.meta};return o(I),I})();return K(this,Ga,!1),this.options.persister?this.options.persister(G,rt,this):G(rt)},b=(()=>{const G={fetchOptions:r,options:this.options,queryKey:this.queryKey,client:p(this,Ya),state:this.state,fetchFn:d};return o(G),G})(),g=p(this,Ln)==="infinite"?dg(this.options.pages):this.options.behavior;g==null||g.onFetch(b,this),K(this,wn,this.state),(this.state.fetchStatus==="idle"||this.state.fetchMeta!==((O=b.fetchOptions)==null?void 0:O.meta))&&st(this,Ue,Al).call(this,{type:"fetch",meta:(T=b.fetchOptions)==null?void 0:T.meta}),K(this,It,Ly({initialPromise:r==null?void 0:r.initialPromise,fn:b.fetchFn,onCancel:G=>{G instanceof Ef&&G.revert&&this.setState({...p(this,wn),fetchStatus:"idle"}),s.abort()},onFail:(G,et)=>{st(this,Ue,Al).call(this,{type:"failed",failureCount:G,error:et})},onPause:()=>{st(this,Ue,Al).call(this,{type:"pause"})},onContinue:()=>{st(this,Ue,Al).call(this,{type:"continue"})},retry:b.options.retry,retryDelay:b.options.retryDelay,networkMode:b.options.networkMode,canRun:()=>!0}));try{const G=await p(this,It).start();if(G===void 0)throw new Error(`${this.queryHash} data is undefined`);return this.setData(G),(Q=(N=p(this,Ve).config).onSuccess)==null||Q.call(N,G,this),(L=(Y=p(this,Ve).config).onSettled)==null||L.call(Y,G,this.state.error,this),G}catch(G){if(G instanceof Ef){if(G.silent)return p(this,It).promise;if(G.revert){if(this.state.data===void 0)throw G;return this.state.data}}throw st(this,Ue,Al).call(this,{type:"error",error:G}),(B=(w=p(this,Ve).config).onError)==null||B.call(w,G,this),(X=(F=p(this,Ve).config).onSettled)==null||X.call(F,this.state.data,G,this),G}finally{this.scheduleGc()}}},Ln=new WeakMap,wa=new WeakMap,wn=new WeakMap,Ve=new WeakMap,Ya=new WeakMap,It=new WeakMap,Pu=new WeakMap,Ga=new WeakMap,Ue=new WeakSet,Yy=function(){return this.state.fetchStatus==="paused"&&this.state.status==="pending"},Al=function(i){const r=s=>{switch(i.type){case"failed":return{...s,fetchFailureCount:i.failureCount,fetchFailureReason:i.error};case"pause":return{...s,fetchStatus:"paused"};case"continue":return{...s,fetchStatus:"fetching"};case"fetch":return{...s,...Gy(s.data,this.options),fetchMeta:i.meta??null};case"success":const o={...s,...hy(i.data,i.dataUpdatedAt),dataUpdateCount:s.dataUpdateCount+1,...!i.manual&&{fetchStatus:"idle",fetchFailureCount:0,fetchFailureReason:null}};return K(this,wn,i.manual?o:void 0),o;case"error":const d=i.error;return{...s,error:d,errorUpdateCount:s.errorUpdateCount+1,errorUpdatedAt:Date.now(),fetchFailureCount:s.fetchFailureCount+1,fetchFailureReason:d,fetchStatus:"idle",status:"error",isInvalidated:!0};case"invalidate":return{...s,isInvalidated:!0};case"setState":return{...s,...i.state}}};this.state=r(this.state),$t.batch(()=>{this.observers.forEach(s=>{s.onQueryUpdate()}),p(this,Ve).notify({query:this,type:"updated",action:i})})},zy);function Gy(u,i){return{fetchFailureCount:0,fetchFailureReason:null,fetchStatus:Qy(i.networkMode)?"fetching":"paused",...u===void 0&&{error:null,status:"pending"}}}function hy(u,i){return{data:u,dataUpdatedAt:i??Date.now(),error:null,isInvalidated:!1,status:"success"}}function dy(u){const i=typeof u.initialData=="function"?u.initialData():u.initialData,r=i!==void 0,s=r?typeof u.initialDataUpdatedAt=="function"?u.initialDataUpdatedAt():u.initialDataUpdatedAt:0;return{data:i,dataUpdateCount:0,dataUpdatedAt:r?s??Date.now():0,error:null,errorUpdateCount:0,errorUpdatedAt:0,fetchFailureCount:0,fetchFailureReason:null,fetchMeta:null,isInvalidated:!1,status:r?"success":"pending",fetchStatus:"idle"}}var be,pt,Iu,fe,Xa,Yn,Cl,oa,ti,Gn,Xn,Za,Ka,ha,Zn,Tt,Fu,Tf,Rf,Of,xf,Af,Cf,zf,Xy,My,vg=(My=class extends kn{constructor(i,r){super();W(this,Tt);W(this,be);W(this,pt);W(this,Iu);W(this,fe);W(this,Xa);W(this,Yn);W(this,Cl);W(this,oa);W(this,ti);W(this,Gn);W(this,Xn);W(this,Za);W(this,Ka);W(this,ha);W(this,Zn,new Set);this.options=r,K(this,be,i),K(this,oa,null),K(this,Cl,Sf()),this.bindMethods(),this.setOptions(r)}bindMethods(){this.refetch=this.refetch.bind(this)}onSubscribe(){this.listeners.size===1&&(p(this,pt).addObserver(this),my(p(this,pt),this.options)?st(this,Tt,Fu).call(this):this.updateResult(),st(this,Tt,xf).call(this))}onUnsubscribe(){this.hasListeners()||this.destroy()}shouldFetchOnReconnect(){return Mf(p(this,pt),this.options,this.options.refetchOnReconnect)}shouldFetchOnWindowFocus(){return Mf(p(this,pt),this.options,this.options.refetchOnWindowFocus)}destroy(){this.listeners=new Set,st(this,Tt,Af).call(this),st(this,Tt,Cf).call(this),p(this,pt).removeObserver(this)}setOptions(i){const r=this.options,s=p(this,pt);if(this.options=p(this,be).defaultQueryOptions(i),this.options.enabled!==void 0&&typeof this.options.enabled!="boolean"&&typeof this.options.enabled!="function"&&typeof Ne(this.options.enabled,p(this,pt))!="boolean")throw new Error("Expected enabled to be a boolean or a callback that returns a boolean");st(this,Tt,zf).call(this),p(this,pt).setOptions(this.options),r._defaulted&&!jc(this.options,r)&&p(this,be).getQueryCache().notify({type:"observerOptionsUpdated",query:p(this,pt),observer:this});const o=this.hasListeners();o&&yy(p(this,pt),s,this.options,r)&&st(this,Tt,Fu).call(this),this.updateResult(),o&&(p(this,pt)!==s||Ne(this.options.enabled,p(this,pt))!==Ne(r.enabled,p(this,pt))||pa(this.options.staleTime,p(this,pt))!==pa(r.staleTime,p(this,pt)))&&st(this,Tt,Tf).call(this);const d=st(this,Tt,Rf).call(this);o&&(p(this,pt)!==s||Ne(this.options.enabled,p(this,pt))!==Ne(r.enabled,p(this,pt))||d!==p(this,ha))&&st(this,Tt,Of).call(this,d)}getOptimisticResult(i){const r=p(this,be).getQueryCache().build(p(this,be),i),s=this.createResult(r,i);return gg(this,s)&&(K(this,fe,s),K(this,Yn,this.options),K(this,Xa,p(this,pt).state)),s}getCurrentResult(){return p(this,fe)}trackResult(i,r){return new Proxy(i,{get:(s,o)=>(this.trackProp(o),r==null||r(o),o==="promise"&&(this.trackProp("data"),!this.options.experimental_prefetchInRender&&p(this,Cl).status==="pending"&&p(this,Cl).reject(new Error("experimental_prefetchInRender feature flag is not enabled"))),Reflect.get(s,o))})}trackProp(i){p(this,Zn).add(i)}getCurrentQuery(){return p(this,pt)}refetch({...i}={}){return this.fetch({...i})}fetchOptimistic(i){const r=p(this,be).defaultQueryOptions(i),s=p(this,be).getQueryCache().build(p(this,be),r);return s.fetch().then(()=>this.createResult(s,r))}fetch(i){return st(this,Tt,Fu).call(this,{...i,cancelRefetch:i.cancelRefetch??!0}).then(()=>(this.updateResult(),p(this,fe)))}createResult(i,r){var yt;const s=p(this,pt),o=this.options,d=p(this,fe),y=p(this,Xa),b=p(this,Yn),m=i!==s?i.state:p(this,Iu),{state:O}=i;let T={...O},N=!1,Q;if(r._optimisticResults){const Dt=this.hasListeners(),wt=!Dt&&my(i,r),Yt=Dt&&yy(i,s,r,o);(wt||Yt)&&(T={...T,...Gy(O.data,i.options)}),r._optimisticResults==="isRestoring"&&(T.fetchStatus="idle")}let{error:Y,errorUpdatedAt:L,status:w}=T;Q=T.data;let B=!1;if(r.placeholderData!==void 0&&Q===void 0&&w==="pending"){let Dt;d!=null&&d.isPlaceholderData&&r.placeholderData===(b==null?void 0:b.placeholderData)?(Dt=d.data,B=!0):Dt=typeof r.placeholderData=="function"?r.placeholderData((yt=p(this,Xn))==null?void 0:yt.state.data,p(this,Xn)):r.placeholderData,Dt!==void 0&&(w="success",Q=bf(d==null?void 0:d.data,Dt,r),N=!0)}if(r.select&&Q!==void 0&&!B)if(d&&Q===(y==null?void 0:y.data)&&r.select===p(this,ti))Q=p(this,Gn);else try{K(this,ti,r.select),Q=r.select(Q),Q=bf(d==null?void 0:d.data,Q,r),K(this,Gn,Q),K(this,oa,null)}catch(Dt){K(this,oa,Dt)}p(this,oa)&&(Y=p(this,oa),Q=p(this,Gn),L=Date.now(),w="error");const F=T.fetchStatus==="fetching",X=w==="pending",G=w==="error",et=X&&F,rt=Q!==void 0,lt={status:w,fetchStatus:T.fetchStatus,isPending:X,isSuccess:w==="success",isError:G,isInitialLoading:et,isLoading:et,data:Q,dataUpdatedAt:T.dataUpdatedAt,error:Y,errorUpdatedAt:L,failureCount:T.fetchFailureCount,failureReason:T.fetchFailureReason,errorUpdateCount:T.errorUpdateCount,isFetched:i.isFetched(),isFetchedAfterMount:T.dataUpdateCount>m.dataUpdateCount||T.errorUpdateCount>m.errorUpdateCount,isFetching:F,isRefetching:F&&!X,isLoadingError:G&&!rt,isPaused:T.fetchStatus==="paused",isPlaceholderData:N,isRefetchError:G&&rt,isStale:Lf(i,r),refetch:this.refetch,promise:p(this,Cl),isEnabled:Ne(r.enabled,i)!==!1};if(this.options.experimental_prefetchInRender){const Dt=lt.data!==void 0,wt=lt.status==="error"&&!Dt,Yt=Ut=>{wt?Ut.reject(lt.error):Dt&&Ut.resolve(lt.data)},Ee=()=>{const Ut=K(this,Cl,lt.promise=Sf());Yt(Ut)},te=p(this,Cl);switch(te.status){case"pending":i.queryHash===s.queryHash&&Yt(te);break;case"fulfilled":(wt||lt.data!==te.value)&&Ee();break;case"rejected":(!wt||lt.error!==te.reason)&&Ee();break}}return lt}updateResult(){const i=p(this,fe),r=this.createResult(p(this,pt),this.options);if(K(this,Xa,p(this,pt).state),K(this,Yn,this.options),p(this,Xa).data!==void 0&&K(this,Xn,p(this,pt)),jc(r,i))return;K(this,fe,r);const s=()=>{if(!i)return!0;const{notifyOnChangeProps:o}=this.options,d=typeof o=="function"?o():o;if(d==="all"||!d&&!p(this,Zn).size)return!0;const y=new Set(d??p(this,Zn));return this.options.throwOnError&&y.add("error"),Object.keys(p(this,fe)).some(b=>{const g=b;return p(this,fe)[g]!==i[g]&&y.has(g)})};st(this,Tt,Xy).call(this,{listeners:s()})}onQueryUpdate(){this.updateResult(),this.hasListeners()&&st(this,Tt,xf).call(this)}},be=new WeakMap,pt=new WeakMap,Iu=new WeakMap,fe=new WeakMap,Xa=new WeakMap,Yn=new WeakMap,Cl=new WeakMap,oa=new WeakMap,ti=new WeakMap,Gn=new WeakMap,Xn=new WeakMap,Za=new WeakMap,Ka=new WeakMap,ha=new WeakMap,Zn=new WeakMap,Tt=new WeakSet,Fu=function(i){st(this,Tt,zf).call(this);let r=p(this,pt).fetch(this.options,i);return i!=null&&i.throwOnError||(r=r.catch(oe)),r},Tf=function(){st(this,Tt,Af).call(this);const i=pa(this.options.staleTime,p(this,pt));if($u.isServer()||p(this,fe).isStale||!pf(i))return;const s=Hy(p(this,fe).dataUpdatedAt,i)+1;K(this,Za,Ba.setTimeout(()=>{p(this,fe).isStale||this.updateResult()},s))},Rf=function(){return(typeof this.options.refetchInterval=="function"?this.options.refetchInterval(p(this,pt)):this.options.refetchInterval)??!1},Of=function(i){st(this,Tt,Cf).call(this),K(this,ha,i),!($u.isServer()||Ne(this.options.enabled,p(this,pt))===!1||!pf(p(this,ha))||p(this,ha)===0)&&K(this,Ka,Ba.setInterval(()=>{(this.options.refetchIntervalInBackground||Hf.isFocused())&&st(this,Tt,Fu).call(this)},p(this,ha)))},xf=function(){st(this,Tt,Tf).call(this),st(this,Tt,Of).call(this,st(this,Tt,Rf).call(this))},Af=function(){p(this,Za)!==void 0&&(Ba.clearTimeout(p(this,Za)),K(this,Za,void 0))},Cf=function(){p(this,Ka)!==void 0&&(Ba.clearInterval(p(this,Ka)),K(this,Ka,void 0))},zf=function(){const i=p(this,be).getQueryCache().build(p(this,be),this.options);if(i===p(this,pt))return;const r=p(this,pt);K(this,pt,i),K(this,Iu,i.state),this.hasListeners()&&(r==null||r.removeObserver(this),i.addObserver(this))},Xy=function(i){$t.batch(()=>{i.listeners&&this.listeners.forEach(r=>{r(p(this,fe))}),p(this,be).getQueryCache().notify({query:p(this,pt),type:"observerResultsUpdated"})})},My);function pg(u,i){return Ne(i.enabled,u)!==!1&&u.state.data===void 0&&!(u.state.status==="error"&&Ne(i.retryOnMount,u)===!1)}function my(u,i){return pg(u,i)||u.state.data!==void 0&&Mf(u,i,i.refetchOnMount)}function Mf(u,i,r){if(Ne(i.enabled,u)!==!1&&pa(i.staleTime,u)!=="static"){const s=typeof r=="function"?r(u):r;return s==="always"||s!==!1&&Lf(u,i)}return!1}function yy(u,i,r,s){return(u!==i||Ne(s.enabled,u)===!1)&&(!r.suspense||u.state.status!=="error")&&Lf(u,r)}function Lf(u,i){return Ne(i.enabled,u)!==!1&&u.isStaleByTime(pa(i.staleTime,u))}function gg(u,i){return!jc(u.getCurrentResult(),i)}var ei,al,ce,Va,nl,ca,_y,bg=(_y=class extends wy{constructor(i){super();W(this,nl);W(this,ei);W(this,al);W(this,ce);W(this,Va);K(this,ei,i.client),this.mutationId=i.mutationId,K(this,ce,i.mutationCache),K(this,al,[]),this.state=i.state||Zy(),this.setOptions(i.options),this.scheduleGc()}setOptions(i){this.options=i,this.updateGcTime(this.options.gcTime)}get meta(){return this.options.meta}addObserver(i){p(this,al).includes(i)||(p(this,al).push(i),this.clearGcTimeout(),p(this,ce).notify({type:"observerAdded",mutation:this,observer:i}))}removeObserver(i){K(this,al,p(this,al).filter(r=>r!==i)),this.scheduleGc(),p(this,ce).notify({type:"observerRemoved",mutation:this,observer:i})}optionalRemove(){p(this,al).length||(this.state.status==="pending"?this.scheduleGc():p(this,ce).remove(this))}continue(){var i;return((i=p(this,Va))==null?void 0:i.continue())??this.execute(this.state.variables)}async execute(i){var y,b,g,m,O,T,N,Q,Y,L,w,B,F,X,G,et,rt,I;const r=()=>{st(this,nl,ca).call(this,{type:"continue"})},s={client:p(this,ei),meta:this.options.meta,mutationKey:this.options.mutationKey};K(this,Va,Ly({fn:()=>this.options.mutationFn?this.options.mutationFn(i,s):Promise.reject(new Error("No mutationFn found")),onFail:(lt,yt)=>{st(this,nl,ca).call(this,{type:"failed",failureCount:lt,error:yt})},onPause:()=>{st(this,nl,ca).call(this,{type:"pause"})},onContinue:r,retry:this.options.retry??0,retryDelay:this.options.retryDelay,networkMode:this.options.networkMode,canRun:()=>p(this,ce).canRun(this)}));const o=this.state.status==="pending",d=!p(this,Va).canStart();try{if(o)r();else{st(this,nl,ca).call(this,{type:"pending",variables:i,isPaused:d}),p(this,ce).config.onMutate&&await p(this,ce).config.onMutate(i,this,s);const yt=await((b=(y=this.options).onMutate)==null?void 0:b.call(y,i,s));yt!==this.state.context&&st(this,nl,ca).call(this,{type:"pending",context:yt,variables:i,isPaused:d})}const lt=await p(this,Va).start();return await((m=(g=p(this,ce).config).onSuccess)==null?void 0:m.call(g,lt,i,this.state.context,this,s)),await((T=(O=this.options).onSuccess)==null?void 0:T.call(O,lt,i,this.state.context,s)),await((Q=(N=p(this,ce).config).onSettled)==null?void 0:Q.call(N,lt,null,this.state.variables,this.state.context,this,s)),await((L=(Y=this.options).onSettled)==null?void 0:L.call(Y,lt,null,i,this.state.context,s)),st(this,nl,ca).call(this,{type:"success",data:lt}),lt}catch(lt){try{await((B=(w=p(this,ce).config).onError)==null?void 0:B.call(w,lt,i,this.state.context,this,s))}catch(yt){Promise.reject(yt)}try{await((X=(F=this.options).onError)==null?void 0:X.call(F,lt,i,this.state.context,s))}catch(yt){Promise.reject(yt)}try{await((et=(G=p(this,ce).config).onSettled)==null?void 0:et.call(G,void 0,lt,this.state.variables,this.state.context,this,s))}catch(yt){Promise.reject(yt)}try{await((I=(rt=this.options).onSettled)==null?void 0:I.call(rt,void 0,lt,i,this.state.context,s))}catch(yt){Promise.reject(yt)}throw st(this,nl,ca).call(this,{type:"error",error:lt}),lt}finally{p(this,ce).runNext(this)}}},ei=new WeakMap,al=new WeakMap,ce=new WeakMap,Va=new WeakMap,nl=new WeakSet,ca=function(i){const r=s=>{switch(i.type){case"failed":return{...s,failureCount:i.failureCount,failureReason:i.error};case"pause":return{...s,isPaused:!0};case"continue":return{...s,isPaused:!1};case"pending":return{...s,context:i.context,data:void 0,failureCount:0,failureReason:null,error:null,isPaused:i.isPaused,status:"pending",variables:i.variables,submittedAt:Date.now()};case"success":return{...s,data:i.data,failureCount:0,failureReason:null,error:null,status:"success",isPaused:!1};case"error":return{...s,data:void 0,error:i.error,failureCount:s.failureCount+1,failureReason:i.error,isPaused:!1,status:"error"}}};this.state=r(this.state),$t.batch(()=>{p(this,al).forEach(s=>{s.onMutationUpdate(i)}),p(this,ce).notify({mutation:this,type:"updated",action:i})})},_y);function Zy(){return{context:void 0,data:void 0,error:null,failureCount:0,failureReason:null,isPaused:!1,status:"idle",variables:void 0,submittedAt:0}}var zl,We,li,Dy,Sg=(Dy=class extends kn{constructor(i={}){super();W(this,zl);W(this,We);W(this,li);this.config=i,K(this,zl,new Set),K(this,We,new Map),K(this,li,0)}build(i,r,s){const o=new bg({client:i,mutationCache:this,mutationId:++Cc(this,li)._,options:i.defaultMutationOptions(r),state:s});return this.add(o),o}add(i){p(this,zl).add(i);const r=zc(i);if(typeof r=="string"){const s=p(this,We).get(r);s?s.push(i):p(this,We).set(r,[i])}this.notify({type:"added",mutation:i})}remove(i){if(p(this,zl).delete(i)){const r=zc(i);if(typeof r=="string"){const s=p(this,We).get(r);if(s)if(s.length>1){const o=s.indexOf(i);o!==-1&&s.splice(o,1)}else s[0]===i&&p(this,We).delete(r)}}this.notify({type:"removed",mutation:i})}canRun(i){const r=zc(i);if(typeof r=="string"){const s=p(this,We).get(r),o=s==null?void 0:s.find(d=>d.state.status==="pending");return!o||o===i}else return!0}runNext(i){var s;const r=zc(i);if(typeof r=="string"){const o=(s=p(this,We).get(r))==null?void 0:s.find(d=>d!==i&&d.state.isPaused);return(o==null?void 0:o.continue())??Promise.resolve()}else return Promise.resolve()}clear(){$t.batch(()=>{p(this,zl).forEach(i=>{this.notify({type:"removed",mutation:i})}),p(this,zl).clear(),p(this,We).clear()})}getAll(){return Array.from(p(this,zl))}find(i){const r={exact:!0,...i};return this.getAll().find(s=>sy(r,s))}findAll(i={}){return this.getAll().filter(r=>sy(i,r))}notify(i){$t.batch(()=>{this.listeners.forEach(r=>{r(i)})})}resumePausedMutations(){const i=this.getAll().filter(r=>r.state.isPaused);return $t.batch(()=>Promise.all(i.map(r=>r.continue().catch(oe))))}},zl=new WeakMap,We=new WeakMap,li=new WeakMap,Dy);function zc(u){var i;return(i=u.options.scope)==null?void 0:i.id}var Ml,da,Se,_l,Nl,Dc,_f,Uy,Eg=(Uy=class extends kn{constructor(r,s){super();W(this,Nl);W(this,Ml);W(this,da);W(this,Se);W(this,_l);K(this,Ml,r),this.setOptions(s),this.bindMethods(),st(this,Nl,Dc).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(r){var o;const s=this.options;this.options=p(this,Ml).defaultMutationOptions(r),jc(this.options,s)||p(this,Ml).getMutationCache().notify({type:"observerOptionsUpdated",mutation:p(this,Se),observer:this}),s!=null&&s.mutationKey&&this.options.mutationKey&&ka(s.mutationKey)!==ka(this.options.mutationKey)?this.reset():((o=p(this,Se))==null?void 0:o.state.status)==="pending"&&p(this,Se).setOptions(this.options)}onUnsubscribe(){var r;this.hasListeners()||(r=p(this,Se))==null||r.removeObserver(this)}onMutationUpdate(r){st(this,Nl,Dc).call(this),st(this,Nl,_f).call(this,r)}getCurrentResult(){return p(this,da)}reset(){var r;(r=p(this,Se))==null||r.removeObserver(this),K(this,Se,void 0),st(this,Nl,Dc).call(this),st(this,Nl,_f).call(this)}mutate(r,s){var o;return K(this,_l,s),(o=p(this,Se))==null||o.removeObserver(this),K(this,Se,p(this,Ml).getMutationCache().build(p(this,Ml),this.options)),p(this,Se).addObserver(this),p(this,Se).execute(r)}},Ml=new WeakMap,da=new WeakMap,Se=new WeakMap,_l=new WeakMap,Nl=new WeakSet,Dc=function(){var s;const r=((s=p(this,Se))==null?void 0:s.state)??Zy();K(this,da,{...r,isPending:r.status==="pending",isSuccess:r.status==="success",isError:r.status==="error",isIdle:r.status==="idle",mutate:this.mutate,reset:this.reset})},_f=function(r){$t.batch(()=>{var s,o,d,y,b,g,m,O;if(p(this,_l)&&this.hasListeners()){const T=p(this,da).variables,N=p(this,da).context,Q={client:p(this,Ml),meta:this.options.meta,mutationKey:this.options.mutationKey};if((r==null?void 0:r.type)==="success"){try{(o=(s=p(this,_l)).onSuccess)==null||o.call(s,r.data,T,N,Q)}catch(Y){Promise.reject(Y)}try{(y=(d=p(this,_l)).onSettled)==null||y.call(d,r.data,null,T,N,Q)}catch(Y){Promise.reject(Y)}}else if((r==null?void 0:r.type)==="error"){try{(g=(b=p(this,_l)).onError)==null||g.call(b,r.error,T,N,Q)}catch(Y){Promise.reject(Y)}try{(O=(m=p(this,_l)).onSettled)==null||O.call(m,void 0,r.error,T,N,Q)}catch(Y){Promise.reject(Y)}}}this.listeners.forEach(T=>{T(p(this,da))})})},Uy),ul,Ny,Tg=(Ny=class extends kn{constructor(i={}){super();W(this,ul);this.config=i,K(this,ul,new Map)}build(i,r,s){const o=r.queryKey,d=r.queryHash??qf(o,r);let y=this.get(d);return y||(y=new yg({client:i,queryKey:o,queryHash:d,options:i.defaultQueryOptions(r),state:s,defaultOptions:i.getQueryDefaults(o)}),this.add(y)),y}add(i){p(this,ul).has(i.queryHash)||(p(this,ul).set(i.queryHash,i),this.notify({type:"added",query:i}))}remove(i){const r=p(this,ul).get(i.queryHash);r&&(i.destroy(),r===i&&p(this,ul).delete(i.queryHash),this.notify({type:"removed",query:i}))}clear(){$t.batch(()=>{this.getAll().forEach(i=>{this.remove(i)})})}get(i){return p(this,ul).get(i)}getAll(){return[...p(this,ul).values()]}find(i){const r={exact:!0,...i};return this.getAll().find(s=>cy(r,s))}findAll(i={}){const r=this.getAll();return Object.keys(i).length>0?r.filter(s=>cy(i,s)):r}notify(i){$t.batch(()=>{this.listeners.forEach(r=>{r(i)})})}onFocus(){$t.batch(()=>{this.getAll().forEach(i=>{i.onFocus()})})}onOnline(){$t.batch(()=>{this.getAll().forEach(i=>{i.onOnline()})})}},ul=new WeakMap,Ny),Lt,ma,ya,Kn,Vn,va,Jn,Fn,jy,Rg=(jy=class{constructor(u={}){W(this,Lt);W(this,ma);W(this,ya);W(this,Kn);W(this,Vn);W(this,va);W(this,Jn);W(this,Fn);K(this,Lt,u.queryCache||new Tg),K(this,ma,u.mutationCache||new Sg),K(this,ya,u.defaultOptions||{}),K(this,Kn,new Map),K(this,Vn,new Map),K(this,va,0)}mount(){Cc(this,va)._++,p(this,va)===1&&(K(this,Jn,Hf.subscribe(async u=>{u&&(await this.resumePausedMutations(),p(this,Lt).onFocus())})),K(this,Fn,Hc.subscribe(async u=>{u&&(await this.resumePausedMutations(),p(this,Lt).onOnline())})))}unmount(){var u,i;Cc(this,va)._--,p(this,va)===0&&((u=p(this,Jn))==null||u.call(this),K(this,Jn,void 0),(i=p(this,Fn))==null||i.call(this),K(this,Fn,void 0))}isFetching(u){return p(this,Lt).findAll({...u,fetchStatus:"fetching"}).length}isMutating(u){return p(this,ma).findAll({...u,status:"pending"}).length}getQueryData(u){var r;const i=this.defaultQueryOptions({queryKey:u});return(r=p(this,Lt).get(i.queryHash))==null?void 0:r.state.data}ensureQueryData(u){const i=this.defaultQueryOptions(u),r=p(this,Lt).build(this,i),s=r.state.data;return s===void 0?this.fetchQuery(u):(u.revalidateIfStale&&r.isStaleByTime(pa(i.staleTime,r))&&this.prefetchQuery(i),Promise.resolve(s))}getQueriesData(u){return p(this,Lt).findAll(u).map(({queryKey:i,state:r})=>{const s=r.data;return[i,s]})}setQueryData(u,i,r){const s=this.defaultQueryOptions({queryKey:u}),o=p(this,Lt).get(s.queryHash),d=o==null?void 0:o.state.data,y=ag(i,d);if(y!==void 0)return p(this,Lt).build(this,s).setData(y,{...r,manual:!0})}setQueriesData(u,i,r){return $t.batch(()=>p(this,Lt).findAll(u).map(({queryKey:s})=>[s,this.setQueryData(s,i,r)]))}getQueryState(u){var r;const i=this.defaultQueryOptions({queryKey:u});return(r=p(this,Lt).get(i.queryHash))==null?void 0:r.state}removeQueries(u){const i=p(this,Lt);$t.batch(()=>{i.findAll(u).forEach(r=>{i.remove(r)})})}resetQueries(u,i){const r=p(this,Lt);return $t.batch(()=>(r.findAll(u).forEach(s=>{s.reset()}),this.refetchQueries({type:"active",...u},i)))}cancelQueries(u,i={}){const r={revert:!0,...i},s=$t.batch(()=>p(this,Lt).findAll(u).map(o=>o.cancel(r)));return Promise.all(s).then(oe).catch(oe)}invalidateQueries(u,i={}){return $t.batch(()=>(p(this,Lt).findAll(u).forEach(r=>{r.invalidate()}),(u==null?void 0:u.refetchType)==="none"?Promise.resolve():this.refetchQueries({...u,type:(u==null?void 0:u.refetchType)??(u==null?void 0:u.type)??"active"},i)))}refetchQueries(u,i={}){const r={...i,cancelRefetch:i.cancelRefetch??!0},s=$t.batch(()=>p(this,Lt).findAll(u).filter(o=>!o.isDisabled()&&!o.isStatic()).map(o=>{let d=o.fetch(void 0,r);return r.throwOnError||(d=d.catch(oe)),o.state.fetchStatus==="paused"?Promise.resolve():d}));return Promise.all(s).then(oe)}fetchQuery(u){const i=this.defaultQueryOptions(u);i.retry===void 0&&(i.retry=!1);const r=p(this,Lt).build(this,i);return r.isStaleByTime(pa(i.staleTime,r))?r.fetch(i):Promise.resolve(r.state.data)}prefetchQuery(u){return this.fetchQuery(u).then(oe).catch(oe)}fetchInfiniteQuery(u){return u._type="infinite",this.fetchQuery(u)}prefetchInfiniteQuery(u){return this.fetchInfiniteQuery(u).then(oe).catch(oe)}ensureInfiniteQueryData(u){return u._type="infinite",this.ensureQueryData(u)}resumePausedMutations(){return Hc.isOnline()?p(this,ma).resumePausedMutations():Promise.resolve()}getQueryCache(){return p(this,Lt)}getMutationCache(){return p(this,ma)}getDefaultOptions(){return p(this,ya)}setDefaultOptions(u){K(this,ya,u)}setQueryDefaults(u,i){p(this,Kn).set(ka(u),{queryKey:u,defaultOptions:i})}getQueryDefaults(u){const i=[...p(this,Kn).values()],r={};return i.forEach(s=>{ku(u,s.queryKey)&&Object.assign(r,s.defaultOptions)}),r}setMutationDefaults(u,i){p(this,Vn).set(ka(u),{mutationKey:u,defaultOptions:i})}getMutationDefaults(u){const i=[...p(this,Vn).values()],r={};return i.forEach(s=>{ku(u,s.mutationKey)&&Object.assign(r,s.defaultOptions)}),r}defaultQueryOptions(u){if(u._defaulted)return u;const i={...p(this,ya).queries,...this.getQueryDefaults(u.queryKey),...u,_defaulted:!0};return i.queryHash||(i.queryHash=qf(i.queryKey,i)),i.refetchOnReconnect===void 0&&(i.refetchOnReconnect=i.networkMode!=="always"),i.throwOnError===void 0&&(i.throwOnError=!!i.suspense),!i.networkMode&&i.persister&&(i.networkMode="offlineFirst"),i.queryFn===Bf&&(i.enabled=!1),i}defaultMutationOptions(u){return u!=null&&u._defaulted?u:{...p(this,ya).mutations,...(u==null?void 0:u.mutationKey)&&this.getMutationDefaults(u.mutationKey),...u,_defaulted:!0}}clear(){p(this,Lt).clear(),p(this,ma).clear()}},Lt=new WeakMap,ma=new WeakMap,ya=new WeakMap,Kn=new WeakMap,Vn=new WeakMap,va=new WeakMap,Jn=new WeakMap,Fn=new WeakMap,jy),Ky=z.createContext(void 0),ai=u=>{const i=z.useContext(Ky);if(!i)throw new Error("No QueryClient set, use QueryClientProvider to set one");return i},Og=({client:u,children:i})=>(z.useEffect(()=>(u.mount(),()=>{u.unmount()}),[u]),D.jsx(Ky.Provider,{value:u,children:i})),Vy=z.createContext(!1),xg=()=>z.useContext(Vy);Vy.Provider;function Ag(){let u=!1;return{clearReset:()=>{u=!1},reset:()=>{u=!0},isReset:()=>u}}var Cg=z.createContext(Ag()),zg=()=>z.useContext(Cg),Mg=(u,i,r)=>{const s=r!=null&&r.state.error&&typeof u.throwOnError=="function"?Qf(u.throwOnError,[r.state.error,r]):u.throwOnError;(u.suspense||u.experimental_prefetchInRender||s)&&(i.isReset()||(u.retryOnMount=!1))},_g=u=>{z.useEffect(()=>{u.clearReset()},[u])},Dg=({result:u,errorResetBoundary:i,throwOnError:r,query:s,suspense:o})=>u.isError&&!i.isReset()&&!u.isFetching&&s&&(o&&u.data===void 0||Qf(r,[u.error,s])),Ug=u=>{if(u.suspense){const r=o=>o==="static"?o:Math.max(o??1e3,1e3),s=u.staleTime;u.staleTime=typeof s=="function"?(...o)=>r(s(...o)):r(s),typeof u.gcTime=="number"&&(u.gcTime=Math.max(u.gcTime,1e3))}},Ng=(u,i)=>u.isLoading&&u.isFetching&&!i,jg=(u,i)=>(u==null?void 0:u.suspense)&&i.isPending,vy=(u,i,r)=>i.fetchOptimistic(u).catch(()=>{r.clearReset()});function Hg(u,i,r){var Q,Y,L,w;const s=xg(),o=zg(),d=ai(),y=d.defaultQueryOptions(u);(Y=(Q=d.getDefaultOptions().queries)==null?void 0:Q._experimental_beforeQuery)==null||Y.call(Q,y);const b=d.getQueryCache().get(y.queryHash),g=u.subscribed!==!1;y._optimisticResults=s?"isRestoring":g?"optimistic":void 0,Ug(y),Mg(y,o,b),_g(o);const m=!d.getQueryCache().get(y.queryHash),[O]=z.useState(()=>new i(d,y)),T=O.getOptimisticResult(y),N=!s&&g;if(z.useSyncExternalStore(z.useCallback(B=>{const F=N?O.subscribe($t.batchCalls(B)):oe;return O.updateResult(),F},[O,N]),()=>O.getCurrentResult(),()=>O.getCurrentResult()),z.useEffect(()=>{O.setOptions(y)},[y,O]),jg(y,T))throw vy(y,O,o);if(Dg({result:T,errorResetBoundary:o,throwOnError:y.throwOnError,query:b,suspense:y.suspense}))throw T.error;if((w=(L=d.getDefaultOptions().queries)==null?void 0:L._experimental_afterQuery)==null||w.call(L,y,T),y.experimental_prefetchInRender&&!$u.isServer()&&Ng(T,s)){const B=m?vy(y,O,o):b==null?void 0:b.promise;B==null||B.catch(oe).finally(()=>{O.updateResult()})}return y.notifyOnChangeProps?T:O.trackResult(T)}function ni(u,i){return Hg(u,vg)}function Ja(u,i){const r=ai(),[s]=z.useState(()=>new Eg(r,u));z.useEffect(()=>{s.setOptions(u)},[s,u]);const o=z.useSyncExternalStore(z.useCallback(y=>s.subscribe($t.batchCalls(y)),[s]),()=>s.getCurrentResult(),()=>s.getCurrentResult()),d=z.useCallback((y,b)=>{s.mutate(y,b).catch(oe)},[s]);if(o.error&&Qf(s.options.throwOnError,[o.error]))throw o.error;return{...o,mutate:d,mutateAsync:o.mutate}}/** + * react-router v7.18.0 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */var wf=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,Jy=/^[\\/]{2}/;function qg(u,i){return i+u.replace(/\\/g,"/")}var py="popstate";function gy(u){return typeof u=="object"&&u!=null&&"pathname"in u&&"search"in u&&"hash"in u&&"state"in u&&"key"in u}function Bg(u={}){function i(s,o){var m;let d=(m=o.state)==null?void 0:m.masked,{pathname:y,search:b,hash:g}=d||s.location;return Df("",{pathname:y,search:b,hash:g},o.state&&o.state.usr||null,o.state&&o.state.key||"default",d?{pathname:s.location.pathname,search:s.location.search,hash:s.location.hash}:void 0)}function r(s,o){return typeof o=="string"?o:Wu(o)}return Lg(i,r,null,u)}function qt(u,i){if(u===!1||u===null||typeof u>"u")throw new Error(i)}function il(u,i){if(!u){typeof console<"u"&&console.warn(i);try{throw new Error(i)}catch{}}}function Qg(){return Math.random().toString(36).substring(2,10)}function by(u,i){return{usr:u.state,key:u.key,idx:i,masked:u.mask?{pathname:u.pathname,search:u.search,hash:u.hash}:void 0}}function Df(u,i,r=null,s,o){return{pathname:typeof u=="string"?u:u.pathname,search:"",hash:"",...typeof i=="string"?$n(i):i,state:r,key:i&&i.key||s||Qg(),mask:o}}function Wu({pathname:u="/",search:i="",hash:r=""}){return i&&i!=="?"&&(u+=i.charAt(0)==="?"?i:"?"+i),r&&r!=="#"&&(u+=r.charAt(0)==="#"?r:"#"+r),u}function $n(u){let i={};if(u){let r=u.indexOf("#");r>=0&&(i.hash=u.substring(r),u=u.substring(0,r));let s=u.indexOf("?");s>=0&&(i.search=u.substring(s),u=u.substring(0,s)),u&&(i.pathname=u)}return i}function Lg(u,i,r,s={}){let{window:o=document.defaultView,v5Compat:d=!1}=s,y=o.history,b="POP",g=null,m=O();m==null&&(m=0,y.replaceState({...y.state,idx:m},""));function O(){return(y.state||{idx:null}).idx}function T(){b="POP";let w=O(),B=w==null?null:w-m;m=w,g&&g({action:b,location:L.location,delta:B})}function N(w,B){b="PUSH";let F=gy(w)?w:Df(L.location,w,B);m=O()+1;let X=by(F,m),G=L.createHref(F.mask||F);try{y.pushState(X,"",G)}catch(et){if(et instanceof DOMException&&et.name==="DataCloneError")throw et;o.location.assign(G)}d&&g&&g({action:b,location:L.location,delta:1})}function Q(w,B){b="REPLACE";let F=gy(w)?w:Df(L.location,w,B);m=O();let X=by(F,m),G=L.createHref(F.mask||F);y.replaceState(X,"",G),d&&g&&g({action:b,location:L.location,delta:0})}function Y(w){return wg(o,w)}let L={get action(){return b},get location(){return u(o,y)},listen(w){if(g)throw new Error("A history only accepts one active listener");return o.addEventListener(py,T),g=w,()=>{o.removeEventListener(py,T),g=null}},createHref(w){return i(o,w)},createURL:Y,encodeLocation(w){let B=Y(w);return{pathname:B.pathname,search:B.search,hash:B.hash}},push:N,replace:Q,go(w){return y.go(w)}};return L}function wg(u,i,r=!1){let s="http://localhost";u&&(s=u.location.origin!=="null"?u.location.origin:u.location.href),qt(s,"No window.location.(origin|href) available to create URL");let o=typeof i=="string"?i:Wu(i);return o=o.replace(/ $/,"%20"),!r&&Jy.test(o)&&(o=s+o),new URL(o,s)}function Fy(u,i,r="/"){return Yg(u,i,r,!1)}function Yg(u,i,r,s,o){let d=typeof i=="string"?$n(i):i,y=jl(d.pathname||"/",r);if(y==null)return null;let b=Gg(u),g=null,m=Ig(y);for(let O=0;g==null&&O{let O={relativePath:m===void 0?y.path||"":m,caseSensitive:y.caseSensitive===!0,childrenIndex:b,route:y};if(O.relativePath.startsWith("/")){if(!O.relativePath.startsWith(s)&&g)return;qt(O.relativePath.startsWith(s),`Absolute route path "${O.relativePath}" nested under path "${s}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),O.relativePath=O.relativePath.slice(s.length)}let T=Pe([s,O.relativePath]),N=r.concat(O);y.children&&y.children.length>0&&(qt(y.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${T}".`),ky(y.children,i,N,T,g)),!(y.path==null&&!y.index)&&i.push({path:T,score:$g(T,y.index),routesMeta:N.map((Q,Y)=>{let[L,w]=Py(Q.relativePath,Q.caseSensitive,Y===N.length-1);return{...Q,matcher:L,compiledParams:w}})})};return u.forEach((y,b)=>{var g;if(y.path===""||!((g=y.path)!=null&&g.includes("?")))d(y,b);else for(let m of $y(y.path))d(y,b,!0,m)}),i}function $y(u){let i=u.split("/");if(i.length===0)return[];let[r,...s]=i,o=r.endsWith("?"),d=r.replace(/\?$/,"");if(s.length===0)return o?[d,""]:[d];let y=$y(s.join("/")),b=[];return b.push(...y.map(g=>g===""?d:[d,g].join("/"))),o&&b.push(...y),b.map(g=>u.startsWith("/")&&g===""?"/":g)}function Xg(u){u.sort((i,r)=>i.score!==r.score?r.score-i.score:Wg(i.routesMeta.map(s=>s.childrenIndex),r.routesMeta.map(s=>s.childrenIndex)))}var Zg=/^:[\w-]+$/,Kg=3,Vg=2,Jg=1,Fg=10,kg=-2,Sy=u=>u==="*";function $g(u,i){let r=u.split("/"),s=r.length;return r.some(Sy)&&(s+=kg),i&&(s+=Vg),r.filter(o=>!Sy(o)).reduce((o,d)=>o+(Zg.test(d)?Kg:d===""?Jg:Fg),s)}function Wg(u,i){return u.length===i.length&&u.slice(0,-1).every((s,o)=>s===i[o])?u[u.length-1]-i[i.length-1]:0}function Pg(u,i,r=!1){let{routesMeta:s}=u,o={},d="/",y=[];for(let b=0;b{if(O==="*"){let Y=b[N]||"";y=d.slice(0,d.length-Y.length).replace(/(.)\/+$/,"$1")}const Q=b[N];return T&&!Q?m[O]=void 0:m[O]=(Q||"").replace(/%2F/g,"/"),m},{}),pathname:d,pathnameBase:y,pattern:u}}function Py(u,i=!1,r=!0){il(u==="*"||!u.endsWith("*")||u.endsWith("/*"),`Route path "${u}" will be treated as if it were "${u.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${u.replace(/\*$/,"/*")}".`);let s=[],o="^"+u.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(y,b,g,m,O)=>{if(s.push({paramName:b,isOptional:g!=null}),g){let T=O.charAt(m+y.length);return T&&T!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return u.endsWith("*")?(s.push({paramName:"*"}),o+=u==="*"||u==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):r?o+="\\/*$":u!==""&&u!=="/"&&(o+="(?:(?=\\/|$))"),[new RegExp(o,i?void 0:"i"),s]}function Ig(u){try{return u.split("/").map(i=>decodeURIComponent(i).replace(/\//g,"%2F")).join("/")}catch(i){return il(!1,`The URL path "${u}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${i}).`),u}}function jl(u,i){if(i==="/")return u;if(!u.toLowerCase().startsWith(i.toLowerCase()))return null;let r=i.endsWith("/")?i.length-1:i.length,s=u.charAt(r);return s&&s!=="/"?null:u.slice(r)||"/"}function t1(u,i="/"){let{pathname:r,search:s="",hash:o=""}=typeof u=="string"?$n(u):u,d;return r?(r=tv(r),r.startsWith("/")?d=Ey(r.substring(1),"/"):d=Ey(r,i)):d=i,{pathname:d,search:a1(s),hash:n1(o)}}function Ey(u,i){let r=Bc(i).split("/");return u.split("/").forEach(o=>{o===".."?r.length>1&&r.pop():o!=="."&&r.push(o)}),r.length>1?r.join("/"):"/"}function mf(u,i,r,s){return`Cannot include a '${u}' character in a manually specified \`to.${i}\` field [${JSON.stringify(s)}]. Please separate it out to the \`to.${r}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function e1(u){return u.filter((i,r)=>r===0||i.route.path&&i.route.path.length>0)}function Iy(u){let i=e1(u);return i.map((r,s)=>s===i.length-1?r.pathname:r.pathnameBase)}function Yf(u,i,r,s=!1){let o;typeof u=="string"?o=$n(u):(o={...u},qt(!o.pathname||!o.pathname.includes("?"),mf("?","pathname","search",o)),qt(!o.pathname||!o.pathname.includes("#"),mf("#","pathname","hash",o)),qt(!o.search||!o.search.includes("#"),mf("#","search","hash",o)));let d=u===""||o.pathname==="",y=d?"/":o.pathname,b;if(y==null)b=r;else{let T=i.length-1;if(!s&&y.startsWith("..")){let N=y.split("/");for(;N[0]==="..";)N.shift(),T-=1;o.pathname=N.join("/")}b=T>=0?i[T]:"/"}let g=t1(o,b),m=y&&y!=="/"&&y.endsWith("/"),O=(d||y===".")&&r.endsWith("/");return!g.pathname.endsWith("/")&&(m||O)&&(g.pathname+="/"),g}var tv=u=>u.replace(/[\\/]{2,}/g,"/"),Pe=u=>tv(u.join("/")),Bc=u=>u.replace(/\/+$/,""),l1=u=>Bc(u).replace(/^\/*/,"/"),a1=u=>!u||u==="?"?"":u.startsWith("?")?u:"?"+u,n1=u=>!u||u==="#"?"":u.startsWith("#")?u:"#"+u,u1=class{constructor(u,i,r,s=!1){this.status=u,this.statusText=i||"",this.internal=s,r instanceof Error?(this.data=r.toString(),this.error=r):this.data=r}};function i1(u){return u!=null&&typeof u.status=="number"&&typeof u.statusText=="string"&&typeof u.internal=="boolean"&&"data"in u}function c1(u){let i=u.map(r=>r.route.path).filter(Boolean);return Pe(i)||"/"}var ev=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function lv(u,i){let r=u;if(typeof r!="string"||!wf.test(r))return{absoluteURL:void 0,isExternal:!1,to:r};let s=r,o=!1;if(ev)try{let d=new URL(window.location.href),y=Jy.test(r)?new URL(qg(r,d.protocol)):new URL(r),b=jl(y.pathname,i);y.origin===d.origin&&b!=null?r=b+y.search+y.hash:o=!0}catch{il(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:s,isExternal:o,to:r}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var av=["POST","PUT","PATCH","DELETE"];new Set(av);var s1=["GET",...av];new Set(s1);var r1=["about:","blob:","chrome:","chrome-untrusted:","content:","data:","devtools:","file:","filesystem:","javascript:"];function f1(u){try{return r1.includes(new URL(u).protocol)}catch{return!1}}var Wn=z.createContext(null);Wn.displayName="DataRouter";var Qc=z.createContext(null);Qc.displayName="DataRouterState";var nv=z.createContext(!1);function o1(){return z.useContext(nv)}var uv=z.createContext({isTransitioning:!1});uv.displayName="ViewTransition";var h1=z.createContext(new Map);h1.displayName="Fetchers";var d1=z.createContext(null);d1.displayName="Await";var Je=z.createContext(null);Je.displayName="Navigation";var ui=z.createContext(null);ui.displayName="Location";var cl=z.createContext({outlet:null,matches:[],isDataRoute:!1});cl.displayName="Route";var Gf=z.createContext(null);Gf.displayName="RouteError";var iv="REACT_ROUTER_ERROR",m1="REDIRECT",y1="ROUTE_ERROR_RESPONSE";function v1(u){if(u.startsWith(`${iv}:${m1}:{`))try{let i=JSON.parse(u.slice(28));if(typeof i=="object"&&i&&typeof i.status=="number"&&typeof i.statusText=="string"&&typeof i.location=="string"&&typeof i.reloadDocument=="boolean"&&typeof i.replace=="boolean")return i}catch{}}function p1(u){if(u.startsWith(`${iv}:${y1}:{`))try{let i=JSON.parse(u.slice(40));if(typeof i=="object"&&i&&typeof i.status=="number"&&typeof i.statusText=="string")return new u1(i.status,i.statusText,i.data)}catch{}}function g1(u,{relative:i}={}){qt(ii(),"useHref() may be used only in the context of a component.");let{basename:r,navigator:s}=z.useContext(Je),{hash:o,pathname:d,search:y}=ci(u,{relative:i}),b=d;return r!=="/"&&(b=d==="/"?r:Pe([r,d])),s.createHref({pathname:b,search:y,hash:o})}function ii(){return z.useContext(ui)!=null}function Hl(){return qt(ii(),"useLocation() may be used only in the context of a component."),z.useContext(ui).location}var cv="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function sv(u){z.useContext(Je).static||z.useLayoutEffect(u)}function b1(){let{isDataRoute:u}=z.useContext(cl);return u?j1():S1()}function S1(){qt(ii(),"useNavigate() may be used only in the context of a component.");let u=z.useContext(Wn),{basename:i,navigator:r}=z.useContext(Je),{matches:s}=z.useContext(cl),{pathname:o}=Hl(),d=JSON.stringify(Iy(s)),y=z.useRef(!1);return sv(()=>{y.current=!0}),z.useCallback((g,m={})=>{if(il(y.current,cv),!y.current)return;if(typeof g=="number"){r.go(g);return}let O=Yf(g,JSON.parse(d),o,m.relative==="path");u==null&&i!=="/"&&(O.pathname=O.pathname==="/"?i:Pe([i,O.pathname])),(m.replace?r.replace:r.push)(O,m.state,m)},[i,r,d,o,u])}var E1=z.createContext(null);function T1(u){let i=z.useContext(cl).outlet;return z.useMemo(()=>i&&z.createElement(E1.Provider,{value:u},i),[i,u])}function ci(u,{relative:i}={}){let{matches:r}=z.useContext(cl),{pathname:s}=Hl(),o=JSON.stringify(Iy(r));return z.useMemo(()=>Yf(u,JSON.parse(o),s,i==="path"),[u,o,s,i])}function R1(u,i){return rv(u,i)}function rv(u,i,r){var w;qt(ii(),"useRoutes() may be used only in the context of a component.");let{navigator:s}=z.useContext(Je),{matches:o}=z.useContext(cl),d=o[o.length-1],y=d?d.params:{},b=d?d.pathname:"/",g=d?d.pathnameBase:"/",m=d&&d.route;{let B=m&&m.path||"";ov(b,!m||B.endsWith("*")||B.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${b}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let O=Hl(),T;if(i){let B=typeof i=="string"?$n(i):i;qt(g==="/"||((w=B.pathname)==null?void 0:w.startsWith(g)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${g}" but pathname "${B.pathname}" was given in the \`location\` prop.`),T=B}else T=O;let N=T.pathname||"/",Q=N;if(g!=="/"){let B=g.replace(/^\//,"").split("/");Q="/"+N.replace(/^\//,"").split("/").slice(B.length).join("/")}let Y=r&&r.state.matches.length?r.state.matches.map(B=>Object.assign(B,{route:r.manifest[B.route.id]||B.route})):Fy(u,{pathname:Q});il(m||Y!=null,`No routes matched location "${T.pathname}${T.search}${T.hash}" `),il(Y==null||Y[Y.length-1].route.element!==void 0||Y[Y.length-1].route.Component!==void 0||Y[Y.length-1].route.lazy!==void 0,`Matched leaf route at location "${T.pathname}${T.search}${T.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let L=z1(Y&&Y.map(B=>Object.assign({},B,{params:Object.assign({},y,B.params),pathname:Pe([g,s.encodeLocation?s.encodeLocation(B.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:B.pathname]),pathnameBase:B.pathnameBase==="/"?g:Pe([g,s.encodeLocation?s.encodeLocation(B.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:B.pathnameBase])})),o,r);return i&&L?z.createElement(ui.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",mask:void 0,...T},navigationType:"POP"}},L):L}function O1(){let u=N1(),i=i1(u)?`${u.status} ${u.statusText}`:u instanceof Error?u.message:JSON.stringify(u),r=u instanceof Error?u.stack:null,s="rgba(200,200,200, 0.5)",o={padding:"0.5rem",backgroundColor:s},d={padding:"2px 4px",backgroundColor:s},y=null;return console.error("Error handled by React Router default ErrorBoundary:",u),y=z.createElement(z.Fragment,null,z.createElement("p",null,"💿 Hey developer 👋"),z.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",z.createElement("code",{style:d},"ErrorBoundary")," or"," ",z.createElement("code",{style:d},"errorElement")," prop on your route.")),z.createElement(z.Fragment,null,z.createElement("h2",null,"Unexpected Application Error!"),z.createElement("h3",{style:{fontStyle:"italic"}},i),r?z.createElement("pre",{style:o},r):null,y)}var x1=z.createElement(O1,null),fv=class extends z.Component{constructor(u){super(u),this.state={location:u.location,revalidation:u.revalidation,error:u.error}}static getDerivedStateFromError(u){return{error:u}}static getDerivedStateFromProps(u,i){return i.location!==u.location||i.revalidation!=="idle"&&u.revalidation==="idle"?{error:u.error,location:u.location,revalidation:u.revalidation}:{error:u.error!==void 0?u.error:i.error,location:i.location,revalidation:u.revalidation||i.revalidation}}componentDidCatch(u,i){this.props.onError?this.props.onError(u,i):console.error("React Router caught the following error during render",u)}render(){let u=this.state.error;if(this.context&&typeof u=="object"&&u&&"digest"in u&&typeof u.digest=="string"){const r=p1(u.digest);r&&(u=r)}let i=u!==void 0?z.createElement(cl.Provider,{value:this.props.routeContext},z.createElement(Gf.Provider,{value:u,children:this.props.component})):this.props.children;return this.context?z.createElement(A1,{error:u},i):i}};fv.contextType=nv;var yf=new WeakMap;function A1({children:u,error:i}){let{basename:r}=z.useContext(Je);if(typeof i=="object"&&i&&"digest"in i&&typeof i.digest=="string"){let s=v1(i.digest);if(s){let o=yf.get(i);if(o)throw o;let d=lv(s.location,r),y=d.absoluteURL||d.to;if(f1(y))throw new Error("Invalid redirect location");if(ev&&!yf.get(i))if(d.isExternal||s.reloadDocument)window.location.href=y;else{const b=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(d.to,{replace:s.replace}));throw yf.set(i,b),b}return z.createElement("meta",{httpEquiv:"refresh",content:`0;url=${y}`})}}return u}function C1({routeContext:u,match:i,children:r}){let s=z.useContext(Wn);return s&&s.static&&s.staticContext&&(i.route.errorElement||i.route.ErrorBoundary)&&(s.staticContext._deepestRenderedBoundaryId=i.route.id),z.createElement(cl.Provider,{value:u},r)}function z1(u,i=[],r){let s=r==null?void 0:r.state;if(u==null){if(!s)return null;if(s.errors)u=s.matches;else if(i.length===0&&!s.initialized&&s.matches.length>0)u=s.matches;else return null}let o=u,d=s==null?void 0:s.errors;if(d!=null){let O=o.findIndex(T=>T.route.id&&(d==null?void 0:d[T.route.id])!==void 0);qt(O>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(d).join(",")}`),o=o.slice(0,Math.min(o.length,O+1))}let y=!1,b=-1;if(r&&s){y=s.renderFallback;for(let O=0;O=0?o=o.slice(0,b+1):o=[o[0]];break}}}}let g=r==null?void 0:r.onError,m=s&&g?(O,T)=>{var N,Q;g(O,{location:s.location,params:((Q=(N=s.matches)==null?void 0:N[0])==null?void 0:Q.params)??{},pattern:c1(s.matches),errorInfo:T})}:void 0;return o.reduceRight((O,T,N)=>{let Q,Y=!1,L=null,w=null;s&&(Q=d&&T.route.id?d[T.route.id]:void 0,L=T.route.errorElement||x1,y&&(b<0&&N===0?(ov("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),Y=!0,w=null):b===N&&(Y=!0,w=T.route.hydrateFallbackElement||null)));let B=i.concat(o.slice(0,N+1)),F=()=>{let X;return Q?X=L:Y?X=w:T.route.Component?X=z.createElement(T.route.Component,null):T.route.element?X=T.route.element:X=O,z.createElement(C1,{match:T,routeContext:{outlet:O,matches:B,isDataRoute:s!=null},children:X})};return s&&(T.route.ErrorBoundary||T.route.errorElement||N===0)?z.createElement(fv,{location:s.location,revalidation:s.revalidation,component:L,error:Q,children:F(),routeContext:{outlet:null,matches:B,isDataRoute:!0},onError:m}):F()},null)}function Xf(u){return`${u} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function M1(u){let i=z.useContext(Wn);return qt(i,Xf(u)),i}function _1(u){let i=z.useContext(Qc);return qt(i,Xf(u)),i}function D1(u){let i=z.useContext(cl);return qt(i,Xf(u)),i}function Zf(u){let i=D1(u),r=i.matches[i.matches.length-1];return qt(r.route.id,`${u} can only be used on routes that contain a unique "id"`),r.route.id}function U1(){return Zf("useRouteId")}function N1(){var s;let u=z.useContext(Gf),i=_1("useRouteError"),r=Zf("useRouteError");return u!==void 0?u:(s=i.errors)==null?void 0:s[r]}function j1(){let{router:u}=M1("useNavigate"),i=Zf("useNavigate"),r=z.useRef(!1);return sv(()=>{r.current=!0}),z.useCallback(async(o,d={})=>{il(r.current,cv),r.current&&(typeof o=="number"?await u.navigate(o):await u.navigate(o,{fromRouteId:i,...d}))},[u,i])}var Ty={};function ov(u,i,r){!i&&!Ty[u]&&(Ty[u]=!0,il(!1,r))}z.memo(H1);function H1({routes:u,manifest:i,future:r,state:s,isStatic:o,onError:d}){return rv(u,void 0,{manifest:i,state:s,isStatic:o,onError:d})}function q1(u){return T1(u.context)}function Hn(u){qt(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function B1({basename:u="/",children:i=null,location:r,navigationType:s="POP",navigator:o,static:d=!1,useTransitions:y}){qt(!ii(),"You cannot render a inside another . You should never have more than one in your app.");let b=u.replace(/^\/*/,"/"),g=z.useMemo(()=>({basename:b,navigator:o,static:d,useTransitions:y,future:{}}),[b,o,d,y]);typeof r=="string"&&(r=$n(r));let{pathname:m="/",search:O="",hash:T="",state:N=null,key:Q="default",mask:Y}=r,L=z.useMemo(()=>{let w=jl(m,b);return w==null?null:{location:{pathname:w,search:O,hash:T,state:N,key:Q,mask:Y},navigationType:s}},[b,m,O,T,N,Q,s,Y]);return il(L!=null,` is not able to match the URL "${m}${O}${T}" because it does not start with the basename, so the won't render anything.`),L==null?null:z.createElement(Je.Provider,{value:g},z.createElement(ui.Provider,{children:i,value:L}))}function Q1({children:u,location:i}){return R1(Uf(u),i)}function Uf(u,i=[]){let r=[];return z.Children.forEach(u,(s,o)=>{if(!z.isValidElement(s))return;let d=[...i,o];if(s.type===z.Fragment){r.push.apply(r,Uf(s.props.children,d));return}qt(s.type===Hn,`[${typeof s.type=="string"?s.type:s.type.name}] is not a component. All component children of must be a or `),qt(!s.props.index||!s.props.children,"An index route cannot have child routes.");let y={id:s.props.id||d.join("-"),caseSensitive:s.props.caseSensitive,element:s.props.element,Component:s.props.Component,index:s.props.index,path:s.props.path,middleware:s.props.middleware,loader:s.props.loader,action:s.props.action,hydrateFallbackElement:s.props.hydrateFallbackElement,HydrateFallback:s.props.HydrateFallback,errorElement:s.props.errorElement,ErrorBoundary:s.props.ErrorBoundary,hasErrorBoundary:s.props.hasErrorBoundary===!0||s.props.ErrorBoundary!=null||s.props.errorElement!=null,shouldRevalidate:s.props.shouldRevalidate,handle:s.props.handle,lazy:s.props.lazy};s.props.children&&(y.children=Uf(s.props.children,d)),r.push(y)}),r}var Uc="get",Nc="application/x-www-form-urlencoded";function Lc(u){return typeof HTMLElement<"u"&&u instanceof HTMLElement}function L1(u){return Lc(u)&&u.tagName.toLowerCase()==="button"}function w1(u){return Lc(u)&&u.tagName.toLowerCase()==="form"}function Y1(u){return Lc(u)&&u.tagName.toLowerCase()==="input"}function G1(u){return!!(u.metaKey||u.altKey||u.ctrlKey||u.shiftKey)}function X1(u,i){return u.button===0&&(!i||i==="_self")&&!G1(u)}var Mc=null;function Z1(){if(Mc===null)try{new FormData(document.createElement("form"),0),Mc=!1}catch{Mc=!0}return Mc}var K1=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function vf(u){return u!=null&&!K1.has(u)?(il(!1,`"${u}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Nc}"`),null):u}function V1(u,i){let r,s,o,d,y;if(w1(u)){let b=u.getAttribute("action");s=b?jl(b,i):null,r=u.getAttribute("method")||Uc,o=vf(u.getAttribute("enctype"))||Nc,d=new FormData(u)}else if(L1(u)||Y1(u)&&(u.type==="submit"||u.type==="image")){let b=u.form;if(b==null)throw new Error('Cannot submit a + + {data?.path ? {data.path} : null} + + + {error ? {(error as Error).message} : null} + + +
+          {isLoading
+            ? "Loading log…"
+            : data?.lines.length
+              ? data.lines.join("\n")
+              : "Log file empty or not found."}
+        
+
+ + ); +} diff --git a/intentframe-control-plane/web/src/pages/Governance.tsx b/intentframe-control-plane/web/src/pages/Governance.tsx new file mode 100644 index 0000000..3785b5f --- /dev/null +++ b/intentframe-control-plane/web/src/pages/Governance.tsx @@ -0,0 +1,117 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMemo, useState } from "react"; +import { Alert, Button, Card, PageHeader } from "../components/ui"; +import { api } from "../api/client"; + +export default function GovernancePage() { + const queryClient = useQueryClient(); + const { data, isLoading, error } = useQuery({ + queryKey: ["governance"], + queryFn: api.governance, + }); + + const [pending, setPending] = useState>({}); + + const dirty = useMemo(() => { + if (!data) return false; + return data.tools.some((tool) => pending[tool.name] !== undefined && pending[tool.name] !== tool.enabled); + }, [data, pending]); + + const toggleMutation = useMutation({ + mutationFn: async ({ name, enabled }: { name: string; enabled: boolean }) => { + if (enabled) await api.enableTool(name); + else await api.disableTool(name); + }, + }); + + const applyMutation = useMutation({ + mutationFn: async () => { + if (!data) return; + for (const tool of data.tools) { + const desired = pending[tool.name]; + if (desired === undefined || desired === tool.enabled) continue; + await toggleMutation.mutateAsync({ name: tool.name, enabled: desired }); + } + await api.applyGovernance(); + }, + onSuccess: () => { + setPending({}); + queryClient.invalidateQueries({ queryKey: ["governance"] }); + queryClient.invalidateQueries({ queryKey: ["status"] }); + }, + }); + + return ( + <> + + + Changes require a gateway restart. Use Apply & restart gateway after editing toggles. + + + {error ? {(error as Error).message} : null} + + + {isLoading || !data ? ( +
Loading governed tools…
+ ) : ( +
+ + + + + + + + + + {data.tools.map((tool) => { + const enabled = pending[tool.name] ?? tool.enabled; + return ( + + + + + + ); + })} + +
ToolRuntimeGoverned
{tool.name} + {tool.enabled ? "enabled" : "disabled"} + + +
+
+ )} +
+ +
+ + +
+ + {applyMutation.error ? ( + {(applyMutation.error as Error).message} + ) : null} + + ); +} diff --git a/intentframe-control-plane/web/src/pages/Overview.tsx b/intentframe-control-plane/web/src/pages/Overview.tsx new file mode 100644 index 0000000..35c7ccb --- /dev/null +++ b/intentframe-control-plane/web/src/pages/Overview.tsx @@ -0,0 +1,115 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Alert, Badge, Button, Card, PageHeader } from "../components/ui"; +import { api } from "../api/client"; + +export default function OverviewPage() { + const queryClient = useQueryClient(); + const { data, isLoading, error, refetch } = useQuery({ + queryKey: ["status"], + queryFn: api.status, + refetchInterval: 5000, + }); + + const upMutation = useMutation({ + mutationFn: api.stackUp, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["status"] }), + }); + + const stopMutation = useMutation({ + mutationFn: api.stackStop, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["status"] }), + }); + + const adapter = data?.adapters[0]; + const stackUp = + data?.bridge_present && adapter?.running && data.gateway_running; + + return ( + <> + + + {!data?.openai_api_key_set ? ( + + OPENAI_API_KEY is not set in the environment. Export it in your shell, then start the + enforcement stack. + + ) : null} + + {error ? ( + {(error as Error).message} + ) : isLoading || !data ? ( + Loading status… + ) : ( +
+ +

+ Enforcement +

+
+
+ Backend bridge + +
+
+ Adapter ({adapter?.agent_id ?? "hermes"}) + +
+
+ Hermes gateway + +
+
+
+ + +

+ Control plane +

+
+
+ UI server + +
+
{data.control_plane.url}
+
+
+
+ )} + +
+ + + +
+ + {(upMutation.error || stopMutation.error) && ( + + {((upMutation.error ?? stopMutation.error) as Error).message} + + )} + + ); +} diff --git a/intentframe-control-plane/web/src/pages/Policy.tsx b/intentframe-control-plane/web/src/pages/Policy.tsx new file mode 100644 index 0000000..7b9597c --- /dev/null +++ b/intentframe-control-plane/web/src/pages/Policy.tsx @@ -0,0 +1,94 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useRef } from "react"; +import { Alert, Button, Card, PageHeader } from "../components/ui"; +import { api } from "../api/client"; + +export default function PolicyPage() { + const queryClient = useQueryClient(); + const fileRef = useRef(null); + + const { data, isLoading, error } = useQuery({ + queryKey: ["policy"], + queryFn: api.policy, + }); + + const reloadMutation = useMutation({ + mutationFn: api.reloadPolicy, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["policy"] }), + }); + + const resetMutation = useMutation({ + mutationFn: api.resetPolicy, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["policy"] }), + }); + + const uploadMutation = useMutation({ + mutationFn: (file: File) => api.applyPolicyFile(file), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["policy"] }), + }); + + return ( + <> + + + {error ? {(error as Error).message} : null} + + + {isLoading || !data ? ( +
Loading policy…
+ ) : ( +
+
+ Runtime: {String(data.meta.runtime_path)} +
+
Registry: {String(data.meta.registry_message)}
+
+ )} +
+ + +
+          {data?.yaml || (isLoading ? "Loading…" : "No policy file")}
+        
+
+ +
+ { + const file = e.target.files?.[0]; + if (file) uploadMutation.mutate(file); + e.target.value = ""; + }} + /> + + + +
+ + {(reloadMutation.error || resetMutation.error || uploadMutation.error) && ( + + {((reloadMutation.error ?? resetMutation.error ?? uploadMutation.error) as Error).message} + + )} + + ); +} diff --git a/intentframe-control-plane/web/src/vite-env.d.ts b/intentframe-control-plane/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/intentframe-control-plane/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/intentframe-control-plane/web/tailwind.config.js b/intentframe-control-plane/web/tailwind.config.js new file mode 100644 index 0000000..7342ead --- /dev/null +++ b/intentframe-control-plane/web/tailwind.config.js @@ -0,0 +1,15 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + colors: { + brand: { + DEFAULT: "#22d3ee", + muted: "#0891b2", + }, + }, + }, + }, + plugins: [], +}; diff --git a/intentframe-control-plane/web/tsconfig.json b/intentframe-control-plane/web/tsconfig.json new file mode 100644 index 0000000..93e40ea --- /dev/null +++ b/intentframe-control-plane/web/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/intentframe-control-plane/web/vite.config.ts b/intentframe-control-plane/web/vite.config.ts new file mode 100644 index 0000000..7ad7184 --- /dev/null +++ b/intentframe-control-plane/web/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + build: { + // Git-tracked output — shipped with the Python package; installs skip npm when present. + outDir: "../src/intentframe_control_plane/static", + emptyOutDir: true, + }, + server: { + port: 5173, + proxy: { + "/api": "http://127.0.0.1:9720", + }, + }, +}); diff --git a/intentframe-integrations-cli/README.md b/intentframe-integrations-cli/README.md index 9f62473..5d892ed 100644 --- a/intentframe-integrations-cli/README.md +++ b/intentframe-integrations-cli/README.md @@ -8,10 +8,14 @@ Full command reference: [docs/hermes-cli.md](../docs/hermes-cli.md). ## Hermes (summary) +After install, open **IntentFrame Control Plane** at `http://127.0.0.1:9720` (started by the installer). + +Details: [docs/intentframe-control-plane.md](../docs/intentframe-control-plane.md). + ```bash -export OPENAI_API_KEY=sk-... -intentframe-integrations up hermes -hermes dashboard +intentframe-integrations control-plane start # if not already running +intentframe-integrations up hermes # from UI or CLI +hermes dashboard # http://127.0.0.1:9119/chat ``` ```bash diff --git a/intentframe-integrations-cli/pyproject.toml b/intentframe-integrations-cli/pyproject.toml index 8aa0b2a..16db86a 100644 --- a/intentframe-integrations-cli/pyproject.toml +++ b/intentframe-integrations-cli/pyproject.toml @@ -4,13 +4,14 @@ build-backend = "hatchling.build" [project] name = "intentframe-integrations-cli" -version = "0.2.0" +version = "0.2.1" description = "User-facing CLI for IntentFrame agent integrations (Hermes, OpenClaw, …)" readme = "README.md" requires-python = ">=3.14" license = "Apache-2.0" dependencies = [ "if-integration-backend", + "intentframe-control-plane", ] [project.scripts] @@ -21,3 +22,4 @@ packages = ["src/intentframe_integrations"] [tool.uv.sources] if-integration-backend = { workspace = true } +intentframe-control-plane = { workspace = true } diff --git a/intentframe-integrations-cli/src/intentframe_integrations/__version__.py b/intentframe-integrations-cli/src/intentframe_integrations/__version__.py index d3ec452..3ced358 100644 --- a/intentframe-integrations-cli/src/intentframe_integrations/__version__.py +++ b/intentframe-integrations-cli/src/intentframe_integrations/__version__.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/intentframe-integrations-cli/src/intentframe_integrations/cli.py b/intentframe-integrations-cli/src/intentframe_integrations/cli.py index 2db517c..28090b1 100644 --- a/intentframe-integrations-cli/src/intentframe_integrations/cli.py +++ b/intentframe-integrations-cli/src/intentframe_integrations/cli.py @@ -7,6 +7,7 @@ from __future__ import annotations import argparse +import json import os import subprocess import sys @@ -26,6 +27,7 @@ HermesGatewayError, gateway_log_file, is_gateway_running, + normalize_hermes_gateway_argv, start_hermes_gateway, stop_hermes_gateway, ) @@ -60,7 +62,9 @@ policy_reset, policy_set, policy_show, + policy_show_to_dict, ) +from intentframe_integrations.status_report import status_report_json from intentframe_integrations.paths import agent_config_path, list_agents from intentframe_integrations.runtime_lifecycle import ( backend_ready_for_pack, @@ -305,7 +309,10 @@ def _cmd_stop() -> int: return _run_backend(["stop"]) -def _cmd_status() -> int: +def _cmd_status(*, as_json: bool = False) -> int: + if as_json: + print(status_report_json()) + return 0 ec = _run_backend(["status"]) for agent in list_agents(): try: @@ -374,12 +381,15 @@ def _cmd_doctor( return 0 -def _cmd_policy_show(agent: str) -> int: +def _cmd_policy_show(agent: str, *, as_json: bool = False) -> int: try: report = policy_show(agent) except (PolicyError, FileNotFoundError, ValueError) as exc: print(f"ERROR: {exc}", file=sys.stderr) return 1 + if as_json: + print(json.dumps(policy_show_to_dict(report), indent=2)) + return 0 print(format_policy_show(report)) return 0 @@ -542,7 +552,7 @@ def _cmd_gateway_stop(agent: str) -> int: return 0 -def _cmd_governance_list(agent: str) -> int: +def _cmd_governance_list(agent: str, *, as_json: bool = False) -> int: if agent != "hermes": print(f"ERROR: governance is only implemented for hermes (got {agent!r})", file=sys.stderr) return 1 @@ -552,6 +562,16 @@ def _cmd_governance_list(agent: str) -> int: print(f"ERROR: {exc}", file=sys.stderr) return 1 + if as_json: + governed = runtime_governed_tool_names(agent) + payload = { + "agent": agent, + "tools": [{"name": name, "enabled": enabled} for name, enabled in entries], + "runtime_governed": governed, + } + print(json.dumps(payload, indent=2)) + return 0 + print(f"Governed tool catalog ({agent}):") for name, enabled in entries: state = "enabled" if enabled else "disabled" @@ -603,6 +623,48 @@ def _ensure_runtime(pack: IntegrationPack) -> int: return 0 +def _cmd_control_plane_start(*, host: str | None, port: int | None) -> int: + from intentframe_control_plane.lifecycle import ControlPlaneError, start_control_plane + + try: + status = start_control_plane(host=host, port=port) + except ControlPlaneError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + except ValueError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + print(status.url) + return 0 + + +def _cmd_control_plane_stop() -> int: + from intentframe_control_plane.lifecycle import stop_control_plane + + stop_control_plane() + return 0 + + +def _cmd_control_plane_status() -> int: + from intentframe_control_plane.lifecycle import control_plane_status, format_status_line + + print(format_status_line(control_plane_status())) + return 0 + + +def _cmd_control_plane_serve(*, host: str | None, port: int | None) -> int: + from intentframe_control_plane.lifecycle import ControlPlaneError, serve_control_plane + + try: + serve_control_plane(host=host, port=port) + except ValueError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + except KeyboardInterrupt: + return 130 + return 0 + + def _cmd_run(agent: str, *, gateway_args: list[str]) -> int: if agent != "hermes": print(f"ERROR: run is only implemented for hermes (got {agent!r})", file=sys.stderr) @@ -703,8 +765,12 @@ def main(argv: list[str] | None = None) -> int: help="Pass --skip-if-exists to seed-policy", ) - sub.add_parser("stop", help="Stop agent adapters, IntentFrame runtime, and bridge") - sub.add_parser("status", help="Runtime, bridge, and adapter status") + sub.add_parser( + "stop", + help="Stop enforcement stack (gateway, adapters, backend — not control plane)", + ) + p_status = sub.add_parser("status", help="Runtime, bridge, and adapter status") + p_status.add_argument("--json", action="store_true", help="Emit JSON status report") p_seed = sub.add_parser("seed", help="Seed policy for an agent profile") p_seed.add_argument("agent", nargs="?", choices=agents) @@ -796,6 +862,7 @@ def main(argv: list[str] | None = None) -> int: p_policy_show = policy_sub.add_parser("show", help="Show runtime policy path and registry status") p_policy_show.add_argument("agent", choices=agents) + p_policy_show.add_argument("--json", action="store_true", help="Emit JSON policy report") p_policy_reload = policy_sub.add_parser( "reload", @@ -873,6 +940,7 @@ def main(argv: list[str] | None = None) -> int: p_governance_list = governance_sub.add_parser("list", help="Show catalog and runtime governed tools") p_governance_list.add_argument("agent", choices=agents) + p_governance_list.add_argument("--json", action="store_true", help="Emit JSON tool catalog") p_governance_enable = governance_sub.add_parser("enable", help="Enable a governed tool") p_governance_enable.add_argument("agent", choices=agents) @@ -882,6 +950,23 @@ def main(argv: list[str] | None = None) -> int: p_governance_disable.add_argument("agent", choices=agents) p_governance_disable.add_argument("tool", help="Hermes tool name from governance/tools.yaml") + p_cp = sub.add_parser( + "control-plane", + help="IntentFrame operator control plane (http://127.0.0.1:9720)", + ) + cp_sub = p_cp.add_subparsers(dest="control_plane_command", required=True) + + p_cp_start = cp_sub.add_parser("start", help="Start control plane in background") + p_cp_start.add_argument("--host", default=None, help="Bind host (default: 127.0.0.1)") + p_cp_start.add_argument("--port", type=int, default=None, help="Bind port (default: 9720)") + + cp_sub.add_parser("stop", help="Stop control plane only") + cp_sub.add_parser("status", help="Show control plane status") + + p_cp_serve = cp_sub.add_parser("serve", help="Run control plane in foreground") + p_cp_serve.add_argument("--host", default=None) + p_cp_serve.add_argument("--port", type=int, default=None) + args = parser.parse_args(argv) match args.command: @@ -910,7 +995,7 @@ def main(argv: list[str] | None = None) -> int: case "stop": return _cmd_stop() case "status": - return _cmd_status() + return _cmd_status(as_json=args.json) case "seed": if args.agent_config is not None: if args.agent is not None: @@ -950,7 +1035,7 @@ def main(argv: list[str] | None = None) -> int: case "policy": match args.policy_command: case "show": - return _cmd_policy_show(args.agent) + return _cmd_policy_show(args.agent, as_json=args.json) case "reload": return _cmd_policy_reload(args.agent) case "set": @@ -987,7 +1072,7 @@ def main(argv: list[str] | None = None) -> int: case "governance": match args.governance_command: case "list": - return _cmd_governance_list(args.agent) + return _cmd_governance_list(args.agent, as_json=args.json) case "enable": return _cmd_governance_set(args.agent, args.tool, enabled=True) case "disable": @@ -995,6 +1080,19 @@ def main(argv: list[str] | None = None) -> int: case _: parser.error(f"Unknown governance command: {args.governance_command}") return 2 + case "control-plane": + match args.control_plane_command: + case "start": + return _cmd_control_plane_start(host=args.host, port=args.port) + case "stop": + return _cmd_control_plane_stop() + case "status": + return _cmd_control_plane_status() + case "serve": + return _cmd_control_plane_serve(host=args.host, port=args.port) + case _: + parser.error(f"Unknown control-plane command: {args.control_plane_command}") + return 2 case _: parser.error(f"Unknown command: {args.command}") return 2 diff --git a/intentframe-integrations-cli/src/intentframe_integrations/hermes_uninstall.py b/intentframe-integrations-cli/src/intentframe_integrations/hermes_uninstall.py index 817af2d..3d83907 100644 --- a/intentframe-integrations-cli/src/intentframe_integrations/hermes_uninstall.py +++ b/intentframe-integrations-cli/src/intentframe_integrations/hermes_uninstall.py @@ -132,6 +132,14 @@ def uninstall_hermes( messages: list[str] = [] env_keys = frozenset(pack.agent.env) + try: + from intentframe_control_plane.lifecycle import stop_control_plane + + stop_control_plane(quiet=True) + messages.append("Stopped IntentFrame control plane") + except Exception: + pass + plugin_dest = plugin_install_path() if plugin_dest.exists() or plugin_dest.is_symlink(): _remove_path(plugin_dest, messages, "Hermes plugin") diff --git a/intentframe-integrations-cli/src/intentframe_integrations/policy_manage.py b/intentframe-integrations-cli/src/intentframe_integrations/policy_manage.py index fdbafe2..e9de6fa 100644 --- a/intentframe-integrations-cli/src/intentframe_integrations/policy_manage.py +++ b/intentframe-integrations-cli/src/intentframe_integrations/policy_manage.py @@ -159,6 +159,19 @@ def policy_show(agent: str) -> PolicyShowReport: ) +def policy_show_to_dict(report: PolicyShowReport) -> dict[str, object]: + return { + "agent_id": report.agent_id, + "user_id": report.user_id, + "runtime_path": str(report.runtime_path), + "runtime_exists": report.runtime_exists, + "shipped_template": str(report.shipped_template), + "registry_loaded": report.registry_loaded, + "registry_action_count": report.registry_action_count, + "registry_message": report.registry_message, + } + + def format_policy_show(report: PolicyShowReport) -> str: lines = [ f"Policy ({report.agent_id}):", diff --git a/intentframe-integrations-cli/src/intentframe_integrations/status_report.py b/intentframe-integrations-cli/src/intentframe_integrations/status_report.py new file mode 100644 index 0000000..742ba1a --- /dev/null +++ b/intentframe-integrations-cli/src/intentframe_integrations/status_report.py @@ -0,0 +1,86 @@ +"""Structured status report for ``intentframe-integrations status --json``. + +Enforcement stack fields are shared with the control plane ``read_models`` views. +""" + +from __future__ import annotations + +import json +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any + +from intentframe_integrations.adapter_lifecycle import ( + adapter_pid_file, + is_adapter_running, +) +from intentframe_integrations.hermes_gateway import gateway_pid_file, is_gateway_running +from intentframe_integrations.integration_pack import load_and_activate_pack +from intentframe_integrations.paths import list_agents + + +@dataclass +class AdapterStatus: + agent_id: str + running: bool + pid: int | None + socket: str + + +@dataclass +class StatusReport: + bridge_socket: str + bridge_present: bool + gateway_running: bool + gateway_pid: int | None + adapters: list[AdapterStatus] + backend_message: str | None = None + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +def _read_pid(path: Path) -> int | None: + if not path.is_file(): + return None + try: + return int(path.read_text(encoding="utf-8").strip()) + except (OSError, ValueError): + return None + + +def collect_status_report() -> StatusReport: + bridge = Path.home() / ".intentframe" / "backend" / "bridge.sock" + adapters: list[AdapterStatus] = [] + + for agent in list_agents(): + try: + pack = load_and_activate_pack(agent) + except (FileNotFoundError, ValueError): + continue + if pack.adapter is None: + continue + agent_id = pack.agent.agent_id + sock = pack.adapter.socket_path() + running = is_adapter_running(agent_id) and sock.exists() + adapters.append( + AdapterStatus( + agent_id=agent_id, + running=running, + pid=_read_pid(adapter_pid_file(agent_id)), + socket=str(sock), + ) + ) + + gw_pid = _read_pid(gateway_pid_file()) + return StatusReport( + bridge_socket=str(bridge), + bridge_present=bridge.exists(), + gateway_running=is_gateway_running(), + gateway_pid=gw_pid if is_gateway_running() else None, + adapters=adapters, + ) + + +def status_report_json() -> str: + return json.dumps(collect_status_report().to_dict(), indent=2) diff --git a/pyproject.toml b/pyproject.toml index 4a7e547..1ac3543 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-integrations" -version = "0.2.0" +version = "0.2.1" description = "IntentFrame agent integrations monorepo (workspace root)" requires-python = ">=3.14" dependencies = [] @@ -9,6 +9,7 @@ dependencies = [] members = [ "if-integration-backend", "if-integration-clients/python", + "intentframe-control-plane", "intentframe-integrations-cli", "integrations/hermes/adapter", "integrations/hermes/shared", diff --git a/scripts/install-hermes-plugin.sh b/scripts/install-hermes-plugin.sh index f549a22..77d3dca 100755 --- a/scripts/install-hermes-plugin.sh +++ b/scripts/install-hermes-plugin.sh @@ -1,22 +1,27 @@ #!/usr/bin/env bash -# Install Hermes (if missing) + IntentFrame plugin. +# Install Hermes (if missing) + IntentFrame plugin + Control Plane UI. # # Latest (rolling): # curl -fsSL https://github.com/intentframe/agent-integrations/raw/main/scripts/install-hermes-plugin.sh | bash # # Pinned release (script URL and pack ref should match): -# curl -fsSL https://github.com/intentframe/agent-integrations/raw/v0.2.0/scripts/install-hermes-plugin.sh | bash -s -- --ref v0.2.0 +# curl -fsSL https://github.com/intentframe/agent-integrations/raw/v0.2.1/scripts/install-hermes-plugin.sh | bash -s -- --ref v0.2.1 # -# Docker / CI (skip Hermes setup wizard + browser engine): -# curl -fsSL .../install-hermes-plugin.sh | bash -s -- --headless --ref main +# Docker / CI (skip Hermes setup wizard + browser engine; defer control plane start): +# curl -fsSL .../install-hermes-plugin.sh | bash -s -- --headless --no-control-plane --ref main # -# Then: -# export OPENAI_API_KEY=sk-... -# intentframe-integrations up hermes +# Control plane UI (auto-started unless --no-control-plane): +# http://127.0.0.1:9720 — React SPA + /api/* served by one uvicorn process +# Static assets are pre-built in the integration pack (git-tracked); npm build runs +# only when static/index.html is missing. +# +# Hermes chat (separate, after enforcement stack is up): # hermes dashboard # http://localhost:9119 set -euo pipefail HEADLESS=false +NO_CONTROL_PLANE=false +NO_OPEN=false REF="${REF:-}" while [[ $# -gt 0 ]]; do @@ -25,6 +30,14 @@ while [[ $# -gt 0 ]]; do HEADLESS=true shift ;; + --no-control-plane) + NO_CONTROL_PLANE=true + shift + ;; + --no-open) + NO_OPEN=true + shift + ;; --ref) if [[ $# -lt 2 ]]; then echo "ERROR: --ref requires a value" >&2 @@ -39,10 +52,12 @@ while [[ $# -gt 0 ]]; do ;; -h|--help) cat <<'EOF' -Usage: install-hermes-plugin.sh [--headless] [--ref REF] +Usage: install-hermes-plugin.sh [--headless] [--no-control-plane] [--no-open] [--ref REF] - --headless Skip Hermes setup wizard and browser engine (faster; for testers/CI/Docker). - Default: full Hermes install for end users. + --headless Skip Hermes setup wizard and browser engine (also skips browser open). + --no-control-plane Do not start IntentFrame Control Plane after install (CI/Docker). + Docker entrypoint starts CP separately with 0.0.0.0 bind. + --no-open Do not open http://127.0.0.1:9720 in a browser after install. --ref REF Git ref for the integration pack (branch, tag, or commit SHA). Also set via REF= env. VERSION= is a deprecated alias for REF=. @@ -51,7 +66,7 @@ Usage: install-hermes-plugin.sh [--headless] [--ref REF] Install tiers (use the same ref in the script URL and --ref): Latest: curl .../raw/main/scripts/install-hermes-plugin.sh | bash - Release: curl .../raw/v0.2.0/... | bash -s -- --ref v0.2.0 + Release: curl .../raw/v0.2.1/... | bash -s -- --ref v0.2.1 Locked: curl .../raw//... | bash -s -- --ref EOF exit 0 @@ -63,6 +78,10 @@ EOF esac done +if [[ "${HEADLESS}" == true ]]; then + NO_OPEN=true +fi + ORG="${ORG:-intentframe}" REPO="${REPO:-agent-integrations}" REF="${REF:-${VERSION:-main}}" @@ -70,6 +89,8 @@ INSTALL_DIR="${INSTALL_DIR:-${HOME}/.intentframe/agent-integrations}" LOCAL_BIN="${HOME}/.local/bin" SYSTEM_BIN="/usr/local/bin" HERMES_ENV="${HOME}/.hermes/.env" +IF_ENV="${HOME}/.intentframe/.env" +CONTROL_PLANE_URL="http://127.0.0.1:9720" CLI_SYSTEM="" HERMES_SUMMARY="" PACK_REF="" @@ -122,6 +143,69 @@ ensure_local_bin_on_path() { export PATH="${LOCAL_BIN}:${SYSTEM_BIN}:${PATH}" } +open_browser() { + local url="$1" + if [[ "${NO_OPEN}" == true ]]; then + return 0 + fi + if [[ -n "${DISPLAY:-}" ]] || [[ "$(uname -s)" == "Darwin" ]]; then + : + else + return 0 + fi + if [[ "$(uname -s)" == "Darwin" ]] && have open; then + open "${url}" >/dev/null 2>&1 || true + return 0 + fi + if have xdg-open; then + xdg-open "${url}" >/dev/null 2>&1 || true + fi +} + +seed_intentframe_env() { + mkdir -p "${HOME}/.intentframe" + touch "${IF_ENV}" + for line in \ + "INTENTFRAME_CONTROL_PLANE_HOST=127.0.0.1" \ + "INTENTFRAME_CONTROL_PLANE_PORT=9720" \ + "INTENTFRAME_INTEGRATIONS_BIN=${IF_CLI}"; do + key="${line%%=*}" + grep -q "^${key}=" "${IF_ENV}" || echo "${line}" >> "${IF_ENV}" + done +} + +build_control_plane_frontend() { + # Vite output is git-tracked under intentframe_control_plane/static/ so most installs + # (GitHub tarball, Docker) skip npm. Rebuild only when static/index.html is absent. + local web_dir="${INSTALL_DIR}/intentframe-control-plane/web" + local static_index="${INSTALL_DIR}/intentframe-control-plane/src/intentframe_control_plane/static/index.html" + if [[ -f "${static_index}" ]]; then + return 0 + fi + if [[ ! -d "${web_dir}" ]]; then + echo "WARNING: control plane frontend source missing; UI may be unavailable" >&2 + return 0 + fi + if ! have npm; then + echo "WARNING: npm not found; skipping control plane frontend build" >&2 + return 0 + fi + step "Building IntentFrame Control Plane frontend" + (cd "${web_dir}" && npm ci && npm run build) +} + +start_control_plane() { + if [[ "${NO_CONTROL_PLANE}" == true ]]; then + return 0 + fi + step "Starting IntentFrame Control Plane" + if ! "${IF_CLI}" control-plane start; then + echo "WARNING: control plane failed to start (port may be in use). Open ${CONTROL_PLANE_URL} manually after: intentframe-integrations control-plane start" >&2 + return 0 + fi + open_browser "${CONTROL_PLANE_URL}" +} + install_cli_on_path() { mkdir -p "${LOCAL_BIN}" ln -sf "${IF_CLI}" "${LOCAL_BIN}/intentframe-integrations" @@ -177,6 +261,8 @@ IF_CLI="${INSTALL_DIR}/.venv/bin/intentframe-integrations" step "Installing Python workspace" uv sync --all-packages +build_control_plane_frontend + step "Installing IntentFrame plugin into Hermes" "${IF_CLI}" integrate hermes --copy @@ -194,6 +280,11 @@ for line in \ grep -q "^${key}=" "${HERMES_ENV}" || echo "${line}" >> "${HERMES_ENV}" done +step "Writing IntentFrame env to ${IF_ENV}" +seed_intentframe_env + +start_control_plane + PATH_HINT='intentframe-integrations is on PATH in this shell.' if ! have intentframe-integrations; then PATH_HINT='Open a new terminal, or run: source ~/.zshrc # if intentframe-integrations is not found' @@ -211,17 +302,26 @@ if [[ "${HEADLESS}" == true ]]; then Hermes (headless): run hermes setup if API keys are not configured yet." fi +CP_LINE=" Control Plane: ${CONTROL_PLANE_URL}" +if [[ "${NO_CONTROL_PLANE}" == true ]]; then + CP_LINE=" Control Plane: not started (--no-control-plane; run: intentframe-integrations control-plane start)" +fi + cat < # # Deprecated alias (still works): @@ -15,3 +15,7 @@ OPENAI_API_KEY=sk-your-key-here # Optional: dashboard basic auth (defaults hermes / docker-test) # HERMES_DASHBOARD_USER=hermes # HERMES_DASHBOARD_PASSWORD=docker-test + +# Optional: host ports (defaults 9720 / 9119) +# CONTROL_PLANE_PORT=9720 +# DASHBOARD_PORT=9119 diff --git a/tests/docker/README.md b/tests/docker/README.md index 5c9f0cc..ae8ddfd 100644 --- a/tests/docker/README.md +++ b/tests/docker/README.md @@ -1,6 +1,24 @@ # Docker test: Hermes web chat user journey -Production-like install: the container runs the same GitHub install script as a real user (`curl …/install-hermes-plugin.sh | bash -s -- --headless`). Only `entrypoint.sh` is mounted — it seeds Docker-only config (OpenAI provider, dashboard auth for `0.0.0.0`) and starts services. +Production-like install: the container runs the same GitHub install script as a real user (`curl …/install-hermes-plugin.sh | bash -s -- --headless --no-control-plane`). Only `entrypoint.sh` is mounted — it seeds Docker-only config, starts the **IntentFrame Control Plane** on `0.0.0.0:9720`, then brings up the enforcement stack and Hermes dashboard. + +### Why `--no-control-plane` during install? + +The installer normally starts the control plane at the end of install. Docker uses `--no-control-plane` so the entrypoint can: + +1. Seed `INTENTFRAME_CONTROL_PLANE_HOST=0.0.0.0` and `ALLOW_REMOTE=1` (required for port publishing) +2. Start the control plane **after** config is written + +The installer may print `Control Plane: not started` — that is expected; the entrypoint starts it on the next step. + +### How the control plane UI is served in Docker + +Same as a local install: one **uvicorn** process on `:9720` serves the pre-built React static files (`static/index.html`, `/assets/*`) and the JSON API (`/api/*`). There is no separate nginx or Vite container. App code and static assets come from the GitHub integration pack tarball; only `entrypoint.sh` is bind-mounted from this repo. + +```text +Host browser → localhost:9720 → container uvicorn → static/ + /api/* +Host browser → localhost:9119 → hermes dashboard (separate process) +``` User-facing install and chat flow: [README.md](../../README.md). @@ -11,14 +29,23 @@ export OPENAI_API_KEY=sk-... docker compose -f tests/docker/docker-compose.test.yml up ``` -Open **http://localhost:9119/chat** — sign in with default credentials `hermes` / `docker-test` (override via `HERMES_DASHBOARD_USER` / `HERMES_DASHBOARD_PASSWORD`). If you already have a session cookie, use **Log out** first to see the login screen. +### User journey (from your host) + +| Step | URL | Notes | +|------|-----|-------| +| 1. Control Plane | **http://localhost:9720** | Governance, policy, stack status | +| 2. Hermes chat | **http://localhost:9119/chat** | Sign in with `hermes` / `docker-test` | -The entrypoint clears Hermes’s default OpenRouter `base_url` so `OPENAI_API_KEY` hits OpenAI directly, and runs `intentframe-integrations up hermes` before the dashboard (IntentFrame + adapter + gateway). +The entrypoint auto-starts the enforcement stack (`intentframe-integrations up hermes`) so chat is ready immediately; you can also start/stop it from the Control Plane Overview page. + +If you already have a Hermes session cookie, use **Log out** first to see the login screen. + +The entrypoint clears Hermes’s default OpenRouter `base_url` so `OPENAI_API_KEY` hits OpenAI directly. Pin a GitHub **ref** (branch, tag, or commit) for the install script and integration pack — use the same ref for both: ```bash -export REF=my-branch # or REF=v0.2.0, REF= +export REF=my-branch # or REF=v0.2.1, REF= # VERSION= is a deprecated alias for REF= ``` @@ -36,6 +63,7 @@ All paths below are inside the container (`hermes-intentframe-test`). Run from t | Path | What it shows | |------|----------------| +| `/root/.intentframe/logs/control-plane.log` | IntentFrame Control Plane (uvicorn) | | `/root/.intentframe/logs/intentframe-server.log` | **Primary** — pretty INTENT boxes (FILE SHIELD, deterministic Guardian, AE, Guardian ALLOW/BLOCK) | | `/root/.intentframe/logs/bundle-sdk.log` | JSON per bundle hook (`enforce_constraints`, `structural_gates`, …) with full intent + evidence | | `/root/.intentframe/logs/analysis_outputs.log` | Analysis Engine JSON (scope mismatch, risk, hidden behaviors) | @@ -171,6 +199,9 @@ After a path-policy block, chat should show tool `status: blocked` and a file-mu | Chat 401 to OpenRouter | Stale volume — `down -v` and restart; entrypoint clears `base_url` for OpenAI | | `adapter.log` only shows `200 OK` | Normal; use `intentframe-server.log` and `state.db` for detail | | Config change ignored | Named volumes persist — `docker compose … down -v` | +| Control Plane 503 / empty UI | Git ref may lack pre-built `static/` assets; use a ref that includes the built frontend, or rebuild locally before pinning `REF=` | +| Overview shows UI server **unhealthy** but page loads | Fixed in current branch — was uvicorn self-deadlock on `/api/status`; update ref or rebuild | +| Overview URL shows `http://0.0.0.0:9720` | Display only (bind address from `.env`); use **http://localhost:9720** in the browser | More on IntentFrame log layers: `tests/hermes_gateway/README.md` (sandbox paths; same filenames under `/root/.intentframe` in Docker). @@ -210,7 +241,7 @@ command -v intentframe-integrations env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin intentframe-integrations --help ``` -Or pin your ref in compose: `export REF=my-branch` (or `REF=v0.2.0`) before `up` (script is fetched from GitHub). +Or pin your ref in compose: `export REF=my-branch` (or `REF=v0.2.1`) before `up` (script is fetched from GitHub). Reset (fresh install): @@ -218,6 +249,16 @@ Reset (fresh install): docker compose -f tests/docker/docker-compose.test.yml down -v ``` +### Control plane smoke (local, no Hermes) + +Throwaway container using the **local repo** (not GitHub tarball). Exercises lifecycle on `0.0.0.0:9720`, external health probe, and SPA index: + +```bash +bash tests/docker/test_control_plane_smoke.sh +``` + +See [control_plane_smoke_inner.sh](./control_plane_smoke_inner.sh). + ### Test uninstall (inside container) ```bash diff --git a/tests/docker/control_plane_smoke_inner.sh b/tests/docker/control_plane_smoke_inner.sh new file mode 100755 index 0000000..024f8fd --- /dev/null +++ b/tests/docker/control_plane_smoke_inner.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Throwaway Docker smoke: control-plane start on 0.0.0.0 with PID file + fast /api/health. +# Uses local repo tarball (not GitHub install). External probes only — in-process /api/status +# health is covered by tests/intentframe_control_plane/test_server.py. +set -euo pipefail + +export HOME="${HOME:-/tmp/cp-smoke-home}" +export INSTALL_DIR="${HOME}/.intentframe/agent-integrations" +export DEBIAN_FRONTEND=noninteractive + +step() { printf '\n==> %s\n' "$*"; } +fail() { echo "FAIL: $*" >&2; exit 1; } + +step "Installing OS packages" +apt-get update -qq +apt-get install -y -qq curl ca-certificates +rm -rf /var/lib/apt/lists/* + +step "Copy local repo into ${INSTALL_DIR}" +rm -rf "${INSTALL_DIR}" +mkdir -p "${INSTALL_DIR}" +tar -cC /repo --exclude=.git . | tar -xC "${INSTALL_DIR}" + +step "Install control plane workspace" +cd "${INSTALL_DIR}" +uv sync --package intentframe-control-plane --package intentframe-integrations-cli + +IF_CLI="${INSTALL_DIR}/.venv/bin/intentframe-integrations" +test -x "${IF_CLI}" + +step "Seed IntentFrame env for Docker bind" +mkdir -p "${HOME}/.intentframe/logs" +cat >"${HOME}/.intentframe/.env" </dev/null 2>&1; then + apt-get update -qq && apt-get install -y -qq curl +fi +health_json="$(curl -fsS --max-time 1 http://127.0.0.1:9720/api/health)" +echo "${health_json}" | grep -q '"ok":true\|"ok": true' +echo "${health_json}" | grep -q 'intentframe-control-plane' + +step "Verify SPA index is served" +curl -fsS --max-time 2 http://127.0.0.1:9720/ | grep -qi html || true + +step "Stop control plane" +"${IF_CLI}" control-plane stop + +step "Control plane Docker smoke passed" diff --git a/tests/docker/docker-compose.test.yml b/tests/docker/docker-compose.test.yml index 46ce2b5..1a0d7d6 100644 --- a/tests/docker/docker-compose.test.yml +++ b/tests/docker/docker-compose.test.yml @@ -3,9 +3,12 @@ # export OPENAI_API_KEY=sk-... # docker compose -f tests/docker/docker-compose.test.yml up # +# Control plane (:9720): uvicorn serves pre-built React static/ + /api/* from GitHub pack. +# Hermes chat (:9119): separate hermes dashboard process. +# # Or: cp tests/docker/.env.example tests/docker/.env (set key there) # -# Open http://localhost:9119/chat — ask Hermes to run a terminal command. +# Open http://localhost:9720 (IntentFrame Control Plane) and http://localhost:9119/chat (Hermes). # Verify gating / logs: tests/docker/README.md#logs-and-analysis-inside-the-container # docker compose -f tests/docker/docker-compose.test.yml exec hermes-intentframe \ # tail -f /root/.intentframe/logs/intentframe-server.log @@ -21,6 +24,7 @@ services: container_name: hermes-intentframe-test ports: - "${DASHBOARD_PORT:-9119}:9119" + - "${CONTROL_PLANE_PORT:-9720}:9720" environment: OPENAI_API_KEY: ${OPENAI_API_KEY:?set OPENAI_API_KEY or use tests/docker/.env} REF: ${REF:-${VERSION:-main}} @@ -28,6 +32,11 @@ services: INTENTFRAME_HERMES_E2E_MODEL: ${INTENTFRAME_HERMES_E2E_MODEL:-gpt-4o-mini} HERMES_DASHBOARD_USER: ${HERMES_DASHBOARD_USER:-hermes} HERMES_DASHBOARD_PASSWORD: ${HERMES_DASHBOARD_PASSWORD:-docker-test} + INTENTFRAME_CONTROL_PLANE_HOST: 0.0.0.0 + INTENTFRAME_CONTROL_PLANE_PORT: ${CONTROL_PLANE_PORT:-9720} + INTENTFRAME_CONTROL_PLANE_ALLOW_REMOTE: "1" + HERMES_DASHBOARD_HOST: 127.0.0.1 + HERMES_DASHBOARD_PORT: ${DASHBOARD_PORT:-9119} volumes: - intentframe-data:/root/.intentframe - hermes-data:/root/.hermes diff --git a/tests/docker/entrypoint.sh b/tests/docker/entrypoint.sh index b19b3a4..683b457 100755 --- a/tests/docker/entrypoint.sh +++ b/tests/docker/entrypoint.sh @@ -1,10 +1,23 @@ #!/usr/bin/env bash -# Docker harness: GitHub install → seed OpenAI + dashboard auth (0.0.0.0 only) → up hermes → dashboard. +# Docker harness: GitHub install → control plane (9720) → up hermes → dashboard (9119). +# +# Flow: +# 1. install-hermes-plugin.sh --headless --no-control-plane (pack + uv sync; CP deferred) +# 2. seed ~/.intentframe/.env (0.0.0.0:9720, ALLOW_REMOTE=1) +# 3. intentframe-integrations control-plane start +# 4. intentframe-integrations up hermes +# 5. exec hermes dashboard --host 0.0.0.0 +# +# Frontend: pre-built static/ from the GitHub pack; uvicorn on :9720 serves SPA + /api/*. +# Only this entrypoint is bind-mounted; app code comes from the tarball at REF=. set -euo pipefail export PATH="/root/.local/bin:/usr/local/bin:${PATH}" HERMES_HOME="${HERMES_HOME:-/root/.hermes}" +IF_ENV="${HOME:-/root}/.intentframe/.env" +CONTROL_PLANE_PORT="${INTENTFRAME_CONTROL_PLANE_PORT:-9720}" +HERMES_DASHBOARD_PORT="${HERMES_DASHBOARD_PORT:-9119}" HERMES_PROVIDER="${HERMES_E2E_OPENAI_PROVIDER:-openai-api}" HERMES_API_MODE="${HERMES_E2E_OPENAI_API_MODE:-chat_completions}" HERMES_MODEL="${INTENTFRAME_HERMES_E2E_MODEL:-gpt-4o-mini}" @@ -22,7 +35,7 @@ if ! have intentframe-integrations; then ref="${REF:-${VERSION:-main}}" url="https://github.com/intentframe/agent-integrations/raw/${ref}/scripts/install-hermes-plugin.sh" step "Installing from ${url} (ref=${ref})" - curl -fsSL "${url}" | bash -s -- --headless --ref "${ref}" + curl -fsSL "${url}" | bash -s -- --headless --no-control-plane --ref "${ref}" fi if [[ -z "${OPENAI_API_KEY:-}" ]]; then @@ -118,12 +131,45 @@ PY seed_hermes_runtime_config +seed_control_plane_docker_config() { + step "Seeding IntentFrame Control Plane config (bind 0.0.0.0:${CONTROL_PLANE_PORT})" + # ALLOW_REMOTE=1 is required to bind 0.0.0.0; host maps published port in compose. + mkdir -p "$(dirname "${IF_ENV}")" + touch "${IF_ENV}" + for line in \ + "INTENTFRAME_CONTROL_PLANE_HOST=0.0.0.0" \ + "INTENTFRAME_CONTROL_PLANE_PORT=${CONTROL_PLANE_PORT}" \ + "INTENTFRAME_CONTROL_PLANE_ALLOW_REMOTE=1" \ + "HERMES_DASHBOARD_HOST=127.0.0.1" \ + "HERMES_DASHBOARD_PORT=${HERMES_DASHBOARD_PORT}"; do + key="${line%%=*}" + if grep -q "^${key}=" "${IF_ENV}"; then + sed -i "s|^${key}=.*|${line}|" "${IF_ENV}" + else + echo "${line}" >> "${IF_ENV}" + fi + done +} + +start_control_plane() { + step "Starting IntentFrame Control Plane on 0.0.0.0:${CONTROL_PLANE_PORT}" + # Installer used --no-control-plane; we start here after Docker env is seeded. + export INTENTFRAME_CONTROL_PLANE_ALLOW_REMOTE=1 + if ! intentframe-integrations control-plane start --host 0.0.0.0 --port "${CONTROL_PLANE_PORT}"; then + echo "WARNING: control plane failed to start (see /root/.intentframe/logs/control-plane.log)" >&2 + fi +} + +seed_control_plane_docker_config +start_control_plane + step "Starting Hermes + IntentFrame stack (chat-ready)" intentframe-integrations up hermes step "Starting Hermes dashboard on 0.0.0.0:9119" echo "" -echo " Open http://localhost:9119/chat" +echo " IntentFrame Control Plane: http://localhost:${CONTROL_PLANE_PORT}" +echo " Hermes chat: http://localhost:${HERMES_DASHBOARD_PORT}/chat" echo " Dashboard auth: ${HERMES_DASHBOARD_USER:-hermes} / ${HERMES_DASHBOARD_PASSWORD:-docker-test}" echo " Logs / gating analysis: tests/docker/README.md#logs-and-analysis-inside-the-container" echo " Tail policy log: docker compose -f tests/docker/docker-compose.test.yml exec hermes-intentframe \\" diff --git a/tests/docker/test_control_plane_smoke.sh b/tests/docker/test_control_plane_smoke.sh new file mode 100755 index 0000000..a0d692b --- /dev/null +++ b/tests/docker/test_control_plane_smoke.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Run control-plane lifecycle smoke in a throwaway container (local repo, no Hermes install). +# Verifies: start on 0.0.0.0, external health probe, /api/status, SPA index, stop. +# See tests/docker/control_plane_smoke_inner.sh and tests/docker/README.md. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +IMAGE="${CONTROL_PLANE_SMOKE_IMAGE:-ghcr.io/astral-sh/uv:python3.14-bookworm-slim}" + +docker run --rm \ + -v "${ROOT}:/repo:ro" \ + -w /repo \ + "${IMAGE}" \ + bash /repo/tests/docker/control_plane_smoke_inner.sh diff --git a/tests/install/test_installer_bootstrap_docker.sh b/tests/install/test_installer_bootstrap_docker.sh index 6ff1865..a4f7f7b 100644 --- a/tests/install/test_installer_bootstrap_docker.sh +++ b/tests/install/test_installer_bootstrap_docker.sh @@ -104,7 +104,7 @@ EOF chmod +x /tmp/bin/curl export PATH="/tmp/bin:${PATH}" -bash -s -- --headless --ref test-ref None: + from intentframe_control_plane.lifecycle import _health_host + + self.assertEqual(_health_host("0.0.0.0"), "127.0.0.1") + self.assertEqual(_health_host("::"), "127.0.0.1") + self.assertEqual(_health_host("127.0.0.1"), "127.0.0.1") + + def test_validate_loopback_ok(self) -> None: + validate_bind_host("127.0.0.1", allow_remote=False) + + def test_validate_remote_blocked(self) -> None: + with self.assertRaises(ValueError): + validate_bind_host("0.0.0.0", allow_remote=False) + + def test_validate_remote_allowed(self) -> None: + validate_bind_host("0.0.0.0", allow_remote=True) + + +class TestControlPlaneStatus(unittest.TestCase): + @patch("intentframe_control_plane.lifecycle._health_check", return_value=False) + @patch("intentframe_control_plane.lifecycle._read_pid", return_value=None) + def test_status_stopped(self, _pid, _health) -> None: + status = control_plane_status( + ControlPlaneSettings(host="127.0.0.1", port=9720, token=None, allow_remote=False) + ) + self.assertFalse(status.running) + self.assertIn("9720", status.url) + + @patch("intentframe_control_plane.lifecycle._health_check", return_value=True) + @patch("intentframe_control_plane.lifecycle._pid_alive", return_value=True) + @patch("intentframe_control_plane.lifecycle._read_pid", return_value=4242) + def test_status_running(self, _pid, _alive, _health) -> None: + status = control_plane_status( + ControlPlaneSettings(host="127.0.0.1", port=9720, token=None, allow_remote=False) + ) + self.assertTrue(status.running) + self.assertTrue(status.healthy) + line = format_status_line(status) + self.assertIn("control-plane: running", line) + + @patch("intentframe_control_plane.lifecycle._health_check", return_value=False) + @patch("intentframe_control_plane.lifecycle._pid_alive", return_value=True) + @patch("intentframe_control_plane.lifecycle._read_pid") + def test_status_in_process_skips_http_probe(self, mock_read_pid, _alive, mock_health) -> None: + import os + + mock_read_pid.return_value = os.getpid() + status = control_plane_status( + ControlPlaneSettings(host="0.0.0.0", port=9720, token=None, allow_remote=True) + ) + self.assertTrue(status.running) + self.assertTrue(status.healthy) + mock_health.assert_not_called() + + @patch("intentframe_control_plane.lifecycle._health_check", return_value=True) + @patch("intentframe_control_plane.lifecycle._pid_alive", return_value=True) + @patch("intentframe_control_plane.lifecycle._read_pid", return_value=4242) + def test_status_wildcard_bind_probes_loopback(self, _pid, _alive, mock_health) -> None: + status = control_plane_status( + ControlPlaneSettings(host="0.0.0.0", port=9720, token=None, allow_remote=True) + ) + self.assertTrue(status.healthy) + mock_health.assert_called_once_with("127.0.0.1", 9720) + + +class TestStartControlPlaneCleanup(unittest.TestCase): + @patch("intentframe_control_plane.lifecycle._kill_pid") + @patch("intentframe_control_plane.lifecycle._terminate_pid") + @patch("intentframe_control_plane.lifecycle._health_check", return_value=False) + @patch("intentframe_control_plane.lifecycle._pid_alive", return_value=True) + @patch("intentframe_control_plane.lifecycle.subprocess.Popen") + @patch("intentframe_control_plane.lifecycle.control_plane_status") + @patch("intentframe_control_plane.lifecycle.time.monotonic", side_effect=[0.0, 0.0, 31.0, 31.0, 36.0, 36.0, 41.0]) + def test_start_timeout_kills_child( + self, + _monotonic, + mock_status, + mock_popen, + _alive, + _health, + mock_terminate, + mock_kill, + ) -> None: + import tempfile + from pathlib import Path + + from intentframe_control_plane.lifecycle import ControlPlaneError, start_control_plane + + with tempfile.TemporaryDirectory() as tmp: + log_path = Path(tmp) / "control-plane.log" + pid_path = Path(tmp) / "control-plane.pid" + mock_status.return_value.running = False + mock_status.return_value.healthy = False + proc = mock_popen.return_value + proc.pid = 9999 + + with patch("intentframe_control_plane.lifecycle.LOG_FILE", log_path): + with patch("intentframe_control_plane.lifecycle.PID_FILE", pid_path): + with patch("intentframe_control_plane.lifecycle.time.sleep"): + with self.assertRaises(ControlPlaneError): + start_control_plane(quiet=True) + + mock_terminate.assert_called_once_with(9999) + mock_kill.assert_called_once_with(9999) + self.assertFalse(pid_path.exists()) + + @patch("intentframe_control_plane.lifecycle._health_check", return_value=True) + @patch("intentframe_control_plane.lifecycle.subprocess.Popen") + @patch("intentframe_control_plane.lifecycle.control_plane_status") + def test_start_wildcard_bind_probes_loopback( + self, + mock_status, + mock_popen, + mock_health, + ) -> None: + import tempfile + from pathlib import Path + + from intentframe_control_plane.lifecycle import start_control_plane + + with tempfile.TemporaryDirectory() as tmp: + log_path = Path(tmp) / "control-plane.log" + pid_path = Path(tmp) / "control-plane.pid" + mock_status.return_value.running = False + mock_status.return_value.healthy = False + mock_popen.return_value.pid = 9999 + + with patch("intentframe_control_plane.lifecycle.LOG_FILE", log_path): + with patch("intentframe_control_plane.lifecycle.PID_FILE", pid_path): + with patch.dict("os.environ", {"INTENTFRAME_CONTROL_PLANE_ALLOW_REMOTE": "1"}, clear=False): + start_control_plane(host="0.0.0.0", port=9720, quiet=True) + + mock_health.assert_called_with("127.0.0.1", 9720, timeout=1.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/intentframe_control_plane/test_read_models.py b/tests/intentframe_control_plane/test_read_models.py new file mode 100644 index 0000000..4488500 --- /dev/null +++ b/tests/intentframe_control_plane/test_read_models.py @@ -0,0 +1,50 @@ +"""Tests for read-only control plane models.""" + +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from intentframe_control_plane import read_models + + +class TestReadModels(unittest.TestCase): + def test_tail_log_lines_empty(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "missing.log" + self.assertEqual(read_models.tail_log_lines(path, max_lines=5), []) + + def test_tail_log_lines_last_n(self) -> None: + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", delete=False) as fh: + fh.write("\n".join(f"line-{i}" for i in range(10))) + path = Path(fh.name) + try: + lines = read_models.tail_log_lines(path, max_lines=3) + self.assertEqual(lines, ["line-7", "line-8", "line-9"]) + finally: + path.unlink(missing_ok=True) + + def test_load_governance_dict(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + gov = Path(tmp) / "governance" / "tools.yaml" + gov.parent.mkdir(parents=True) + gov.write_text( + "tools:\n" + " shell:\n" + " enabled: false\n" + " read:\n" + " enabled: true\n", + encoding="utf-8", + ) + with patch.object(read_models, "GOVERNANCE_YAML", gov): + data = read_models.load_governance_dict() + self.assertEqual(data["agent"], "hermes") + self.assertEqual(data["runtime_governed"], ["read"]) + names = [tool["name"] for tool in data["tools"]] + self.assertEqual(names, ["read", "shell"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/intentframe_control_plane/test_server.py b/tests/intentframe_control_plane/test_server.py new file mode 100644 index 0000000..56f0798 --- /dev/null +++ b/tests/intentframe_control_plane/test_server.py @@ -0,0 +1,131 @@ +"""API tests for control plane server.""" + +from __future__ import annotations + +import unittest +from unittest.mock import patch + +from fastapi.testclient import TestClient + +from intentframe_control_plane.cli_runner import CliResult +from intentframe_control_plane.server import app + + +class TestControlPlaneApi(unittest.TestCase): + def setUp(self) -> None: + self.client = TestClient(app) + + def test_health(self) -> None: + resp = self.client.get("/api/health") + self.assertEqual(resp.status_code, 200) + body = resp.json() + self.assertTrue(body["ok"]) + self.assertEqual(body["data"]["service"], "intentframe-control-plane") + self.assertEqual(body["data"]["status"], "ok") + + def test_health_fast_with_pid_file(self) -> None: + import os + import tempfile + import time + from pathlib import Path + from unittest.mock import patch + + with tempfile.TemporaryDirectory() as tmp: + pid_path = Path(tmp) / "control-plane.pid" + pid_path.write_text(str(os.getpid()), encoding="utf-8") + with patch("intentframe_control_plane.config.PID_FILE", pid_path): + start = time.monotonic() + resp = self.client.get("/api/health") + elapsed = time.monotonic() - start + self.assertEqual(resp.status_code, 200) + self.assertLess(elapsed, 0.5) + self.assertEqual(resp.json()["data"]["status"], "ok") + + def test_status_json(self) -> None: + resp = self.client.get("/api/status") + self.assertEqual(resp.status_code, 200) + body = resp.json() + self.assertTrue(body["ok"]) + self.assertIn("control_plane", body["data"]) + self.assertIn("bridge_present", body["data"]) + + def test_status_healthy_in_process_without_self_probe(self) -> None: + import os + import tempfile + import time + from pathlib import Path + + with tempfile.TemporaryDirectory() as tmp: + pid_path = Path(tmp) / "control-plane.pid" + pid_path.write_text(str(os.getpid()), encoding="utf-8") + with patch("intentframe_control_plane.lifecycle.PID_FILE", pid_path): + with patch( + "intentframe_control_plane.lifecycle._health_check", + side_effect=AssertionError("must not HTTP-probe self"), + ): + start = time.monotonic() + resp = self.client.get("/api/status") + elapsed = time.monotonic() - start + self.assertEqual(resp.status_code, 200) + self.assertLess(elapsed, 0.5) + cp = resp.json()["data"]["control_plane"] + self.assertTrue(cp["running"]) + self.assertTrue(cp["healthy"]) + + def test_governance_read(self) -> None: + resp = self.client.get("/api/governance") + self.assertEqual(resp.status_code, 200) + body = resp.json() + self.assertTrue(body["ok"]) + self.assertEqual(body["data"]["agent"], "hermes") + self.assertIn("tools", body["data"]) + + def test_policy_read(self) -> None: + resp = self.client.get("/api/policy") + self.assertEqual(resp.status_code, 200) + body = resp.json() + self.assertTrue(body["ok"]) + self.assertIn("meta", body["data"]) + self.assertIn("yaml", body["data"]) + + def test_config(self) -> None: + resp = self.client.get("/api/config") + self.assertEqual(resp.status_code, 200) + body = resp.json() + self.assertTrue(body["ok"]) + self.assertIn("hermes_chat_url", body["data"]) + + @patch("intentframe_control_plane.server.run_cli") + def test_stack_stop_requires_confirm(self, mock_run) -> None: + resp = self.client.post("/api/stack/stop") + self.assertEqual(resp.status_code, 400) + body = resp.json() + self.assertFalse(body["ok"]) + self.assertIn("error", body) + mock_run.assert_not_called() + + @patch("intentframe_control_plane.server.run_cli") + def test_stack_stop_with_confirm(self, mock_run) -> None: + mock_run.return_value = CliResult( + argv=["intentframe-integrations", "stop"], + returncode=0, + stdout="stopped", + stderr="", + ) + resp = self.client.post("/api/stack/stop", headers={"X-Confirm": "true"}) + self.assertEqual(resp.status_code, 200) + body = resp.json() + self.assertTrue(body["ok"]) + + def test_spa_fallback(self) -> None: + from intentframe_control_plane.server import INDEX_HTML + + if not INDEX_HTML.is_file(): + self.skipTest("frontend not built") + resp = self.client.get("/governance") + self.assertEqual(resp.status_code, 200) + self.assertIn("text/html", resp.headers.get("content-type", "")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/intentframe_integrations/test_pack_install.py b/tests/intentframe_integrations/test_pack_install.py index 2a86c17..ac7f3c9 100644 --- a/tests/intentframe_integrations/test_pack_install.py +++ b/tests/intentframe_integrations/test_pack_install.py @@ -33,7 +33,7 @@ def test_status_lines_with_manifest(self) -> None: manifest.write_text( json.dumps( { - "ref": "v0.2.0", + "ref": "v0.2.1", "installed_at": "2026-06-27T12:00:00Z", } ), @@ -45,7 +45,7 @@ def test_status_lines_with_manifest(self) -> None: ): lines = pack_install_status_lines() self.assertEqual(len(lines), 1) - self.assertIn("ref v0.2.0", lines[0]) + self.assertIn("ref v0.2.1", lines[0]) self.assertIn("installed 2026-06-27T12:00:00Z", lines[0]) def test_load_manifest(self) -> None: diff --git a/uv.lock b/uv.lock index 92958ad..5d22c59 100644 --- a/uv.lock +++ b/uv.lock @@ -9,12 +9,13 @@ members = [ "hermes-governance", "if-integration-backend", "if-integration-bridge-client", + "intentframe-control-plane", "intentframe-integrations-cli", ] [[package]] name = "agent-integrations" -version = "0.2.0" +version = "0.2.1" source = { virtual = "." } [[package]] @@ -287,7 +288,7 @@ wheels = [ [[package]] name = "hermes-adapter" -version = "0.2.0" +version = "0.2.1" source = { editable = "integrations/hermes/adapter" } dependencies = [ { name = "hermes-governance" }, @@ -306,7 +307,7 @@ requires-dist = [ [[package]] name = "hermes-governance" -version = "0.2.0" +version = "0.2.1" source = { editable = "integrations/hermes/shared" } dependencies = [ { name = "pyyaml" }, @@ -385,7 +386,7 @@ wheels = [ [[package]] name = "if-integration-backend" -version = "0.2.0" +version = "0.2.1" source = { editable = "if-integration-backend" } dependencies = [ { name = "httpx" }, @@ -410,7 +411,7 @@ requires-dist = [ [[package]] name = "if-integration-bridge-client" -version = "0.2.0" +version = "0.2.1" source = { editable = "if-integration-clients/python" } dependencies = [ { name = "httpx" }, @@ -476,6 +477,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5e/a9f406f59e3b35fceaa4612aeb7a4fd5653b0fc218bca33bf942d3325684/intentframe_components-0.1.1-py3-none-any.whl", hash = "sha256:84cfe2c157c368c0e29cf299c691e93c88e7f9973a1dfeee83273c517063afee", size = 43371, upload-time = "2026-06-21T05:41:47.475Z" }, ] +[[package]] +name = "intentframe-control-plane" +version = "0.2.1" +source = { editable = "intentframe-control-plane" } +dependencies = [ + { name = "fastapi" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.115.0" }, + { name = "python-multipart", specifier = ">=0.0.18" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.32.0" }, +] + [[package]] name = "intentframe-core" version = "0.1.1" @@ -552,14 +572,18 @@ wheels = [ [[package]] name = "intentframe-integrations-cli" -version = "0.2.0" +version = "0.2.1" source = { editable = "intentframe-integrations-cli" } dependencies = [ { name = "if-integration-backend" }, + { name = "intentframe-control-plane" }, ] [package.metadata] -requires-dist = [{ name = "if-integration-backend", editable = "if-integration-backend" }] +requires-dist = [ + { name = "if-integration-backend", editable = "if-integration-backend" }, + { name = "intentframe-control-plane", editable = "intentframe-control-plane" }, +] [[package]] name = "intentframe-native-kit"